Skip to content
Merged
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions WooCommerce/Classes/Extensions/UIImage+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import SwiftUI

struct AISettingsView: View {
var body: some View {
EmptyView()
}
}
2 changes: 2 additions & 0 deletions WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<AnyCancellable> = []

let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker
Expand Down Expand Up @@ -356,6 +361,10 @@ private extension HubMenuViewModel {
Payments(iconBadge: shouldShowBadgeOnPayments ? .dot : nil)
]

if shouldShowAISettings {
items.append(AISettings())
}

if eligibleForGoogleAds {
items.append(GoogleAds())
}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
12 changes: 12 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -4786,6 +4787,7 @@
68AC9D282ACE598B0042F784 /* ProductImageThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductImageThumbnail.swift; sourceTree = "<group>"; };
68AF3C3A2D01481A006F1ED2 /* POSReceiptEligibilityBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSReceiptEligibilityBanner.swift; sourceTree = "<group>"; };
68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCouponsController.swift; sourceTree = "<group>"; };
68B3BA252D9147440000B2F2 /* AISettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AISettingsView.swift; sourceTree = "<group>"; };
68B6F22A2ADE7ED500D171FC /* TooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipView.swift; sourceTree = "<group>"; };
68C31B702A8617C500AE5C5A /* NewNoteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNoteViewModel.swift; sourceTree = "<group>"; };
68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -9917,6 +9919,14 @@
path = Analytics;
sourceTree = "<group>";
};
68B3BA242D91473D0000B2F2 /* AI Settings */ = {
isa = PBXGroup;
children = (
68B3BA252D9147440000B2F2 /* AISettingsView.swift */,
);
path = "AI Settings";
sourceTree = "<group>";
};
68DF5A8B2CB38EC5000154C9 /* Coupons */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -10756,6 +10766,7 @@
B56DB3EF2049C06D00D4AA8E /* ViewRelated */ = {
isa = PBXGroup;
children = (
68B3BA242D91473D0000B2F2 /* AI Settings */,
B626C7192876599B0083820C /* Custom Fields */,
86023FA82B15CA8D00A28F07 /* Themes */,
DED91DF72AD78A0C00CDCC53 /* Blaze */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -117,6 +120,8 @@ final class MockFeatureFlagService: FeatureFlagService {
return hideSitesInStorePicker
case .notificationSettings:
return notificationSettings
case .allowMerchantAIAPIKey:
return allowMerchantAIAPIKey
default:
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand All @@ -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,
Expand Down