Skip to content

Commit c8788ef

Browse files
authored
[POS as a tab i1] Cache POS tab visibility per site for initial value while checking eligibility async (#15753)
2 parents b6c2f01 + 7b29c57 commit c8788ef

File tree

16 files changed

+433
-12
lines changed

16 files changed

+433
-12
lines changed

Storage/Storage/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ extension Storage.GeneralStoreSettings {
114114
lastSelectedStockType: NullableCopiableProp<String> = .copy,
115115
lastSelectedOrderStatus: NullableCopiableProp<String> = .copy,
116116
favoriteProductIDs: CopiableProp<[Int64]> = .copy,
117-
searchTermsByKey: CopiableProp<[String: [String]]> = .copy
117+
searchTermsByKey: CopiableProp<[String: [String]]> = .copy,
118+
isPOSTabVisible: NullableCopiableProp<Bool> = .copy
118119
) -> Storage.GeneralStoreSettings {
119120
let storeID = storeID ?? self.storeID
120121
let isTelemetryAvailable = isTelemetryAvailable ?? self.isTelemetryAvailable
@@ -135,6 +136,7 @@ extension Storage.GeneralStoreSettings {
135136
let lastSelectedOrderStatus = lastSelectedOrderStatus ?? self.lastSelectedOrderStatus
136137
let favoriteProductIDs = favoriteProductIDs ?? self.favoriteProductIDs
137138
let searchTermsByKey = searchTermsByKey ?? self.searchTermsByKey
139+
let isPOSTabVisible = isPOSTabVisible ?? self.isPOSTabVisible
138140

139141
return Storage.GeneralStoreSettings(
140142
storeID: storeID,
@@ -155,7 +157,8 @@ extension Storage.GeneralStoreSettings {
155157
lastSelectedStockType: lastSelectedStockType,
156158
lastSelectedOrderStatus: lastSelectedOrderStatus,
157159
favoriteProductIDs: favoriteProductIDs,
158-
searchTermsByKey: searchTermsByKey
160+
searchTermsByKey: searchTermsByKey,
161+
isPOSTabVisible: isPOSTabVisible
159162
)
160163
}
161164
}

Storage/Storage/Model/GeneralStoreSettings.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
8282
///
8383
public var searchTermsByKey: [String: [String]]
8484

85+
/// Whether the POS tab is visible for this store.
86+
///
87+
public var isPOSTabVisible: Bool?
88+
8589
public init(storeID: String? = nil,
8690
isTelemetryAvailable: Bool = false,
8791
telemetryLastReportedTime: Date? = nil,
@@ -100,7 +104,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
100104
lastSelectedStockType: String? = nil,
101105
lastSelectedOrderStatus: String? = nil,
102106
favoriteProductIDs: [Int64] = [],
103-
searchTermsByKey: [String: [String]] = [:]) {
107+
searchTermsByKey: [String: [String]] = [:],
108+
isPOSTabVisible: Bool? = nil) {
104109
self.storeID = storeID
105110
self.isTelemetryAvailable = isTelemetryAvailable
106111
self.telemetryLastReportedTime = telemetryLastReportedTime
@@ -120,6 +125,7 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
120125
self.lastSelectedOrderStatus = lastSelectedOrderStatus
121126
self.favoriteProductIDs = favoriteProductIDs
122127
self.searchTermsByKey = searchTermsByKey
128+
self.isPOSTabVisible = isPOSTabVisible
123129
}
124130

125131
public func erasingSelectedTaxRateID() -> GeneralStoreSettings {
@@ -140,7 +146,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
140146
lastSelectedStockType: lastSelectedStockType,
141147
lastSelectedOrderStatus: lastSelectedOrderStatus,
142148
favoriteProductIDs: favoriteProductIDs,
143-
searchTermsByKey: searchTermsByKey)
149+
searchTermsByKey: searchTermsByKey,
150+
isPOSTabVisible: isPOSTabVisible)
144151
}
145152
}
146153

@@ -174,6 +181,8 @@ extension GeneralStoreSettings {
174181
forKey: .favoriteProductIDs) ?? []
175182
self.searchTermsByKey = try container.decodeIfPresent([String: [String]].self, forKey: .searchTermsByKey) ?? [:]
176183

184+
self.isPOSTabVisible = try container.decodeIfPresent(Bool.self, forKey: .isPOSTabVisible)
185+
177186
// Decode new properties with `decodeIfPresent` and provide a default value if necessary.
178187
}
179188
}

WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private extension POSTabCoordinator {
8787
guard let self else { return }
8888
let collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics()
8989
let cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID,
90+
stores: storesManager,
9091
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker)
9192
if let receiptService = POSReceiptService(siteID: siteID,
9293
credentials: credentials),

WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import enum WooFoundation.CountryCode
55
import enum WooFoundation.CurrencyCode
66
import protocol Experiments.FeatureFlagService
77
import struct Yosemite.SiteSetting
8+
import protocol Yosemite.POSEligibilityServiceProtocol
89
import protocol Yosemite.StoresManager
10+
import class Yosemite.POSEligibilityService
911
import struct Yosemite.SystemPlugin
10-
import enum Yosemite.SystemStatusAction
1112
import enum Yosemite.FeatureFlagAction
1213
import enum Yosemite.SettingAction
1314
import protocol Yosemite.PluginsServiceProtocol
@@ -35,6 +36,8 @@ enum POSEligibilityState: Equatable {
3536
}
3637

3738
protocol POSEntryPointEligibilityCheckerProtocol {
39+
/// Checks the initial visibility of the POS tab.
40+
func checkInitialVisibility() -> Bool
3841
/// Determines whether the site is eligible for POS.
3942
func checkEligibility() async -> POSEligibilityState
4043
}
@@ -45,6 +48,7 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
4548
private let siteSettings: SelectedSiteSettings
4649
private let currencySettings: CurrencySettings
4750
private let pluginsService: PluginsServiceProtocol
51+
private let eligibilityService: POSEligibilityServiceProtocol
4852
private let stores: StoresManager
4953
private let featureFlagService: FeatureFlagService
5054

@@ -53,17 +57,24 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
5357
siteSettings: SelectedSiteSettings = ServiceLocator.selectedSiteSettings,
5458
currencySettings: CurrencySettings = ServiceLocator.currencySettings,
5559
pluginsService: PluginsServiceProtocol = PluginsService(storageManager: ServiceLocator.storageManager),
60+
eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(),
5661
stores: StoresManager = ServiceLocator.stores,
5762
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
5863
self.siteID = siteID
5964
self.userInterfaceIdiom = userInterfaceIdiom
6065
self.siteSettings = siteSettings
6166
self.currencySettings = currencySettings
6267
self.pluginsService = pluginsService
68+
self.eligibilityService = eligibilityService
6369
self.stores = stores
6470
self.featureFlagService = featureFlagService
6571
}
6672

