From b0c1d39d2ff4b76469ccf2ced5c44a509fe86d04 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 5 Sep 2025 14:33:17 +0300 Subject: [PATCH 1/6] Rely Blaze eligibility on CIAB feature status --- .../Classes/Blaze/BlazeEligibilityChecker.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/Blaze/BlazeEligibilityChecker.swift b/WooCommerce/Classes/Blaze/BlazeEligibilityChecker.swift index 16506b55a30..49987fd7d70 100644 --- a/WooCommerce/Classes/Blaze/BlazeEligibilityChecker.swift +++ b/WooCommerce/Classes/Blaze/BlazeEligibilityChecker.swift @@ -13,9 +13,14 @@ protocol BlazeEligibilityCheckerProtocol { /// Checks for Blaze eligibility for a site and its products. final class BlazeEligibilityChecker: BlazeEligibilityCheckerProtocol { private let stores: StoresManager + private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol - init(stores: StoresManager = ServiceLocator.stores) { + init( + stores: StoresManager = ServiceLocator.stores, + siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker() + ) { self.stores = stores + self.siteCIABEligibilityChecker = siteCIABEligibilityChecker } /// Checks if the site is eligible for Blaze. @@ -39,7 +44,11 @@ final class BlazeEligibilityChecker: BlazeEligibilityCheckerProtocol { private extension BlazeEligibilityChecker { @MainActor func checkSiteEligibility(_ site: Site) async -> Bool { - guard site.isAdmin && site.canBlaze else { + guard + site.isAdmin, + site.canBlaze, + siteCIABEligibilityChecker.isFeatureSupported(.blaze, for: site) + else { return false } From c85ccdff92d12b34129e3dc6a50cec2dfc3dcadc Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Fri, 5 Sep 2025 15:03:37 +0300 Subject: [PATCH 2/6] Hide Payments menu option for CIAB sites --- .../Hub Menu/HubMenuViewModel.swift | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index c2ec4b2548c..c45795497ac 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -85,11 +85,13 @@ final class HubMenuViewModel: ObservableObject { private let inboxEligibilityChecker: InboxEligibilityChecker private let blazeEligibilityChecker: BlazeEligibilityCheckerProtocol private let googleAdsEligibilityChecker: GoogleAdsEligibilityChecker + private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol private(set) lazy var inboxViewModel = InboxViewModel(siteID: siteID) @Published private(set) var shouldShowNewFeatureBadgeOnPayments: Bool = false + @Published private var isSiteEligibleForPayments = false @Published private var isSiteEligibleForBlaze = false @Published private var isSiteEligibleForGoogleAds = false @Published private var isSiteEligibleForInbox = false @@ -125,6 +127,7 @@ final class HubMenuViewModel: ObservableObject { inboxEligibilityChecker: InboxEligibilityChecker = InboxEligibilityUseCase(), blazeEligibilityChecker: BlazeEligibilityCheckerProtocol = BlazeEligibilityChecker(), googleAdsEligibilityChecker: GoogleAdsEligibilityChecker = DefaultGoogleAdsEligibilityChecker(), + siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(), analytics: Analytics = ServiceLocator.analytics) { self.siteID = siteID self.credentials = stores.sessionManager.defaultCredentials @@ -136,6 +139,7 @@ final class HubMenuViewModel: ObservableObject { self.inboxEligibilityChecker = inboxEligibilityChecker self.blazeEligibilityChecker = blazeEligibilityChecker self.googleAdsEligibilityChecker = googleAdsEligibilityChecker + self.siteCIABEligibilityChecker = siteCIABEligibilityChecker self.cardPresentPaymentsOnboarding = CardPresentPaymentsOnboardingUseCase() self.analytics = analytics observeSiteForUIUpdates() @@ -251,30 +255,55 @@ private extension HubMenuViewModel { } func setupGeneralElements() { - $shouldShowNewFeatureBadgeOnPayments - .combineLatest($isSiteEligibleForInbox, - $isSiteEligibleForBlaze, - $isSiteEligibleForGoogleAds) - .map { [weak self] combinedResult -> [HubMenuItem] in - guard let self else { return [] } - let (shouldShowBadgeOnPayments, eligibleForInbox, eligibleForBlaze, eligibleForGoogleAds) = combinedResult - return createGeneralElements( - shouldShowBadgeOnPayments: shouldShowBadgeOnPayments, - eligibleForGoogleAds: eligibleForGoogleAds, - eligibleForBlaze: eligibleForBlaze, - eligibleForInbox: eligibleForInbox - ) - } - .assign(to: &$generalElements) + Publishers.CombineLatest( + $shouldShowNewFeatureBadgeOnPayments, + $isSiteEligibleForPayments + ) + .combineLatest( + Publishers.CombineLatest3( + $isSiteEligibleForInbox, + $isSiteEligibleForBlaze, + $isSiteEligibleForGoogleAds + ) + ) + .map { [weak self] combinedResults -> [HubMenuItem] in + guard let self else { return [] } + + let ((shouldShowBadgeOnPayments, eligibleForPayments), (eligibleForInbox, eligibleForBlaze, eligibleForGoogleAds)) = combinedResults + + let paymentsEligibility: PaymentsFeatureEligibility = eligibleForPayments ? + .eligible(shouldShowBadgeOnPayments: shouldShowBadgeOnPayments) : + .ineligible + + return createGeneralElements( + paymentsEligibility: paymentsEligibility, + eligibleForGoogleAds: eligibleForGoogleAds, + eligibleForBlaze: eligibleForBlaze, + eligibleForInbox: eligibleForInbox + ) + } + .assign(to: &$generalElements) + } + + enum PaymentsFeatureEligibility { + case ineligible + case eligible(shouldShowBadgeOnPayments: Bool) } - func createGeneralElements(shouldShowBadgeOnPayments: Bool, + func createGeneralElements(paymentsEligibility: PaymentsFeatureEligibility, eligibleForGoogleAds: Bool, eligibleForBlaze: Bool, eligibleForInbox: Bool) -> [HubMenuItem] { - var items: [HubMenuItem] = [ - Payments(iconBadge: shouldShowBadgeOnPayments ? .dot : nil) - ] + var items: [HubMenuItem] = [] + + switch paymentsEligibility { + case .ineligible: + break + case .eligible(let shouldShowBadgeOnPayments): + items.append( + Payments(iconBadge: shouldShowBadgeOnPayments ? .dot : nil) + ) + } if shouldShowAISettings { items.append(AISettings()) @@ -354,7 +383,7 @@ private extension HubMenuViewModel { } func updateMenuItemEligibility(with site: Yosemite.Site) { - + isSiteEligibleForPayments = siteCIABEligibilityChecker.isFeatureSupported(.payments, for: site) isSiteEligibleForInbox = inboxEligibilityChecker.isEligibleForInbox(siteID: site.siteID) Task { @MainActor in From 8f124313af128af1979b38781d3b63599043552b Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Wed, 10 Sep 2025 15:33:06 +0300 Subject: [PATCH 3/6] Add tests for payments and blaze menu items in CIAB mode --- .../HubMenu/HubMenuViewModelTests.swift | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift index fad691722d4..36a35c045e4 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift @@ -166,6 +166,58 @@ final class HubMenuViewModelTests: XCTestCase { XCTAssertTrue(expectedElementsIDs.isSubset(of: generalElementsIds)) } + @MainActor + func test_generalElements_does_not_include_blaze_when_site_is_CIAB_site() { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + // Setting site ID is required before setting `Site`. + stores.updateDefaultStore(storeID: sampleSiteID) + stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) + + let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: false) + let mockCIABEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true) + + // When + let viewModel = HubMenuViewModel( + siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + stores: stores, + blazeEligibilityChecker: blazeEligibilityChecker, + siteCIABEligibilityChecker: mockCIABEligibilityChecker + ) + + viewModel.setupMenuElements() + + // Then + XCTAssertNil(viewModel.generalElements.firstIndex(where: { $0.id == HubMenuViewModel.Blaze.id })) + } + + @MainActor + func test_generalElements_does_not_include_payments_when_site_is_CIAB_site() { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + // Setting site ID is required before setting `Site`. + stores.updateDefaultStore(storeID: sampleSiteID) + stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) + + let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: false) + let mockCIABEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true) + + // When + let viewModel = HubMenuViewModel( + siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + stores: stores, + blazeEligibilityChecker: blazeEligibilityChecker, + siteCIABEligibilityChecker: mockCIABEligibilityChecker + ) + + viewModel.setupMenuElements() + + // Then + XCTAssertNil(viewModel.generalElements.firstIndex(where: { $0.id == HubMenuViewModel.Payments.id })) + } + @MainActor func test_generalElements_does_not_include_blaze_when_default_site_is_not_set() { // When From 3de52d837476b6f0782aab588255bb21bf82d228 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 11 Sep 2025 17:11:41 +0300 Subject: [PATCH 4/6] Fix MockCIABEligibilityChecker feature availability condition --- .../WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift index 059e4c0d37c..d2507f01465 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockCIABEligibilityChecker.swift @@ -26,6 +26,6 @@ final class MockCIABEligibilityChecker: CIABEligibilityCheckerProtocol { } func isFeatureSupported(_ feature: CIABAffectedFeature, for site: Site) -> Bool { - return !mockedCIABDisabledFeatures.contains(feature) && mockedCIABSites.contains(site) + return !mockedCIABDisabledFeatures.contains(feature) || !mockedCIABSites.contains(site) } } From c337898401d5d8fd2e076e759bcabaf132311944 Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 11 Sep 2025 17:12:06 +0300 Subject: [PATCH 5/6] Add CIAB related tests for BlazeEligibilityChecker --- .../Blaze/BlazeEligibilityCheckerTests.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeEligibilityCheckerTests.swift index 225736c6760..4b24d0a8cb5 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeEligibilityCheckerTests.swift @@ -131,6 +131,53 @@ final class BlazeEligibilityCheckerTests: XCTestCase { XCTAssertTrue(isEligible) } + @MainActor + func test_isEligible_is_true_when_site_is_non_ciab_and_other_requirements_met() async { + // Given + stores.authenticate(credentials: .wpcom(username: "", authToken: "", siteAddress: "")) + let site = mockSite(isEligibleForBlaze: true, + isJetpackThePluginInstalled: false, + isJetpackConnected: true) + let siteIsCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: false, + ) + let blazeEligibilityChecker = BlazeEligibilityChecker( + stores: stores, + siteCIABEligibilityChecker: siteIsCIABChecker + ) + mockPluginFetch(remotePlugin: .fake().copy(plugin: Self.pluginSlug, active: true)) + + // When + let isEligible = await blazeEligibilityChecker.isSiteEligible(site) + + // Then + XCTAssertTrue(isEligible) + } + + @MainActor + func test_isEligible_is_false_when_site_is_ciab_and_other_requirements_met() async { + // Given + stores.authenticate(credentials: .wpcom(username: "", authToken: "", siteAddress: "")) + let site = mockSite(isEligibleForBlaze: true, + isJetpackThePluginInstalled: false, + isJetpackConnected: true) + let siteIsCIABChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABSites: [site] + ) + let blazeEligibilityChecker = BlazeEligibilityChecker( + stores: stores, + siteCIABEligibilityChecker: siteIsCIABChecker + ) + mockPluginFetch(remotePlugin: .fake().copy(plugin: Self.pluginSlug, active: true)) + + // When + let isEligible = await blazeEligibilityChecker.isSiteEligible(site) + + // Then + XCTAssertFalse(isEligible) + } + // MARK: - `isProductEligible` @MainActor From c5dc30795c9fc2e213c41070c9c3efe52dafc7ee Mon Sep 17 00:00:00 2001 From: RafaelKayumov Date: Thu, 11 Sep 2025 17:46:20 +0300 Subject: [PATCH 6/6] Fix HubMenuViewModel CIAB related tests --- .../ViewRelated/HubMenu/HubMenuViewModelTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift index 36a35c045e4..2e3a34d9faf 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift @@ -175,7 +175,10 @@ final class HubMenuViewModelTests: XCTestCase { stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: false) - let mockCIABEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true) + let mockCIABEligibilityChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABSites: [stores.sessionManager.defaultSite ?? .fake()] + ) // When let viewModel = HubMenuViewModel( @@ -201,7 +204,10 @@ final class HubMenuViewModelTests: XCTestCase { stores.updateDefaultStore(.fake().copy(siteID: sampleSiteID)) let blazeEligibilityChecker = MockBlazeEligibilityChecker(isSiteEligible: false) - let mockCIABEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true) + let mockCIABEligibilityChecker = MockCIABEligibilityChecker( + mockedIsCurrentSiteCIAB: true, + mockedCIABSites: [stores.sessionManager.defaultSite ?? .fake()] + ) // When let viewModel = HubMenuViewModel(