diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 08045ad8250..63880ec9c2e 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -97,6 +97,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return buildConfig == .localDeveloper || buildConfig == .alpha case .notificationSettings: return true + case .allowMerchantAIAPIKey: + return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 84e44a7bc0c..54139cb6e66 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -208,4 +208,8 @@ public enum FeatureFlag: Int { /// Supports managing notification settings from the app settings /// case notificationSettings + + /// Allows merchants to use their own API keys for AI-powered features + /// + case allowMerchantAIAPIKey } diff --git a/WooCommerce/Classes/Extensions/UIImage+Woo.swift b/WooCommerce/Classes/Extensions/UIImage+Woo.swift index 360ca78a48d..809003ead6a 100644 --- a/WooCommerce/Classes/Extensions/UIImage+Woo.swift +++ b/WooCommerce/Classes/Extensions/UIImage+Woo.swift @@ -569,6 +569,10 @@ extension UIImage { return UIImage.gridicon(.cog) } + static var wandAndRaysInverse: UIImage { + return UIImage(systemName: "wand.and.rays.inverse")! + } + static func prologueBackgroundBubbles(tint: UIColor) -> UIImage { let image = UIImage(named: "login-prologue-background-bubbles")! return image.withTintColor(tint) diff --git a/WooCommerce/Classes/ViewRelated/AI Settings/AISettingsView.swift b/WooCommerce/Classes/ViewRelated/AI Settings/AISettingsView.swift new file mode 100644 index 00000000000..b7333dcf909 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/AI Settings/AISettingsView.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct AISettingsView: View { + var body: some View { + EmptyView() + } +} diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index ff054b558c8..b00445937c1 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -184,6 +184,8 @@ private extension HubMenu { BlazeCampaignListHostingControllerRepresentable(siteID: viewModel.siteID, selectedCampaignID: campaignID) case .blazeCampaignCreation: BlazeCampaignListHostingControllerRepresentable(siteID: viewModel.siteID, startsCampaignCreationOnAppear: true) + case .aiSettings: + AISettingsView() } } .navigationBarTitleDisplayMode(.inline) diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index a826298e2a5..16deb4181a8 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -16,6 +16,7 @@ extension NSNotification.Name { /// Destination views that the hub menu can navigate to. enum HubMenuNavigationDestination: Hashable { case payments + case aiSettings case settings case blaze case blazeCampaignDetails(campaignID: String) @@ -122,6 +123,10 @@ final class HubMenuViewModel: ObservableObject { @Published private var isSiteEligibleForGoogleAds = false @Published private var isSiteEligibleForInbox = false + private var shouldShowAISettings: Bool { + featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) + } + private var cancellables: Set = [] let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker @@ -356,6 +361,10 @@ private extension HubMenuViewModel { Payments(iconBadge: shouldShowBadgeOnPayments ? .dot : nil) ] + if shouldShowAISettings { + items.append(AISettings()) + } + if eligibleForGoogleAds { items.append(GoogleAds()) } @@ -556,6 +565,23 @@ extension HubMenuViewModel { let navigationDestination: HubMenuNavigationDestination? = .settings } + struct AISettings: HubMenuItem { + static var id = "ai-settings" + + let title: String = Localization.aiSettings + let description: String = Localization.aiSettingsDescription + let icon: UIImage = .wandAndRaysInverse + let iconColor: UIColor = .primary + let accessibilityIdentifier: String = "ai-settings" + let trackingOption: String = "ai-settings" + let iconBadge: HubMenuBadgeType? + let navigationDestination: HubMenuNavigationDestination? = .aiSettings + + init(iconBadge: HubMenuBadgeType? = nil) { + self.iconBadge = iconBadge + } + } + struct Payments: HubMenuItem { static var id = "payments" @@ -732,6 +758,16 @@ extension HubMenuViewModel { "Payments", comment: "Title of the hub menu payments button") + static let aiSettings = NSLocalizedString( + "hubMenuViewModel.aiSettings", + value: "AI Settings", + comment: "Title of the hub menu AI settings button") + + static let aiSettingsDescription = NSLocalizedString( + "hubMenuViewModel.aiSettingsDescription", + value: "Manage your store's AI-powered features", + comment: "Description of the hub menu AI settings button") + static let paymentsDescription = NSLocalizedString( "Take payments on the go", comment: "Description of the hub menu payments button") diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b74a171e660..f75f7e0a668 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1608,6 +1608,7 @@ 68AC9D292ACE598B0042F784 /* ProductImageThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68AC9D282ACE598B0042F784 /* ProductImageThumbnail.swift */; }; 68AF3C3B2D01481C006F1ED2 /* POSReceiptEligibilityBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68AF3C3A2D01481A006F1ED2 /* POSReceiptEligibilityBanner.swift */; }; 68B681162D9257810098D5CD /* PointOfSaleCouponsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */; }; + 68B3BA262D9147480000B2F2 /* AISettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B3BA252D9147440000B2F2 /* AISettingsView.swift */; }; 68B6F22B2ADE7ED500D171FC /* TooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B6F22A2ADE7ED500D171FC /* TooltipView.swift */; }; 68C31B712A8617C500AE5C5A /* NewNoteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68C31B702A8617C500AE5C5A /* NewNoteViewModel.swift */; }; 68C53CBE2C1FE59B00C6D80B /* ItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */; }; @@ -4786,6 +4787,7 @@ 68AC9D282ACE598B0042F784 /* ProductImageThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductImageThumbnail.swift; sourceTree = ""; }; 68AF3C3A2D01481A006F1ED2 /* POSReceiptEligibilityBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSReceiptEligibilityBanner.swift; sourceTree = ""; }; 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCouponsController.swift; sourceTree = ""; }; + 68B3BA252D9147440000B2F2 /* AISettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AISettingsView.swift; sourceTree = ""; }; 68B6F22A2ADE7ED500D171FC /* TooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipView.swift; sourceTree = ""; }; 68C31B702A8617C500AE5C5A /* NewNoteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteViewModel.swift; sourceTree = ""; }; 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListView.swift; sourceTree = ""; }; @@ -9917,6 +9919,14 @@ path = Analytics; sourceTree = ""; }; + 68B3BA242D91473D0000B2F2 /* AI Settings */ = { + isa = PBXGroup; + children = ( + 68B3BA252D9147440000B2F2 /* AISettingsView.swift */, + ); + path = "AI Settings"; + sourceTree = ""; + }; 68DF5A8B2CB38EC5000154C9 /* Coupons */ = { isa = PBXGroup; children = ( @@ -10756,6 +10766,7 @@ B56DB3EF2049C06D00D4AA8E /* ViewRelated */ = { isa = PBXGroup; children = ( + 68B3BA242D91473D0000B2F2 /* AI Settings */, B626C7192876599B0083820C /* Custom Fields */, 86023FA82B15CA8D00A28F07 /* Themes */, DED91DF72AD78A0C00CDCC53 /* Blaze */, @@ -16897,6 +16908,7 @@ B6C78B90293BAF37008934A1 /* AnalyticsHubLastYearRangeData.swift in Sources */, 314265B12645A07800500598 /* CardReaderSettingsConnectedViewController.swift in Sources */, B57C744520F55BA600EEFC87 /* NSObject+Helpers.swift in Sources */, + 68B3BA262D9147480000B2F2 /* AISettingsView.swift in Sources */, 0269576A23726304001BA0BF /* KeyboardFrameObserver.swift in Sources */, 45CE2D852625D7ED00E3CA00 /* SelectableItemRow.swift in Sources */, CEEC9B6421E7AB850055EEF0 /* AppRatingManager.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index 1456f157030..e612a1a20e1 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -24,6 +24,7 @@ final class MockFeatureFlagService: FeatureFlagService { var isProductGlobalUniqueIdentifierSupported: Bool var hideSitesInStorePicker: Bool var notificationSettings: Bool + var allowMerchantAIAPIKey: Bool init(isInboxOn: Bool = false, isShowInboxCTAEnabled: Bool = false, @@ -46,7 +47,8 @@ final class MockFeatureFlagService: FeatureFlagService { favoriteProducts: Bool = false, isProductGlobalUniqueIdentifierSupported: Bool = false, hideSitesInStorePicker: Bool = false, - notificationSettings: Bool = false) { + notificationSettings: Bool = false, + allowMerchantAIAPIKey: Bool = false) { self.isInboxOn = isInboxOn self.isShowInboxCTAEnabled = isShowInboxCTAEnabled self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn @@ -69,6 +71,7 @@ final class MockFeatureFlagService: FeatureFlagService { self.isProductGlobalUniqueIdentifierSupported = isProductGlobalUniqueIdentifierSupported self.hideSitesInStorePicker = hideSitesInStorePicker self.notificationSettings = notificationSettings + self.allowMerchantAIAPIKey = allowMerchantAIAPIKey } func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool { @@ -117,6 +120,8 @@ final class MockFeatureFlagService: FeatureFlagService { return hideSitesInStorePicker case .notificationSettings: return notificationSettings + case .allowMerchantAIAPIKey: + return allowMerchantAIAPIKey default: return false } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift index 67d08d1dcb7..0c41ee9be98 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift @@ -206,11 +206,13 @@ final class HubMenuViewModelTests: XCTestCase { stores.updateDefaultStore(storeID: sampleSiteID) stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) + let featureFlagService = MockFeatureFlagService(allowMerchantAIAPIKey: false) let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: true) // When let viewModel = HubMenuViewModel(siteID: sampleSiteID, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, stores: stores, blazeEligibilityChecker: blazeEligibilityChecker) @@ -267,11 +269,13 @@ final class HubMenuViewModelTests: XCTestCase { stores.updateDefaultStore(storeID: sampleSiteID) stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) + let featureFlagService = MockFeatureFlagService(allowMerchantAIAPIKey: false) let checker = MockGoogleAdsEligibilityChecker(isEligible: true) // When let viewModel = HubMenuViewModel(siteID: sampleSiteID, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, stores: stores, googleAdsEligibilityChecker: checker) waitUntil { @@ -568,6 +572,7 @@ final class HubMenuViewModelTests: XCTestCase { let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: true) let googleAdsEligibilityChecker = MockGoogleAdsEligibilityChecker(isEligible: true) let inboxEligibilityChecker = MockInboxEligibilityChecker() + let featureFlagService = MockFeatureFlagService(allowMerchantAIAPIKey: false) inboxEligibilityChecker.isEligible = true let stores = MockStoresManager(sessionManager: .makeForTesting()) @@ -578,6 +583,7 @@ final class HubMenuViewModelTests: XCTestCase { let navigationPath = NavigationPath(["testPath1", "testPath2"]) let viewModel = HubMenuViewModel(siteID: sampleSiteID, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, stores: stores, generalAppSettings: generalAppSettings, inboxEligibilityChecker: inboxEligibilityChecker,