73+
/// Checks the initial visibility of the POS tab without dependance on network requests.
74+
func checkInitialVisibility() -> Bool {
75+
eligibilityService.loadCachedPOSTabVisibility(siteID: siteID) ?? false
76+
}
77+
6778
/// Determines whether the POS entry point can be shown based on the selected store and feature gates.
6879
func checkEligibility() async -> POSEligibilityState {
6980
guard #available(iOS 17.0, *) else {

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ final class HubMenuCoordinator {
6060
///
6161
func activate(siteID: Int64) {
6262
hubMenuController = HubMenuViewController(siteID: siteID,
63+
stores: storesManager,
6364
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker)
6465
if let hubMenuController = hubMenuController {
6566
let navigationController = UINavigationController(rootViewController: hubMenuController)

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ final class HubMenuViewController: UIHostingController<HubMenu> {
1313
private var shouldShowNavigationBar = false
1414

1515
init(siteID: Int64,
16+
stores: StoresManager = ServiceLocator.stores,
1617
tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker) {
1718
self.viewModel = HubMenuViewModel(siteID: siteID,
18-
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker)
19+
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker,
20+
stores: stores)
1921

2022
self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker
2123
super.init(rootView: HubMenu(viewModel: viewModel))

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ private extension HubMenuViewModel {
309309
func createCardPresentPaymentService() {
310310
Task {
311311
self.cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID,
312+
stores: stores,
312313
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker)
313314
}
314315
}

WooCommerce/Classes/ViewRelated/MainTabBarController.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ final class MainTabBarController: UITabBarController {
123123
private let stores: StoresManager
124124
private let analytics: Analytics
125125
private let posEligibilityCheckerFactory: ((_ siteID: Int64) -> POSEntryPointEligibilityCheckerProtocol)
126+
private let posEligibilityService: POSEligibilityServiceProtocol
126127

127128
private var productImageUploadErrorsSubscription: AnyCancellable?
128129

@@ -139,7 +140,8 @@ final class MainTabBarController: UITabBarController {
139140
productImageUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
140141
analytics: Analytics = ServiceLocator.analytics,
141142
stores: StoresManager = ServiceLocator.stores,
142-
posEligibilityCheckerFactory: ((Int64) -> POSEntryPointEligibilityCheckerProtocol)? = nil) {
143+
posEligibilityCheckerFactory: ((Int64) -> POSEntryPointEligibilityCheckerProtocol)? = nil,
144+
posEligibilityService: POSEligibilityServiceProtocol = POSEligibilityService()) {
143145
self.featureFlagService = featureFlagService
144146
self.noticePresenter = noticePresenter
145147
self.productImageUploader = productImageUploader
@@ -148,6 +150,7 @@ final class MainTabBarController: UITabBarController {
148150
self.posEligibilityCheckerFactory = posEligibilityCheckerFactory ?? { siteID in
149151
POSTabEligibilityChecker(siteID: siteID)
150152
}
153+
self.posEligibilityService = posEligibilityService
151154
super.init(coder: coder)
152155
}
153156

@@ -160,6 +163,7 @@ final class MainTabBarController: UITabBarController {
160163
self.posEligibilityCheckerFactory = { siteID in
161164
POSTabEligibilityChecker(siteID: siteID)
162165
}
166+
self.posEligibilityService = POSEligibilityService()
163167
super.init(coder: coder)
164168
}
165169

@@ -650,29 +654,34 @@ extension MainTabBarController: DeepLinkNavigator {
650654
//
651655
private extension MainTabBarController {
652656
func observePOSEligibilityForPOSTabVisibility(siteID: Int64) {
653-
guard featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi1) else {
657+
guard let posEligibilityChecker, featureFlagService.isFeatureFlagEnabled(.pointOfSaleAsATabi1) else {
654658
updateTabViewControllers(isPOSTabVisible: false)
655659
viewModel.loadHubMenuTabBadge()
656660
return
657661
}
658662

659-
// Hides POS tab initially.
660-
updateTabViewControllers(isPOSTabVisible: false)
663+
// Sets POS tab initial visibility based on cached value if available.
664+
let initialVisibility = posEligibilityChecker.checkInitialVisibility()
665+
updateTabViewControllers(isPOSTabVisible: initialVisibility)
661666

662667
// Cancels any existing task.
663668
posEligibilityCheckTask?.cancel()
664669

665670
// Starts observing the POS eligibility state.
666671
posEligibilityCheckTask = Task { @MainActor [weak self] in
667-
guard let self, let posEligibilityChecker else { return }
672+
guard let self, let posEligibilityChecker = self.posEligibilityChecker else { return }
668673
let eligibility = await posEligibilityChecker.checkEligibility()
669674
let isPOSTabVisible = eligibility == .eligible
675+
cachePOSTabVisibility(siteID: siteID, isPOSTabVisible: isPOSTabVisible)
670676
updateTabViewControllers(isPOSTabVisible: isPOSTabVisible)
671677
viewModel.loadHubMenuTabBadge()
672678
}
673679
}
674680

675681
func updateTabViewControllers(isPOSTabVisible: Bool) {
682+
guard isPOSTabVisible != self.isPOSTabVisible || (viewControllers?.count ?? 0) == 0 else {
683+
return
684+
}
676685
var controllers = [UIViewController]()
677686
let tabs = WooTab.visibleTabs(isPOSTabVisible: isPOSTabVisible)
678687
tabs.forEach { tab in
@@ -736,7 +745,8 @@ private extension MainTabBarController {
736745
posTabCoordinator = POSTabCoordinator(
737746
siteID: siteID,
738747
tabContainerController: posContainerController,
739-
viewControllerToPresent: self
748+
viewControllerToPresent: self,
749+
storesManager: stores
740750
)
741751

742752
// Configure hub menu tab coordinator once per logged in session potentially with multiple sites.
@@ -923,6 +933,12 @@ private extension MainTabBarController {
923933
}
924934
}
925935

936+
private extension MainTabBarController {
937+
func cachePOSTabVisibility(siteID: Int64, isPOSTabVisible: Bool) {
938+
posEligibilityService.cachePOSTabVisibility(siteID: siteID, isVisible: isPOSTabVisible)
939+
}
940+
}
941+
926942
private extension MainTabBarController {
927943
enum Constants {
928944
// Used to delay a second navigation after the previous one is called,

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@
626626
02EEB5C52424AFAA00B8A701 /* TextFieldTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */; };
627627
02EFF81A2ABC28BA0015ABB2 /* GiftCardInputViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EFF8192ABC28BA0015ABB2 /* GiftCardInputViewModelTests.swift */; };
628628
02F1E6BD2A39805C00C3E4C7 /* ProductDescriptionAICoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F1E6BC2A39805C00C3E4C7 /* ProductDescriptionAICoordinatorTests.swift */; };
629+
02F36C402E0130EF00DD8CB6 /* MockPOSEligibilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */; };
629630
02F3884C2D6C38BB00619396 /* POSErrorAndAlertIconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3884B2D6C38BB00619396 /* POSErrorAndAlertIconSize.swift */; };
630631
02F3A6842A618CD7004CD2E8 /* WordPressMediaLibraryPickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3A6832A618CD7004CD2E8 /* WordPressMediaLibraryPickerCoordinator.swift */; };
631632
02F3A6862A619270004CD2E8 /* WordPressMediaLibraryImagePickerCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3A6852A619270004CD2E8 /* WordPressMediaLibraryImagePickerCoordinatorTests.swift */; };
@@ -3871,6 +3872,7 @@
38713872
02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextFieldTableViewCell.xib; sourceTree = "<group>"; };
38723873
02EFF8192ABC28BA0015ABB2 /* GiftCardInputViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiftCardInputViewModelTests.swift; sourceTree = "<group>"; };
38733874
02F1E6BC2A39805C00C3E4C7 /* ProductDescriptionAICoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDescriptionAICoordinatorTests.swift; sourceTree = "<group>"; };
3875+
02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPOSEligibilityService.swift; sourceTree = "<group>"; };
38743876
02F3884B2D6C38BB00619396 /* POSErrorAndAlertIconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSErrorAndAlertIconSize.swift; sourceTree = "<group>"; };
38753877
02F3A6832A618CD7004CD2E8 /* WordPressMediaLibraryPickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMediaLibraryPickerCoordinator.swift; sourceTree = "<group>"; };
38763878
02F3A6852A619270004CD2E8 /* WordPressMediaLibraryImagePickerCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressMediaLibraryImagePickerCoordinatorTests.swift; sourceTree = "<group>"; };
@@ -10056,6 +10058,7 @@
1005610058
746791642108D853007CF1DC /* Mocks */ = {
1005710059
isa = PBXGroup;
1005810060
children = (
10061+
02F36C3F2E0130E900DD8CB6 /* MockPOSEligibilityService.swift */,
1005910062
02B8E41A2DFBC33C001D01FD /* MockPOSEligibilityChecker.swift */,
1006010063
01F067EC2D0C5D56001C5805 /* MockLocationService.swift */,
1006110064
0182C8BD2CE3B10E00474355 /* MockReceiptEligibilityUseCase.swift */,
@@ -17442,6 +17445,7 @@
1744217445
D82DFB4C225F303200EFE2CB /* EmptyListMessageWithActionTests.swift in Sources */,
1744317446
0242CFB829F278B70080F500 /* ProductFormAIEligibilityCheckerTests.swift in Sources */,
1744417447
E10DFC78267331590083AFF2 /* ApplicationLogViewModelTests.swift in Sources */,
17448+
02F36C402E0130EF00DD8CB6 /* MockPOSEligibilityService.swift in Sources */,
1744517449
CE6A8FB82B7291760063564D /* AnalyticsReportLinkViewModelTests.swift in Sources */,
1744617450
B98968572A98F227007A2FBE /* TaxEducationalDialogViewModelTests.swift in Sources */,
1744717451
CE63024A2BAC46A200E3325C /* CustomerDetailViewModelTests.swift in Sources */,

WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import Foundation
22
@testable import WooCommerce
33

44
final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
5+
var initialVisibility: Bool = false
56
var result: POSEligibilityState = .eligible
67

8+
func checkInitialVisibility() -> Bool {
9+
initialVisibility
10+
}
11+
712
@MainActor
813
func checkEligibility() async -> POSEligibilityState {
914
result

0 commit comments

Comments
 (0)