diff --git a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift index ba00fab13bd..6018c8ae409 100644 --- a/Modules/Sources/Experiments/DefaultFeatureFlagService.swift +++ b/Modules/Sources/Experiments/DefaultFeatureFlagService.swift @@ -112,6 +112,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { case .showPointOfSaleBarcodeSimulator: // Enables a simulated barcode scanner in dev builds for testing. Do not ship this one! return buildConfig == .localDeveloper || buildConfig == .alpha + case .pointOfSaleAsATabi1: + return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/Modules/Sources/Experiments/FeatureFlag.swift b/Modules/Sources/Experiments/FeatureFlag.swift index df6847bcc01..75cc31ff55b 100644 --- a/Modules/Sources/Experiments/FeatureFlag.swift +++ b/Modules/Sources/Experiments/FeatureFlag.swift @@ -236,4 +236,8 @@ public enum FeatureFlag: Int { /// Enables a simulated barcode scanner for testing in POS. Do not ship this one! /// case showPointOfSaleBarcodeSimulator + + /// Enables displaying POS as a tab in the tab bar with the same eligibility as the previous entry point + /// + case pointOfSaleAsATabi1 } diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift new file mode 100644 index 00000000000..3765f6fcc82 --- /dev/null +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -0,0 +1,152 @@ +import Foundation +import UIKit +import SwiftUI +import Yosemite +import class WooFoundation.CurrencySettings +import protocol Storage.StorageManagerType + +/// 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 { + override func viewDidLoad() { + super.viewDidLoad() + + tabBarItem.title = NSLocalizedString("pos.tab.title", value: "Point of Sale", comment: "Title for the Point of Sale tab.") + tabBarItem.image = .creditCardImage + tabBarItem.accessibilityIdentifier = "tab-bar-pos-item" + } +} + +/// Coordinator for the Point of Sale tab. +/// +final class POSTabCoordinator { + private let siteID: Int64 + private let tabContainerController: TabContainerController + private let viewControllerToPresent: UIViewController + private let storesManager: StoresManager + private let credentials: Credentials? + private let storageManager: StorageManagerType + private let currencySettings: CurrencySettings + private let pushNotesManager: PushNotesManager + + private lazy var posItemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory = { + PointOfSaleItemFetchStrategyFactory(siteID: siteID, credentials: credentials) + }() + + private lazy var posPopularItemFetchStrategyFactory: PointOfSaleFixedItemFetchStrategyFactory = { + PointOfSaleFixedItemFetchStrategyFactory(fixedStrategy: posItemFetchStrategyFactory.popularStrategy()) + }() + + private lazy var posCouponFetchStrategyFactory: PointOfSaleCouponFetchStrategyFactory = { + PointOfSaleCouponFetchStrategyFactory(siteID: siteID, + currencySettings: currencySettings, + credentials: credentials, + storage: storageManager) + }() + + private lazy var posCouponProvider: PointOfSaleCouponServiceProtocol = { + return PointOfSaleCouponService(siteID: siteID, + currencySettings: currencySettings, + credentials: credentials, + storage: storageManager) + }() + + private lazy var barcodeScanService: PointOfSaleBarcodeScanService = { + PointOfSaleBarcodeScanService(siteID: siteID, + credentials: credentials, + currencySettings: currencySettings) + }() + + init(siteID: Int64, + tabContainerController: TabContainerController, + viewControllerToPresent: UIViewController, + storesManager: StoresManager = ServiceLocator.stores, + storageManager: StorageManagerType = ServiceLocator.storageManager, + currencySettings: CurrencySettings = ServiceLocator.currencySettings, + pushNotesManager: PushNotesManager = ServiceLocator.pushNotesManager) { + self.siteID = siteID + self.storesManager = storesManager + self.tabContainerController = tabContainerController + self.viewControllerToPresent = viewControllerToPresent + self.credentials = storesManager.sessionManager.defaultCredentials + self.storageManager = storageManager + self.currencySettings = currencySettings + self.pushNotesManager = pushNotesManager + + tabContainerController.wrappedController = POSTabViewController() + } + + func onTabSelected() { + presentPOSView() + } +} + +private extension POSTabCoordinator { + func presentPOSView() { + Task { @MainActor [weak self] in + guard let self else { return } + let collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics() + let cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID, + collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker) + if let receiptService = POSReceiptService(siteID: siteID, + credentials: credentials), + let orderService = POSOrderService(siteID: siteID, + credentials: credentials), + #available(iOS 17.0, *) { + let posView = PointOfSaleEntryPointView( + itemsController: PointOfSaleItemsController( + itemProvider: PointOfSaleItemService( + currencySettings: currencySettings), + itemFetchStrategyFactory: posItemFetchStrategyFactory), + purchasableItemsSearchController: PointOfSaleItemsController( + itemProvider: PointOfSaleItemService( + currencySettings: currencySettings), + itemFetchStrategyFactory: posItemFetchStrategyFactory, + initialState: .init(containerState: .content, + itemsStack: .init(root: .loaded([], hasMoreItems: true), itemStates: [:]))), + couponsController: PointOfSaleCouponsController(itemProvider: posCouponProvider, + fetchStrategyFactory: posCouponFetchStrategyFactory), + couponsSearchController: PointOfSaleCouponsController(itemProvider: posCouponProvider, + fetchStrategyFactory: posCouponFetchStrategyFactory), + onPointOfSaleModeActiveStateChange: { [weak self] isEnabled in + self?.updateDefaultConfigurationForPointOfSale(isEnabled) + }, + cardPresentPaymentService: cardPresentPaymentService, + orderController: PointOfSaleOrderController(orderService: orderService, + receiptService: receiptService), + collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker, + searchHistoryService: POSSearchHistoryService(siteID: siteID), + popularPurchasableItemsController: PointOfSaleItemsController( + itemProvider: PointOfSaleItemService(currencySettings: currencySettings), + itemFetchStrategyFactory: posPopularItemFetchStrategyFactory + ), + barcodeScanService: barcodeScanService + ) + let hostingController = UIHostingController(rootView: posView) + hostingController.modalPresentationStyle = .fullScreen + viewControllerToPresent.present(hostingController, animated: true) + } + } + } +} + +private extension POSTabCoordinator { + func updateDefaultConfigurationForPointOfSale(_ isPointOfSaleActive: Bool) { + updateInAppNotifications(isPointOfSaleActive) + updateTrackEventPrefix(isPointOfSaleActive) + } + + /// Disables foreground in-app notifications when Point of Sale is active. + func updateInAppNotifications(_ isPointOfSaleActive: Bool) { + if isPointOfSaleActive { + pushNotesManager.disableInAppNotifications() + } else { + pushNotesManager.enableInAppNotifications() + } + } + + /// Decorates track events with a different prefix when Point of Sale is active. + func updateTrackEventPrefix(_ isPointOfSaleActive: Bool) { + TracksProvider.setPOSMode(isPointOfSaleActive) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift index ab5fda38607..3e8819edc99 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift @@ -42,9 +42,9 @@ final class HubMenuCoordinator { } convenience init(tabContainerController: TabContainerController, + storesManager: StoresManager = ServiceLocator.stores, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker, willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) { - let storesManager = ServiceLocator.stores self.init(tabContainerController: tabContainerController, storesManager: storesManager, switchStoreUseCase: SwitchStoreUseCase(stores: storesManager), diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 148601bfd1d..acd6307a107 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -314,6 +314,10 @@ private extension HubMenuViewModel { } func setupPOSElement() { + guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi1) == false else { + return + } + posEligibilityChecker.isEligible.map { isEligibleForPOS in if isEligibleForPOS { return PointOfSaleEntryPoint() diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index f7b22a88d4c..cac23a15200 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -22,6 +22,10 @@ enum WooTab { /// case products + /// Point of Sale Tab + /// + case pointOfSale + /// Hub Menu Tab /// case hubMenu @@ -32,14 +36,16 @@ extension WooTab { /// /// - Parameters: /// - visibleIndex: the index of visible tabs on the tab bar - init(visibleIndex: Int) { - let tabs = WooTab.visibleTabs() + /// - isPOSTabVisible: indicates if the Point of Sale tab is visible. + init(visibleIndex: Int, isPOSTabVisible: Bool) { + let tabs = WooTab.visibleTabs(isPOSTabVisible: isPOSTabVisible) self = tabs[visibleIndex] } /// Returns the visible tab index. - func visibleIndex() -> Int { - let tabs = WooTab.visibleTabs() + /// - Parameter isPOSTabVisible: indicates if the Point of Sale tab is visible. + func visibleIndex(isPOSTabVisible: Bool) -> Int { + let tabs = WooTab.visibleTabs(isPOSTabVisible: isPOSTabVisible) guard let tabIndex = tabs.firstIndex(where: { $0 == self }) else { assertionFailure("Trying to get the visible tab index for tab \(self) while the visible tabs are: \(tabs)") return 0 @@ -47,9 +53,16 @@ extension WooTab { return tabIndex } - // Note: currently only the Dashboard tab (My Store) view controller is set up in Main.storyboard. - private static func visibleTabs() -> [WooTab] { - [.myStore, .orders, .products, .hubMenu] + /// Note: currently only the Dashboard tab (My Store) view controller is set up in Main.storyboard. + /// + /// - Parameter isPOSTabVisible: indicates if the Point of Sale tab is visible. + /// - Returns: visible tabs in the tab bar. + static func visibleTabs(isPOSTabVisible: Bool) -> [WooTab] { + if isPOSTabVisible { + return [.myStore, .orders, .products, .pointOfSale, .hubMenu] + } else { + return [.myStore, .orders, .products, .hubMenu] + } } } @@ -97,7 +110,9 @@ final class MainTabBarController: UITabBarController { /// remove when .splitViewInProductsTab is removed. private let productsNavigationController = WooTabNavigationController() - private let reviewsNavigationController = WooTabNavigationController() + private let posContainerController = TabContainerController() + private var posTabCoordinator: POSTabCoordinator? + private let hubMenuContainerController = TabContainerController() private var hubMenuTabCoordinator: HubMenuCoordinator? @@ -105,22 +120,34 @@ final class MainTabBarController: UITabBarController { private let featureFlagService: FeatureFlagService private let noticePresenter: NoticePresenter private let productImageUploader: ProductImageUploaderProtocol - private let stores: StoresManager = ServiceLocator.stores + private let stores: StoresManager private let analytics: Analytics + private let posEligibilityCheckerFactory: ((_ siteID: Int64) -> POSEntryPointEligibilityCheckerProtocol) private var productImageUploadErrorsSubscription: AnyCancellable? + private var posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol? + private var posEligibilityCheckTask: Task? + + private var isPOSTabVisible: Bool = false + private lazy var isProductsSplitViewFeatureFlagOn = featureFlagService.isFeatureFlagEnabled(.splitViewInProductsTab) init?(coder: NSCoder, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, noticePresenter: NoticePresenter = ServiceLocator.noticePresenter, productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader, - analytics: Analytics = ServiceLocator.analytics) { + analytics: Analytics = ServiceLocator.analytics, + stores: StoresManager = ServiceLocator.stores, + posEligibilityCheckerFactory: ((Int64) -> POSEntryPointEligibilityCheckerProtocol)? = nil) { self.featureFlagService = featureFlagService self.noticePresenter = noticePresenter self.productImageUploader = productImageUploader self.analytics = analytics + self.stores = stores + self.posEligibilityCheckerFactory = posEligibilityCheckerFactory ?? { siteID in + POSTabEligibilityChecker(siteID: siteID) + } super.init(coder: coder) } @@ -129,11 +156,16 @@ final class MainTabBarController: UITabBarController { self.noticePresenter = ServiceLocator.noticePresenter self.productImageUploader = ServiceLocator.productImageUploader self.analytics = ServiceLocator.analytics + self.stores = ServiceLocator.stores + self.posEligibilityCheckerFactory = { siteID in + POSTabEligibilityChecker(siteID: siteID) + } super.init(coder: coder) } deinit { cancellableSiteID?.cancel() + posEligibilityCheckTask?.cancel() } // MARK: - Overridden Methods @@ -142,14 +174,16 @@ final class MainTabBarController: UITabBarController { super.viewDidLoad() setNeedsStatusBarAppearanceUpdate() // call this to refresh status bar changes happening at runtime + delegate = self + fixTabBarTraitCollectionOnIpadForiOS18() - configureTabViewControllers() + // POS tab is hidden by default. + updateTabViewControllers(isPOSTabVisible: false) observeSiteIDForViewControllers() observeProductImageUploadStatusUpdates() startListeningToHubMenuTabBadgeUpdates() - viewModel.loadHubMenuTabBadge() } override func viewWillAppear(_ animated: Bool) { @@ -170,11 +204,11 @@ final class MainTabBarController: UITabBarController { } override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { - let currentlySelectedTab = WooTab(visibleIndex: selectedIndex) + let currentlySelectedTab = WooTab(visibleIndex: selectedIndex, isPOSTabVisible: isPOSTabVisible) guard let userSelectedIndex = tabBar.items?.firstIndex(of: item) else { return } - let userSelectedTab = WooTab(visibleIndex: userSelectedIndex) + let userSelectedTab = WooTab(visibleIndex: userSelectedIndex, isPOSTabVisible: isPOSTabVisible) // Did we reselect the already-selected tab? if currentlySelectedTab == userSelectedTab { @@ -204,7 +238,7 @@ final class MainTabBarController: UITabBarController { func navigateToTabWithViewController(_ tab: WooTab, animated: Bool = false, completion: ((UIViewController) -> Void)? = nil) { dismiss(animated: animated) { [weak self] in guard let self else { return } - selectedIndex = tab.visibleIndex() + selectedIndex = tab.visibleIndex(isPOSTabVisible: isPOSTabVisible) guard let selectedViewController else { return } @@ -258,6 +292,17 @@ final class MainTabBarController: UITabBarController { } } +// MARK: - UITabBarControllerDelegate +// +extension MainTabBarController: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + let isSelectingPOSTab = viewController == posContainerController + if isSelectingPOSTab { + posTabCoordinator?.onTabSelected() + } + return !isSelectingPOSTab + } +} // MARK: - UIViewControllerTransitioningDelegate // @@ -302,6 +347,8 @@ private extension MainTabBarController { event: .Products.productListSelected(horizontalSizeClass: UITraitCollection.current.horizontalSizeClass)) case .hubMenu: ServiceLocator.analytics.track(.hubMenuTabSelected) + case .pointOfSale: + // TODO: WOOMOB-571 - analytics break } } @@ -321,6 +368,9 @@ private extension MainTabBarController { case .hubMenu: ServiceLocator.analytics.track(.hubMenuTabReselected) break + case .pointOfSale: + // TODO: WOOMOB-571 - analytics + break } } } @@ -599,18 +649,39 @@ extension MainTabBarController: DeepLinkNavigator { // MARK: - Site ID observation for updating tab view controllers // private extension MainTabBarController { - func configureTabViewControllers() { - viewControllers = { - var controllers = [UIViewController]() - - let tabs: [WooTab] = [.myStore, .orders, .products, .hubMenu] - tabs.forEach { tab in - let tabIndex = tab.visibleIndex() - let tabViewController = rootTabViewController(tab: tab) - controllers.insert(tabViewController, at: tabIndex) - } - return controllers - }() + func observePOSEligibilityForPOSTabVisibility(siteID: Int64) { + guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi1) else { + updateTabViewControllers(isPOSTabVisible: false) + viewModel.loadHubMenuTabBadge() + return + } + + // Hides POS tab initially. + updateTabViewControllers(isPOSTabVisible: false) + + // Cancels any existing task. + posEligibilityCheckTask?.cancel() + + // Starts observing the POS eligibility state. + posEligibilityCheckTask = Task { @MainActor [weak self] in + guard let self, let posEligibilityChecker else { return } + let eligibility = await posEligibilityChecker.checkEligibility() + let isPOSTabVisible = eligibility == .eligible + updateTabViewControllers(isPOSTabVisible: isPOSTabVisible) + viewModel.loadHubMenuTabBadge() + } + } + + func updateTabViewControllers(isPOSTabVisible: Bool) { + var controllers = [UIViewController]() + let tabs = WooTab.visibleTabs(isPOSTabVisible: isPOSTabVisible) + tabs.forEach { tab in + let tabIndex = tab.visibleIndex(isPOSTabVisible: isPOSTabVisible) + let tabViewController = rootTabViewController(tab: tab) + controllers.insert(tabViewController, at: tabIndex) + } + viewControllers = controllers + self.isPOSTabVisible = isPOSTabVisible } func rootTabViewController(tab: WooTab) -> UIViewController { @@ -623,6 +694,8 @@ private extension MainTabBarController { return isProductsSplitViewFeatureFlagOn ? productsContainerController: productsNavigationController case .hubMenu: return hubMenuContainerController + case .pointOfSale: + return posContainerController } } @@ -657,6 +730,15 @@ private extension MainTabBarController { navigateToContent: { _ in })] } + // Configures POS tab coordinator once per logged in site session. + let posEligibilityChecker = posEligibilityCheckerFactory(siteID) + self.posEligibilityChecker = posEligibilityChecker + posTabCoordinator = POSTabCoordinator( + siteID: siteID, + tabContainerController: posContainerController, + viewControllerToPresent: self + ) + // Configure hub menu tab coordinator once per logged in session potentially with multiple sites. if hubMenuTabCoordinator == nil { let hubTabCoordinator = createHubMenuTabCoordinator() @@ -664,10 +746,10 @@ private extension MainTabBarController { } hubMenuTabCoordinator?.activate(siteID: siteID) - viewModel.loadHubMenuTabBadge() - // Set dashboard to be the default tab. - selectedIndex = WooTab.myStore.visibleIndex() + selectedIndex = WooTab.myStore.visibleIndex(isPOSTabVisible: isPOSTabVisible) + + observePOSEligibilityForPOSTabVisibility(siteID: siteID) } func createDashboardViewController(siteID: Int64) -> UIViewController { @@ -680,6 +762,7 @@ private extension MainTabBarController { func createHubMenuTabCoordinator() -> HubMenuCoordinator { HubMenuCoordinator(tabContainerController: hubMenuContainerController, + storesManager: stores, tapToPayBadgePromotionChecker: viewModel.tapToPayBadgePromotionChecker, willPresentReviewDetailsFromPushNotification: { [weak self] in await withCheckedContinuation { [weak self] continuation in @@ -709,7 +792,7 @@ private extension MainTabBarController { func updateMenuTabBadge(with action: NotificationBadgeActionType) { let tab = WooTab.hubMenu - let tabIndex = tab.visibleIndex() + let tabIndex = tab.visibleIndex(isPOSTabVisible: isPOSTabVisible) let input = NotificationsBadgeInput(action: action, tab: tab, tabBar: self.tabBar, tabIndex: tabIndex) self.notificationsBadge.updateBadge(with: input) @@ -726,7 +809,7 @@ private extension MainTabBarController { } let tab = WooTab.orders - let tabIndex = tab.visibleIndex() + let tabIndex = tab.visibleIndex(isPOSTabVisible: isPOSTabVisible) guard let orderTab: UITabBarItem = self.tabBar.items?[tabIndex] else { return diff --git a/WooCommerce/Classes/ViewRelated/TabBar/WooTab+Tag.swift b/WooCommerce/Classes/ViewRelated/TabBar/WooTab+Tag.swift index ffb2ac972fe..2a192fd477d 100644 --- a/WooCommerce/Classes/ViewRelated/TabBar/WooTab+Tag.swift +++ b/WooCommerce/Classes/ViewRelated/TabBar/WooTab+Tag.swift @@ -10,8 +10,10 @@ extension WooTab { return 1 case .products: return 2 - case .hubMenu: + case .pointOfSale: return 3 + case .hubMenu: + return 4 } } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index c424bdd9954..9f57ae9abed 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -495,6 +495,7 @@ 02A9BCD62737F73C00159C79 /* JetpackBenefitItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9BCD52737F73C00159C79 /* JetpackBenefitItem.swift */; }; 02AA586628531D0E0068B6F0 /* CloseAccountCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AA586528531D0E0068B6F0 /* CloseAccountCoordinatorTests.swift */; }; 02AAD54525023A8300BA1E26 /* ProductFormRemoteActionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AAD54425023A8300BA1E26 /* ProductFormRemoteActionUseCase.swift */; }; + 02ABF9BB2DF7F8F100348186 /* POSTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ABF9BA2DF7F8EF00348186 /* POSTabCoordinator.swift */; }; 02AC30CF2888EC8100146A25 /* WooAnalyticsEvent+LoginOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AC30CE2888EC8100146A25 /* WooAnalyticsEvent+LoginOnboarding.swift */; }; 02AC822C2498BC9700A615FB /* ProductFormViewModel+UpdatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AC822B2498BC9700A615FB /* ProductFormViewModel+UpdatesTests.swift */; }; 02ACD25A2852E11700EC928E /* CloseAccountCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ACD2592852E11700EC928E /* CloseAccountCoordinator.swift */; }; @@ -521,6 +522,8 @@ 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 */; }; + 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 */; }; 02BA12852461674B008D8325 /* Optional+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BA12842461674B008D8325 /* Optional+String.swift */; }; 02BA128B24616B48008D8325 /* ProductFormActionsFactory+VisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BA128A24616B48008D8325 /* ProductFormActionsFactory+VisibilityTests.swift */; }; @@ -3751,6 +3754,7 @@ 02AA586528531D0E0068B6F0 /* CloseAccountCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseAccountCoordinatorTests.swift; sourceTree = ""; }; 02AAD54425023A8300BA1E26 /* ProductFormRemoteActionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormRemoteActionUseCase.swift; sourceTree = ""; }; 02AB82EB27069D5D008D7334 /* Experiments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Experiments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02ABF9BA2DF7F8EF00348186 /* POSTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSTabCoordinator.swift; sourceTree = ""; }; 02AC30CE2888EC8100146A25 /* WooAnalyticsEvent+LoginOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+LoginOnboarding.swift"; sourceTree = ""; }; 02AC822B2498BC9700A615FB /* ProductFormViewModel+UpdatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductFormViewModel+UpdatesTests.swift"; sourceTree = ""; }; 02ACD2592852E11700EC928E /* CloseAccountCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseAccountCoordinator.swift; sourceTree = ""; }; @@ -3777,6 +3781,8 @@ 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 = ""; }; + 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 = ""; }; 02BA12842461674B008D8325 /* Optional+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+String.swift"; sourceTree = ""; }; 02BA128A24616B48008D8325 /* ProductFormActionsFactory+VisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductFormActionsFactory+VisibilityTests.swift"; sourceTree = ""; }; @@ -7616,6 +7622,7 @@ 026826912BF59D7A0036F959 /* ViewHelpers */, 02D1D2D82CD3CD710069A93F /* Analytics */, 2004E2C02C076CCA00D62521 /* Card Present Payments */, + 02ABF9B92DF7F8E200348186 /* TabBar */, 026826972BF59D9E0036F959 /* Utils */, ); path = POS; @@ -7677,6 +7684,14 @@ path = BottomSheetListSelector; sourceTree = ""; }; + 02ABF9B92DF7F8E200348186 /* TabBar */ = { + isa = PBXGroup; + children = ( + 02ABF9BA2DF7F8EF00348186 /* POSTabCoordinator.swift */, + ); + path = TabBar; + sourceTree = ""; + }; 02B1914A2CCF278100CF38C9 /* Payments Onboarding */ = { isa = PBXGroup; children = ( @@ -10067,6 +10082,7 @@ 746791642108D853007CF1DC /* Mocks */ = { isa = PBXGroup; children = ( + 02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */, 01F067EC2D0C5D56001C5805 /* MockLocationService.swift */, 0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */, EEA3C2202CA5440A000E82EC /* MockFavoriteProductsUseCase.swift */, @@ -12985,6 +13001,7 @@ 45C8B2682316B2440002FA77 /* BillingAddressTableViewCellTests.swift */, 02FE89C6231FAA4100E85EF8 /* MainTabBarControllerTests.swift */, 953728F72B23635300FDF1D1 /* UIAlertController+helpers.swift */, + 02B8E4182DFBC218001D01FD /* MainTabBarController+TabsTests.swift */, ); path = ViewRelated; sourceTree = ""; @@ -15863,6 +15880,7 @@ 01C9C59F2DA3D98400CD81D8 /* CartRowRemoveButton.swift in Sources */, 457509E4267B9E91005FA2EA /* AggregatedProductListViewController.swift in Sources */, D8815B0D263861A400EDAD62 /* CardPresentModalSuccess.swift in Sources */, + 02ABF9BB2DF7F8F100348186 /* POSTabCoordinator.swift in Sources */, CCE73D2529EDAB5C0064E797 /* SubscriptionPeriod+UI.swift in Sources */, 0235595524496B6D004BE2B8 /* BottomSheetListSelectorCommand.swift in Sources */, EE1905862B57BBE300617C53 /* BlazePaymentMethodsView.swift in Sources */, @@ -17670,6 +17688,7 @@ 0246405F258B122100C10A7D /* PrintShippingLabelCoordinatorTests.swift in Sources */, 314DC4C3268D2F1000444C9E /* MockAppSettingsStoresManager.swift in Sources */, 9379E1A6225537D0006A6BE4 /* TestingAppDelegate.swift in Sources */, + 02B8E4192DFBC218001D01FD /* MainTabBarController+TabsTests.swift in Sources */, 453904F323BB88B5007C4956 /* ProductTaxStatusListSelectorCommandTests.swift in Sources */, DA25ADDF2C87403900AE81FE /* PushNotificationTests.swift in Sources */, 02BAB02124D0235F00F8B06E /* ProductPriceSettingsViewModel+ProductVariationTests.swift in Sources */, @@ -17871,6 +17890,7 @@ D8C11A6222E24C4A00D4A88D /* LedgerTableViewCellTests.swift in Sources */, 027111422913B9FC00F5269A /* AccountCreationFormViewModelTests.swift in Sources */, 2084B7A82C776E1000EFBD2E /* PointOfSaleCardPresentPaymentFoundMultipleReadersAlertViewModelTests.swift in Sources */, + 02B8E41B2DFBC33D001D01FD /* MockPOSEligibilityChecker.swift in Sources */, DE50295328BF4A8A00551736 /* JetpackConnectionWebViewModelTests.swift in Sources */, D802549126552FE1001B2CC1 /* CardPresentModalScanningForReaderTests.swift in Sources */, B98FF4402AAA096200326D16 /* AddressWooTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift new file mode 100644 index 00000000000..ed50554e258 --- /dev/null +++ b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift @@ -0,0 +1,11 @@ +import Foundation +@testable import WooCommerce + +final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { + var result: POSEligibilityState = .eligible + + @MainActor + func checkEligibility() async -> POSEligibilityState { + result + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift new file mode 100644 index 00000000000..5732b6cdfb9 --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarController+TabsTests.swift @@ -0,0 +1,257 @@ +import TestKit +import XCTest +@testable import WooCommerce + +final class MainTabBarController_TabsTests: XCTestCase { + override func setUp() { + super.setUp() + SessionManager.removeTestingDatabase() + } + + func test_tab_view_controllers_are_not_empty_after_updating_default_site() throws { + // Arrange + // Sets mock `FeatureFlagService` before `MainTabBarController` is initialized so that the feature flags are set correctly. + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = false + + let storesManager = MockStoresManager(sessionManager: .makeForTesting()) + + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, featureFlagService: featureFlagService, stores: storesManager) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // Action + storesManager.updateDefaultStore(storeID: 980) + + // Assert + XCTAssertEqual(tabBarController.viewControllers?.count, 4) + assertThat(tabBarController.tabRootViewController(tab: .myStore, isPOSTabVisible: false), + isAnInstanceOf: DashboardViewHostingController.self) + assertThat(tabBarController.tabRootViewController(tab: .orders, isPOSTabVisible: false), + isAnInstanceOf: OrdersSplitViewWrapperController.self) + assertThat(tabBarController.tabRootViewController(tab: .products, isPOSTabVisible: false), + isAnInstanceOf: ProductsViewController.self) + + let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu, isPOSTabVisible: false) as? UINavigationController) + assertThat(hubMenuNavigationController.topViewController, + isAnInstanceOf: HubMenuViewController.self) + } + + func test_tab_view_controllers_include_pos_tab_when_pos_is_eligible() throws { + // Given + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true + + let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + mockPOSEligibilityChecker.result = .eligible + + let storesManager = MockStoresManager(sessionManager: .makeForTesting()) + + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, + featureFlagService: featureFlagService, + stores: storesManager, + posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // When + storesManager.updateDefaultStore(storeID: 314) + + // Then + waitUntil { + tabBarController.tabRootViewControllers.count == 5 + } + assertThat(tabBarController.tabRootViewController(tab: .myStore, isPOSTabVisible: true), + isAnInstanceOf: DashboardViewHostingController.self) + assertThat(tabBarController.tabRootViewController(tab: .orders, isPOSTabVisible: true), + isAnInstanceOf: OrdersSplitViewWrapperController.self) + assertThat(tabBarController.tabRootViewController(tab: .products, isPOSTabVisible: true), + isAnInstanceOf: ProductsViewController.self) + assertThat(tabBarController.tabRootViewController(tab: .pointOfSale, isPOSTabVisible: true), + isAnInstanceOf: POSTabViewController.self) + + let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu, isPOSTabVisible: true) as? UINavigationController) + assertThat(hubMenuNavigationController.topViewController, + isAnInstanceOf: HubMenuViewController.self) + } + + func test_tab_view_controllers_exclude_pos_tab_when_pos_is_not_eligible() throws { + // Given + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true + + let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + mockPOSEligibilityChecker.result = .ineligible(reason: .notTablet) + + let storesManager = MockStoresManager(sessionManager: .makeForTesting()) + + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, + featureFlagService: featureFlagService, + stores: storesManager, + posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // When + storesManager.updateDefaultStore(storeID: 707) + + // Then + waitUntil { + tabBarController.tabRootViewControllers.count == 4 + } + assertThat(tabBarController.tabRootViewController(tab: .myStore, isPOSTabVisible: false), + isAnInstanceOf: DashboardViewHostingController.self) + assertThat(tabBarController.tabRootViewController(tab: .orders, isPOSTabVisible: false), + isAnInstanceOf: OrdersSplitViewWrapperController.self) + assertThat(tabBarController.tabRootViewController(tab: .products, isPOSTabVisible: false), + isAnInstanceOf: ProductsViewController.self) + + let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu, isPOSTabVisible: false) as? UINavigationController) + assertThat(hubMenuNavigationController.topViewController, + isAnInstanceOf: HubMenuViewController.self) + } + + func test_tab_view_controllers_exclude_pos_tab_when_feature_flag_is_disabled() throws { + // Given + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = false + + let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + mockPOSEligibilityChecker.result = .eligible + + let storesManager = MockStoresManager(sessionManager: .makeForTesting()) + + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, + featureFlagService: featureFlagService, + stores: storesManager, + posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // When + storesManager.updateDefaultStore(storeID: 802) + + // Then + XCTAssertEqual(tabBarController.viewControllers?.count, 4) + assertThat(tabBarController.tabRootViewController(tab: .myStore, isPOSTabVisible: false), + isAnInstanceOf: DashboardViewHostingController.self) + assertThat(tabBarController.tabRootViewController(tab: .orders, isPOSTabVisible: false), + isAnInstanceOf: OrdersSplitViewWrapperController.self) + assertThat(tabBarController.tabRootViewController(tab: .products, isPOSTabVisible: false), + isAnInstanceOf: ProductsViewController.self) + + let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu, isPOSTabVisible: false) as? UINavigationController) + assertThat(hubMenuNavigationController.topViewController, + isAnInstanceOf: HubMenuViewController.self) + } + + func test_tab_view_controllers_do_not_change_when_pos_eligibility_changes() throws { + // Given + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true + + let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled) + + let storesManager = MockStoresManager(sessionManager: .makeForTesting()) + + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, + featureFlagService: featureFlagService, + stores: storesManager, + posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // When + storesManager.updateDefaultStore(storeID: 303) + + // Then initial state + waitUntil { + tabBarController.tabRootViewControllers.count == 4 + } + + // When - change POS eligibility + mockPOSEligibilityChecker.result = .eligible + + // Then tabs remain the same + XCTAssertEqual(tabBarController.tabRootViewControllers.count, 4) + } + + func test_tab_root_viewControllers_are_replaced_after_updating_to_a_different_site() throws { + // Arrange + let stores = MockStoresManager(sessionManager: .makeForTesting()) + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, featureFlagService: MockFeatureFlagService(), stores: stores) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // Action + stores.updateDefaultStore(storeID: 134) + let viewControllersBeforeSiteChange = tabBarController.tabRootViewControllers + stores.updateDefaultStore(storeID: 630) + let viewControllersAfterSiteChange = tabBarController.tabRootViewControllers + + // Assert + XCTAssertEqual(viewControllersBeforeSiteChange.count, viewControllersAfterSiteChange.count) + XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.myStore.visibleIndex(isPOSTabVisible: false)], + viewControllersAfterSiteChange[WooTab.myStore.visibleIndex(isPOSTabVisible: false)]) + XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.orders.visibleIndex(isPOSTabVisible: false)], + viewControllersAfterSiteChange[WooTab.orders.visibleIndex(isPOSTabVisible: false)]) + XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.products.visibleIndex(isPOSTabVisible: false)], + viewControllersAfterSiteChange[WooTab.products.visibleIndex(isPOSTabVisible: false)]) + XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.hubMenu.visibleIndex(isPOSTabVisible: false)], + viewControllersAfterSiteChange[WooTab.hubMenu.visibleIndex(isPOSTabVisible: false)]) + } + + func test_tab_view_controllers_stay_the_same_after_updating_to_the_same_site() throws { + // Arrange + let stores = MockStoresManager(sessionManager: .makeForTesting()) + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, stores: stores) + }) else { + return + } + + // Trigger `viewDidLoad` + XCTAssertNotNil(tabBarController.view) + + // Action + let siteID: Int64 = 610 + stores.updateDefaultStore(storeID: siteID) + let viewControllersBeforeSiteChange = try XCTUnwrap(tabBarController.viewControllers) + stores.updateDefaultStore(storeID: siteID) + let viewControllersAfterSiteChange = try XCTUnwrap(tabBarController.viewControllers) + + // Assert + XCTAssertEqual(viewControllersBeforeSiteChange, viewControllersAfterSiteChange) + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift index 714fef6b797..2c4c18322cd 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift @@ -39,117 +39,6 @@ final class MainTabBarControllerTests: XCTestCase { super.tearDown() } - func test_tab_view_controllers_are_not_empty_after_updating_default_site() throws { - - // Arrange - // Sets mock `FeatureFlagService` before `MainTabBarController` is initialized so that the feature flags are set correctly. - let featureFlagService = MockFeatureFlagService() - guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in - return MainTabBarController(coder: coder, featureFlagService: featureFlagService) - }) else { - return - } - - // Trigger `viewDidLoad` - XCTAssertNotNil(tabBarController.view) - - // Action - let siteID: Int64 = 134 - stores.updateDefaultStore(storeID: siteID) - - // Assert - XCTAssertEqual(tabBarController.viewControllers?.count, 4) - assertThat(tabBarController.tabRootViewController(tab: .myStore), - isAnInstanceOf: DashboardViewHostingController.self) - assertThat(tabBarController.tabRootViewController(tab: .orders), - isAnInstanceOf: OrdersSplitViewWrapperController.self) - assertThat(tabBarController.tabRootViewController(tab: .products), - isAnInstanceOf: ProductsViewController.self) - - let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu) as? UINavigationController) - assertThat(hubMenuNavigationController.topViewController, - isAnInstanceOf: HubMenuViewController.self) - } - - func test_tab_view_controllers_returns_expected_values() throws { - // Arrange - let featureFlagService = MockFeatureFlagService() - - guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in - return MainTabBarController(coder: coder, featureFlagService: featureFlagService) - }) else { - return - } - - // Trigger `viewDidLoad` - XCTAssertNotNil(tabBarController.view) - - // Action - let siteID: Int64 = 134 - stores.updateDefaultStore(storeID: siteID) - - // Assert - XCTAssertEqual(tabBarController.viewControllers?.count, 4) - assertThat(tabBarController.tabRootViewController(tab: .myStore), - isAnInstanceOf: DashboardViewHostingController.self) - assertThat(tabBarController.tabRootViewController(tab: .orders), - isAnInstanceOf: OrdersSplitViewWrapperController.self) - assertThat(tabBarController.tabRootViewController(tab: .products), - isAnInstanceOf: ProductsViewController.self) - - let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu) as? UINavigationController) - assertThat(hubMenuNavigationController.topViewController, - isAnInstanceOf: HubMenuViewController.self) - } - - func test_tab_root_viewControllers_are_replaced_after_updating_to_a_different_site() throws { - // Arrange - ServiceLocator.setFeatureFlagService(MockFeatureFlagService()) - guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MainTabBarController else { - return - } - - // Trigger `viewDidLoad` - XCTAssertNotNil(tabBarController.view) - - // Action - stores.updateDefaultStore(storeID: 134) - let viewControllersBeforeSiteChange = tabBarController.tabRootViewControllers - stores.updateDefaultStore(storeID: 630) - let viewControllersAfterSiteChange = tabBarController.tabRootViewControllers - - // Assert - XCTAssertEqual(viewControllersBeforeSiteChange.count, viewControllersAfterSiteChange.count) - XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.myStore.visibleIndex()], - viewControllersAfterSiteChange[WooTab.myStore.visibleIndex()]) - XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.orders.visibleIndex()], - viewControllersAfterSiteChange[WooTab.orders.visibleIndex()]) - XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.products.visibleIndex()], - viewControllersAfterSiteChange[WooTab.products.visibleIndex()]) - XCTAssertNotEqual(viewControllersBeforeSiteChange[WooTab.hubMenu.visibleIndex()], - viewControllersAfterSiteChange[WooTab.hubMenu.visibleIndex()]) - } - - func test_tab_view_controllers_stay_the_same_after_updating_to_the_same_site() throws { - // Arrange - guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MainTabBarController else { - return - } - - // Trigger `viewDidLoad` - XCTAssertNotNil(tabBarController.view) - - // Action - let siteID: Int64 = 134 - stores.updateDefaultStore(storeID: siteID) - let viewControllersBeforeSiteChange = try XCTUnwrap(tabBarController.viewControllers) - stores.updateDefaultStore(storeID: siteID) - let viewControllersAfterSiteChange = try XCTUnwrap(tabBarController.viewControllers) - - // Assert - XCTAssertEqual(viewControllersBeforeSiteChange, viewControllersAfterSiteChange) - } - func test_selected_tab_is_dashboard_after_navigating_to_products_tab_then_updating_to_a_different_site() throws { // Arrange ServiceLocator.setFeatureFlagService(MockFeatureFlagService()) @@ -172,27 +61,39 @@ final class MainTabBarControllerTests: XCTestCase { let selectedTabIndexAfterSiteChange = tabBarController.selectedIndex // Assert - XCTAssertEqual(selectedTabIndexBeforeSiteChange, WooTab.products.visibleIndex()) - XCTAssertEqual(selectedTabIndexAfterSiteChange, WooTab.myStore.visibleIndex()) + XCTAssertEqual(selectedTabIndexBeforeSiteChange, WooTab.products.visibleIndex(isPOSTabVisible: false)) + XCTAssertEqual(selectedTabIndexAfterSiteChange, WooTab.myStore.visibleIndex(isPOSTabVisible: false)) } func test_when_receiving_a_review_notification_from_a_different_site_navigates_to_hubMenu_tab() throws { // Arrange - guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MainTabBarController else { - return - } - let pushNotificationsManager = MockPushNotificationsManager() ServiceLocator.setPushNotesManager(pushNotificationsManager) + let featureFlagService = MockFeatureFlagService() + featureFlagService.isFeatureFlagEnabledReturnValue[.pointOfSaleAsATabi1] = true + + // Hides POS tab. + let mockPOSEligibilityChecker = MockPOSEligibilityChecker() + mockPOSEligibilityChecker.result = .ineligible(reason: .featureFlagDisabled) + let storesManager = MockStoresManager(sessionManager: .testingInstance) // Reset `receivedActions` storesManager.reset() ServiceLocator.setStores(storesManager) + guard let tabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController(creator: { coder in + return MainTabBarController(coder: coder, + featureFlagService: featureFlagService, + stores: storesManager, + posEligibilityCheckerFactory: { _ in mockPOSEligibilityChecker }) + }) else { + return + } + // Trigger `viewDidLoad` XCTAssertNotNil(tabBarController.view) - stores.updateDefaultStore(storeID: 134) + storesManager.updateDefaultStore(storeID: 782) // Simulate successful state resetting after logging out from push notification store switching storesManager.whenReceivingAction(ofType: StatsActionV4.self) { action in @@ -211,7 +112,7 @@ final class MainTabBarControllerTests: XCTestCase { } } - let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu) as? UINavigationController) + let hubMenuNavigationController = try XCTUnwrap(tabBarController.tabRootViewController(tab: .hubMenu, isPOSTabVisible: false) as? UINavigationController) assertThat(hubMenuNavigationController.topViewController, isAnInstanceOf: HubMenuViewController.self) // Action @@ -232,7 +133,7 @@ final class MainTabBarControllerTests: XCTestCase { } waitUntil { - tabBarController.selectedIndex == WooTab.hubMenu.visibleIndex() + tabBarController.selectedIndex == WooTab.hubMenu.visibleIndex(isPOSTabVisible: false) } } @@ -351,7 +252,7 @@ final class MainTabBarControllerTests: XCTestCase { let notice = try XCTUnwrap(noticePresenter.queuedNotices.first) notice.actionHandler?() - let productsNavigationController = try XCTUnwrap(tabBarController.tabContainerController(tab: .products)) + let productsNavigationController = try XCTUnwrap(tabBarController.tabContainerController(tab: .products, isPOSTabVisible: false)) waitUntil { productsNavigationController.presentedViewController != nil } @@ -393,7 +294,7 @@ final class MainTabBarControllerTests: XCTestCase { let notice = try XCTUnwrap(noticePresenter.queuedNotices.first) notice.actionHandler?() - let productsNavigationController = try XCTUnwrap(tabBarController.tabContainerController(tab: .products)) + let productsNavigationController = try XCTUnwrap(tabBarController.tabContainerController(tab: .products, isPOSTabVisible: false)) waitUntil { productsNavigationController.presentedViewController != nil } @@ -435,7 +336,7 @@ final class MainTabBarControllerTests: XCTestCase { notice.actionHandler?() let productsNavigationController = try XCTUnwrap(tabBarController - .tabContainerController(tab: .products)) + .tabContainerController(tab: .products, isPOSTabVisible: false)) waitUntil { productsNavigationController.presentedViewController != nil } @@ -466,7 +367,7 @@ final class MainTabBarControllerTests: XCTestCase { } // Then - XCTAssertEqual(tabBarController.selectedIndex, WooTab.products.visibleIndex()) + XCTAssertEqual(tabBarController.selectedIndex, WooTab.products.visibleIndex(isPOSTabVisible: false)) XCTAssertEqual(tabBarController.selectedViewController, navigationController) } @@ -491,7 +392,7 @@ final class MainTabBarControllerTests: XCTestCase { } // Then - XCTAssertEqual(tabBarController.selectedIndex, WooTab.orders.visibleIndex()) + XCTAssertEqual(tabBarController.selectedIndex, WooTab.orders.visibleIndex(isPOSTabVisible: false)) let tabContainerController = try XCTUnwrap(tabBarController.selectedViewController as? TabContainerController) let ordersSplitViewWrapper = try XCTUnwrap(tabContainerController.wrappedController as? OrdersSplitViewWrapperController) let splitViewController = try XCTUnwrap(ordersSplitViewWrapper.children.first as? UISplitViewController) @@ -537,7 +438,7 @@ final class MainTabBarControllerTests: XCTestCase { } // Then - XCTAssertEqual(tabBarController.selectedIndex, WooTab.orders.visibleIndex()) + XCTAssertEqual(tabBarController.selectedIndex, WooTab.orders.visibleIndex(isPOSTabVisible: false)) let tabContainerController = try XCTUnwrap(tabBarController.selectedViewController as? TabContainerController) let ordersSplitViewWrapper = try XCTUnwrap(tabContainerController.wrappedController as? OrdersSplitViewWrapperController) let splitViewController = try XCTUnwrap(ordersSplitViewWrapper.children.first as? UISplitViewController) @@ -551,7 +452,7 @@ final class MainTabBarControllerTests: XCTestCase { } } -private extension MainTabBarController { +extension MainTabBarController { var tabRootViewControllers: [UIViewController] { var rootViewControllers = [UIViewController]() @@ -576,17 +477,17 @@ private extension MainTabBarController { return rootViewControllers } - func tabRootViewController(tab: WooTab) -> UIViewController? { + func tabRootViewController(tab: WooTab, isPOSTabVisible: Bool) -> UIViewController? { // swiftlint:disable:next empty_enum_arguments - guard let viewController = tabRootViewControllers[safe: tab.visibleIndex()] else { + guard let viewController = tabRootViewControllers[safe: tab.visibleIndex(isPOSTabVisible: isPOSTabVisible)] else { XCTFail("Unexpected access to root controller at tab: \(tab)") return nil } return viewController } - func tabContainerController(tab: WooTab) -> UIViewController? { - guard let viewController = viewControllers?[tab.visibleIndex()] else { + func tabContainerController(tab: WooTab, isPOSTabVisible: Bool) -> UIViewController? { + guard let viewController = viewControllers?[tab.visibleIndex(isPOSTabVisible: isPOSTabVisible)] else { XCTFail("Unexpected access to container controller at tab: \(tab)") return nil }