Skip to content

Commit 53e3bca

Browse files
authored
[POS as a tab i2] Refresh POS eligibility in ineligible UI: WC plugin ineligible cases (#15908)
2 parents bf55b57 + 1098a34 commit 53e3bca

File tree

10 files changed

+303
-187
lines changed

10 files changed

+303
-187
lines changed

Modules/Sources/Storage/Protocols/StorageManagerType.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,17 @@ public extension StorageManagerType {
5757
func reset() {
5858
reset(onCompletion: nil)
5959
}
60+
61+
/// Async/await version of `performAndSave`.
62+
///
63+
/// - Parameters:
64+
/// - operation: A closure which uses the given `StorageType` to make data changes in background.
65+
/// - queue: A queue on which to execute the completion closure.
66+
func performAndSaveAsync(_ operation: @escaping (StorageType) -> Void, on queue: DispatchQueue = .main) async {
67+
await withCheckedContinuation { continuation in
68+
performAndSave(operation, completion: {
69+
continuation.resume()
70+
}, on: queue)
71+
}
72+
}
6073
}

Modules/Sources/Yosemite/PointOfSale/Eligibility/POSSystemStatusService.swift

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Networking
3+
import Storage
34

45
public protocol POSSystemStatusServiceProtocol {
56
/// Loads WooCommerce plugin and POS feature switch value remotely for eligibility checks.
@@ -23,27 +24,36 @@ public struct POSPluginAndFeatureInfo {
2324
/// Service for fetching POS-related system status information.
2425
public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
2526
private let remote: SystemStatusRemote
27+
private let storageManager: StorageManagerType
2628

27-
public init(credentials: Credentials?) {
29+
public init(credentials: Credentials?, storageManager: StorageManagerType) {
2830
let network = AlamofireNetwork(credentials: credentials)
29-
remote = SystemStatusRemote(network: network)
31+
self.remote = SystemStatusRemote(network: network)
32+
self.storageManager = storageManager
3033
}
3134

3235
/// Test-friendly initializer that accepts a network implementation.
33-
init(network: Network) {
34-
remote = SystemStatusRemote(network: network)
36+
init(network: Network, storageManager: StorageManagerType) {
37+
self.remote = SystemStatusRemote(network: network)
38+
self.storageManager = storageManager
3539
}
3640

41+
@MainActor
3742
public func loadWooCommercePluginAndPOSFeatureSwitch(siteID: Int64) async throws -> POSPluginAndFeatureInfo {
3843
let mapper = SingleItemMapper<POSPluginEligibilitySystemStatus>(siteID: siteID)
3944
let systemStatus: POSPluginEligibilitySystemStatus = try await remote.loadSystemStatus(
4045
for: siteID,
41-
fields: [.activePlugins, .settings],
46+
fields: [.activePlugins, .inactivePlugins, .settings],
4247
mapper: mapper
4348
)
4449

45-
// Finds WooCommerce plugin from active plugins response.
46-
guard let wcPlugin = systemStatus.activePlugins.first(where: { $0.plugin == Constants.wcPluginPath }) else {
50+
// Upserts all plugins in storage.
51+
await storageManager.performAndSaveAsync({ [weak self] storage in
52+
self?.upsertSystemPlugins(siteID: siteID, systemStatus: systemStatus, in: storage)
53+
})
54+
55+
// Loads WooCommerce plugin from storage.
56+
guard let wcPlugin = storageManager.viewStorage.loadSystemPlugin(siteID: siteID, path: Constants.wcPluginPath)?.toReadOnly() else {
4757
return POSPluginAndFeatureInfo(wcPlugin: nil, featureValue: nil)
4858
}
4959

@@ -53,6 +63,42 @@ public final class POSSystemStatusService: POSSystemStatusServiceProtocol {
5363
}
5464
}
5565

66+
private extension POSSystemStatusService {
67+
/// Updates or inserts system plugins in storage.
68+
func upsertSystemPlugins(siteID: Int64, systemStatus: POSPluginEligibilitySystemStatus, in storage: StorageType) {
69+
// Active and inactive plugins share identical structure, but are stored in separate parts of the remote response
70+
// (and without an active attribute in the response). So we apply the correct value for active (or not)
71+
let readonlySystemPlugins: [SystemPlugin] = {
72+
let activePlugins = systemStatus.activePlugins.map {
73+
$0.copy(active: true)
74+
}
75+
76+
let inactivePlugins = systemStatus.inactivePlugins.map {
77+
$0.copy(active: false)
78+
}
79+
80+
return activePlugins + inactivePlugins
81+
}()
82+
83+
let storedPlugins = storage.loadSystemPlugins(siteID: siteID, matching: readonlySystemPlugins.map { $0.name })
84+
readonlySystemPlugins.forEach { readonlySystemPlugin in
85+
// Loads or creates new StorageSystemPlugin matching the readonly one.
86+
let storageSystemPlugin: StorageSystemPlugin = {
87+
if let systemPlugin = storedPlugins.first(where: { $0.name == readonlySystemPlugin.name }) {
88+
return systemPlugin
89+
}
90+
return storage.insertNewObject(ofType: StorageSystemPlugin.self)
91+
}()
92+
93+
storageSystemPlugin.update(with: readonlySystemPlugin)
94+
}
95+
96+
// Removes stale system plugins.
97+
let currentSystemPlugins = readonlySystemPlugins.map(\.name)
98+
storage.deleteStaleSystemPlugins(siteID: siteID, currentSystemPlugins: currentSystemPlugins)
99+
}
100+
}
101+
56102
private extension POSSystemStatusService {
57103
enum Constants {
58104
static let wcPluginPath = "woocommerce/woocommerce.php"
@@ -63,10 +109,12 @@ private extension POSSystemStatusService {
63109

64110
private struct POSPluginEligibilitySystemStatus: Decodable {
65111
let activePlugins: [SystemPlugin]
112+
let inactivePlugins: [SystemPlugin]
66113
let settings: POSEligibilitySystemStatusSettings
67114

68115
enum CodingKeys: String, CodingKey {
69116
case activePlugins = "active_plugins"
117+
case inactivePlugins = "inactive_plugins"
70118
case settings
71119
}
72120
}

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-and-pos-feature-disabled.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
{
56
"plugin": "woocommerce/woocommerce.php",

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-and-pos-feature-enabled.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
{
56
"plugin": "woocommerce/woocommerce.php",

Modules/Tests/NetworkingTests/Responses/system-status-wc-plugin-missing.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"data": {
3+
"inactive_plugins": [],
34
"active_plugins":[
45
],
56
"settings": {

Modules/Tests/YosemiteTests/PointOfSale/POSSystemStatusServiceTests.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import Foundation
22
import Testing
33
import TestKit
44
@testable import Networking
5+
@testable import Storage
56
@testable import Yosemite
67

78
@MainActor
89
struct POSSystemStatusServiceTests {
910
private let network = MockNetwork()
11+
private let storageManager = MockStorageManager()
1012
private let sampleSiteID: Int64 = 134
1113
private let sut: POSSystemStatusService
1214

1315
init() async throws {
1416
network.removeAllSimulatedResponses()
15-
sut = POSSystemStatusService(network: network)
17+
sut = POSSystemStatusService(network: network, storageManager: storageManager)
1618
}
1719

1820
// MARK: - loadWooCommercePluginAndPOSFeatureSwitch Tests
1921

20-
@Test func loadWooCommercePluginAndPOSFeatureSwitch_returns_plugin_and_nil_feature_when_settings_response_does_not_include_enabled_featuers() async throws {
22+
@Test func loadWooCommercePluginAndPOSFeatureSwitch_returns_plugin_and_nil_feature_when_settings_response_does_not_include_enabled_features() async throws {
2123
// Given
2224
network.simulateResponse(requestUrlSuffix: "system_status", filename: "systemStatus")
2325

@@ -37,6 +39,10 @@ struct POSSystemStatusServiceTests {
3739
// Given
3840
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-and-pos-feature-enabled")
3941

42+
// Inserts WooCommerce plugin into storage with an older version and inactive.
43+
let storageWCPlugin = createWCPlugin(version: "9.5.2", active: false)
44+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
45+
4046
// When
4147
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
4248

@@ -53,6 +59,10 @@ struct POSSystemStatusServiceTests {
5359
// Given
5460
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-and-pos-feature-disabled")
5561

62+
// Inserts WooCommerce plugin into storage with an older version and inactive.
63+
let storageWCPlugin = createWCPlugin(version: "9.5.2", active: false)
64+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
65+
5666
// When
5767
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
5868

@@ -69,6 +79,10 @@ struct POSSystemStatusServiceTests {
6979
// Given
7080
network.simulateResponse(requestUrlSuffix: "system_status", filename: "system-status-wc-plugin-missing")
7181

82+
// Inserts WooCommerce plugin eligible for POS into storage.
83+
let storageWCPlugin = createWCPlugin(version: "9.9.0", active: true)
84+
storageManager.insertSampleSystemPlugin(readOnlySystemPlugin: storageWCPlugin)
85+
7286
// When
7387
let result = try await sut.loadWooCommercePluginAndPOSFeatureSwitch(siteID: sampleSiteID)
7488

@@ -87,3 +101,15 @@ struct POSSystemStatusServiceTests {
87101
}
88102
}
89103
}
104+
105+
private extension POSSystemStatusServiceTests {
106+
func createWCPlugin(version: String = "5.8.0", active: Bool = true) -> Yosemite.SystemPlugin {
107+
.fake().copy(
108+
siteID: sampleSiteID,
109+
plugin: "woocommerce/woocommerce.php",
110+
version: version,
111+
versionLatest: version,
112+
active: active
113+
)
114+
}
115+
}

WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,6 @@ struct POSIneligibleView: View {
9797
value: "Point of Sale must be enabled to proceed. " +
9898
"Please enable the POS feature from your WordPress admin under WooCommerce settings > Advanced > Features.",
9999
comment: "Suggestion for disabled feature switch: enable feature in WooCommerce settings")
100-
case .featureSwitchSyncFailure:
101-
return NSLocalizedString("pos.ineligible.suggestion.featureSwitchSyncFailure",
102-
value: "Please check your internet connection and try again.",
103-
comment: "Suggestion for feature switch sync failure: check connection and retry")
104100
case let .unsupportedCurrency(supportedCurrencies):
105101
let currencyList = supportedCurrencies.map { $0.rawValue }
106102
let formattedCurrencyList = ListFormatter.localizedString(byJoining: currencyList)
@@ -194,15 +190,6 @@ private extension POSIneligibleView {
194190
}
195191
}
196192

197-
#Preview("Feature switch sync failure") {
198-
if #available(iOS 17.0, *) {
199-
POSIneligibleView(
200-
reason: .featureSwitchSyncFailure,
201-
onRefresh: {}
202-
)
203-
}
204-
}
205-
206193
#Preview("Unsupported WooCommerce version") {
207194
if #available(iOS 17.0, *) {
208195
POSIneligibleView(

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

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import class Yosemite.POSEligibilityService
1111
import struct Yosemite.SystemPlugin
1212
import enum Yosemite.FeatureFlagAction
1313
import enum Yosemite.SettingAction
14-
import protocol Yosemite.PluginsServiceProtocol
15-
import class Yosemite.PluginsService
14+
import protocol Yosemite.POSSystemStatusServiceProtocol
15+
import class Yosemite.POSSystemStatusService
1616

1717
/// Represents the reasons why a site may be ineligible for POS.
1818
enum POSIneligibleReason: Equatable {
@@ -21,7 +21,6 @@ enum POSIneligibleReason: Equatable {
2121
case siteSettingsNotAvailable
2222
case wooCommercePluginNotFound
2323
case featureSwitchDisabled
24-
case featureSwitchSyncFailure
2524
case unsupportedCurrency(supportedCurrencies: [CurrencyCode])
2625
case selfDeallocated
2726
}
@@ -47,25 +46,26 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
4746
private let siteID: Int64
4847
private let userInterfaceIdiom: UIUserInterfaceIdiom
4948
private let siteSettings: SelectedSiteSettingsProtocol
50-
private let pluginsService: PluginsServiceProtocol
5149
private let eligibilityService: POSEligibilityServiceProtocol
5250
private let stores: StoresManager
5351
private let featureFlagService: FeatureFlagService
52+
private let systemStatusService: POSSystemStatusServiceProtocol
5453

5554
init(siteID: Int64,
5655
userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom,
5756
siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings,
58-
pluginsService: PluginsServiceProtocol = PluginsService(storageManager: ServiceLocator.storageManager),
5957
eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(),
6058
stores: StoresManager = ServiceLocator.stores,
61-
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
59+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
60+
systemStatusService: POSSystemStatusServiceProtocol = POSSystemStatusService(credentials: ServiceLocator.stores.sessionManager.defaultCredentials,
61+
storageManager: ServiceLocator.storageManager)) {
6262
self.siteID = siteID
6363
self.userInterfaceIdiom = userInterfaceIdiom
6464
self.siteSettings = siteSettings
65-
self.pluginsService = pluginsService
6665
self.eligibilityService = eligibilityService
6766
self.stores = stores
6867
self.featureFlagService = featureFlagService
68+
self.systemStatusService = systemStatusService
6969
}
7070

7171
/// Checks the initial visibility of the POS tab without dependance on network requests.
@@ -131,14 +131,12 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
131131
throw error
132132
}
133133
case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound:
134-
// TODO: WOOMOB-799 - sync the WooCommerce plugin then check eligibility again.
135-
// For now, it requires relaunching the app or switching stores to refresh the plugin info.
136134
return await checkEligibility()
137135
case .featureSwitchDisabled:
138136
// TODO: WOOMOB-759 - enable feature switch via API and check eligibility again
139137
// For now, just checks eligibility again.
140138
return await checkEligibility()
141-
case .featureSwitchSyncFailure, .selfDeallocated:
139+
case .selfDeallocated:
142140
return await checkEligibility()
143141
}
144142
}
@@ -147,8 +145,38 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol {
147145
// MARK: - WC Plugin Related Eligibility Check
148146

149147
private extension POSTabEligibilityChecker {
148+
/// Checks the eligibility of the WooCommerce plugin and plugin version based POS feature switch value.
149+
///
150+
/// - Parameter pluginEligibility: An optional parameter that can provide pre-fetched plugin eligibility state.
151+
/// - Returns: The eligibility state for POS based on the WooCommerce plugin and POS feature switch.
150152
func checkPluginEligibility() async -> POSEligibilityState {
151-
let wcPlugin = await fetchWooCommercePlugin(siteID: siteID)
153+
do {
154+
let info = try await systemStatusService.loadWooCommercePluginAndPOSFeatureSwitch(siteID: siteID)
155+
let wcPluginEligibility = checkWooCommercePluginEligibility(wcPlugin: info.wcPlugin)
156+
switch wcPluginEligibility {
157+
case .eligible:
158+
return .eligible
159+
case .ineligible(let reason):
160+
return .ineligible(reason: reason)
161+
case .pendingFeatureSwitchCheck:
162+
let isFeatureSwitchEnabled = info.featureValue == true
163+
return isFeatureSwitchEnabled ? .eligible : .ineligible(reason: .featureSwitchDisabled)
164+
}
165+
} catch {
166+
return .ineligible(reason: .wooCommercePluginNotFound)
167+
}
168+
}
169+
170+
enum PluginEligibilityState {
171+
case eligible
172+
case ineligible(reason: POSIneligibleReason)
173+
case pendingFeatureSwitchCheck
174+
}
175+
176+
func checkWooCommercePluginEligibility(wcPlugin: SystemPlugin?) -> PluginEligibilityState {
177+
guard let wcPlugin, wcPlugin.active else {
178+
return .ineligible(reason: .wooCommercePluginNotFound)
179+
}
152180

153181
guard VersionHelpers.isVersionSupported(version: wcPlugin.version,
154182
minimumRequired: Constants.wcPluginMinimumVersion) else {
@@ -163,31 +191,8 @@ private extension POSTabEligibilityChecker {
163191
return .eligible
164192
}
165193

166-
// For versions that support the feature switch, checks if the feature switch is enabled.
167-
return await checkFeatureSwitchEnabled(siteID: siteID)
168-
}
169-
170-
@MainActor
171-
func fetchWooCommercePlugin(siteID: Int64) async -> SystemPlugin {
172-
await pluginsService.waitForPluginInStorage(siteID: siteID, pluginPath: Constants.wcPlugin, isActive: true)
173-
}
174-
175-
@MainActor
176-
func checkFeatureSwitchEnabled(siteID: Int64) async -> POSEligibilityState {
177-
await withCheckedContinuation { [weak self] continuation in
178-
guard let self else {
179-
return continuation.resume(returning: .ineligible(reason: .selfDeallocated))
180-
}
181-
let action = SettingAction.isFeatureEnabled(siteID: siteID, feature: .pointOfSale) { result in
182-
switch result {
183-
case .success(let isEnabled):
184-
continuation.resume(returning: isEnabled ? .eligible : .ineligible(reason: .featureSwitchDisabled))
185-
case .failure:
186-
continuation.resume(returning: .ineligible(reason: .featureSwitchSyncFailure))
187-
}
188-
}
189-
stores.dispatch(action)
190-
}
194+
// For versions that support the feature switch, checks if the feature switch is enabled separately.
195+
return .pendingFeatureSwitchCheck
191196
}
192197
}
193198

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct HubMenu: View {
6565
itemProvider: PointOfSaleItemService(currencySettings: ServiceLocator.currencySettings),
6666
itemFetchStrategyFactory: viewModel.posPopularItemFetchStrategyFactory),
6767
barcodeScanService: viewModel.barcodeScanService,
68-
posEligibilityChecker: POSTabEligibilityChecker(siteID: viewModel.siteID))
68+
posEligibilityChecker: LegacyPOSTabEligibilityChecker(siteID: viewModel.siteID))
6969
} else {
7070
// TODO: When we have a singleton for the card payment service, this should not be required.
7171
Text("Error creating card payment service")

0 commit comments

Comments
 (0)