diff --git a/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift new file mode 100644 index 00000000000..07fd319bc7c --- /dev/null +++ b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift @@ -0,0 +1,28 @@ +import SwiftUI +import protocol Experiments.FeatureFlagService + +@available(iOS 17.0, *) +@Observable final class POSEntryPointController { + private(set) var eligibilityState: POSEligibilityState? + private let posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol + private let featureFlagService: FeatureFlagService + + init(eligibilityChecker: POSEntryPointEligibilityCheckerProtocol, + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { + self.posEligibilityChecker = eligibilityChecker + self.featureFlagService = featureFlagService + + guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi2) else { + self.eligibilityState = .eligible + return + } + Task { @MainActor in + eligibilityState = await posEligibilityChecker.checkEligibility() + } + } + + @MainActor + func refreshEligibility() async throws { + // TODO: WOOMOB-720 - refresh eligibility + } +} diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleLoadingView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleLoadingView.swift index 1b60e219edc..d96510217f8 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleLoadingView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/PointOfSaleLoadingView.swift @@ -21,6 +21,7 @@ struct PointOfSaleLoadingView: View { .onDisappear { trackElapsedTimeOnDisappear() } + .background(Color.posSurface) } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 01455a1ee68..6e9f4185cdc 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -6,6 +6,7 @@ import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol struct PointOfSaleEntryPointView: View { @State private var posModel: PointOfSaleAggregateModel? @StateObject private var posModalManager = POSModalManager() + @State private var posEntryPointController: POSEntryPointController @Environment(\.horizontalSizeClass) private var horizontalSizeClass private let onPointOfSaleModeActiveStateChange: ((Bool) -> Void) @@ -30,7 +31,8 @@ struct PointOfSaleEntryPointView: View { collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking, searchHistoryService: POSSearchHistoryProviding, popularPurchasableItemsController: PointOfSaleItemsControllerProtocol, - barcodeScanService: PointOfSaleBarcodeScanServiceProtocol) { + barcodeScanService: PointOfSaleBarcodeScanServiceProtocol, + posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol) { self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange self.itemsController = itemsController @@ -43,15 +45,25 @@ struct PointOfSaleEntryPointView: View { self.searchHistoryService = searchHistoryService self.popularPurchasableItemsController = popularPurchasableItemsController self.barcodeScanService = barcodeScanService + self.posEntryPointController = POSEntryPointController(eligibilityChecker: posEligibilityChecker) } var body: some View { Group { - if let posModel = posModel { - PointOfSaleDashboardView() - .environment(posModel) - } else { + switch posEntryPointController.eligibilityState { + case .none: PointOfSaleLoadingView() + case .eligible: + if let posModel = posModel { + PointOfSaleDashboardView() + .environment(posModel) + } else { + PointOfSaleLoadingView() + } + case let .ineligible(reason): + POSIneligibleView(reason: reason, onRefresh: { + try await posEntryPointController.refreshEligibility() + }) } } .task { @@ -96,7 +108,8 @@ struct PointOfSaleEntryPointView: View { collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics(), searchHistoryService: PointOfSalePreviewHistoryService(), popularPurchasableItemsController: PointOfSalePreviewItemsController(), - barcodeScanService: PointOfSalePreviewBarcodeScanService()) + barcodeScanService: PointOfSalePreviewBarcodeScanService(), + posEligibilityChecker: POSTabEligibilityChecker(siteID: 0)) } #endif diff --git a/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift new file mode 100644 index 00000000000..0131f547719 --- /dev/null +++ b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift @@ -0,0 +1,130 @@ +import SwiftUI + +/// A view that displays when the Point of Sale (POS) feature is not available for the current store. +/// Shows the specific reason why POS is ineligible and provides a button to re-check eligibility. +struct POSIneligibleView: View { + let reason: POSIneligibleReason + let onRefresh: () async throws -> Void + @Environment(\.dismiss) private var dismiss + @State private var isLoading: Bool = false + + var body: some View { + VStack(spacing: POSSpacing.large) { + HStack { + Spacer() + Button { + dismiss() + } label: { + Text(Image(systemName: "xmark")) + .font(POSFontStyle.posButtonSymbolLarge.font()) + } + .foregroundColor(Color.posOnSurfaceVariantLowest) + } + + Spacer() + + VStack(spacing: POSSpacing.medium) { + Image(PointOfSaleAssets.exclamationMark.imageName) + .resizable() + .frame(width: POSErrorAndAlertIconSize.large.dimension, + height: POSErrorAndAlertIconSize.large.dimension) + + Text(reasonText) + .font(POSFontStyle.posHeadingBold.font()) + .multilineTextAlignment(.center) + .foregroundColor(Color.posOnSurface) + + Button { + Task { @MainActor in + do { + isLoading = true + try await onRefresh() + isLoading = false + } catch { + // TODO-jc: handle error if needed, e.g., show an error message + print("Error refreshing eligibility: \(error)") + isLoading = false + } + } + } label: { + Text(Localization.refreshEligibility) + } + .buttonStyle(POSFilledButtonStyle(size: .normal, isLoading: isLoading)) + } + + Spacer() + } + .padding(POSPadding.large) + } + + private var reasonText: String { + switch reason { + case .notTablet: + return NSLocalizedString("pos.ineligible.reason.notTablet", + value: "POS is only available on iPad.", + comment: "Ineligible reason: not a tablet") + case .unsupportedIOSVersion: + return NSLocalizedString("pos.ineligible.reason.unsupportedIOSVersion", + value: "POS requires a newer version of iOS 17 and above.", + comment: "Ineligible reason: iOS version too low") + case .unsupportedWooCommerceVersion: + return NSLocalizedString("pos.ineligible.reason.unsupportedWooCommerceVersion", + value: "Please update WooCommerce plugin to use POS.", + comment: "Ineligible reason: WooCommerce version too low") + case .wooCommercePluginNotFound: + return NSLocalizedString("pos.ineligible.reason.wooCommercePluginNotFound", + value: "WooCommerce plugin not found.", + comment: "Ineligible reason: plugin missing") + case .featureSwitchDisabled: + return NSLocalizedString("pos.ineligible.reason.featureSwitchDisabled", + value: "POS feature is not enabled for your store.", + comment: "Ineligible reason: feature switch off") + case .featureSwitchSyncFailure: + return NSLocalizedString("pos.ineligible.reason.featureSwitchSyncFailure", + value: "Could not verify POS feature status.", + comment: "Ineligible reason: feature switch sync failed") + case .unsupportedCountry: + return NSLocalizedString("pos.ineligible.reason.unsupportedCountry", + value: "POS is not available in your country.", + comment: "Ineligible reason: country not supported") + case .unsupportedCurrency: + return NSLocalizedString("pos.ineligible.reason.unsupportedCurrency", + value: "POS is not available for your store's currency.", + comment: "Ineligible reason: currency not supported") + case .siteSettingsNotAvailable: + return NSLocalizedString("pos.ineligible.reason.siteSettingsNotAvailable", + value: "Unable to load store settings for POS.", + comment: "Ineligible reason: site settings unavailable") + case .featureFlagDisabled: + return NSLocalizedString("pos.ineligible.reason.featureFlagDisabled", + value: "POS feature is currently disabled.", + comment: "Ineligible reason: feature flag disabled") + case .selfDeallocated: + return Localization.defaultReason + } + } +} + +private extension POSIneligibleView { + enum Localization { + static let refreshEligibility = NSLocalizedString( + "pos.ineligible.refresh.button.title", + value: "Check Eligibility Again", + comment: "Button title to refresh POS eligibility check" + ) + + /// Default message shown when POS eligibility reason is not available. + static let defaultReason = NSLocalizedString( + "pos.ineligible.default.reason", + value: "Your store is not eligible for POS at this time.", + comment: "Default message shown when POS eligibility reason is not available" + ) + } +} + +#Preview { + POSIneligibleView( + reason: .unsupportedCurrency, + onRefresh: {} + ) +} diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 185c4767e09..23492bdb61b 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -28,6 +28,7 @@ final class POSTabCoordinator { private let storageManager: StorageManagerType private let currencySettings: CurrencySettings private let pushNotesManager: PushNotesManager + private let eligibilityChecker: POSEntryPointEligibilityCheckerProtocol private lazy var posItemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory = { PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials) @@ -63,7 +64,8 @@ final class POSTabCoordinator { storesManager: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager, currencySettings: CurrencySettings = ServiceLocator.currencySettings, - pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager) { + pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager, + eligibilityChecker: POSEntryPointEligibilityCheckerProtocol) { self.siteID = siteID self.storesManager = storesManager self.tabContainerController = tabContainerController @@ -72,6 +74,7 @@ final class POSTabCoordinator { self.storageManager = storageManager self.currencySettings = currencySettings self.pushNotesManager = pushNotesManager + self.eligibilityChecker = eligibilityChecker tabContainerController.wrappedController = POSTabViewController() } @@ -121,7 +124,8 @@ private extension POSTabCoordinator { itemProvider: PointOfSaleItemService(currencySettings: currencySettings), itemFetchStrategyFactory: posPopularItemFetchStrategyFactory ), - barcodeScanService: barcodeScanService + barcodeScanService: barcodeScanService, + posEligibilityChecker: eligibilityChecker ) let hostingController = UIHostingController(rootView: posView) hostingController.modalPresentationStyle = .fullScreen diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index b24efe37673..23898fd81bd 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -45,6 +45,10 @@ protocol POSEntryPointEligibilityCheckerProtocol { } final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { + private var siteSettingsEligibility: POSEligibilityState? + private var featureFlagEligibility: POSEligibilityState? + private var siteSettingsTask: Task<[SiteSetting], Never>? + private let siteID: Int64 private let userInterfaceIdiom: UIUserInterfaceIdiom private let siteSettings: SelectedSiteSettingsProtocol @@ -144,6 +148,7 @@ private extension POSTabEligibilityChecker { async let siteSettingsEligibility = checkSiteSettingsEligibility() async let featureFlagEligibility = checkRemoteFeatureEligibility() + self.siteSettingsEligibility = await siteSettingsEligibility switch await siteSettingsEligibility { case .eligible: break @@ -155,6 +160,7 @@ private extension POSTabEligibilityChecker { } } + self.featureFlagEligibility = await featureFlagEligibility switch await featureFlagEligibility { case .eligible: return true @@ -215,6 +221,10 @@ private extension POSTabEligibilityChecker { private extension POSTabEligibilityChecker { func checkSiteSettingsEligibility() async -> POSEligibilityState { + if let siteSettingsEligibility { + return siteSettingsEligibility + } + // Waits for the first site settings that matches the given site ID. let siteSettings = await waitForSiteSettingsRefresh() guard siteSettings.isNotEmpty else { @@ -229,14 +239,28 @@ private extension POSTabEligibilityChecker { } func waitForSiteSettingsRefresh() async -> [SiteSetting] { - for await siteSettings in siteSettings.settingsStream { - guard siteSettings.siteID == siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { - continue + // Uses a shared task so that multiple calls can await the same result since site settings can be emitted only once. + if let existingTask = siteSettingsTask { + return await existingTask.value + } + + let task = Task<[SiteSetting], Never> { [weak self] in + guard let self else { return [] } + + for await siteSettings in siteSettings.settingsStream { + guard siteSettings.siteID == self.siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { + continue + } + siteSettingsTask = nil + return siteSettings.settings } - return siteSettings.settings + // If we get here, the stream completed without yielding any values for our site ID which is unexpected. + siteSettingsTask = nil + return [] } - // If we get here, the stream completed without yielding any values for our site ID which is unexpected. - return [] + + siteSettingsTask = task + return await task.value } func isEligibleFromCountryAndCurrencyCode(countryCode: CountryCode, currencyCode: CurrencyCode) -> POSEligibilityState { @@ -263,9 +287,13 @@ private extension POSTabEligibilityChecker { private extension POSTabEligibilityChecker { @MainActor func checkRemoteFeatureEligibility() async -> POSEligibilityState { + if let featureFlagEligibility { + return featureFlagEligibility + } + // 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 + return await withCheckedContinuation { [weak self] continuation in guard let self else { return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index 9e6ca37492a..8394022eb2b 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -64,7 +64,8 @@ struct HubMenu: View { popularPurchasableItemsController: PointOfSaleItemsController( itemProvider: PointOfSaleItemService(currencySettings: ServiceLocator.currencySettings), itemFetchStrategyFactory: viewModel.posPopularItemFetchStrategyFactory), - barcodeScanService: viewModel.barcodeScanService) + barcodeScanService: viewModel.barcodeScanService, + posEligibilityChecker: POSTabEligibilityChecker(siteID: viewModel.siteID)) } else { // TODO: When we have a singleton for the card payment service, this should not be required. Text("Error creating card payment service") diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index d4127bbb079..680da103335 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -746,7 +746,8 @@ private extension MainTabBarController { siteID: siteID, tabContainerController: posContainerController, viewControllerToPresent: self, - storesManager: stores + storesManager: stores, + eligibilityChecker: posEligibilityChecker ) // Configure hub menu tab coordinator once per logged in session potentially with multiple sites. diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7f62a62b709..6b90c9b628f 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -601,6 +601,9 @@ 02E493EF245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E493EE245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift */; }; 02E4A0832BFB1C4F006D4F87 /* POSEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4A0822BFB1C4F006D4F87 /* POSEligibilityChecker.swift */; }; 02E4AF7126FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4AF7026FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift */; }; + 02E4E7442E0EEF80003A31E7 /* POSIneligibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4E7432E0EEF76003A31E7 /* POSIneligibleView.swift */; }; + 02E4E7462E0EF84B003A31E7 /* POSEntryPointController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4E7452E0EF847003A31E7 /* POSEntryPointController.swift */; }; + 02E4F2702E0F2C75003A31E7 /* POSEntryPointControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */; }; 02E4FD7E2306A8180049610C /* StatsTimeRangeBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4FD7D2306A8180049610C /* StatsTimeRangeBarViewModel.swift */; }; 02E4FD812306AA890049610C /* StatsTimeRangeBarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4FD802306AA890049610C /* StatsTimeRangeBarViewModelTests.swift */; }; 02E6B97823853D81000A36F0 /* TitleAndValueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E6B97623853D81000A36F0 /* TitleAndValueTableViewCell.swift */; }; @@ -3749,6 +3752,9 @@ 02E493EE245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormBottomSheetListSelectorCommandTests.swift; sourceTree = ""; }; 02E4A0822BFB1C4F006D4F87 /* POSEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSEligibilityChecker.swift; sourceTree = ""; }; 02E4AF7026FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductReviewFromNoteParcelFactory.swift; sourceTree = ""; }; + 02E4E7432E0EEF76003A31E7 /* POSIneligibleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSIneligibleView.swift; sourceTree = ""; }; + 02E4E7452E0EF847003A31E7 /* POSEntryPointController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSEntryPointController.swift; sourceTree = ""; }; + 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSEntryPointControllerTests.swift; sourceTree = ""; }; 02E4FD7D2306A8180049610C /* StatsTimeRangeBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeBarViewModel.swift; sourceTree = ""; }; 02E4FD802306AA890049610C /* StatsTimeRangeBarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeBarViewModelTests.swift; sourceTree = ""; }; 02E6B97623853D81000A36F0 /* TitleAndValueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndValueTableViewCell.swift; sourceTree = ""; }; @@ -7488,6 +7494,7 @@ 02ABF9B92DF7F8E200348186 /* TabBar */ = { isa = PBXGroup; children = ( + 02E4E7432E0EEF76003A31E7 /* POSIneligibleView.swift */, 02ABF9BA2DF7F8EF00348186 /* POSTabCoordinator.swift */, ); path = TabBar; @@ -8062,6 +8069,7 @@ 200BA1572CF092150006DC5B /* Controllers */ = { isa = PBXGroup; children = ( + 02E4E7452E0EF847003A31E7 /* POSEntryPointController.swift */, 68B681152D92577F0098D5CD /* PointOfSaleCouponsController.swift */, 200BA1582CF092280006DC5B /* PointOfSaleItemsController.swift */, 20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */, @@ -8075,6 +8083,7 @@ 6818E7C02D93C76200677C16 /* PointOfSaleCouponsControllerTests.swift */, 20DB185C2CF5E7560018D3E1 /* PointOfSaleOrderControllerTests.swift */, 200BA15D2CF0A9EB0006DC5B /* PointOfSaleItemsControllerTests.swift */, + 02E4F26F2E0F2C75003A31E7 /* POSEntryPointControllerTests.swift */, ); path = Controllers; sourceTree = ""; @@ -15258,6 +15267,7 @@ 0245465F24EE9106004F531C /* ProductVariationFormEventLogger.swift in Sources */, 269A2F47295CC683000828A8 /* GenerateVariationsSelectorCommand.swift in Sources */, CE852E1F2D2EDC5400C7DBB6 /* WooShippingEditAddressView.swift in Sources */, + 02E4E7462E0EF84B003A31E7 /* POSEntryPointController.swift in Sources */, DE19BB1826C3B5C300AB70D9 /* ShippingLabelCustomsFormItemDetails.swift in Sources */, DE1B0310268EB2FB00804330 /* ReviewOrderViewModel.swift in Sources */, 02DC2ED2242061BF002F9676 /* ProductPriceSettingsViewModel.swift in Sources */, @@ -16564,6 +16574,7 @@ B9B7E37E2AF105EF00A959CA /* PencilEditButton.swift in Sources */, 02162726237963AF000208D2 /* ProductFormViewController.swift in Sources */, 0313651728ACE9F400EEE571 /* InPersonPaymentsCashOnDeliveryPaymentGatewayNotSetUpViewModel.swift in Sources */, + 02E4E7442E0EEF80003A31E7 /* POSIneligibleView.swift in Sources */, 571CDD5A250ACC470076B8CC /* UITableViewDiffableDataSource+Helpers.swift in Sources */, DE7E5E7D2B4BB617002E28D2 /* BlazeTargetLanguagePickerView.swift in Sources */, 029149782D26658A00F7B3B3 /* VariationCardView.swift in Sources */, @@ -17218,6 +17229,7 @@ 0246405F258B122100C10A7D /* PrintShippingLabelCoordinatorTests.swift in Sources */, 314DC4C3268D2F1000444C9E /* MockAppSettingsStoresManager.swift in Sources */, 9379E1A6225537D0006A6BE4 /* TestingAppDelegate.swift in Sources */, + 02E4F2702E0F2C75003A31E7 /* POSEntryPointControllerTests.swift in Sources */, 02B8E4192DFBC218001D01FD /* MainTabBarController+TabsTests.swift in Sources */, 453904F323BB88B5007C4956 /* ProductTaxStatusListSelectorCommandTests.swift in Sources */, DA25ADDF2C87403900AE81FE /* PushNotificationTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift b/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift new file mode 100644 index 00000000000..36a43271d76 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Controllers/POSEntryPointControllerTests.swift @@ -0,0 +1,62 @@ +import Testing +@testable import WooCommerce + +struct POSEntryPointControllerTests { + @available(iOS 17.0, *) + @Test func eligibilityState_is_set_to_eligible_when_i2_feature_is_disabled() async throws { + // Given + let mockEligibilityChecker = MockPOSEligibilityChecker() + mockEligibilityChecker.eligibility = .ineligible(reason: .notTablet) + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: false) + + // When + let controller = POSEntryPointController( + eligibilityChecker: mockEligibilityChecker, + featureFlagService: featureFlagService + ) + + // Then + #expect(controller.eligibilityState == .eligible) + } + + @available(iOS 17.0, *) + @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: .notTablet) + mockEligibilityChecker.eligibility = expectedState + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) + + // When + let controller = POSEntryPointController( + eligibilityChecker: mockEligibilityChecker, + featureFlagService: featureFlagService + ) + while controller.eligibilityState == nil { + await Task.yield() + } + + // Then + #expect(controller.eligibilityState == expectedState) + } + + @available(iOS 17.0, *) + @Test func eligibilityState_is_set_to_eligible_when_i2_feature_is_enabled_and_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 + ) + while controller.eligibilityState == nil { + await Task.yield() + } + + // Then + #expect(controller.eligibilityState == .eligible) + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift index 68a4cddd9fa..19f69050f7c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift @@ -529,6 +529,33 @@ struct POSTabEligibilityCheckerTests { // Then #expect(result == false) } + + @Test func checkEligibility_uses_cached_values_after_checkVisibility_when_i2_feature_is_enabled() async throws { + // Given + let featureFlagService = MockFeatureFlagService(isPointOfSaleAsATabi2Enabled: true) + setupCountry(country: .us) + accountWhitelistedInBackend(true) + let checker = POSTabEligibilityChecker(siteID: siteID, + userInterfaceIdiom: .pad, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores, + featureFlagService: featureFlagService) + + // When checkVisibility first (which caches siteSettingsEligibility and featureFlagEligibility) + let visibilityResult = await checker.checkVisibility() + + // And site settings and feature flag eligibility changes + setupCountry(country: .ca, currency: .AMD) + accountWhitelistedInBackend(false) + + // Then checkEligibility should use cached values for site settings and feature flags + let eligibilityResult = await checker.checkEligibility() + + // Then - both should return the expected results, demonstrating caching works + #expect(visibilityResult == true) + #expect(eligibilityResult == .eligible) + } } private extension POSTabEligibilityCheckerTests {