diff --git a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift index 0c512172ed1..7d7154edf4a 100644 --- a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift +++ b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift @@ -86,8 +86,6 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return false case .productImageOptimizedHandling: return true - case .pointOfSaleAsATabi2: - return true case .pointOfSaleOrdersi1: return true case .pointOfSaleOrdersi2: diff --git a/Modules/Sources/Experiments/FeatureFlag.swift b/Modules/Sources/Experiments/FeatureFlag.swift index 7f64d4ed372..9ce41916142 100644 --- a/Modules/Sources/Experiments/FeatureFlag.swift +++ b/Modules/Sources/Experiments/FeatureFlag.swift @@ -179,10 +179,6 @@ public enum FeatureFlag: Int { /// case inventoryProductLabelsInPOS - /// 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/Modules/Sources/Yosemite/Tools/Plugins/PluginsService.swift b/Modules/Sources/Yosemite/Tools/Plugins/PluginsService.swift index 358a20cc227..d5396097101 100644 --- a/Modules/Sources/Yosemite/Tools/Plugins/PluginsService.swift +++ b/Modules/Sources/Yosemite/Tools/Plugins/PluginsService.swift @@ -1,17 +1,8 @@ import Foundation -import WooFoundation import protocol Storage.StorageManagerType /// A service for system plugins. public protocol PluginsServiceProtocol { - /// Waits for a specific plugin to be available in storage. - /// - Parameters: - /// - siteID: The site ID to search for the plugin. - /// - pluginPath: The plugin's file path (e.g., "woocommerce/woocommerce.php" for WooCommerce). - /// - isActive: Whether the plugin is active or not. - /// - Returns: The SystemPlugin when found in storage. - func waitForPluginInStorage(siteID: Int64, pluginPath: String, isActive: Bool) async -> SystemPlugin - /// Loads a specific plugin from storage synchronously. /// - Parameters: /// - siteID: The site ID to search for the plugin. @@ -37,36 +28,6 @@ public class PluginsService: PluginsServiceProtocol { self.storageManager = storageManager } - @MainActor - public func waitForPluginInStorage(siteID: Int64, pluginPath: String, isActive: Bool) async -> SystemPlugin { - let predicate = \StorageSystemPlugin.siteID == siteID && \StorageSystemPlugin.plugin == pluginPath && \StorageSystemPlugin.active == isActive - let pluginDescriptor = NSSortDescriptor(keyPath: \StorageSystemPlugin.plugin, ascending: true) - let resultsController = ResultsController(storageManager: storageManager, - matching: predicate, - fetchLimit: 1, - sortedBy: [pluginDescriptor]) - do { - try resultsController.performFetch() - if let plugin = resultsController.fetchedObjects.first { - return plugin - } - } catch { - DDLogError("Error loading plugin \(pluginPath) for site \(siteID) initially: \(error.localizedDescription)") - } - - return await withCheckedContinuation { continuation in - var hasResumed = false - resultsController.onDidChangeContent = { - guard let plugin = resultsController.fetchedObjects.first, - !hasResumed else { - return - } - hasResumed = true - continuation.resume(returning: plugin) - } - } - } - @MainActor public func loadPluginInStorage(siteID: Int64, plugin: Plugin, isActive: Bool?) -> SystemPlugin? { storageManager.viewStorage.loadSystemPlugin(siteID: siteID, diff --git a/Modules/Tests/YosemiteTests/Tools/Plugins/PluginsServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/Plugins/PluginsServiceTests.swift index bf06d2b6f5f..11f22a917d2 100644 --- a/Modules/Tests/YosemiteTests/Tools/Plugins/PluginsServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/Plugins/PluginsServiceTests.swift @@ -13,40 +13,6 @@ struct PluginsServiceTests { sut = PluginsService(storageManager: storageManager) } - @Test func waitForPluginInStorage_returns_plugin_when_already_in_storage() async { - // Given - await storageManager.reset() - storageManager.insertPlugin(siteID: siteID, plugin: .wooCommerce, isActive: true, version: "1.0.0") - - // When - let result = await sut.waitForPluginInStorage(siteID: siteID, pluginPath: "woocommerce/woocommerce.php", isActive: true) - - // Then - #expect(result.siteID == siteID) - #expect(result.plugin == "woocommerce/woocommerce.php") - #expect(result.active == true) - #expect(result.version == "1.0.0") - } - - @Test func waitForPluginInStorage_waits_to_return_plugin_when_not_in_storage_initially() async { - // Given - // Resets any existing state, otherwise test might fail if run multiple times. - await storageManager.reset() - - // When - async let plugin = sut.waitForPluginInStorage(siteID: siteID, pluginPath: "woocommerce/woocommerce.php", isActive: true) - #expect(storageManager.viewStorage.loadSystemPlugins(siteID: siteID).count == 0) - storageManager.insertPlugin(siteID: siteID, plugin: .wooCommerce, isActive: true, version: "2.0.0") - #expect(storageManager.viewStorage.loadSystemPlugins(siteID: siteID).count == 1) - - // Then - let result = await plugin - #expect(result.siteID == siteID) - #expect(result.plugin == "woocommerce/woocommerce.php") - #expect(result.active == true) - #expect(result.version == "2.0.0") - } - // MARK: - `loadPluginInStorage` @Test(arguments: [(Plugin.wooCommerce, true, "1.5.0"), diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSaleIneligibleUI.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSaleIneligibleUI.swift index 48cd7ee928b..04fdef262b3 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSaleIneligibleUI.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSaleIneligibleUI.swift @@ -26,10 +26,6 @@ private extension POSIneligibleReason { return "feature_switch_disabled" case .wooCommercePluginNotFound: return "unknown_wc_plugin" - case .unsupportedIOSVersion: - return "ios_version" - case .unsupportedInCIABSites: - return "feature_unsupported_in_ciab" case .siteSettingsNotAvailable, .selfDeallocated: return "other" diff --git a/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift index efa2ed34656..7095d3cf8cb 100644 --- a/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift +++ b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift @@ -1,20 +1,19 @@ import SwiftUI -import protocol Experiments.FeatureFlagService + +protocol POSEntryPointEligibilityCheckerProtocol { + /// Determines whether the site is eligible for POS. + func checkEligibility() async -> POSEligibilityState + /// Refreshes the eligibility state based on the provided ineligible reason. + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState +} @Observable final class POSEntryPointController { private(set) var eligibilityState: POSEligibilityState? private let posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol - private let featureFlagService: POSFeatureFlagProviding - init(eligibilityChecker: POSEntryPointEligibilityCheckerProtocol, - featureFlagService: POSFeatureFlagProviding) { + init(eligibilityChecker: POSEntryPointEligibilityCheckerProtocol) { self.posEligibilityChecker = eligibilityChecker - self.featureFlagService = featureFlagService - guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) else { - self.eligibilityState = .eligible - return - } Task { @MainActor in eligibilityState = await posEligibilityChecker.checkEligibility() } diff --git a/WooCommerce/Classes/POS/Models/POSIneligibleReason.swift b/WooCommerce/Classes/POS/Models/POSIneligibleReason.swift index e6ef5cc4b6a..a79044823c1 100644 --- a/WooCommerce/Classes/POS/Models/POSIneligibleReason.swift +++ b/WooCommerce/Classes/POS/Models/POSIneligibleReason.swift @@ -4,14 +4,12 @@ import enum WooFoundation.CurrencyCode /// Represents the reasons why a site may be ineligible for POS. enum POSIneligibleReason: Equatable { - case unsupportedIOSVersion case unsupportedWooCommerceVersion(minimumVersion: String) case siteSettingsNotAvailable case wooCommercePluginNotFound case featureSwitchDisabled case unsupportedCurrency(countryCode: CountryCode, supportedCurrencies: [CurrencyCode]) case selfDeallocated - case unsupportedInCIABSites } /// Represents the eligibility state for POS. diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift b/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift index e071c0257ef..d1a1e010489 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift @@ -89,19 +89,6 @@ final class MockPointOfSaleSettingsService: PointOfSaleSettingsServiceProtocol { } final class PluginsServicePreview: PluginsServiceProtocol { - func waitForPluginInStorage(siteID: Int64, pluginPath: String, isActive: Bool) async -> SystemPlugin { - return SystemPlugin(siteID: 1234, - plugin: "", - name: "", - version: "", - versionLatest: "", - url: "", - authorName: "", - authorUrl: "", - networkActivated: false, - active: true) - } - func loadPluginInStorage(siteID: Int64, plugin: Plugin, isActive: Bool?) -> SystemPlugin? { return SystemPlugin(siteID: 1234, plugin: "", diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 7dfb6ce2ac7..fd7a4dee71f 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -56,7 +56,7 @@ struct PointOfSaleEntryPointView: View { self.searchHistoryService = searchHistoryService self.popularPurchasableItemsController = popularPurchasableItemsController self.barcodeScanService = barcodeScanService - self.posEntryPointController = POSEntryPointController(eligibilityChecker: posEligibilityChecker, featureFlagService: services.featureFlags) + self.posEntryPointController = POSEntryPointController(eligibilityChecker: posEligibilityChecker) self.orderListModel = POSOrderListModel(ordersController: ordersController, receiptSender: receiptSender) self.siteTimezone = siteTimezone self.services = services @@ -129,7 +129,7 @@ struct PointOfSaleEntryPointView: View { searchHistoryService: PointOfSalePreviewHistoryService(), popularPurchasableItemsController: PointOfSalePreviewItemsController(), barcodeScanService: PointOfSalePreviewBarcodeScanService(), - posEligibilityChecker: POSTabEligibilityChecker(site: .defaultMock()), + posEligibilityChecker: POSTabEligibilityChecker(siteID: 1), services: POSPreviewServices()) } diff --git a/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift index 8bc9d17ccc7..901d8f378b9 100644 --- a/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift +++ b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift @@ -112,10 +112,6 @@ struct POSIneligibleView: View { private var suggestionText: String { switch reason { - case .unsupportedIOSVersion: - return NSLocalizedString("pos.ineligible.suggestion.unsupportedIOSVersion.1", - value: "The POS system requires iOS 17 or later. Please update your device to iOS 17+ to use this feature.", - comment: "Suggestion for unsupported iOS version: update iOS") case let .unsupportedWooCommerceVersion(minimumVersion): let format = NSLocalizedString("pos.ineligible.suggestion.unsupportedWooCommerceVersion", value: "Your WooCommerce version is not supported. " + @@ -155,12 +151,6 @@ struct POSIneligibleView: View { return NSLocalizedString("pos.ineligible.suggestion.selfDeallocated", value: "Try relaunching the app to resolve this issue.", comment: "Suggestion for self deallocated: relaunch") - case .unsupportedInCIABSites: - return NSLocalizedString( - "pos.ineligible.suggestion.notSupportedForCIAB", - value: "The POS system is not supported for your store.", - comment: "Suggestion for CIAB sites: feature is not supported" - ) } } } @@ -184,9 +174,6 @@ private extension POSIneligibleView { private extension POSIneligibleReason { var shouldShowRetryButton: Bool { switch self { - case .unsupportedIOSVersion, - .unsupportedInCIABSites: - return false case .unsupportedWooCommerceVersion, .siteSettingsNotAvailable, .wooCommercePluginNotFound, @@ -205,8 +192,7 @@ private extension POSIneligibleReason { value: "Enable POS feature", comment: "Button title to enable the POS feature switch and refresh POS eligibility check" ) - case .unsupportedIOSVersion, - .unsupportedWooCommerceVersion, + case .unsupportedWooCommerceVersion, .siteSettingsNotAvailable, .wooCommercePluginNotFound, .unsupportedCurrency, @@ -216,9 +202,6 @@ private extension POSIneligibleReason { value: "Retry", comment: "Button title to refresh POS eligibility check" ) - case .unsupportedInCIABSites: - assertionFailure("Retry button should not be shown for `unsupportedInCIABSites`") - return String() } } } @@ -234,15 +217,6 @@ private extension POSIneligibleReason { } } -#Preview("Unsupported iOS version") { - if #available(iOS 17.0, *) { - POSIneligibleView( - reason: .unsupportedIOSVersion, - onRefresh: {} - ) - } -} - #Preview("WooCommerce plugin not found") { if #available(iOS 17.0, *) { POSIneligibleView( diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index bfaf88267da..da74e82c9d8 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -9,6 +9,13 @@ import class WooFoundationCore.CurrencyFormatter import struct NetworkingCore.JetpackSite import struct Combine.AnyPublisher +protocol POSTabVisibilityCheckerProtocol { + /// Checks the initial visibility of the POS tab. + func checkInitialVisibility() -> Bool + /// Checks the final visibility of the POS tab. + func checkVisibility() async -> Bool +} + /// View controller that provides the tab bar item for the Point of Sale tab. /// It is never visible on the screen, only used to provide the tab bar item as all POS UI is full-screen. final class POSTabViewController: UIViewController { @@ -24,7 +31,7 @@ final class POSTabViewController: UIViewController { /// Coordinator for the Point of Sale tab. /// final class POSTabCoordinator { - private(set) var siteID: Int64 + private let siteID: Int64 private let tabContainerController: TabContainerController private let viewControllerToPresent: UIViewController private let storesManager: StoresManager @@ -110,49 +117,6 @@ final class POSTabCoordinator { func onTabSelected() { presentPOSView(siteID: siteID) } - - func didSwitchStore(id: Int64) { - self.siteID = id - - // Resets lazy properties so they get recreated with new siteID - posItemFetchStrategyFactory = PointOfSaleItemFetchStrategyFactory( - siteID: siteID, - credentials: credentials, - selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported - ) - - posPopularItemFetchStrategyFactory = - PointOfSaleFixedItemFetchStrategyFactory( - fixedStrategy: posItemFetchStrategyFactory.popularStrategy() - ) - - posCouponFetchStrategyFactory = PointOfSaleCouponFetchStrategyFactory( - siteID: siteID, - currencySettings: currencySettings, - credentials: credentials, - selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported, - storage: storageManager - ) - - posCouponProvider = PointOfSaleCouponService( - siteID: siteID, - currencySettings: currencySettings, - credentials: credentials, - selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported, - storage: storageManager - ) - - barcodeScanService = PointOfSaleBarcodeScanService( - siteID: siteID, - credentials: credentials, - selectedSite: defaultSitePublisher, - appPasswordSupportState: isAppPasswordSupported, - currencySettings: currencySettings - ) - } } private extension POSTabCoordinator { diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index db00dfda94a..706382d49ec 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -224,13 +224,10 @@ struct POSPreviewHelpers { searchHistoryService: POSSearchHistoryProviding = PointOfSalePreviewHistoryService(), popularItemsController: PointOfSaleItemsControllerProtocol = PointOfSalePreviewItemsController(), barcodeScanService: PointOfSaleBarcodeScanServiceProtocol = PointOfSalePreviewBarcodeScanService(), - analytics: POSAnalyticsProviding = EmptyPOSAnalytics(), - featureFlags: POSFeatureFlagProviding = EmptyPOSFeatureFlags() + analytics: POSAnalyticsProviding = EmptyPOSAnalytics() ) -> PointOfSaleAggregateModel { return PointOfSaleAggregateModel( - entryPointController: POSEntryPointController( - eligibilityChecker: LegacyPOSTabEligibilityChecker(site: Site.defaultMock()), - featureFlagService: featureFlags), + entryPointController: POSEntryPointController(eligibilityChecker: PointOfSalePreviewTabEligibilityChecker()), itemsController: itemsController, purchasableItemsSearchController: purchasableItemsSearchController, couponsController: couponsController, @@ -405,6 +402,11 @@ final class PointOfSalePreviewBarcodeScanService: PointOfSaleBarcodeScanServiceP } } +final class PointOfSalePreviewTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { + func checkEligibility() async -> POSEligibilityState { .eligible } + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { .eligible } +} + final class POSReceiptSenderPreview: POSReceiptSending { func sendReceipt(orderID: Int64, recipientEmail: String) async throws {} } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift deleted file mode 100644 index 3e4eed1cf0e..00000000000 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift +++ /dev/null @@ -1,277 +0,0 @@ -import Foundation -import UIKit -import class WooFoundation.CurrencySettings -import enum WooFoundation.CountryCode -import enum WooFoundation.CurrencyCode -import protocol Experiments.FeatureFlagService -import struct Yosemite.SiteSetting -import struct Yosemite.Site -import protocol Yosemite.POSEligibilityServiceProtocol -import protocol Yosemite.StoresManager -import class Yosemite.POSEligibilityService -import struct Yosemite.SystemPlugin -import enum Yosemite.FeatureFlagAction -import enum Yosemite.SettingAction -import protocol Yosemite.PluginsServiceProtocol -import class Yosemite.PluginsService -import class Yosemite.SiteAddress -import class WooFoundation.VersionHelpers - -/// Legacy enum containing POS invisible reasons + POSIneligibleReason cases for i1. -private enum LegacyPOSIneligibleReason: Equatable { - case notTablet - case unsupportedIOSVersion - case unsupportedWooCommerceVersion(minimumVersion: String) - case siteSettingsNotAvailable - case wooCommercePluginNotFound - case featureFlagDisabled - case featureSwitchDisabled - case featureSwitchSyncFailure - case unsupportedCountry(supportedCountries: [CountryCode]) - case unsupportedCurrency(supportedCurrencies: [CurrencyCode]) - case selfDeallocated - case unsupportedInCIABSites -} - -/// Legacy POS eligibility state for i1. -private enum LegacyPOSEligibilityState: Equatable { - case eligible - case ineligible(reason: LegacyPOSIneligibleReason) -} - -/// POS tab eligibility checker for i1. Will be replaced by `POSTabEligibilityCheckerI2` when removing `pointOfSaleAsATabi2` feature flag. -final class LegacyPOSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { - private let site: Site - private let userInterfaceIdiom: UIUserInterfaceIdiom - private let siteSettings: SelectedSiteSettingsProtocol - private let pluginsService: PluginsServiceProtocol - private let eligibilityService: POSEligibilityServiceProtocol - private let stores: StoresManager - private let featureFlagService: FeatureFlagService - private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol - - init(site: Site, - userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, - siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings, - pluginsService: PluginsServiceProtocol = PluginsService(storageManager: ServiceLocator.storageManager), - eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), - stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker()) { - self.site = site - self.userInterfaceIdiom = userInterfaceIdiom - self.siteSettings = siteSettings - self.pluginsService = pluginsService - self.eligibilityService = eligibilityService - self.stores = stores - self.featureFlagService = featureFlagService - self.siteCIABEligibilityChecker = siteCIABEligibilityChecker - } - - /// Checks the initial visibility of the POS tab without dependance on network requests. - func checkInitialVisibility() -> Bool { - eligibilityService.loadCachedPOSTabVisibility(siteID: site.siteID) ?? false - } - - /// Determines whether the POS entry point can be shown based on the selected store and feature gates. - func checkEligibility() async -> POSEligibilityState { - .eligible - } - - private func checkI1Eligibility() async -> LegacyPOSEligibilityState { - guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else { - return .ineligible(reason: .unsupportedInCIABSites) - } - - switch checkDeviceEligibility() { - case .eligible: - break - case .ineligible(let reason): - return .ineligible(reason: reason) - } - - async let siteSettingsEligibility = checkSiteSettingsEligibility() - async let featureFlagEligibility = checkRemoteFeatureEligibility() - async let pluginEligibility = checkPluginEligibility() - - // Checks site settings first since it's likely to complete fastest. - switch await siteSettingsEligibility { - case .eligible: - break - case .ineligible(let reason): - return .ineligible(reason: reason) - } - - // Then checks feature flag. - switch await featureFlagEligibility { - case .eligible: - break - case .ineligible(let reason): - return .ineligible(reason: reason) - } - - // Finally checks plugin eligibility. - switch await pluginEligibility { - case .eligible: - return .eligible - case .ineligible(let reason): - return .ineligible(reason: reason) - } - } - - /// Checks the final visibility of the POS tab. - func checkVisibility() async -> Bool { - let eligibility = await checkI1Eligibility() - return eligibility == .eligible - } - - func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { - assertionFailure("POS as a tab i1 implementation should not refresh eligibility as the eligibility check is performed in the visibility check.") - return .eligible - } -} - -private extension LegacyPOSTabEligibilityChecker { - func checkDeviceEligibility() -> LegacyPOSEligibilityState { - guard #available(iOS 17.0, *) else { - return .ineligible(reason: .unsupportedIOSVersion) - } - - guard userInterfaceIdiom == .pad else { - return .ineligible(reason: .notTablet) - } - - return .eligible - } -} - -// MARK: - WC Plugin Related Eligibility Check - -private extension LegacyPOSTabEligibilityChecker { - func checkPluginEligibility() async -> LegacyPOSEligibilityState { - let wcPlugin = await fetchWooCommercePlugin(siteID: site.siteID) - - guard VersionHelpers.isVersionSupported(version: wcPlugin.version, - minimumRequired: Constants.wcPluginMinimumVersion) else { - return .ineligible(reason: .unsupportedWooCommerceVersion(minimumVersion: Constants.wcPluginMinimumVersion)) - } - - // For versions below 10.0.0, the feature is enabled by default. - let isFeatureSwitchSupported = VersionHelpers.isVersionSupported(version: wcPlugin.version, - minimumRequired: Constants.wcPluginMinimumVersionWithFeatureSwitch, - includesDevAndBetaVersions: true) - if !isFeatureSwitchSupported { - return .eligible - } - - // For versions that support the feature switch, checks if the feature switch is enabled. - return await checkFeatureSwitchEnabled(siteID: site.siteID) - } - - @MainActor - func fetchWooCommercePlugin(siteID: Int64) async -> SystemPlugin { - await pluginsService.waitForPluginInStorage(siteID: siteID, pluginPath: Constants.wcPlugin, isActive: true) - } - - @MainActor - func checkFeatureSwitchEnabled(siteID: Int64) async -> LegacyPOSEligibilityState { - await withCheckedContinuation { [weak self] continuation in - guard let self else { - return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) - } - let action = SettingAction.isFeatureEnabled(siteID: siteID, feature: .pointOfSale) { result in - switch result { - case .success(let isEnabled): - continuation.resume(returning: isEnabled ? .eligible : .ineligible(reason: .featureSwitchDisabled)) - case .failure: - continuation.resume(returning: .ineligible(reason: .featureSwitchSyncFailure)) - } - } - stores.dispatch(action) - } - } -} - -// MARK: - Site Settings Related Eligibility Check - -private extension LegacyPOSTabEligibilityChecker { - func checkSiteSettingsEligibility() async -> LegacyPOSEligibilityState { - // Waits for the first site settings that matches the given site ID. - let siteSettings = await waitForSiteSettingsRefresh() - guard siteSettings.isNotEmpty else { - return .ineligible(reason: .siteSettingsNotAvailable) - } - - // Conditions that can change if site settings are synced during the lifetime. - let countryCode = SiteAddress(siteSettings: siteSettings).countryCode - let currencyCode = CurrencySettings(siteSettings: siteSettings).currencyCode - - return isEligibleFromCountryAndCurrencyCode(countryCode: countryCode, currencyCode: currencyCode) - } - - func waitForSiteSettingsRefresh() async -> [SiteSetting] { - for await siteSettings in siteSettings.settingsStream.values { - guard siteSettings.siteID == site.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { - continue - } - return siteSettings.settings - } - // If we get here, the stream completed without yielding any values for our site ID which is unexpected. - return [] - } - - func isEligibleFromCountryAndCurrencyCode(countryCode: CountryCode, currencyCode: CurrencyCode) -> LegacyPOSEligibilityState { - let supportedCountries: [CountryCode] = [.US, .GB] - let supportedCurrencies: [CountryCode: [CurrencyCode]] = [.US: [.USD], - .GB: [.GBP]] - - // Checks country first. - guard supportedCountries.contains(countryCode) else { - return .ineligible(reason: .unsupportedCountry(supportedCountries: supportedCountries)) - } - - let supportedCurrenciesForCountry = supportedCurrencies[countryCode] ?? [] - guard supportedCurrenciesForCountry.contains(currencyCode) else { - return .ineligible(reason: .unsupportedCurrency(supportedCurrencies: supportedCurrenciesForCountry)) - } - return .eligible - } -} - -// MARK: - Remote Feature Flag Eligibility Check - -private extension LegacyPOSTabEligibilityChecker { - @MainActor - func checkRemoteFeatureEligibility() async -> LegacyPOSEligibilityState { - // Only whitelisted accounts in WPCOM have the Point of Sale remote feature flag enabled. These can be found at D159901-code - // If the account is whitelisted, then the remote value takes preference over the local feature flag configuration - await withCheckedContinuation { [weak self] continuation in - guard let self else { - return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) - } - let action = FeatureFlagAction.isRemoteFeatureFlagEnabled(.pointOfSale, defaultValue: false) { [weak self] result in - guard let self else { - return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) - } - switch result { - case true: - // The site is whitelisted. - continuation.resume(returning: .eligible) - case false: - // When the site is not whitelisted, check the local feature flag configuration. - let localFeatureFlag = featureFlagService.isFeatureFlagEnabled(.pointOfSale) - continuation.resume(returning: localFeatureFlag ? .eligible : .ineligible(reason: .featureFlagDisabled)) - } - } - self.stores.dispatch(action) - } - } -} - -private extension LegacyPOSTabEligibilityChecker { - enum Constants { - static let wcPlugin = "woocommerce/woocommerce.php" - static let wcPluginMinimumVersion = "9.6.0-beta" - static let wcPluginMinimumVersionWithFeatureSwitch = "10.0.0" - } -} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 2cfcc005782..725033ad853 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -6,9 +6,7 @@ import enum WooFoundation.CurrencyCode import protocol Experiments.FeatureFlagService import struct Yosemite.SiteSetting import struct Yosemite.Site -import protocol Yosemite.POSEligibilityServiceProtocol import protocol Yosemite.StoresManager -import class Yosemite.POSEligibilityService import struct Yosemite.SystemPlugin import enum Yosemite.FeatureFlagAction import enum Yosemite.SettingAction @@ -20,44 +18,22 @@ import class Yosemite.SiteAddress import enum Networking.SiteSettingsFeature import class WooFoundation.VersionHelpers -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 - /// Refreshes the eligibility state based on the provided ineligible reason. - func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState -} - final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { - private let site: Site - private let userInterfaceIdiom: UIUserInterfaceIdiom + private let siteID: Int64 private let siteSettings: SelectedSiteSettingsProtocol - private let eligibilityService: POSEligibilityServiceProtocol private let stores: StoresManager - private let featureFlagService: FeatureFlagService private let systemStatusService: POSSystemStatusServiceProtocol private let siteSettingService: POSSiteSettingServiceProtocol - private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol private let appPasswordSupportState: ApplicationPasswordsExperimentState - init(site: Site, - userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, + init(siteID: Int64, siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings, - eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), stores: StoresManager = ServiceLocator.stores, - featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, systemStatusService: POSSystemStatusServiceProtocol? = nil, - siteSettingService: POSSiteSettingServiceProtocol? = nil, - siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker()) { - self.site = site - self.userInterfaceIdiom = userInterfaceIdiom + siteSettingService: POSSiteSettingServiceProtocol? = nil) { + self.siteID = siteID self.siteSettings = siteSettings - self.eligibilityService = eligibilityService self.stores = stores - self.featureFlagService = featureFlagService self.appPasswordSupportState = ApplicationPasswordsExperimentState() let credentials = stores.sessionManager.defaultCredentials @@ -74,24 +50,10 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { selectedSite: selectedSite, appPasswordSupportState: appPasswordSupport ) - self.siteCIABEligibilityChecker = siteCIABEligibilityChecker - } - - /// Checks the initial visibility of the POS tab without dependance on network requests. - func checkInitialVisibility() -> Bool { - eligibilityService.loadCachedPOSTabVisibility(siteID: site.siteID) ?? false } /// Determines whether the POS entry point can be shown based on the selected store and feature gates. func checkEligibility() async -> POSEligibilityState { - guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else { - return .ineligible(reason: .unsupportedInCIABSites) - } - - guard #available(iOS 17.0, *) else { - return .ineligible(reason: .unsupportedIOSVersion) - } - async let siteSettingsEligibility = checkSiteSettingsEligibility() async let pluginEligibility = checkPluginEligibility() @@ -110,34 +72,8 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { } } - /// Checks the final visibility of the POS tab. - func checkVisibility() async -> Bool { - guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else { - return false - } - - guard userInterfaceIdiom == .pad else { - return false - } - - async let siteSettingsEligibility = waitAndCheckSiteSettingsEligibility() - async let featureFlagEligibility = checkRemoteFeatureEligibility() - - switch await siteSettingsEligibility { - case .ineligible(.unsupportedCountry): - return false - default: - break - } - - return await featureFlagEligibility == .eligible - } - func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { switch ineligibleReason { - case .unsupportedIOSVersion: - // TODO: WOOMOB-768 - hide refresh CTA in this case - return .ineligible(reason: .unsupportedIOSVersion) case .siteSettingsNotAvailable, .unsupportedCurrency: do { try await syncSiteSettingsRemotely() @@ -150,12 +86,10 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound: return await checkEligibility() case .featureSwitchDisabled: - _ = try await siteSettingService.setFeature(siteID: site.siteID, feature: .pointOfSale, enabled: true) + _ = try await siteSettingService.setFeature(siteID: siteID, feature: .pointOfSale, enabled: true) return await checkEligibility() case .selfDeallocated: return await checkEligibility() - case .unsupportedInCIABSites: - return await checkEligibility() } } } @@ -169,7 +103,7 @@ private extension POSTabEligibilityChecker { /// - Returns: The eligibility state for POS based on the WooCommerce plugin and POS feature switch. func checkPluginEligibility() async -> POSEligibilityState { do { - let info = try await systemStatusService.loadWooCommercePluginAndPOSFeatureSwitch(siteID: site.siteID) + let info = try await systemStatusService.loadWooCommercePluginAndPOSFeatureSwitch(siteID: siteID) let wcPluginEligibility = checkWooCommercePluginEligibility(wcPlugin: info.wcPlugin) switch wcPluginEligibility { case .eligible: @@ -262,7 +196,7 @@ private extension POSTabEligibilityChecker { func waitForSiteSettingsRefresh() async -> [SiteSetting] { for await siteSettings in siteSettings.settingsStream.values { - guard siteSettings.siteID == site.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { + guard siteSettings.siteID == siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { continue } return siteSettings.settings @@ -294,7 +228,7 @@ private extension POSTabEligibilityChecker { guard let self else { return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) } - stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: site.siteID) { [weak self] error in + stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: siteID) { [weak self] error in guard let self else { return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) } @@ -308,46 +242,6 @@ private extension POSTabEligibilityChecker { } } -// MARK: - Remote Feature Flag Eligibility Check - -private extension POSTabEligibilityChecker { - enum RemoteFeatureFlagEligibilityState: Equatable { - case eligible - case ineligible(reason: RemoteFeatureFlagIneligibleReason) - } - - enum RemoteFeatureFlagIneligibleReason: Equatable { - case selfDeallocated - case featureFlagDisabled - } - - @MainActor - func checkRemoteFeatureEligibility() async -> RemoteFeatureFlagEligibilityState { - // Only whitelisted accounts in WPCOM have the Point of Sale remote feature flag enabled. These can be found at D159901-code - // If the account is whitelisted, then the remote value takes preference over the local feature flag configuration - await withCheckedContinuation { [weak self] continuation in - guard let self else { - return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) - } - let action = FeatureFlagAction.isRemoteFeatureFlagEnabled(.pointOfSale, defaultValue: false) { [weak self] result in - guard let self else { - return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) - } - switch result { - case true: - // The site is whitelisted. - continuation.resume(returning: .eligible) - case false: - // When the site is not whitelisted, check the local feature flag configuration. - let localFeatureFlag = featureFlagService.isFeatureFlagEnabled(.pointOfSale) - continuation.resume(returning: localFeatureFlag ? .eligible : .ineligible(reason: .featureFlagDisabled)) - } - } - self.stores.dispatch(action) - } - } -} - private enum POSTabEligibilityCheckerError: Error { case selfDeallocated } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabVisibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabVisibilityChecker.swift new file mode 100644 index 00000000000..a50687abd24 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabVisibilityChecker.swift @@ -0,0 +1,164 @@ +import Foundation +import UIKit +import class WooFoundation.CurrencySettings +import enum WooFoundation.CountryCode +import enum WooFoundation.CurrencyCode +import protocol Experiments.FeatureFlagService +import struct Yosemite.SiteSetting +import struct Yosemite.Site +import protocol Yosemite.POSEligibilityServiceProtocol +import protocol Yosemite.StoresManager +import class Yosemite.POSEligibilityService +import enum Yosemite.FeatureFlagAction +import class Yosemite.SiteAddress + +final class POSTabVisibilityChecker: POSTabVisibilityCheckerProtocol { + private let site: Site + private let userInterfaceIdiom: UIUserInterfaceIdiom + private let siteSettings: SelectedSiteSettingsProtocol + private let eligibilityService: POSEligibilityServiceProtocol + private let stores: StoresManager + private let featureFlagService: FeatureFlagService + private let siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol + + init(site: Site, + userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, + siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings, + eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), + stores: StoresManager = ServiceLocator.stores, + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, + siteCIABEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker()) { + self.site = site + self.userInterfaceIdiom = userInterfaceIdiom + self.siteSettings = siteSettings + self.eligibilityService = eligibilityService + self.stores = stores + self.featureFlagService = featureFlagService + self.siteCIABEligibilityChecker = siteCIABEligibilityChecker + } + + /// Checks the initial visibility of the POS tab without dependance on network requests. + func checkInitialVisibility() -> Bool { + eligibilityService.loadCachedPOSTabVisibility(siteID: site.siteID) ?? false + } + + /// Checks the final visibility of the POS tab. + func checkVisibility() async -> Bool { + guard siteCIABEligibilityChecker.isFeatureSupported(.pointOfSale, for: site) else { + return false + } + + guard userInterfaceIdiom == .pad else { + return false + } + + async let siteSettingsEligibility = waitAndCheckSiteSettingsEligibility() + async let featureFlagEligibility = checkRemoteFeatureEligibility() + + switch await siteSettingsEligibility { + case .ineligible(.unsupportedCountry): + return false + default: + break + } + + return await featureFlagEligibility == .eligible + } +} + +// MARK: - Site Settings Related Eligibility Check + +private extension POSTabVisibilityChecker { + enum SiteSettingsEligibilityState { + case eligible + case ineligible(reason: SiteSettingsIneligibleReason) + } + + enum SiteSettingsIneligibleReason { + case siteSettingsNotAvailable + case unsupportedCountry(supportedCountries: [CountryCode]) + case unsupportedCurrency(countryCode: CountryCode, supportedCurrencies: [CurrencyCode]) + } + + func waitAndCheckSiteSettingsEligibility() async -> SiteSettingsEligibilityState { + // Waits for the first site settings that matches the given site ID. + let siteSettings = await waitForSiteSettingsRefresh() + guard siteSettings.isNotEmpty else { + return .ineligible(reason: .siteSettingsNotAvailable) + } + + // Conditions that can change if site settings are synced during the lifetime. + let countryCode = SiteAddress(siteSettings: siteSettings).countryCode + let currencyCode = CurrencySettings(siteSettings: siteSettings).currencyCode + + return isEligibleFromCountryAndCurrencyCode(countryCode: countryCode, currencyCode: currencyCode) + } + + func waitForSiteSettingsRefresh() async -> [SiteSetting] { + for await siteSettings in siteSettings.settingsStream.values { + guard siteSettings.siteID == site.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { + continue + } + return siteSettings.settings + } + // If we get here, the stream completed without yielding any values for our site ID which is unexpected. + return [] + } + + func isEligibleFromCountryAndCurrencyCode(countryCode: CountryCode, currencyCode: CurrencyCode) -> SiteSettingsEligibilityState { + let supportedCountries: [CountryCode] = [.US, .GB] + let supportedCurrencies: [CountryCode: [CurrencyCode]] = [.US: [.USD], + .GB: [.GBP]] + + // Checks country first. + guard supportedCountries.contains(countryCode) else { + return .ineligible(reason: .unsupportedCountry(supportedCountries: supportedCountries)) + } + + let supportedCurrenciesForCountry = supportedCurrencies[countryCode] ?? [] + guard supportedCurrenciesForCountry.contains(currencyCode) else { + return .ineligible(reason: .unsupportedCurrency(countryCode: countryCode, supportedCurrencies: supportedCurrenciesForCountry)) + } + return .eligible + } +} + +// MARK: - Remote Feature Flag Eligibility Check + +private extension POSTabVisibilityChecker { + enum RemoteFeatureFlagEligibilityState: Equatable { + case eligible + case ineligible(reason: RemoteFeatureFlagIneligibleReason) + } + + enum RemoteFeatureFlagIneligibleReason: Equatable { + case selfDeallocated + case featureFlagDisabled + } + + @MainActor + func checkRemoteFeatureEligibility() async -> RemoteFeatureFlagEligibilityState { + // Only whitelisted accounts in WPCOM have the Point of Sale remote feature flag enabled. These can be found at D159901-code + // If the account is whitelisted, then the remote value takes preference over the local feature flag configuration + await withCheckedContinuation { [weak self] continuation in + guard let self else { + return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) + } + let action = FeatureFlagAction.isRemoteFeatureFlagEnabled(.pointOfSale, defaultValue: false) { [weak self] result in + guard let self else { + return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) + } + switch result { + case true: + // The site is whitelisted. + continuation.resume(returning: .eligible) + case false: + // When the site is not whitelisted, check the local feature flag configuration. + let localFeatureFlag = featureFlagService.isFeatureFlagEnabled(.pointOfSale) + continuation.resume(returning: localFeatureFlag ? .eligible : .ineligible(reason: .featureFlagDisabled)) + } + } + self.stores.dispatch(action) + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 0e5ba9eec75..d20de47ebd6 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -142,13 +142,13 @@ final class MainTabBarController: UITabBarController { private let productImageUploader: ProductImageUploaderProtocol private let stores: StoresManager private let analytics: Analytics - private let posEligibilityCheckerFactory: ((_ site: Site) -> POSEntryPointEligibilityCheckerProtocol) + private let posTabVisibilityCheckerFactory: ((_ site: Site) -> POSTabVisibilityCheckerProtocol) private let posEligibilityService: POSEligibilityServiceProtocol private let bookingsEligibilityCheckerFactory: ((_ site: Site) -> BookingsTabEligibilityCheckerProtocol) private var productImageUploadErrorsSubscription: AnyCancellable? - private var posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol? + private var posTabVisibilityChecker: POSTabVisibilityCheckerProtocol? private var posEligibilityCheckTask: Task? private var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol? @@ -168,7 +168,7 @@ final class MainTabBarController: UITabBarController { productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader, analytics: Analytics = ServiceLocator.analytics, stores: StoresManager = ServiceLocator.stores, - posEligibilityCheckerFactory: ((Site) -> POSEntryPointEligibilityCheckerProtocol)? = nil, + posTabVisibilityCheckerFactory: ((Site) -> POSTabVisibilityCheckerProtocol)? = nil, posEligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), bookingsEligibilityCheckerFactory: ((Site) -> BookingsTabEligibilityCheckerProtocol)? = nil) { self.featureFlagService = featureFlagService @@ -176,12 +176,8 @@ final class MainTabBarController: UITabBarController { self.productImageUploader = productImageUploader self.analytics = analytics self.stores = stores - self.posEligibilityCheckerFactory = posEligibilityCheckerFactory ?? { site in - if featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) { - POSTabEligibilityChecker(site: site) - } else { - LegacyPOSTabEligibilityChecker(site: site) - } + self.posTabVisibilityCheckerFactory = posTabVisibilityCheckerFactory ?? { site in + POSTabVisibilityChecker(site: site) } self.posEligibilityService = posEligibilityService self.bookingsEligibilityCheckerFactory = bookingsEligibilityCheckerFactory ?? { site in @@ -197,12 +193,8 @@ final class MainTabBarController: UITabBarController { self.productImageUploader = ServiceLocator.productImageUploader self.analytics = ServiceLocator.analytics self.stores = ServiceLocator.stores - self.posEligibilityCheckerFactory = { site in - if featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) { - POSTabEligibilityChecker(site: site) - } else { - LegacyPOSTabEligibilityChecker(site: site) - } + self.posTabVisibilityCheckerFactory = { site in + POSTabVisibilityChecker(site: site) } self.posEligibilityService = POSEligibilityService() self.bookingsEligibilityCheckerFactory = { site in @@ -706,14 +698,14 @@ extension MainTabBarController: DeepLinkNavigator { // private extension MainTabBarController { func observePOSEligibilityForPOSTabVisibility(siteID: Int64) { - guard let posEligibilityChecker else { + guard let posTabVisibilityChecker else { updateTabViewControllers(isPOSTabVisible: false, isBookingsTabVisible: isBookingsTabVisible) viewModel.loadHubMenuTabBadge() return } // Sets POS tab initial visibility based on cached value if available. - let initialVisibility = posEligibilityChecker.checkInitialVisibility() + let initialVisibility = posTabVisibilityChecker.checkInitialVisibility() updateTabViewControllers(isPOSTabVisible: initialVisibility, isBookingsTabVisible: isBookingsTabVisible) // Cancels any existing task. @@ -721,8 +713,8 @@ 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 isPOSTabVisible = await posEligibilityChecker.checkVisibility() + guard let self, let posTabVisibilityChecker = self.posTabVisibilityChecker else { return } + let isPOSTabVisible = await posTabVisibilityChecker.checkVisibility() analytics.track(.pointOfSaleTabVisibilityChecked, withProperties: ["is_visible": isPOSTabVisible]) cachePOSTabVisibility(siteID: siteID, isPOSTabVisible: isPOSTabVisible) updateTabViewControllers(isPOSTabVisible: isPOSTabVisible, isBookingsTabVisible: isBookingsTabVisible) @@ -791,15 +783,8 @@ private extension MainTabBarController { func observeConditionalTabsAvailabilityWith(_ site: Site) { // Configures POS tab coordinator once per logged in site session. - let posEligibilityChecker = posEligibilityCheckerFactory(site) - self.posEligibilityChecker = posEligibilityChecker - posTabCoordinator = POSTabCoordinator( - siteID: site.siteID, - tabContainerController: posContainerController, - viewControllerToPresent: self, - storesManager: stores, - eligibilityChecker: posEligibilityChecker - ) + let posTabVisibilityChecker = posTabVisibilityCheckerFactory(site) + self.posTabVisibilityChecker = posTabVisibilityChecker observePOSEligibilityForPOSTabVisibility(siteID: site.siteID) @@ -846,8 +831,13 @@ private extension MainTabBarController { selectedIndex = WooTab.myStore.visibleIndex(isPOSTabVisible: isPOSTabVisible, isBookingsTabVisible: isBookingsTabVisible) - // Updates site ID for the POS coordinator to ensure correct data - posTabCoordinator?.didSwitchStore(id: siteID) + posTabCoordinator = POSTabCoordinator( + siteID: siteID, + tabContainerController: posContainerController, + viewControllerToPresent: self, + storesManager: stores, + eligibilityChecker: POSTabEligibilityChecker(siteID: siteID) + ) // Updates site ID for the bookings tab to display correct bookings (bookingsContainerController.wrappedController as? BookingsTabViewHostingController)?.didSwitchStore(id: siteID) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index f0bfcefd6c9..7f192513094 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -572,8 +572,6 @@ 02B653AC2429F7BF00A9C839 /* MockTaxClassStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B653AB2429F7BF00A9C839 /* MockTaxClassStoresManager.swift */; }; 02B7C4F62BE375D800F8E93A /* CollapsibleCustomerCardHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B7C4F52BE375D800F8E93A /* CollapsibleCustomerCardHeaderView.swift */; }; 02B8650F24A9E2D800265779 /* Product+SwiftUIPreviewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B8650E24A9E2D800265779 /* Product+SwiftUIPreviewHelpers.swift */; }; - 02B881832E1857E0009375F5 /* LegacyPOSTabEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B881822E1857DF009375F5 /* LegacyPOSTabEligibilityChecker.swift */; }; - 02B881852E18586E009375F5 /* LegacyPOSTabEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B881842E18586B009375F5 /* LegacyPOSTabEligibilityCheckerTests.swift */; }; 02B8E4192DFBC218001D01FD /* MainTabBarController+TabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B8E4182DFBC218001D01FD /* MainTabBarController+TabsTests.swift */; }; 02B8E41B2DFBC33D001D01FD /* MockPOSEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */; }; 02B9243F2C2200D600DC75F2 /* PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B9243E2C2200D600DC75F2 /* PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryAlertViewModel.swift */; }; @@ -701,6 +699,11 @@ 02F4F50B237AEB8A00E13A9C /* ProductFormTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F4F50A237AEB8A00E13A9C /* ProductFormTableViewDataSource.swift */; }; 02F4F50F237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F4F50D237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.swift */; }; 02F4F510237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02F4F50E237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.xib */; }; + 02F5DE5F2E852486002DEE24 /* POSTabVisibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5DE5E2E852486002DEE24 /* POSTabVisibilityChecker.swift */; }; + 02F5DE612E85290E002DEE24 /* MockPOSTabVisibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5DE602E852909002DEE24 /* MockPOSTabVisibilityChecker.swift */; }; + 02F5DE632E852CD5002DEE24 /* POSTabVisibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5DE622E852CCB002DEE24 /* POSTabVisibilityCheckerTests.swift */; }; + 02F5E5DD2E857A52002DEE24 /* MockPOSSiteSettingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5E5DC2E857A50002DEE24 /* MockPOSSiteSettingService.swift */; }; + 02F5E5DF2E857A92002DEE24 /* MockSelectedSiteSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5E5DE2E857A90002DEE24 /* MockSelectedSiteSettings.swift */; }; 02F5F80E246102240000613A /* FilterProductListViewModel+numberOfActiveFiltersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F5F80D246102240000613A /* FilterProductListViewModel+numberOfActiveFiltersTests.swift */; }; 02F67FEE25805F9C00C3BAD2 /* ShippingLabelTrackingURLGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F67FED25805F9C00C3BAD2 /* ShippingLabelTrackingURLGenerator.swift */; }; 02F67FF525806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F67FF425806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift */; }; @@ -3796,8 +3799,6 @@ 02B653AB2429F7BF00A9C839 /* MockTaxClassStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTaxClassStoresManager.swift; sourceTree = ""; }; 02B7C4F52BE375D800F8E93A /* CollapsibleCustomerCardHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleCustomerCardHeaderView.swift; sourceTree = ""; }; 02B8650E24A9E2D800265779 /* Product+SwiftUIPreviewHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Product+SwiftUIPreviewHelpers.swift"; sourceTree = ""; }; - 02B881822E1857DF009375F5 /* LegacyPOSTabEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPOSTabEligibilityChecker.swift; sourceTree = ""; }; - 02B881842E18586B009375F5 /* LegacyPOSTabEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPOSTabEligibilityCheckerTests.swift; sourceTree = ""; }; 02B8E4182DFBC218001D01FD /* MainTabBarController+TabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainTabBarController+TabsTests.swift"; sourceTree = ""; }; 02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSEligibilityChecker.swift; sourceTree = ""; }; 02B9243E2C2200D600DC75F2 /* PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryAlertViewModel.swift; sourceTree = ""; }; @@ -3925,6 +3926,11 @@ 02F4F50A237AEB8A00E13A9C /* ProductFormTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormTableViewDataSource.swift; sourceTree = ""; }; 02F4F50D237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAndTitleAndTextTableViewCell.swift; sourceTree = ""; }; 02F4F50E237AFC1E00E13A9C /* ImageAndTitleAndTextTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ImageAndTitleAndTextTableViewCell.xib; sourceTree = ""; }; + 02F5DE5E2E852486002DEE24 /* POSTabVisibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSTabVisibilityChecker.swift; sourceTree = ""; }; + 02F5DE602E852909002DEE24 /* MockPOSTabVisibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSTabVisibilityChecker.swift; sourceTree = ""; }; + 02F5DE622E852CCB002DEE24 /* POSTabVisibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSTabVisibilityCheckerTests.swift; sourceTree = ""; }; + 02F5E5DC2E857A50002DEE24 /* MockPOSSiteSettingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSSiteSettingService.swift; sourceTree = ""; }; + 02F5E5DE2E857A90002DEE24 /* MockSelectedSiteSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSelectedSiteSettings.swift; sourceTree = ""; }; 02F5F80D246102240000613A /* FilterProductListViewModel+numberOfActiveFiltersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilterProductListViewModel+numberOfActiveFiltersTests.swift"; sourceTree = ""; }; 02F67FED25805F9C00C3BAD2 /* ShippingLabelTrackingURLGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelTrackingURLGenerator.swift; sourceTree = ""; }; 02F67FF425806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelTrackingURLGeneratorTests.swift; sourceTree = ""; }; @@ -6993,8 +6999,8 @@ 023BD5892BFDCF9500A10D7B /* POS */ = { isa = PBXGroup; children = ( + 02F5DE622E852CCB002DEE24 /* POSTabVisibilityCheckerTests.swift */, 0277889D2DF928E3006F5B8C /* POSTabEligibilityCheckerTests.swift */, - 02B881842E18586B009375F5 /* LegacyPOSTabEligibilityCheckerTests.swift */, ); path = POS; sourceTree = ""; @@ -7877,6 +7883,8 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( + 02F5E5DE2E857A90002DEE24 /* MockSelectedSiteSettings.swift */, + 02F5E5DC2E857A50002DEE24 /* MockPOSSiteSettingService.swift */, 02C470B92E7BDDCF00F5F716 /* MockPOSCatalogSettingsService.swift */, 02C470B72E7BDD9400F5F716 /* MockPOSCatalogSyncCoordinator.swift */, 016582E12E787187001DBB6F /* MockOnboardingViewFactoryConfiguration.swift */, @@ -7978,8 +7986,8 @@ 02E4A0842BFB1D1F006D4F87 /* POS */ = { isa = PBXGroup; children = ( + 02F5DE5E2E852486002DEE24 /* POSTabVisibilityChecker.swift */, 026B2D162DF92290005B8CAA /* POSTabEligibilityChecker.swift */, - 02B881822E1857DF009375F5 /* LegacyPOSTabEligibilityChecker.swift */, ); path = POS; sourceTree = ""; @@ -10202,6 +10210,7 @@ 746791642108D853007CF1DC /* Mocks */ = { isa = PBXGroup; children = ( + 02F5DE602E852909002DEE24 /* MockPOSTabVisibilityChecker.swift */, 2DE9DDFA2E6EF4A300155408 /* MockCIABEligibilityChecker.swift */, 022900892E3019020028F6D7 /* MockPluginsService.swift */, 02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */, @@ -15474,6 +15483,7 @@ 020BE74823B05CF2007FE54C /* ProductInventoryEditableData.swift in Sources */, EEB4E2DE29B61AAD00371C3C /* CouponInputTransformer.swift in Sources */, 035C6DEB273EA12D00F70406 /* SoftwareUpdateTypeProperty.swift in Sources */, + 02F5DE5F2E852486002DEE24 /* POSTabVisibilityChecker.swift in Sources */, B998DF482A989BE100D1C6E8 /* TaxEducationalDialogView.swift in Sources */, DE7E5E912B4FD8F0002E28D2 /* BlazeTargetTopicPickerViewModel.swift in Sources */, 31F92DE125E85F6A00DE04DF /* ConnectedReaderTableViewCell.swift in Sources */, @@ -16974,7 +16984,6 @@ AE7C957D27C3F187007E8E12 /* FeeOrDiscountLineDetailsViewModel.swift in Sources */, 4520A15C2721B2A9001FA573 /* FilterOrderListViewModel.swift in Sources */, B582F95920FFCEAA0060934A /* UITableViewHeaderFooterView+Helpers.swift in Sources */, - 02B881832E1857E0009375F5 /* LegacyPOSTabEligibilityChecker.swift in Sources */, DA41043A2C247B6900E8456A /* PointOfSalePreviewOrderController.swift in Sources */, 20F6A46C2DE5FCEF0066D8CB /* POSItemFetchAnalytics.swift in Sources */, B933CCB02AA6220E00938F3F /* TaxRateRow.swift in Sources */, @@ -17354,6 +17363,7 @@ 020BE77723B4A9D9007FE54C /* AztecLinkFormatBarCommandTests.swift in Sources */, 020BE77323B4A567007FE54C /* AztecInsertMoreFormatBarCommandTests.swift in Sources */, B6440FB9292E74230012D506 /* AnalyticsHubTimeRangeSelectionTests.swift in Sources */, + 02F5E5DF2E857A92002DEE24 /* MockSelectedSiteSettings.swift in Sources */, B57C5C9E21B80E8300FF82B2 /* SessionManager+Internal.swift in Sources */, 2667BFDB252E659A008099D4 /* MockOrderItem.swift in Sources */, 023BD5862BFDCECF00A10D7B /* BetaFeaturesConfigurationViewModelTests.swift in Sources */, @@ -17528,6 +17538,7 @@ 2667BFE92530ECE4008099D4 /* RefundProductsTotalViewModelTests.swift in Sources */, D810F8F82639EDE900437C67 /* CardPresentPaymentsModalViewControllerTests.swift in Sources */, 26C0D1E42B460E9000F6EDA5 /* AppLocalizedString.swift in Sources */, + 02F5E5DD2E857A52002DEE24 /* MockPOSSiteSettingService.swift in Sources */, DEDA8D972B034C260076BF0F /* ProductSubscriptionPeriodPickerUseCaseTests.swift in Sources */, 02A275C623FE9EFC005C560F /* MockFeatureFlagService.swift in Sources */, DE2004782C05C36900660A72 /* MockInboxEligibilityChecker.swift in Sources */, @@ -17694,6 +17705,7 @@ 0225C42A24768CE900C5B4F0 /* FilterProductListViewModelProductListFilterTests.swift in Sources */, EE4C75E12C86D90400F9D860 /* BlazeLocalNotificationSchedulerTests.swift in Sources */, 31AD0B1326E95998000B6391 /* CardPresentModalConnectingFailedTests.swift in Sources */, + 02F5DE632E852CD5002DEE24 /* POSTabVisibilityCheckerTests.swift in Sources */, E16715CD2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift in Sources */, 02691780232600A6002AFC20 /* ProductsTabProductViewModelTests.swift in Sources */, DE69C54F27BCB4B7000BB888 /* CouponRestrictionsViewModelTests.swift in Sources */, @@ -17723,7 +17735,6 @@ 02A9A496244D84AB00757B99 /* ProductsSortOrderBottomSheetListSelectorCommandTests.swift in Sources */, B9B6DEF1283F8EB100901FB7 /* SitePluginsURLTests.swift in Sources */, 6891C3662D364C1A00B5B48C /* CollectCashViewHelperTests.swift in Sources */, - 02B881852E18586E009375F5 /* LegacyPOSTabEligibilityCheckerTests.swift in Sources */, D83F5935225B3CDD00626E75 /* DatePickerTableViewCellTests.swift in Sources */, AEB6903729770B1D00872FE0 /* ProductListViewModelTests.swift in Sources */, 03B9E52B2A1505A7005C77F5 /* TapToPayReconnectionControllerTests.swift in Sources */, @@ -17922,6 +17933,7 @@ CCCFFC5D2934F0BA006130AF /* StatsIntervalDataParserTests.swift in Sources */, 953728F82B23635300FDF1D1 /* UIAlertController+helpers.swift in Sources */, 02645D8A27BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift in Sources */, + 02F5DE612E85290E002DEE24 /* MockPOSTabVisibilityChecker.swift in Sources */, 6856D2A5C2076F5BF14F2C11 /* KeyboardStateProviderTests.swift in Sources */, B9DA153E28101BE100FC67DD /* MockOrderRefundsOptionsDeterminer.swift in Sources */, 20D3D4372C65EF72004CE6E3 /* OrdersRouteTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift index 63ad0c59d7e..25ad70b528a 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift @@ -14,7 +14,6 @@ final class MockFeatureFlagService: FeatureFlagService, POSFeatureFlagProviding var isSubscriptionsInOrderCreationCustomersEnabled: Bool var isSubscriptionsInOrderCreationUIEnabled: Bool var isPointOfSaleEnabled: Bool - var isPointOfSaleAsATabi2Enabled: Bool var googleAdsCampaignCreationOnWebView: Bool var blazeEvergreenCampaigns: Bool var blazeCampaignObjective: Bool @@ -39,7 +38,6 @@ final class MockFeatureFlagService: FeatureFlagService, POSFeatureFlagProviding isSubscriptionsInOrderCreationCustomersEnabled: Bool = false, isSubscriptionsInOrderCreationUIEnabled: Bool = false, isPointOfSaleEnabled: Bool = false, - isPointOfSaleAsATabi2Enabled: Bool = false, googleAdsCampaignCreationOnWebView: Bool = false, blazeEvergreenCampaigns: Bool = false, blazeCampaignObjective: Bool = false, @@ -62,7 +60,6 @@ final class MockFeatureFlagService: FeatureFlagService, POSFeatureFlagProviding self.isSubscriptionsInOrderCreationCustomersEnabled = isSubscriptionsInOrderCreationCustomersEnabled self.isSubscriptionsInOrderCreationUIEnabled = isSubscriptionsInOrderCreationUIEnabled self.isPointOfSaleEnabled = isPointOfSaleEnabled - self.isPointOfSaleAsATabi2Enabled = isPointOfSaleAsATabi2Enabled self.googleAdsCampaignCreationOnWebView = googleAdsCampaignCreationOnWebView self.blazeEvergreenCampaigns = blazeEvergreenCampaigns self.blazeCampaignObjective = blazeCampaignObjective @@ -107,8 +104,6 @@ final class MockFeatureFlagService: FeatureFlagService, POSFeatureFlagProviding 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 bde7359abc2..503c8f6e038 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift @@ -2,19 +2,8 @@ import Foundation @testable import WooCommerce final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { - var initialVisibility: Bool = false - var visibility: Bool = false var eligibility: POSEligibilityState = .eligible - func checkInitialVisibility() -> Bool { - initialVisibility - } - - @MainActor - func checkVisibility() async -> Bool { - visibility - } - @MainActor func checkEligibility() async -> POSEligibilityState { eligibility diff --git a/WooCommerce/WooCommerceTests/Mocks/MockPOSTabVisibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockPOSTabVisibilityChecker.swift new file mode 100644 index 00000000000..a593bb4cb58 --- /dev/null +++ b/WooCommerce/WooCommerceTests/Mocks/MockPOSTabVisibilityChecker.swift @@ -0,0 +1,16 @@ +import Foundation +@testable import WooCommerce + +final class MockPOSTabVisibilityChecker: POSTabVisibilityCheckerProtocol { + var initialVisibility: Bool = false + var visibility: Bool = false + + func checkInitialVisibility() -> Bool { + initialVisibility + } + + @MainActor + func checkVisibility() async -> Bool { + visibility + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift index b4212841beb..320ab7dcc09 100644 --- a/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift @@ -2,33 +2,15 @@ import Testing @testable import WooCommerce struct POSEntryPointControllerTests { - @Test func eligibilityState_is_always_eligible_when_i2_feature_is_disabled_regardless_of_eligibility_checker() async throws { + @Test func eligibilityState_is_set_to_ineligible_when_checker_returns_ineligible() async throws { // Given let mockEligibilityChecker = MockPOSEligibilityChecker() - mockEligibilityChecker.eligibility = .ineligible(reason: .unsupportedIOSVersion) - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - - // When - let controller = POSEntryPointController( - eligibilityChecker: mockEligibilityChecker, - featureFlagService: featureFlagService - ) - - // Then - #expect(controller.eligibilityState == .eligible) - } - - @Test func eligibilityState_is_set_to_ineligible_when_i2_feature_is_enabled_and_checker_returns_ineligible() async throws { - // Given - let mockEligibilityChecker = MockPOSEligibilityChecker() - let expectedState = POSEligibilityState.ineligible(reason: .unsupportedIOSVersion) + let expectedState = POSEligibilityState.ineligible(reason: .featureSwitchDisabled) mockEligibilityChecker.eligibility = expectedState - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) // When let controller = POSEntryPointController( - eligibilityChecker: mockEligibilityChecker, - featureFlagService: featureFlagService + eligibilityChecker: mockEligibilityChecker ) while controller.eligibilityState == nil { await Task.yield() @@ -38,16 +20,14 @@ struct POSEntryPointControllerTests { #expect(controller.eligibilityState == expectedState) } - @Test func eligibilityState_is_set_to_eligible_when_i2_feature_is_enabled_and_checker_returns_eligible() async throws { + @Test func eligibilityState_is_set_to_eligible_when_checker_returns_eligible() async throws { // Given let mockEligibilityChecker = MockPOSEligibilityChecker() mockEligibilityChecker.eligibility = .eligible - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) // When let controller = POSEntryPointController( - eligibilityChecker: mockEligibilityChecker, - featureFlagService: featureFlagService + eligibilityChecker: mockEligibilityChecker ) while controller.eligibilityState == nil { await Task.yield() diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSSiteSettingService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSSiteSettingService.swift new file mode 100644 index 00000000000..578b911b27e --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPOSSiteSettingService.swift @@ -0,0 +1,14 @@ +@testable import Yosemite + +final class MockPOSSiteSettingService: POSSiteSettingServiceProtocol { + var setFeatureResult: Result = .success(true) + + func setFeature(siteID: Int64, feature: SiteSettingsFeature, enabled: Bool) async throws -> Bool { + switch setFeatureResult { + case .success(let result): + return result + case .failure(let error): + throw error + } + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockSelectedSiteSettings.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockSelectedSiteSettings.swift new file mode 100644 index 00000000000..9788bbac284 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockSelectedSiteSettings.swift @@ -0,0 +1,16 @@ +@testable import WooCommerce +@testable import Yosemite +import Combine + +final class MockSelectedSiteSettings: SelectedSiteSettingsProtocol { + var mockSettingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never>? + var siteSettings: [SiteSetting] = [] + + var settingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never> { + return mockSettingsStream ?? Empty().eraseToAnyPublisher() + } + + func refresh() { + // Mock implementation - no action needed. + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 8d276e6239b..4907c11771d 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -966,10 +966,8 @@ private func makeLoadedOrderState(cartTotal: String = "", ) } -@available(iOS 17.0, *) private func makePointOfSaleAggregateModel( - entryPointController: POSEntryPointController = POSEntryPointController(eligibilityChecker: MockPOSEligibilityChecker(), - featureFlagService: MockFeatureFlagService()), + entryPointController: POSEntryPointController = POSEntryPointController(eligibilityChecker: MockPOSEligibilityChecker()), itemsController: PointOfSaleItemsControllerProtocol = MockPointOfSaleItemsController(), purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSalePurchasableItemsSearchController(), couponsController: PointOfSaleCouponsControllerProtocol = MockPointOfSaleCouponsController(), diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift index 1bed32bafb9..94177f9d185 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/POSItemActionHandlerTests.swift @@ -97,10 +97,8 @@ private func makeProductItem() -> POSItem { stockStatusKey: "")) } -@available(iOS 17.0, *) private func makePointOfSaleAggregateModel( - entryPointController: POSEntryPointController = POSEntryPointController(eligibilityChecker: MockPOSEligibilityChecker(), - featureFlagService: MockFeatureFlagService()), + entryPointController: POSEntryPointController = POSEntryPointController(eligibilityChecker: MockPOSEligibilityChecker()), itemsController: PointOfSaleItemsControllerProtocol = MockPointOfSaleItemsController(), purchasableItemsSearchController: PointOfSaleSearchingItemsControllerProtocol = MockPointOfSalePurchasableItemsSearchController(), couponsController: PointOfSaleCouponsControllerProtocol = MockPointOfSaleCouponsController(), diff --git a/WooCommerce/WooCommerceTests/POS/ViewHelpers/PointOfSaleDashboardViewHelperTests.swift b/WooCommerce/WooCommerceTests/POS/ViewHelpers/PointOfSaleDashboardViewHelperTests.swift index 4796b6dad23..fa497c79243 100644 --- a/WooCommerce/WooCommerceTests/POS/ViewHelpers/PointOfSaleDashboardViewHelperTests.swift +++ b/WooCommerce/WooCommerceTests/POS/ViewHelpers/PointOfSaleDashboardViewHelperTests.swift @@ -61,7 +61,6 @@ struct PointOfSaleDashboardViewHelperTests { } @Test(arguments: [ - POSIneligibleReason.unsupportedIOSVersion, POSIneligibleReason.unsupportedWooCommerceVersion(minimumVersion: "9.6.0"), POSIneligibleReason.unsupportedCurrency(countryCode: .US, supportedCurrencies: [.USD, .GBP]), POSIneligibleReason.siteSettingsNotAvailable, @@ -156,7 +155,7 @@ struct PointOfSaleDashboardViewHelperTests { @Test func determineViewState_horizontalSizeClass_takes_priority_over_eligibility_state() async throws { // Given - compact size class should return unsupportedWidth regardless of eligibility - let eligibilityState: POSEligibilityState = .ineligible(reason: .unsupportedIOSVersion) + let eligibilityState: POSEligibilityState = .ineligible(reason: .featureSwitchDisabled) let itemsContainerState: ItemsContainerState = .content let horizontalSizeClass: UserInterfaceSizeClass = .compact @@ -190,7 +189,7 @@ struct PointOfSaleDashboardViewHelperTests { @Test func determineViewState_ineligible_state_takes_priority_over_containerState() async throws { // Given - ineligible state should return ineligible regardless of container state - let eligibilityState: POSEligibilityState = .ineligible(reason: .unsupportedIOSVersion) + let eligibilityState: POSEligibilityState = .ineligible(reason: .featureSwitchDisabled) let itemsContainerState: ItemsContainerState = .content let horizontalSizeClass: UserInterfaceSizeClass = .regular @@ -202,7 +201,7 @@ struct PointOfSaleDashboardViewHelperTests { ) // Then - #expect(result == .ineligible(reason: .unsupportedIOSVersion)) + #expect(result == .ineligible(reason: .featureSwitchDisabled)) } // MARK: - Floating Control Tests @@ -219,7 +218,7 @@ struct PointOfSaleDashboardViewHelperTests { @Test(arguments: [ (PointOfSaleDashboardView.ViewState.loading, false), - (PointOfSaleDashboardView.ViewState.ineligible(reason: .unsupportedIOSVersion), false) + (PointOfSaleDashboardView.ViewState.ineligible(reason: .featureSwitchDisabled), false) ]) func showsFloatingControl_when_loading_or_ineligible_returns_false(viewState: PointOfSaleDashboardView.ViewState, expected: Bool) async throws { // When & Then diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift index 40f4e4b3708..4832e8dbb87 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift @@ -42,7 +42,7 @@ final class MainTabBarController_TabsTests: XCTestCase { func test_tab_view_controllers_include_pos_tab_when_pos_tab_is_visible() throws { // Given - let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + let mockPOSEligibilityChecker = MockPOSTabVisibilityChecker() mockPOSEligibilityChecker.visibility = true let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -51,7 +51,7 @@ final class MainTabBarController_TabsTests: XCTestCase { return MainTabBarController(coder: coder, featureFlagService: MockFeatureFlagService(), stores: storesManager, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } @@ -84,7 +84,7 @@ final class MainTabBarController_TabsTests: XCTestCase { func test_tab_view_controllers_exclude_pos_tab_when_pos_tab_is_not_visible() throws { // Given - let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + let mockPOSEligibilityChecker = MockPOSTabVisibilityChecker() mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -93,7 +93,7 @@ final class MainTabBarController_TabsTests: XCTestCase { return MainTabBarController(coder: coder, featureFlagService: MockFeatureFlagService(), stores: storesManager, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } @@ -124,7 +124,7 @@ final class MainTabBarController_TabsTests: XCTestCase { func test_tab_view_controllers_do_not_change_when_pos_visibility_changes() throws { // Given - let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + let mockPOSEligibilityChecker = MockPOSTabVisibilityChecker() mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -133,7 +133,7 @@ final class MainTabBarController_TabsTests: XCTestCase { return MainTabBarController(coder: coder, featureFlagService: MockFeatureFlagService(), stores: storesManager, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift index c1453c7e3b6..acac2872ebd 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift @@ -76,7 +76,7 @@ final class MainTabBarControllerTests: XCTestCase { ServiceLocator.setPushNotesManager(pushNotificationsManager) // Hides POS tab. - let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + let mockPOSEligibilityChecker = MockPOSTabVisibilityChecker() mockPOSEligibilityChecker.visibility = false let storesManager = MockStoresManager(sessionManager: .testingInstance) @@ -87,7 +87,7 @@ final class MainTabBarControllerTests: XCTestCase { guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in return MainTabBarController(coder: coder, stores: storesManager, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } @@ -475,7 +475,7 @@ final class MainTabBarControllerTests: XCTestCase { guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in return MainTabBarController(coder: coder, stores: stores, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } @@ -539,7 +539,7 @@ final class MainTabBarControllerTests: XCTestCase { guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in return MainTabBarController(coder: coder, stores: stores, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }, + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }, posEligibilityService: mockPOSEligibilityService) }) else { return @@ -564,7 +564,7 @@ final class MainTabBarControllerTests: XCTestCase { func test_event_is_tracked_after_eligibility_check() throws { // Given - let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + let mockPOSEligibilityChecker = MockPOSTabVisibilityChecker() mockPOSEligibilityChecker.visibility = true let storesManager = MockStoresManager(sessionManager: .makeForTesting()) @@ -573,7 +573,7 @@ final class MainTabBarControllerTests: XCTestCase { return MainTabBarController(coder: coder, analytics: self.analytics, stores: storesManager, - posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + posTabVisibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) }) else { return } @@ -713,12 +713,10 @@ extension MainTabBarController { } } -private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { +private final class MockAsyncPOSEligibilityChecker: POSTabVisibilityCheckerProtocol { 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 @@ -728,14 +726,6 @@ private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityChec } } - func setEligibilityResult(_ result: POSEligibilityState) { - eligibilityResult = result - if let continuation = eligibilityContinuation { - eligibilityContinuation = nil - continuation.resume(returning: result) - } - } - func checkInitialVisibility() -> Bool { initialVisibility } @@ -752,23 +742,6 @@ private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityChec } } } - - func checkEligibility() async -> POSEligibilityState { - if let eligibilityResult { - return eligibilityResult - } - return await withCheckedContinuation { continuation in - eligibilityContinuation = continuation - // If we already have a result, return it immediately. - if eligibilityContinuation == nil { - continuation.resume(returning: eligibilityResult ?? .eligible) - } - } - } - - func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { - .ineligible(reason: ineligibleReason) - } } private final class MockAsyncBookingsEligibilityChecker: BookingsTabEligibilityCheckerProtocol { diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/LegacyPOSTabEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/LegacyPOSTabEligibilityCheckerTests.swift deleted file mode 100644 index 97059db57a8..00000000000 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/LegacyPOSTabEligibilityCheckerTests.swift +++ /dev/null @@ -1,461 +0,0 @@ -import Combine -import Foundation -import Testing -import WooFoundation -import Yosemite -@testable import WooCommerce - -@MainActor -struct LegacyPOSTabEligibilityCheckerTests { - private var stores: MockStoresManager! - private var storageManager: MockStorageManager! - private var siteSettings: MockSelectedSiteSettings! - private var pluginsService: MockPluginsService! - private var eligibilityService: MockPOSEligibilityService! - private let site = Site.fake().copy(siteID: 2) - private var siteID: Int64 { site.siteID } - - init() async throws { - stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true)) - stores.updateDefaultStore(storeID: siteID) - storageManager = MockStorageManager() - pluginsService = MockPluginsService() - eligibilityService = MockPOSEligibilityService() - setupWooCommerceVersion() - siteSettings = MockSelectedSiteSettings() - } - - @Test(arguments: [ - (country: Country.us, currency: CurrencyCode.USD), - (country: Country.gb, currency: CurrencyCode.GBP) - ]) - fileprivate func is_visible_when_all_conditions_satisfied(country: Country, currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - @Test func is_invisible_when_account_not_whitelisted_and_feature_flag_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(false) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_invisible_when_device_is_not_iPad() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .phone, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_invisible_when_site_is_CIAB() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - let ciabEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true, - mockedCIABSites: [site], - mockedCIABDisabledFeatures: [.pointOfSale]) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService, - siteCIABEligibilityChecker: ciabEligibilityChecker) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test(arguments: [ - (country: Country.ca, currency: CurrencyCode.CAD), - (country: Country.es, currency: CurrencyCode.EUR) - ]) - fileprivate func is_invisible_when_country_is_not_supported(country: Country, currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - 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.GBP), - (country: Country.us, currency: CurrencyCode.CAD), - (country: Country.gb, currency: CurrencyCode.EUR), - (country: Country.gb, currency: CurrencyCode.USD) - ]) - fileprivate func is_invisible_when_currency_is_not_supported(country: Country, - currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_invisible_when_woocommerce_version_is_below_minimum() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("9.5.0") - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_visible_when_core_version_is_10_0_0_and_POS_feature_enabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0") - setupPOSFeatureEnabled(.success(true)) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - @Test func is_invisible_when_core_version_is_10_0_0_and_POS_feature_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0") - setupPOSFeatureEnabled(.success(false)) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_invisible_when_core_version_is_10_0_0_and_POS_feature_check_fails() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0") - setupPOSFeatureEnabled(.failure(NSError(domain: "test", code: 0))) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_visible_when_core_version_is_below_10_0_0_and_POS_feature_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("9.9.9") - setupPOSFeatureEnabled(.success(false)) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - @Test func checkInitialVisibility_returns_true_when_cached_tab_visibility_is_enabled() async throws { - // Given - let checker = LegacyPOSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: true) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == true) - } - - @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_disabled() async throws { - // Given - let checker = LegacyPOSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: false) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == false) - } - - @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_unavailable() async throws { - // Given - let checker = LegacyPOSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: nil) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == false) - } - - @Test func checkVisibility_skips_settings_from_initialLoad() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - - // Initial settings (cached) - makes site eligible (US) - let initialSettings = [ - mockCountrySetting(country: .us), - mockCurrencySetting(currency: .USD) - ] - // New settings - makes site ineligible (Canada). - let newSettings = [ - mockCountrySetting(country: .ca), - mockCurrencySetting(currency: .USD) - ] - siteSettings.mockSettingsStream = [ - // Emits cached settings first (should be skipped). - (siteID: siteID, settings: initialSettings, source: .initialLoad), - // Emits new settings (should be used for eligibility check). - (siteID: siteID, settings: newSettings, source: .storageChange) - ].publisher.eraseToAnyPublisher() - - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - Should return false because i2 feature flag is disabled - #expect(result == false) - } - - @Test func is_visible_from_filtering_site_settings_by_correct_siteID() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) - - // Settings for a different site. - let wrongSiteSettings = [ - mockCountrySetting(country: .ca, siteID: 999), - mockCurrencySetting(currency: .CAD, siteID: 999) - ] - // Settings for correct site. - let correctSiteSettings = [ - mockCountrySetting(country: .us), - mockCurrencySetting(currency: .USD) - ] - - siteSettings.mockSettingsStream = [ - // Emits settings for a different site (should be filtered out). - (siteID: 999, settings: wrongSiteSettings, source: .storageChange), - // Emits first settings for correct site (should be skipped). - (siteID: siteID, settings: [SiteSetting.fake().copy(siteID: siteID, settingID: "temp")], source: .initialLoad), - // Emits fresh settings for correct site (should be used). - (siteID: siteID, settings: correctSiteSettings, source: .storageChange) - ].publisher.eraseToAnyPublisher() - - accountWhitelistedInBackend(true) - let checker = LegacyPOSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - pluginsService: pluginsService, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } -} - -private extension LegacyPOSTabEligibilityCheckerTests { - func setupCountry(country: Country, currency: CurrencyCode = .USD) { - let countrySetting = mockCountrySetting(country: country) - let currencySetting = mockCurrencySetting(currency: currency) - siteSettings.mockSettingsStream = [ - // Emits cached settings first (should be skipped). - (siteID: siteID, settings: [], source: .storageChange), - // Emits fresh settings (should be used for eligibility check). - (siteID: siteID, settings: [countrySetting, currencySetting], source: .refresh) - ].publisher.eraseToAnyPublisher() - } - - func setupWooCommerceVersion(_ version: String = "9.6.0-beta") { - pluginsService.pluginToReturn = .fake().copy( - siteID: siteID, - plugin: "woocommerce/woocommerce.php", - version: version, - active: true - ) - } - - func accountWhitelistedInBackend(_ isAllowed: Bool = false) { - stores.whenReceivingAction(ofType: FeatureFlagAction.self) { action in - switch action { - case .isRemoteFeatureFlagEnabled(_, _, completion: let completion): - completion(isAllowed) - } - } - } - - func setupPOSFeatureEnabled(_ result: Result) { - stores.whenReceivingAction(ofType: SettingAction.self) { action in - switch action { - case .isFeatureEnabled(_, _, let completion): - completion(result) - default: - break - } - } - } - - func setupPOSTabVisibility(siteID: Int64, isVisible: Bool?) { - eligibilityService.cachedTabVisibility[siteID] = isVisible - } - - enum Country: String { - case us = "US:CA" - case ca = "CA:NS" - case gb = "GB" - case es = "ES" - } - - func mockCountrySetting(country: Country, siteID: Int64? = nil) -> SiteSetting { - SiteSetting.fake() - .copy( - siteID: siteID ?? siteID, - settingID: "woocommerce_default_country", - value: country.rawValue, - settingGroupKey: SiteSettingGroup.general.rawValue - ) - } - - func mockCurrencySetting(currency: CurrencyCode, siteID: Int64? = nil) -> SiteSetting { - SiteSetting.fake() - .copy( - siteID: siteID ?? siteID, - settingID: "woocommerce_currency", - value: currency.rawValue, - settingGroupKey: SiteSettingGroup.general.rawValue - ) - } -} - -private final class MockSelectedSiteSettings: SelectedSiteSettingsProtocol { - var mockSettingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never>? - var siteSettings: [SiteSetting] = [] - - var settingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never> { - return mockSettingsStream ?? Empty().eraseToAnyPublisher() - } - - func refresh() { - // Mock implementation - no action needed. - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift index 8eea672dece..dfaf890ca2c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift @@ -27,178 +27,10 @@ struct POSTabEligibilityCheckerTests { setupWooCommerceVersion() } - // MARK: `checkVisibility` - - @Test(arguments: [ - (country: Country.us, currency: CurrencyCode.USD), - (country: Country.gb, currency: CurrencyCode.GBP) - ]) - fileprivate func is_visible_when_all_conditions_satisfied(country: Country, currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - 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 is_invisible_when_country_is_not_supported(country: Country, currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - - @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_visible_when_currency_is_not_supported(country: Country, currency: CurrencyCode) async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - - func is_visible_when_woocommerce_version_is_below_minimum() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("9.5.0") - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - @Test func is_visible_when_core_version_is_10_0_0_and_POS_feature_enabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0", featureSwitchEnabled: true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - - func is_visible_when_core_version_is_10_0_0_and_POS_feature_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0", featureSwitchEnabled: false) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - - func is_visible_when_core_version_is_10_0_0_and_POS_feature_check_fails() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("10.0.0", featureSwitchEnabled: nil) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } - - @Test func is_visible_when_core_version_is_below_10_0_0_and_POS_feature_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - setupWooCommerceVersion("9.9.9", featureSwitchEnabled: false) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == true) - } + // MARK: - `checkEligibility` Tests - @Test func is_visible_when_site_settings_are_from_correct_siteID() async throws { + @Test func is_eligible_when_site_settings_are_from_correct_siteID() async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) // Settings for a different site. let wrongSiteSettings = [ @@ -220,13 +52,10 @@ struct POSTabEligibilityCheckerTests { (siteID: siteID, settings: correctSiteSettings, source: .storageChange) ].publisher.eraseToAnyPublisher() - accountWhitelistedInBackend(true) setupWooCommerceVersion("9.6.0") - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -236,103 +65,8 @@ struct POSTabEligibilityCheckerTests { #expect(result == .eligible) } - @Test func is_invisible_when_remote_feature_flag_disabled() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(false) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func checkVisibility_skips_settings_from_initialLoad() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - - // Initial settings (cached) - makes site eligible (US) - let initialSettings = [ - mockCountrySetting(country: .us), - mockCurrencySetting(currency: .USD) - ] - // New settings - makes site ineligible (Canada). - let newSettings = [ - mockCountrySetting(country: .ca), - mockCurrencySetting(currency: .USD) - ] - siteSettings.mockSettingsStream = [ - // Emits cached settings first (should be skipped). - (siteID: siteID, settings: initialSettings, source: .initialLoad), - // Emits new settings (should be used for eligibility check). - (siteID: siteID, settings: newSettings, source: .storageChange) - ].publisher.eraseToAnyPublisher() - - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - Should be invisible because fresh settings show CA (not cached US) - #expect(result == false) - } - - @Test func is_invisible_when_device_is_not_iPad() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .phone, // Not iPad - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func is_invisible_when_site_is_CIAB() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us, currency: .USD) - accountWhitelistedInBackend(true) - let ciabEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true, - mockedCIABSites: [site], - mockedCIABDisabledFeatures: [.pointOfSale]) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService, - siteCIABEligibilityChecker: ciabEligibilityChecker) - - // When - let result = await checker.checkVisibility() - - // Then - #expect(result == false) - } - - @Test func checkVisibility_and_checkEligibility_return_expected_result_after_site_settings_available() async throws { + @Test func checkEligibility_returns_expected_result_after_site_settings_available() async throws { // Given - no site settings are immediately available (empty stream that will emit values later) - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - accountWhitelistedInBackend(true) // Creates a publisher that will emit values after a delay to simulate site settings loading let countrySetting = mockCountrySetting(country: .us) @@ -341,15 +75,12 @@ struct POSTabEligibilityCheckerTests { siteSettings.mockSettingsStream = settingsSubject.eraseToAnyPublisher() setupWooCommerceVersion("9.6.0") - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) - // When - Call checkVisibility and checkEligibility concurrently before site settings are available - async let visibilityTask = checker.checkVisibility() + // When - Call checkEligibility before site settings are available async let eligibilityTask = checker.checkEligibility() // Simulate site settings becoming available after methods are called @@ -358,91 +89,22 @@ struct POSTabEligibilityCheckerTests { settingsSubject.send(completion: .finished) } - let visibilityResult = await visibilityTask let eligibilityResult = await eligibilityTask // Then - both methods should wait for site settings and return expected results. - #expect(visibilityResult == true) #expect(eligibilityResult == .eligible) } - @Test func is_ineligible_when_site_is_CIAB() async throws { - // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) - setupCountry(country: .us, currency: .USD) - accountWhitelistedInBackend(true) - let ciabEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true, - mockedCIABSites: [site], - mockedCIABDisabledFeatures: [.pointOfSale]) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, - siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService, - systemStatusService: mockSystemStatusService, - siteCIABEligibilityChecker: ciabEligibilityChecker) - - // When - let result = await checker.checkEligibility() - - // Then - #expect(result == .ineligible(reason: .unsupportedInCIABSites)) - } - - // MARK: - `checkInitialVisibility Tests - - @Test func checkInitialVisibility_returns_true_when_cached_tab_visibility_is_enabled() async throws { - // Given - let checker = POSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: true) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == true) - } - - @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_disabled() async throws { - // Given - let checker = POSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: false) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == false) - } - - @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_unavailable() async throws { - // Given - let checker = POSTabEligibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) - setupPOSTabVisibility(siteID: siteID, isVisible: nil) - - // When - let result = checker.checkInitialVisibility() - - // Then - #expect(result == false) - } - - // MARK: - `checkEligibility` Tests - @Test(arguments: [ (country: Country.us, currency: CurrencyCode.USD), (country: Country.gb, currency: CurrencyCode.GBP) ]) fileprivate func is_eligible_when_all_conditions_satisfied(country: Country, currency: CurrencyCode) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -458,14 +120,10 @@ struct POSTabEligibilityCheckerTests { ]) fileprivate func is_ineligible_when_country_is_not_supported(country: Country, currency: CurrencyCode) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) + stores: stores) // When let result = await checker.checkEligibility() @@ -484,14 +142,10 @@ struct POSTabEligibilityCheckerTests { currency: CurrencyCode, expectedSupportedCurrencies: [CurrencyCode]) async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: country, currency: currency) - accountWhitelistedInBackend(true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, - stores: stores, - featureFlagService: featureFlagService) + stores: stores) // When let result = await checker.checkEligibility() @@ -502,15 +156,11 @@ struct POSTabEligibilityCheckerTests { func is_ineligible_when_woocommerce_version_is_below_minimum() async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: .us) - accountWhitelistedInBackend(true) setupWooCommerceVersion("9.5.0") - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -522,15 +172,11 @@ struct POSTabEligibilityCheckerTests { func is_eligible_when_core_version_is_10_0_0_and_POS_feature_enabled() async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: .us) - accountWhitelistedInBackend(true) setupWooCommerceVersion("10.0.0", featureSwitchEnabled: true) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -542,15 +188,11 @@ struct POSTabEligibilityCheckerTests { func is_ineligible_when_core_version_is_10_0_0_and_POS_feature_disabled() async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: .us) - accountWhitelistedInBackend(true) setupWooCommerceVersion("10.0.0", featureSwitchEnabled: false) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -562,15 +204,11 @@ struct POSTabEligibilityCheckerTests { func is_eligible_when_core_version_is_below_10_0_0_and_POS_feature_disabled() async throws { // Given - let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) setupCountry(country: .us) - accountWhitelistedInBackend(true) setupWooCommerceVersion("9.9.9", featureSwitchEnabled: false) - let checker = POSTabEligibilityChecker(site: site, - userInterfaceIdiom: .pad, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, - featureFlagService: featureFlagService, systemStatusService: mockSystemStatusService) // When @@ -582,19 +220,6 @@ struct POSTabEligibilityCheckerTests { // MARK: - `refreshEligibility` Tests - @Test func refreshEligibility_returns_ineligible_for_unsupportedIOSVersion() async throws { - // Given - let checker = POSTabEligibilityChecker(site: site, - siteSettings: siteSettings, - stores: stores) - - // When - let result = try await checker.refreshEligibility(ineligibleReason: .unsupportedIOSVersion) - - // Then - #expect(result == .ineligible(reason: .unsupportedIOSVersion)) - } - @Test(arguments: [ POSIneligibleReason.siteSettingsNotAvailable, POSIneligibleReason.unsupportedCurrency(countryCode: .US, supportedCurrencies: [.USD]) @@ -617,7 +242,7 @@ struct POSTabEligibilityCheckerTests { } } - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -650,7 +275,7 @@ struct POSTabEligibilityCheckerTests { } } - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores) @@ -667,7 +292,7 @@ struct POSTabEligibilityCheckerTests { setupCountry(country: .us, currency: .USD) setupWooCommerceVersion("10.0.0", featureSwitchEnabled: true) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService, @@ -685,7 +310,7 @@ struct POSTabEligibilityCheckerTests { setupCountry(country: .us, currency: .USD) setupWooCommerceVersion("9.6.0", featureSwitchEnabled: true) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -709,7 +334,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .success(POSPluginAndFeatureInfo(wcPlugin: wcPlugin, featureValue: nil)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -731,7 +356,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .success(POSPluginAndFeatureInfo(wcPlugin: wcPlugin, featureValue: true)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -752,7 +377,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .success(POSPluginAndFeatureInfo(wcPlugin: nil, featureValue: nil)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -774,7 +399,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .success(POSPluginAndFeatureInfo(wcPlugin: wcPlugin, featureValue: nil)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -796,7 +421,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .success(POSPluginAndFeatureInfo(wcPlugin: wcPlugin, featureValue: nil)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -817,7 +442,7 @@ struct POSTabEligibilityCheckerTests { mockSystemStatusService.resultToReturn = .failure(NSError(domain: "test", code: 500)) setupCountry(country: .us, currency: .USD) - let checker = POSTabEligibilityChecker(site: site, + let checker = POSTabEligibilityChecker(siteID: siteID, siteSettings: siteSettings, stores: stores, systemStatusService: mockSystemStatusService) @@ -856,19 +481,6 @@ private extension POSTabEligibilityCheckerTests { ) } - func accountWhitelistedInBackend(_ isAllowed: Bool = false) { - stores.whenReceivingAction(ofType: FeatureFlagAction.self) { action in - switch action { - case .isRemoteFeatureFlagEnabled(_, _, completion: let completion): - completion(isAllowed) - } - } - } - - func setupPOSTabVisibility(siteID: Int64, isVisible: Bool?) { - eligibilityService.cachedTabVisibility[siteID] = isVisible - } - enum Country: String { case us = "US:CA" case ca = "CA:NS" @@ -910,19 +522,6 @@ private extension POSTabEligibilityCheckerTests { } } -private final class MockSelectedSiteSettings: SelectedSiteSettingsProtocol { - var mockSettingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never>? - var siteSettings: [SiteSetting] = [] - - var settingsStream: AnyPublisher<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never> { - return mockSettingsStream ?? Empty().eraseToAnyPublisher() - } - - func refresh() { - // Mock implementation - no action needed. - } -} - private final class MockPOSSystemStatusService: POSSystemStatusServiceProtocol { var resultToReturn: Result = .success(POSPluginAndFeatureInfo(wcPlugin: nil, featureValue: nil)) @@ -935,16 +534,3 @@ private final class MockPOSSystemStatusService: POSSystemStatusServiceProtocol { } } } - -private final class MockPOSSiteSettingService: POSSiteSettingServiceProtocol { - var setFeatureResult: Result = .success(true) - - func setFeature(siteID: Int64, feature: SiteSettingsFeature, enabled: Bool) async throws -> Bool { - switch setFeatureResult { - case .success(let result): - return result - case .failure(let error): - throw error - } - } -} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabVisibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabVisibilityCheckerTests.swift new file mode 100644 index 00000000000..cfaeacea16d --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabVisibilityCheckerTests.swift @@ -0,0 +1,345 @@ +import Combine +import Foundation +import Testing +import WooFoundation +import Yosemite +@testable import WooCommerce + +@MainActor +struct POSTabVisibilityCheckerTests { + private var stores: MockStoresManager! + private var storageManager: MockStorageManager! + private var siteSettings: MockSelectedSiteSettings! + private var eligibilityService: MockPOSEligibilityService! + private var mockSiteSettingService: MockPOSSiteSettingService! + private let site = Site.fake().copy(siteID: 2) + private var siteID: Int64 { site.siteID } + + init() async throws { + stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true)) + stores.updateDefaultStore(storeID: siteID) + storageManager = MockStorageManager() + eligibilityService = MockPOSEligibilityService() + siteSettings = MockSelectedSiteSettings() + mockSiteSettingService = MockPOSSiteSettingService() + } + + // MARK: `checkVisibility` + + @Test(arguments: [ + (country: Country.us, currency: CurrencyCode.USD), + (country: Country.gb, currency: CurrencyCode.GBP) + ]) + fileprivate func is_visible_when_all_conditions_satisfied(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + 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 is_invisible_when_country_is_not_supported(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + + @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_visible_when_currency_is_not_supported(country: Country, currency: CurrencyCode) async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: country, currency: currency) + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == true) + } + + + func is_visible_when_woocommerce_version_is_below_minimum() async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: .us) + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == true) + } + + @Test func is_invisible_when_remote_feature_flag_disabled() async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: .us) + accountWhitelistedInBackend(false) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test func checkVisibility_skips_settings_from_initialLoad() async throws { + // Given + let featureFlagService = MockFeatureFlagService() + + // Initial settings (cached) - makes site eligible (US) + let initialSettings = [ + mockCountrySetting(country: .us), + mockCurrencySetting(currency: .USD) + ] + // New settings - makes site ineligible (Canada). + let newSettings = [ + mockCountrySetting(country: .ca), + mockCurrencySetting(currency: .USD) + ] + siteSettings.mockSettingsStream = [ + // Emits cached settings first (should be skipped). + (siteID: siteID, settings: initialSettings, source: .initialLoad), + // Emits new settings (should be used for eligibility check). + (siteID: siteID, settings: newSettings, source: .storageChange) + ].publisher.eraseToAnyPublisher() + + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then - Should be invisible because fresh settings show CA (not cached US) + #expect(result == false) + } + + @Test func is_invisible_when_device_is_not_iPad() async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: .us) + accountWhitelistedInBackend(true) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .phone, // Not iPad + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test func is_invisible_when_site_is_CIAB() async throws { + // Given + let featureFlagService = MockFeatureFlagService() + setupCountry(country: .us, currency: .USD) + accountWhitelistedInBackend(true) + let ciabEligibilityChecker = MockCIABEligibilityChecker(mockedIsCurrentSiteCIAB: true, + mockedCIABSites: [site], + mockedCIABDisabledFeatures: [.pointOfSale]) + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService, + siteCIABEligibilityChecker: ciabEligibilityChecker) + + // When + let result = await checker.checkVisibility() + + // Then + #expect(result == false) + } + + @Test func checkVisibility_returns_expected_result_after_site_settings_available() async throws { + // Given - no site settings are immediately available (empty stream that will emit values later) + let featureFlagService = MockFeatureFlagService() + accountWhitelistedInBackend(true) + + // Creates a publisher that will emit values after a delay to simulate site settings loading + let countrySetting = mockCountrySetting(country: .us) + let currencySetting = mockCurrencySetting(currency: .USD) + let settingsSubject = PassthroughSubject<(siteID: Int64, settings: [SiteSetting], source: SettingsUpdateSource), Never>() + siteSettings.mockSettingsStream = settingsSubject.eraseToAnyPublisher() + + let checker = POSTabVisibilityChecker(site: site, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + stores: stores, + featureFlagService: featureFlagService) + + // When - Call checkVisibility before site settings are available + async let visibilityTask = checker.checkVisibility() + + // Simulate site settings becoming available after methods are called + Task { + settingsSubject.send((siteID: siteID, settings: [countrySetting, currencySetting], source: .refresh)) + settingsSubject.send(completion: .finished) + } + + let visibilityResult = await visibilityTask + + // Then - both methods should wait for site settings and return expected results. + #expect(visibilityResult == true) + } + + // MARK: - `checkInitialVisibility Tests + + @Test func checkInitialVisibility_returns_true_when_cached_tab_visibility_is_enabled() async throws { + // Given + let checker = POSTabVisibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) + setupPOSTabVisibility(siteID: siteID, isVisible: true) + + // When + let result = checker.checkInitialVisibility() + + // Then + #expect(result == true) + } + + @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_disabled() async throws { + // Given + let checker = POSTabVisibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) + setupPOSTabVisibility(siteID: siteID, isVisible: false) + + // When + let result = checker.checkInitialVisibility() + + // Then + #expect(result == false) + } + + @Test func checkInitialVisibility_returns_false_when_cached_tab_visibility_is_unavailable() async throws { + // Given + let checker = POSTabVisibilityChecker(site: site, eligibilityService: eligibilityService, stores: stores) + setupPOSTabVisibility(siteID: siteID, isVisible: nil) + + // When + let result = checker.checkInitialVisibility() + + // Then + #expect(result == false) + } +} + +private extension POSTabVisibilityCheckerTests { + func setupCountry(country: Country, currency: CurrencyCode = .USD) { + let countrySetting = mockCountrySetting(country: country) + let currencySetting = mockCurrencySetting(currency: currency) + siteSettings.mockSettingsStream = [ + // Emits cached settings first (should be skipped). + (siteID: siteID, settings: [], source: .storageChange), + // Emits fresh settings (should be used for eligibility check). + (siteID: siteID, settings: [countrySetting, currencySetting], source: .refresh) + ].publisher.eraseToAnyPublisher() + } + + func accountWhitelistedInBackend(_ isAllowed: Bool = false) { + stores.whenReceivingAction(ofType: FeatureFlagAction.self) { action in + switch action { + case .isRemoteFeatureFlagEnabled(_, _, completion: let completion): + completion(isAllowed) + } + } + } + + func setupPOSTabVisibility(siteID: Int64, isVisible: Bool?) { + eligibilityService.cachedTabVisibility[siteID] = isVisible + } + + enum Country: String { + case us = "US:CA" + case ca = "CA:NS" + case gb = "GB" + case es = "ES" + + var countryCode: CountryCode { + switch self { + case .us: + return .US + case .ca: + return .CA + case .gb: + return .GB + case .es: + return .ES + } + } + } + + func mockCountrySetting(country: Country, siteID: Int64? = nil) -> SiteSetting { + SiteSetting.fake() + .copy( + siteID: siteID ?? siteID, + settingID: "woocommerce_default_country", + value: country.rawValue, + settingGroupKey: SiteSettingGroup.general.rawValue + ) + } + + func mockCurrencySetting(currency: CurrencyCode, siteID: Int64? = nil) -> SiteSetting { + SiteSetting.fake() + .copy( + siteID: siteID ?? siteID, + settingID: "woocommerce_currency", + value: currency.rawValue, + settingGroupKey: SiteSettingGroup.general.rawValue + ) + } +}