From 2adc901181b9d8d337ac3f5d3f52ee8a193e4adb Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 10 Jul 2025 15:18:29 -0400 Subject: [PATCH 1/5] Add `refreshEligibility(ineligibleReason:)` to `POSEntryPointEligibilityCheckerProtocol` with basic implementation in `POSTabEligibilityChecker`. --- .../Controllers/POSEntryPointController.swift | 4 +- .../PointOfSaleEntryPointView.swift | 2 +- .../POS/LegacyPOSTabEligibilityChecker.swift | 5 ++ .../POS/POSTabEligibilityChecker.swift | 51 +++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift index 07fd319bc7c..093b74f7ff0 100644 --- a/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift +++ b/WooCommerce/Classes/POS/Controllers/POSEntryPointController.swift @@ -22,7 +22,7 @@ import protocol Experiments.FeatureFlagService } @MainActor - func refreshEligibility() async throws { - // TODO: WOOMOB-720 - refresh eligibility + func refreshEligibility(reason: POSIneligibleReason) async throws { + eligibilityState = try await posEligibilityChecker.refreshEligibility(ineligibleReason: reason) } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 6e9f4185cdc..08ae79dc8c0 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -62,7 +62,7 @@ struct PointOfSaleEntryPointView: View { } case let .ineligible(reason): POSIneligibleView(reason: reason, onRefresh: { - try await posEntryPointController.refreshEligibility() + try await posEntryPointController.refreshEligibility(reason: reason) }) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift index 2d18286a44f..61345b49f38 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/LegacyPOSTabEligibilityChecker.swift @@ -113,6 +113,11 @@ final class LegacyPOSTabEligibilityChecker: POSEntryPointEligibilityCheckerProto let eligibility = await checkI1Eligibility() return eligibility == .eligible } + + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { + assertionFailure("POS as a tab i1 implementation should not refresh eligibility as the eligibility check is performed in the visibility check.") + return .eligible + } } private extension LegacyPOSTabEligibilityChecker { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 0579e6621f4..46271d10253 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -39,6 +39,8 @@ protocol POSEntryPointEligibilityCheckerProtocol { func checkVisibility() async -> Bool /// Determines whether the site is eligible for POS. func checkEligibility() async -> POSEligibilityState + /// Refreshes the eligibility state based on the provided ineligible reason. + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState } final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { @@ -113,11 +115,56 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { return await featureFlagEligibility == .eligible } + + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { + switch ineligibleReason { + case .unsupportedIOSVersion: + // TODO: WOOMOB-768 - hide refresh CTA in this case + return .ineligible(reason: .unsupportedIOSVersion) + case .siteSettingsNotAvailable, .unsupportedCurrency: + do { + try await syncSiteSettingsRemotely() + return await checkEligibility() + } catch POSTabEligibilityCheckerError.selfDeallocated { + return .ineligible(reason: .selfDeallocated) + } catch { + return await checkEligibility() + } + case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound: + // TODO: sync the WooCommerce plugin then check eligibility again. + return await checkEligibility() + case .featureSwitchDisabled: + // TODO: WOOMOB-759 - enable feature switch via API and check eligibility again + // For now, just checks eligibility again. + return await checkEligibility() + case .featureSwitchSyncFailure, .selfDeallocated: + return await checkEligibility() + } + } } // MARK: - WC Plugin Related Eligibility Check private extension POSTabEligibilityChecker { + @MainActor + func syncSiteSettingsRemotely() async throws { + try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in + guard let self else { + return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) + } + stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: siteID) { [weak self] error in + guard let self else { + return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) + } + if let error { + return continuation.resume(throwing: error) + } + siteSettings.refresh() + continuation.resume(returning: ()) + }) + } + } + func checkPluginEligibility() async -> POSEligibilityState { let wcPlugin = await fetchWooCommercePlugin(siteID: siteID) @@ -277,6 +324,10 @@ private extension POSTabEligibilityChecker { } } +private enum POSTabEligibilityCheckerError: Error { + case selfDeallocated +} + private extension POSTabEligibilityChecker { enum Constants { static let wcPluginName = "WooCommerce" From 5ce0a9f096bc409372817f58ef68c6a8aebc05c4 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 10 Jul 2025 16:08:40 -0400 Subject: [PATCH 2/5] Move site settings sync function to the appropriate section. --- .../POS/POSTabEligibilityChecker.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 46271d10253..0adcd36fba6 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -146,25 +146,6 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { // MARK: - WC Plugin Related Eligibility Check private extension POSTabEligibilityChecker { - @MainActor - func syncSiteSettingsRemotely() async throws { - try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in - guard let self else { - return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) - } - stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: siteID) { [weak self] error in - guard let self else { - return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) - } - if let error { - return continuation.resume(throwing: error) - } - siteSettings.refresh() - continuation.resume(returning: ()) - }) - } - } - func checkPluginEligibility() async -> POSEligibilityState { let wcPlugin = await fetchWooCommercePlugin(siteID: siteID) @@ -282,6 +263,25 @@ private extension POSTabEligibilityChecker { } return .eligible } + + @MainActor + func syncSiteSettingsRemotely() async throws { + try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation) in + guard let self else { + return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) + } + stores.dispatch(SettingAction.synchronizeGeneralSiteSettings(siteID: siteID) { [weak self] error in + guard let self else { + return continuation.resume(throwing: POSTabEligibilityCheckerError.selfDeallocated) + } + if let error { + return continuation.resume(throwing: error) + } + siteSettings.refresh() + continuation.resume(returning: ()) + }) + } + } } // MARK: - Remote Feature Flag Eligibility Check From f3fa93022b81e2efcaa5925eafc473963845e720 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 10 Jul 2025 16:08:56 -0400 Subject: [PATCH 3/5] Update TODO comment for refreshing WC plugin. --- .../Dashboard/Settings/POS/POSTabEligibilityChecker.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index 0adcd36fba6..f6194ea878a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -131,7 +131,8 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { return await checkEligibility() } case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound: - // TODO: sync the WooCommerce plugin then check eligibility again. + // TODO: WOOMOB-799 - sync the WooCommerce plugin then check eligibility again. + // For now, it requires relaunching the app or switching stores to refresh the plugin info. return await checkEligibility() case .featureSwitchDisabled: // TODO: WOOMOB-759 - enable feature switch via API and check eligibility again From b730d950f01d6be6dfbfb14ce45310a794d6c85c Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 10 Jul 2025 17:00:58 -0400 Subject: [PATCH 4/5] Add test cases for `refreshEligibility`. --- .../POS/POSTabEligibilityChecker.swift | 2 +- .../Mocks/MockPOSEligibilityChecker.swift | 4 + .../MainTabBarControllerTests.swift | 4 + .../POS/POSTabEligibilityCheckerTests.swift | 174 ++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift index f6194ea878a..a9228d22bfe 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityChecker.swift @@ -128,7 +128,7 @@ final class POSTabEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { } catch POSTabEligibilityCheckerError.selfDeallocated { return .ineligible(reason: .selfDeallocated) } catch { - return await checkEligibility() + throw error } case .unsupportedWooCommerceVersion, .wooCommercePluginNotFound: // TODO: WOOMOB-799 - sync the WooCommerce plugin then check eligibility again. diff --git a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift index d1ded5ee948..bde7359abc2 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockPOSEligibilityChecker.swift @@ -19,4 +19,8 @@ final class MockPOSEligibilityChecker: POSEntryPointEligibilityCheckerProtocol { func checkEligibility() async -> POSEligibilityState { eligibility } + + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { + .ineligible(reason: ineligibleReason) + } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift index 2111adc5382..3aa73aa1f60 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/MainTabBarControllerTests.swift @@ -692,4 +692,8 @@ private final class MockAsyncPOSEligibilityChecker: POSEntryPointEligibilityChec } } } + + func refreshEligibility(ineligibleReason: POSIneligibleReason) async throws -> POSEligibilityState { + .ineligible(reason: ineligibleReason) + } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift index 100c1130c1f..9821693a3e3 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Settings/POS/POSTabEligibilityCheckerTests.swift @@ -570,6 +570,180 @@ struct POSTabEligibilityCheckerTests { // Then #expect(result == .eligible) } + + // MARK: - `refreshEligibility` Tests + + @Test func refreshEligibility_returns_ineligible_for_unsupportedIOSVersion() async throws { + // Given + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .unsupportedIOSVersion) + + // Then + #expect(result == .ineligible(reason: .unsupportedIOSVersion)) + } + + @Test(arguments: [ + POSIneligibleReason.siteSettingsNotAvailable, + POSIneligibleReason.unsupportedCurrency(supportedCurrencies: [.USD]) + ]) + fileprivate func refreshEligibility_syncs_site_settings_and_checks_eligibility_for_site_settings_issues(ineligibleReason: POSIneligibleReason) async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("9.6.0") + setupPOSFeatureEnabled(.success(true)) + + var syncCalled = false + stores.whenReceivingAction(ofType: SettingAction.self) { action in + switch action { + case .synchronizeGeneralSiteSettings(_, let completion): + syncCalled = true + completion(nil) // Success + case .isFeatureEnabled(_, _, let completion): + completion(.success(true)) + default: + break + } + } + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: ineligibleReason) + + // Then + #expect(syncCalled == true) + #expect(result == .eligible) + } + + @Test(arguments: [ + POSIneligibleReason.siteSettingsNotAvailable, + POSIneligibleReason.unsupportedCurrency(supportedCurrencies: [.USD]) + ]) + fileprivate func refreshEligibility_returns_siteSettingsNotAvailable_when_site_settings_sync_fails(ineligibleReason: POSIneligibleReason) async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("9.6.0") + + var syncCalled = false + stores.whenReceivingAction(ofType: SettingAction.self) { action in + switch action { + case .synchronizeGeneralSiteSettings(_, let completion): + syncCalled = true + completion(NSError(domain: "test", code: 500)) // Network error + default: + break + } + } + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When & Then - Should throw the network error + #expect(syncCalled == false) // Not called yet + await #expect(throws: NSError.self) { + try await checker.refreshEligibility(ineligibleReason: ineligibleReason) + } + #expect(syncCalled == true) // Called during the attempt + } + + @Test func refreshEligibility_checks_eligibility_for_unsupportedWooCommerceVersion() async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("9.5.0") // Still below minimum + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .unsupportedWooCommerceVersion(minimumVersion: "9.6.0-beta")) + + // Then - Should check eligibility again (still ineligible due to version) + #expect(result == .ineligible(reason: .unsupportedWooCommerceVersion(minimumVersion: "9.6.0-beta"))) + } + + @Test func refreshEligibility_checks_eligibility_for_wooCommercePluginNotFound() async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("9.6.0") // Now eligible version + setupPOSFeatureEnabled(.success(true)) + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .wooCommercePluginNotFound) + + // Then - Should check eligibility again (now eligible) + #expect(result == .eligible) + } + + @Test func refreshEligibility_checks_eligibility_for_featureSwitchDisabled() async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("10.0.0") // Version that supports feature switch + setupPOSFeatureEnabled(.success(true)) // Now enabled + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .featureSwitchDisabled) + + // Then - Should check eligibility again (now eligible) + #expect(result == .eligible) + } + + @Test func refreshEligibility_checks_eligibility_for_featureSwitchSyncFailure() async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("10.0.0") + setupPOSFeatureEnabled(.failure(NSError(domain: "test", code: 0))) // Still failing + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .featureSwitchSyncFailure) + + // Then - Should check eligibility again (still failing) + #expect(result == .ineligible(reason: .featureSwitchSyncFailure)) + } + + @Test func refreshEligibility_checks_eligibility_for_selfDeallocated() async throws { + // Given + setupCountry(country: .us, currency: .USD) + setupWooCommerceVersion("9.6.0") + setupPOSFeatureEnabled(.success(true)) + + let checker = POSTabEligibilityChecker(siteID: siteID, + siteSettings: siteSettings, + pluginsService: pluginsService, + stores: stores) + + // When + let result = try await checker.refreshEligibility(ineligibleReason: .selfDeallocated) + + // Then - Should check eligibility again (now eligible) + #expect(result == .eligible) + } } private extension POSTabEligibilityCheckerTests { From 793a492188db6c92dfc8947ed8b507af3b1645ac Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 10 Jul 2025 17:11:34 -0400 Subject: [PATCH 5/5] Update ineligible UI copy to not mention relaunching the app now that refresh is supported. --- WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift index a44fb0412ed..84dea444012 100644 --- a/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift +++ b/WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift @@ -99,8 +99,8 @@ struct POSIneligibleView: View { comment: "Suggestion for disabled feature switch: enable feature in WooCommerce settings") case .featureSwitchSyncFailure: return NSLocalizedString("pos.ineligible.suggestion.featureSwitchSyncFailure", - value: "Try relaunching the app or check your internet connection and try again.", - comment: "Suggestion for feature switch sync failure: relaunch or check connection") + value: "Please check your internet connection and try again.", + comment: "Suggestion for feature switch sync failure: check connection and retry") case let .unsupportedCurrency(supportedCurrencies): let currencyList = supportedCurrencies.map { $0.rawValue } let formattedCurrencyList = ListFormatter.localizedString(byJoining: currencyList) @@ -114,7 +114,7 @@ struct POSIneligibleView: View { return String.localizedStringWithFormat(format, formattedCurrencyList) case .siteSettingsNotAvailable: return NSLocalizedString("pos.ineligible.suggestion.siteSettingsNotAvailable", - value: "Check your internet connection and try relaunching the app. If the issue persists, please contact support.", + value: "Check your internet connection and try again. If the issue persists, please contact support.", comment: "Suggestion for site settings unavailable: check connection or contact support") case .selfDeallocated: return NSLocalizedString("pos.ineligible.suggestion.selfDeallocated",