Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6677a74
Add feature flag for POS as a tab i1.
jaclync Jun 10, 2025
215fcb5
POSEligibilityChecker: DI `siteID` in initializer and remove unused u…
jaclync Jun 10, 2025
813923a
Initial implementation of POS tab based on the existing POS eligibility.
jaclync Jun 10, 2025
879db1d
SelectedSiteSettings: pass self when posting notification so that the…
jaclync Jun 10, 2025
8981ae2
POSEligibilityChecker: wait for initial site settings notification fr…
jaclync Jun 10, 2025
c8fbc6c
Fix lint errors.
jaclync Jun 10, 2025
deefc7f
Localize POS tab title.
jaclync Jun 10, 2025
0aae697
POSTabCoordinator: DI ServiceLocator dependencies.
jaclync Jun 10, 2025
4dc466b
Disable POS eligibility check when feature flag is disabled.
jaclync Jun 10, 2025
a58b3e5
Add test cases for `MainTabBarController`.
jaclync Jun 10, 2025
74584a9
Revert unnecessary changes.
jaclync Jun 11, 2025
a8f8a1f
Merge branch 'feat/WOOMOB-584-pos-tab-eligibility-checker' of https:/…
jaclync Jun 11, 2025
f235da9
Revert changes `POSEligibilityChecker` and use `POSTabEligibilityChec…
jaclync Jun 11, 2025
4f4ff72
Merge branch 'feat/WOOMOB-584-pos-tab-eligibility-checker' of https:/…
jaclync Jun 11, 2025
c12444e
Revert changes in `POSEligibilityCheckerTests`.
jaclync Jun 11, 2025
a2b1ba2
MainTabBarControllerTests: fix mock eligibility checker.
jaclync Jun 11, 2025
46d2595
Merge branch 'feat/WOOMOB-584-pos-tab-eligibility-checker' into feat/…
jaclync Jun 12, 2025
072f2ab
Mark lazy vars as private as they are not used externally.
jaclync Jun 12, 2025
d619b68
Only check POS eligibility in the Menu tab when `pointOfSaleAsATabi1`…
jaclync Jun 12, 2025
70ad959
Remove unused subscription.
jaclync Jun 12, 2025
9a2d54e
Separate test cases on tabs from `MainTabBarControllerTests` to preve…
jaclync Jun 13, 2025
386fc90
Make POS eligibility check task cancellable in deinit. Simplify code …
jaclync Jun 13, 2025
424cc01
Merge branch 'trunk' into feat/WOOMOB-567-pos-tab
jaclync Jun 13, 2025
214e1e3
Revert unnecessary changes in `MainTabBarControllerTests.test_navigat…
jaclync Jun 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Modules/Sources/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
152 changes: 152 additions & 0 deletions WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading