diff --git a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift index 8d0c26634fc..ee2e132a234 100644 --- a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift +++ b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift @@ -93,6 +93,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return false case .pointOfSaleAsATabi1: return true + case .pointOfSaleAsATabi2: + return buildConfig == .localDeveloper || buildConfig == .alpha case .pointOfSaleOrdersi1: return buildConfig == .localDeveloper || buildConfig == .alpha default: diff --git a/Modules/Sources/Experiments/FeatureFlag.swift b/Modules/Sources/Experiments/FeatureFlag.swift index 1c7375b7536..94ef3bf0e81 100644 --- a/Modules/Sources/Experiments/FeatureFlag.swift +++ b/Modules/Sources/Experiments/FeatureFlag.swift @@ -196,6 +196,10 @@ public enum FeatureFlag: Int { /// case pointOfSaleAsATabi1 + /// Enables displaying POS as a tab in the tab bar for stores in eligible countries + /// + case pointOfSaleAsATabi2 + /// Enables displaying Point Of Sale details in order list and order details /// case pointOfSaleOrdersi1 diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index f1f425866b4..b24efe37673 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -38,6 +38,8 @@ enum POSEligibilityState: Equatable { protocol POSEntryPointEligibilityCheckerProtocol { /// Checks the initial visibility of the POS tab. func checkInitialVisibility() -> Bool + /// Checks the final visibility of the POS tab. + func checkVisibility() async -> Bool /// Determines whether the site is eligible for POS. func checkEligibility() async -> POSEligibilityState } @@ -74,12 +76,11 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { /// Determines whether the POS entry point can be shown based on the selected store and feature gates. func checkEligibility() async -> POSEligibilityState { - guard #available(iOS 17.0, *) else { - return .ineligible(reason: .unsupportedIOSVersion) - } - - guard userInterfaceIdiom == .pad else { - return .ineligible(reason: .notTablet) + switch checkDeviceEligibility() { + case .eligible: + break + case .ineligible(let reason): + return .ineligible(reason: reason) } async let siteSettingsEligibility = checkSiteSettingsEligibility() @@ -110,8 +111,61 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { return .ineligible(reason: reason) } } + + /// Checks the final visibility of the POS tab. + func checkVisibility() async -> Bool { + if featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) { + return await checkVisibilityBasedOnCountryAndRemoteFeatureFlag() + } else { + let eligibility = await checkEligibility() + return eligibility == .eligible + } + } +} + +private extension POSTabEligibilityChecker { + func checkDeviceEligibility() -> POSEligibilityState { + guard #available(iOS 17.0, *) else { + return .ineligible(reason: .unsupportedIOSVersion) + } + + guard userInterfaceIdiom == .pad else { + return .ineligible(reason: .notTablet) + } + + return .eligible + } + + func checkVisibilityBasedOnCountryAndRemoteFeatureFlag() async -> Bool { + guard checkDeviceEligibility() == .eligible else { + return false + } + + async let siteSettingsEligibility = checkSiteSettingsEligibility() + async let featureFlagEligibility = checkRemoteFeatureEligibility() + + switch await siteSettingsEligibility { + case .eligible: + break + case let .ineligible(reason): + if reason == .unsupportedCurrency { + break + } else { + return false + } + } + + switch await featureFlagEligibility { + case .eligible: + return true + case .ineligible: + return false + } + } } +// MARK: - WC Plugin Related Eligibility Check + private extension POSTabEligibilityChecker { func checkPluginEligibility() async -> POSEligibilityState { let wcPlugin = await fetchWooCommercePlugin(siteID: siteID) @@ -157,6 +211,8 @@ private extension POSTabEligibilityChecker { } } +// MARK: - Site Settings Related Eligibility Check + private extension POSTabEligibilityChecker { func checkSiteSettingsEligibility() async -> POSEligibilityState { // Waits for the first site settings that matches the given site ID. @@ -202,6 +258,8 @@ private extension POSTabEligibilityChecker { } } +// MARK: - Remote Feature Flag Eligibility Check + private extension POSTabEligibilityChecker { @MainActor func checkRemoteFeatureEligibility() async -> POSEligibilityState { diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index b27b1649300..d4127bbb079 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -670,8 +670,7 @@ private extension MainTabBarController { // Starts observing the POS eligibility state. posEligibilityCheckTask = Task { @MainActor [weak self] in guard let self, let posEligibilityChecker = self.posEligibilityChecker else { return } - let eligibility = await posEligibilityChecker.checkEligibility() - let isPOSTabVisible = eligibility == .eligible + let isPOSTabVisible = await posEligibilityChecker.checkVisibility() analytics.track(.pointOfSaleTabVisibilityChecked, withProperties: ["is_visible": isPOSTabVisible]) cachePOSTabVisibility(siteID: siteID, isPOSTabVisible: isPOSTabVisible) updateTabViewControllers(isPOSTabVisible: isPOSTabVisible) diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index 8554d0bc658..3e40308c598 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -14,6 +14,7 @@ final class MockFeatureFlagService: FeatureFlagService { var isSubscriptionsInOrderCreationCustomersEnabled: Bool var isSubscriptionsInOrderCreationUIEnabled: Bool var isPointOfSaleEnabled: Bool + var isPointOfSaleAsATabi2Enabled: Bool var googleAdsCampaignCreationOnWebView: Bool var blazeEvergreenCampaigns: Bool var blazeCampaignObjective: Bool @@ -37,6 +38,7 @@ final class MockFeatureFlagService: FeatureFlagService { isSubscriptionsInOrderCreationCustomersEnabled: Bool = false, isSubscriptionsInOrderCreationUIEnabled: Bool = false, isPointOfSaleEnabled: Bool = false, + isPointOfSaleAsATabi2Enabled: Bool = false, googleAdsCampaignCreationOnWebView: Bool = false, blazeEvergreenCampaigns: Bool = false, blazeCampaignObjective: Bool = false, @@ -58,6 +60,7 @@ final class MockFeatureFlagService: FeatureFlagService { self.isSubscriptionsInOrderCreationCustomersEnabled = isSubscriptionsInOrderCreationCustomersEnabled self.isSubscriptionsInOrderCreationUIEnabled = isSubscriptionsInOrderCreationUIEnabled self.isPointOfSaleEnabled = isPointOfSaleEnabled + self.isPointOfSaleAsATabi2Enabled = isPointOfSaleAsATabi2Enabled self.googleAdsCampaignCreationOnWebView = googleAdsCampaignCreationOnWebView self.blazeEvergreenCampaigns = blazeEvergreenCampaigns self.blazeCampaignObjective = blazeCampaignObjective @@ -101,6 +104,8 @@ final class MockFeatureFlagService: FeatureFlagService { return isSubscriptionsInOrderCreationUIEnabled case .pointOfSale: return isPointOfSaleEnabled + case .pointOfSaleAsATabi2: + return isPointOfSaleAsATabi2Enabled case .googleAdsCampaignCreationOnWebView: return googleAdsCampaignCreationOnWebView case .blazeEvergreenCampaigns: diff --git a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift index 7429f16558a..d1ded5ee948 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift @@ -3,14 +3,20 @@ import Foundation final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { var initialVisibility: Bool = false - var result: POSEligibilityState = .eligible + var visibility: Bool = false + var eligibility: POSEligibilityState = .eligible func checkInitialVisibility() -> Bool { initialVisibility } + @MainActor + func checkVisibility() async -> Bool { + visibility + } + @MainActor func checkEligibility() async -> POSEligibilityState { - result + eligibility } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift index 5732b6cdfb9..a362adf3a3a 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift @@ -42,13 +42,13 @@ final class MainTabBarController_TabsTests: XCTestCase { isAnInstanceOf: HubMenuViewController.self) } - func test_tab_view_controllers_include_pos_tab_when_pos_is_eligible() throws { + func test_tab_view_controllers_include_pos_tab_when_pos_tab_is_visible() throws { // Given let featureFlagService = MockFeatureFlagService() featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .eligible + mockPOSEligibilityChecker.visibility = true let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -85,13 +85,13 @@ final class MainTabBarController_TabsTests: XCTestCase { isAnInstanceOf: HubMenuViewController.self) } - func test_tab_view_controllers_exclude_pos_tab_when_pos_is_not_eligible() throws { + func test_tab_view_controllers_exclude_pos_tab_when_pos_tab_is_not_visible() throws { // Given let featureFlagService = MockFeatureFlagService() featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .ineligible(reason: .notTablet) + mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -132,7 +132,7 @@ final class MainTabBarController_TabsTests: XCTestCase { featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = false let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .eligible + mockPOSEligibilityChecker.visibility = true let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -165,13 +165,13 @@ final class MainTabBarController_TabsTests: XCTestCase { isAnInstanceOf: HubMenuViewController.self) } - func test_tab_view_controllers_do_not_change_when_pos_eligibility_changes() throws { + func test_tab_view_controllers_do_not_change_when_pos_visibility_changes() throws { // Given let featureFlagService = MockFeatureFlagService() featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled) + mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -196,7 +196,7 @@ final class MainTabBarController_TabsTests: XCTestCase { } // When - change POS eligibility - mockPOSEligibilityChecker.result = .eligible + mockPOSEligibilityChecker.visibility = true // Then tabs remain the same XCTAssertEqual(tabBarController.tabRootViewControllers.count, 4) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift index cfde0034772..2111adc5382 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift @@ -76,7 +76,7 @@ final class MainTabBarControllerTests: XCTestCase { // Hides POS tab. let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled) + mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .testingInstance) // Reset `receivedActions` @@ -504,8 +504,8 @@ final class MainTabBarControllerTests: XCTestCase { posTabContainerController.presentedViewController is UIHostingController } - // When returning POS eligibility as ineligible - mockPOSEligibilityChecker.setEligibilityResult(.ineligible(reason: .featureFlagDisabled)) + // When POS tab becomes invisible + mockPOSEligibilityChecker.setVisibilityResult(false) // Then POS tab is hidden waitUntil { @@ -551,7 +551,7 @@ final class MainTabBarControllerTests: XCTestCase { // When POS tab initial visibility is set to true stores.updateDefaultStore(storeID: 1216) - mockPOSEligibilityChecker.setEligibilityResult(.eligible) + mockPOSEligibilityChecker.setVisibilityResult(true) waitUntil { tabBarController.tabRootViewControllers.count == 5 @@ -567,7 +567,7 @@ final class MainTabBarControllerTests: XCTestCase { featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true let mockPOSEligibilityChecker = MockPOSEligibilityChecker() - mockPOSEligibilityChecker.result = .eligible + mockPOSEligibilityChecker.visibility = true let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -642,9 +642,19 @@ extension MainTabBarController { private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { var initialVisibility: Bool = false + private var visibilityResult: Bool? + private var visibilityContinuation: CheckedContinuation? private var eligibilityResult: POSEligibilityState? private var eligibilityContinuation: CheckedContinuation? + func setVisibilityResult(_ result: Bool) { + visibilityResult = result + if let continuation = visibilityContinuation { + visibilityContinuation = nil + continuation.resume(returning: result) + } + } + func setEligibilityResult(_ result: POSEligibilityState) { eligibilityResult = result if let continuation = eligibilityContinuation { @@ -657,6 +667,19 @@ private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityChec initialVisibility } + func checkVisibility() async -> Bool { + if let visibilityResult { + return visibilityResult + } + return await withCheckedContinuation { continuation in + visibilityContinuation = continuation + // If we already have a result, return it immediately. + if visibilityContinuation == nil { + continuation.resume(returning: visibilityResult ?? true) + } + } + } + func checkEligibility() async -> POSEligibilityState { if let eligibilityResult { return eligibilityResult diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift index b7f8eba052d..68a4cddd9fa 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift @@ -23,9 +23,10 @@ struct POSTabEligibilityCheckerTests { siteSettings = MockSelectedSiteSettings() } - @Test func is_eligible_when_all_conditions_satisfied() async throws { + @Test(arguments: [true, false]) + func is_eligible_when_all_conditions_satisfied(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -42,9 +43,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .eligible) } - @Test func is_ineligible_when_account_not_whitelisted_and_feature_flag_disabled() async throws { + @Test(arguments: [true, false]) + func is_ineligible_when_account_not_whitelisted_and_feature_flag_disabled(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: false) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(false) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -61,9 +63,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .featureFlagDisabled)) } - @Test func is_ineligible_when_device_is_not_iPad() async throws { + @Test(arguments: [true, false]) + func is_ineligible_when_device_is_not_iPad(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -80,10 +83,15 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .notTablet)) } - @Test(arguments: [(country: Country.us, currency: CurrencyCode.USD), (country: Country.gb, currency: .GBP)]) - fileprivate func is_eligible_when_country_and_currency_are_supported(country: Country, currency: CurrencyCode) async throws { + @Test(arguments: [ + (country: Country.us, currency: CurrencyCode.USD, isPointOfSaleAsATabi2Enabled: true), + (country: Country.us, currency: CurrencyCode.USD, isPointOfSaleAsATabi2Enabled: false), + (country: Country.gb, currency: CurrencyCode.GBP, isPointOfSaleAsATabi2Enabled: true), + (country: Country.gb, currency: CurrencyCode.GBP, isPointOfSaleAsATabi2Enabled: false) + ]) + fileprivate func is_eligible_when_country_and_currency_supported(country: Country, currency: CurrencyCode, isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: country, currency: currency) accountWhitelistedInBackend(true) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -100,10 +108,15 @@ struct POSTabEligibilityCheckerTests { #expect(result == .eligible) } - @Test(arguments: [(country: Country.ca, currency: CurrencyCode.CAD), (country: Country.es, currency: CurrencyCode.EUR)]) - fileprivate func is_ineligible_when_country_is_not_supported(country: Country, currency: CurrencyCode) async throws { + @Test(arguments: [ + (country: Country.ca, currency: CurrencyCode.CAD, isPointOfSaleAsATabi2Enabled: true), + (country: Country.ca, currency: CurrencyCode.CAD, isPointOfSaleAsATabi2Enabled: false), + (country: Country.es, currency: CurrencyCode.EUR, isPointOfSaleAsATabi2Enabled: true), + (country: Country.es, currency: CurrencyCode.EUR, isPointOfSaleAsATabi2Enabled: false) + ]) + fileprivate func is_ineligible_when_country_is_not_supported(country: Country, currency: CurrencyCode, isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: country, currency: currency) accountWhitelistedInBackend(true) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -120,13 +133,19 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .unsupportedCountry)) } - @Test(arguments: [(country: Country.us, currency: CurrencyCode.GBP), - (country: Country.us, currency: CurrencyCode.CAD), - (country: Country.gb, currency: CurrencyCode.EUR), - (country: Country.gb, currency: CurrencyCode.USD)]) - fileprivate func is_ineligible_when_currency_is_not_supported(country: Country, currency: CurrencyCode) async throws { + @Test(arguments: [ + (country: Country.us, currency: CurrencyCode.GBP, isPointOfSaleAsATabi2Enabled: true), + (country: Country.us, currency: CurrencyCode.GBP, isPointOfSaleAsATabi2Enabled: false), + (country: Country.us, currency: CurrencyCode.CAD, isPointOfSaleAsATabi2Enabled: true), + (country: Country.us, currency: CurrencyCode.CAD, isPointOfSaleAsATabi2Enabled: false), + (country: Country.gb, currency: CurrencyCode.EUR, isPointOfSaleAsATabi2Enabled: true), + (country: Country.gb, currency: CurrencyCode.EUR, isPointOfSaleAsATabi2Enabled: false), + (country: Country.gb, currency: CurrencyCode.USD, isPointOfSaleAsATabi2Enabled: true), + (country: Country.gb, currency: CurrencyCode.USD, isPointOfSaleAsATabi2Enabled: false) + ]) + fileprivate func is_ineligible_when_currency_is_not_supported(country: Country, currency: CurrencyCode, isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: country, currency: currency) accountWhitelistedInBackend(true) let checker = POSTabEligibilityChecker(siteID: siteID, @@ -143,9 +162,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .unsupportedCurrency)) } - @Test func is_ineligible_when_woocommerce_version_is_below_minimum() async throws { + @Test(arguments: [true, false]) + func is_ineligible_when_woocommerce_version_is_below_minimum(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) setupWooCommerceVersion("9.5.0") @@ -163,9 +183,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .unsupportedWooCommerceVersion)) } - @Test func is_eligible_when_core_version_is_10_0_0_and_POS_feature_enabled() async throws { + @Test(arguments: [true, false]) + func is_eligible_when_core_version_is_10_0_0_and_POS_feature_enabled(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) setupWooCommerceVersion("10.0.0") @@ -184,9 +205,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .eligible) } - @Test func is_ineligible_when_core_version_is_10_0_0_and_POS_feature_disabled() async throws { + @Test(arguments: [true, false]) + func is_ineligible_when_core_version_is_10_0_0_and_POS_feature_disabled(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) setupWooCommerceVersion("10.0.0") @@ -205,9 +227,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .featureSwitchDisabled)) } - @Test func is_ineligible_when_core_version_is_10_0_0_and_POS_feature_check_fails() async throws { + @Test(arguments: [true, false]) + func is_ineligible_when_core_version_is_10_0_0_and_POS_feature_check_fails(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) setupWooCommerceVersion("10.0.0") @@ -226,9 +249,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .featureSwitchSyncFailure)) } - @Test func is_eligible_when_core_version_is_below_10_0_0_and_POS_feature_disabled() async throws { + @Test(arguments: [true, false]) + func is_eligible_when_core_version_is_below_10_0_0_and_POS_feature_disabled(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) setupCountry(country: .us) accountWhitelistedInBackend(true) setupWooCommerceVersion("9.9.9") @@ -283,9 +307,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == false) } - @Test func checkEligibility_skips_settings_from_initialLoad() async throws { + @Test(arguments: [true, false]) + func checkEligibility_skips_settings_from_initialLoad(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) // Initial settings (cached) - makes site eligible (US) let initialSettings = [ @@ -328,9 +353,10 @@ struct POSTabEligibilityCheckerTests { #expect(result == .ineligible(reason: .unsupportedCountry)) } - @Test func checkEligibility_filters_by_correct_siteID() async throws { + @Test(arguments: [true, false]) + func checkEligibility_filters_by_correct_siteID(isPointOfSaleAsATabi2Enabled: Bool) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleEnabled: true) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) // Settings for a different site. let wrongSiteSettings = [ @@ -375,6 +401,134 @@ struct POSTabEligibilityCheckerTests { // Then #expect(result == .eligible) } + + // MARK: - checkVisibility Tests + + @Test(arguments: [ + // Eligible countries and currencies. + (country: Country.us, currency: CurrencyCode.USD), + (country: Country.gb, currency: CurrencyCode.GBP), + // Eligible countries but ineligible currencies. + (country: Country.us, currency: CurrencyCode.EUR), + (country: Country.gb, currency: CurrencyCode.CAD) + ]) + fileprivate func checkVisibility_returns_true_when_i2_enabled_and_country_remote_feature_eligible(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == true) + } + + @Test(arguments: [(country: Country.ca, currency: CurrencyCode.CAD), (country: Country.es, currency: CurrencyCode.EUR)]) + fileprivate func checkVisibility_returns_false_when_pointOfSaleAsATabi2_enabled_but_country_ineligible(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test(arguments: [(country: Country.us, currency: CurrencyCode.USD), (country: Country.gb, currency: .GBP)]) + fileprivate func checkVisibility_returns_false_when_i2_enabled_but_remote_feature_flag_disabled(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(false) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test func checkVisibility_returns_true_when_pointOfSaleAsATabi2_disabled_and_checkEligibility_eligible() async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) + setupCountry(country: .us) + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == true) + } + + @Test(arguments: [(country: Country.us, currency: CurrencyCode.GBP), (country: Country.gb, currency: .EUR)]) + fileprivate func checkVisibility_returns_false_when_i2_disabled_and_checkEligibility_ineligible(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) + setupCountry(country: country, currency: currency) // Ineligible country/currency combination + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test(arguments: [true, false]) + func checkVisibility_returns_false_when_device_is_not_iPad(isPointOfSaleAsATabi2Enabled: Bool) async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: isPointOfSaleAsATabi2Enabled) + setupCountry(country: .us) + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .phone, // Not iPad + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } } private extension POSTabEligibilityCheckerTests {