-
Notifications
You must be signed in to change notification settings - Fork 121
POSTabEligibilityCheckerI2: i2 logic #15865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
4549cac
Revert "Remove i2 files for a separate PR."
jaclync 7e1c33a
POSTabEligibilityCheckerI2Tests: remove i1 test cases.
jaclync 2fbd6d6
Revert "Revert changes to use `POSTabEligibilityCheckerI2` now that i…
jaclync 1514fb3
Remove unused site settings eligibility and feature flag eligibility …
jaclync 2e21fc4
Merge branch 'feat/WOOMOB-756-separate-ineligible-enum-to-visibility-…
jaclync b06a0f5
Move i2 content (checker and tests) to `POSTabEligibilityChecker` and…
jaclync 297742d
Remove `POSIneligibleReason` cases that aren't in i2.
jaclync b6854d4
POSTabEligibilityChecker: remove unused Combine import.
jaclync File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
260 changes: 260 additions & 0 deletions
260
WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSTabEligibilityCheckerI2.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,260 @@ | ||
| import Combine | ||
| import Foundation | ||
| import UIKit | ||
| import class WooFoundation.CurrencySettings | ||
| import enum WooFoundation.CountryCode | ||
| import enum WooFoundation.CurrencyCode | ||
| import protocol Experiments.FeatureFlagService | ||
| import struct Yosemite.SiteSetting | ||
| import protocol Yosemite.POSEligibilityServiceProtocol | ||
| import protocol Yosemite.StoresManager | ||
| import class Yosemite.POSEligibilityService | ||
| import struct Yosemite.SystemPlugin | ||
| import enum Yosemite.FeatureFlagAction | ||
| import enum Yosemite.SettingAction | ||
| import protocol Yosemite.PluginsServiceProtocol | ||
| import class Yosemite.PluginsService | ||
|
|
||
| final class POSTabEligibilityCheckerI2: POSEntryPointEligibilityCheckerProtocol { | ||
| private let siteID: Int64 | ||
| private let userInterfaceIdiom: UIUserInterfaceIdiom | ||
| private let siteSettings: SelectedSiteSettingsProtocol | ||
| private let pluginsService: PluginsServiceProtocol | ||
| private let eligibilityService: POSEligibilityServiceProtocol | ||
| private let stores: StoresManager | ||
| private let featureFlagService: FeatureFlagService | ||
|
|
||
| init(siteID: Int64, | ||
| userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, | ||
| siteSettings: SelectedSiteSettingsProtocol = ServiceLocator.selectedSiteSettings, | ||
| pluginsService: PluginsServiceProtocol = PluginsService(storageManager: ServiceLocator.storageManager), | ||
| eligibilityService: POSEligibilityServiceProtocol = POSEligibilityService(), | ||
| stores: StoresManager = ServiceLocator.stores, | ||
| featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { | ||
| self.siteID = siteID | ||
| self.userInterfaceIdiom = userInterfaceIdiom | ||
| self.siteSettings = siteSettings | ||
| self.pluginsService = pluginsService | ||
| self.eligibilityService = eligibilityService | ||
| self.stores = stores | ||
| self.featureFlagService = featureFlagService | ||
| } | ||
|
|
||
| /// Checks the initial visibility of the POS tab without dependance on network requests. | ||
| func checkInitialVisibility() -> Bool { | ||
| eligibilityService.loadCachedPOSTabVisibility(siteID: siteID) ?? false | ||
| } | ||
|
|
||
| /// Determines whether the POS entry point can be shown based on the selected store and feature gates. | ||
| func checkEligibility() async -> POSEligibilityState { | ||
| guard #available(iOS 17.0, *) else { | ||
| return .ineligible(reason: .unsupportedIOSVersion) | ||
| } | ||
|
|
||
| async let siteSettingsEligibility = checkSiteSettingsEligibility() | ||
| async let pluginEligibility = checkPluginEligibility() | ||
|
|
||
| switch await siteSettingsEligibility { | ||
| case .eligible: | ||
| break | ||
| case .ineligible(let reason): | ||
| return .ineligible(reason: reason) | ||
| } | ||
|
|
||
| switch await pluginEligibility { | ||
| case .eligible: | ||
| return .eligible | ||
| case .ineligible(let reason): | ||
| return .ineligible(reason: reason) | ||
| } | ||
| } | ||
|
|
||
| /// Checks the final visibility of the POS tab. | ||
| func checkVisibility() async -> Bool { | ||
| guard userInterfaceIdiom == .pad else { | ||
| return false | ||
| } | ||
|
|
||
| async let siteSettingsEligibility = waitAndCheckSiteSettingsEligibility() | ||
| async let featureFlagEligibility = checkRemoteFeatureEligibility() | ||
|
|
||
| switch await siteSettingsEligibility { | ||
| case .ineligible(.unsupportedCountry): | ||
| return false | ||
| default: | ||
| break | ||
| } | ||
|
|
||
| return await featureFlagEligibility == .eligible | ||
| } | ||
| } | ||
|
|
||
| // MARK: - WC Plugin Related Eligibility Check | ||
|
|
||
| private extension POSTabEligibilityCheckerI2 { | ||
| func checkPluginEligibility() async -> POSEligibilityState { | ||
| let wcPlugin = await fetchWooCommercePlugin(siteID: siteID) | ||
|
|
||
| guard VersionHelpers.isVersionSupported(version: wcPlugin.version, | ||
| minimumRequired: Constants.wcPluginMinimumVersion) else { | ||
| return .ineligible(reason: .unsupportedWooCommerceVersion(minimumVersion: Constants.wcPluginMinimumVersion)) | ||
| } | ||
|
|
||
| // For versions below 10.0.0, the feature is enabled by default. | ||
| let isFeatureSwitchSupported = VersionHelpers.isVersionSupported(version: wcPlugin.version, | ||
| minimumRequired: Constants.wcPluginMinimumVersionWithFeatureSwitch, | ||
| includesDevAndBetaVersions: true) | ||
| if !isFeatureSwitchSupported { | ||
| return .eligible | ||
| } | ||
|
|
||
| // For versions that support the feature switch, checks if the feature switch is enabled. | ||
| return await checkFeatureSwitchEnabled(siteID: siteID) | ||
| } | ||
|
|
||
| @MainActor | ||
| func fetchWooCommercePlugin(siteID: Int64) async -> SystemPlugin { | ||
| await pluginsService.waitForPluginInStorage(siteID: siteID, pluginName: Constants.wcPluginName, isActive: true) | ||
| } | ||
|
|
||
| @MainActor | ||
| func checkFeatureSwitchEnabled(siteID: Int64) async -> POSEligibilityState { | ||
| await withCheckedContinuation { [weak self] continuation in | ||
| guard let self else { | ||
| return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) | ||
| } | ||
| let action = SettingAction.isFeatureEnabled(siteID: siteID, feature: .pointOfSale) { result in | ||
| switch result { | ||
| case .success(let isEnabled): | ||
| continuation.resume(returning: isEnabled ? .eligible : .ineligible(reason: .featureSwitchDisabled)) | ||
| case .failure: | ||
| continuation.resume(returning: .ineligible(reason: .featureSwitchSyncFailure)) | ||
| } | ||
| } | ||
| stores.dispatch(action) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Site Settings Related Eligibility Check | ||
|
|
||
| private extension POSTabEligibilityCheckerI2 { | ||
| enum SiteSettingsEligibilityState { | ||
| case eligible | ||
| case ineligible(reason: SiteSettingsIneligibleReason) | ||
| } | ||
|
|
||
| enum SiteSettingsIneligibleReason { | ||
| case siteSettingsNotAvailable | ||
| case unsupportedCountry(supportedCountries: [CountryCode]) | ||
| case unsupportedCurrency(supportedCurrencies: [CurrencyCode]) | ||
| } | ||
|
|
||
| func checkSiteSettingsEligibility() async -> POSEligibilityState { | ||
| let siteSettingsEligibility = await waitAndCheckSiteSettingsEligibility() | ||
| switch siteSettingsEligibility { | ||
| case .eligible: | ||
| return .eligible | ||
| case .ineligible(reason: let reason): | ||
| switch reason { | ||
| case .siteSettingsNotAvailable, .unsupportedCountry: | ||
| // This is an edge case where the store country is expected to be eligible from the visilibity check, but site settings might have | ||
| // changed to an unsupported country during the session. In this case, we return an ineligible reason that prompts the merchant to | ||
| // relaunch the app. | ||
| return .ineligible(reason: .siteSettingsNotAvailable) | ||
| case let .unsupportedCurrency(supportedCurrencies: supportedCurrencies): | ||
| return .ineligible(reason: .unsupportedCurrency(supportedCurrencies: supportedCurrencies)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func waitAndCheckSiteSettingsEligibility() async -> SiteSettingsEligibilityState { | ||
| // Waits for the first site settings that matches the given site ID. | ||
| let siteSettings = await waitForSiteSettingsRefresh() | ||
| guard siteSettings.isNotEmpty else { | ||
| return .ineligible(reason: .siteSettingsNotAvailable) | ||
| } | ||
|
|
||
| // Conditions that can change if site settings are synced during the lifetime. | ||
| let countryCode = SiteAddress(siteSettings: siteSettings).countryCode | ||
| let currencyCode = CurrencySettings(siteSettings: siteSettings).currencyCode | ||
|
|
||
| return isEligibleFromCountryAndCurrencyCode(countryCode: countryCode, currencyCode: currencyCode) | ||
| } | ||
|
|
||
| func waitForSiteSettingsRefresh() async -> [SiteSetting] { | ||
| for await siteSettings in siteSettings.settingsStream.values { | ||
| guard siteSettings.siteID == siteID, siteSettings.settings.isNotEmpty, siteSettings.source != .initialLoad else { | ||
| continue | ||
| } | ||
| return siteSettings.settings | ||
| } | ||
| // If we get here, the stream completed without yielding any values for our site ID which is unexpected. | ||
| return [] | ||
| } | ||
|
|
||
| func isEligibleFromCountryAndCurrencyCode(countryCode: CountryCode, currencyCode: CurrencyCode) -> SiteSettingsEligibilityState { | ||
| let supportedCountries: [CountryCode] = [.US, .GB] | ||
| let supportedCurrencies: [CountryCode: [CurrencyCode]] = [.US: [.USD], | ||
| .GB: [.GBP]] | ||
|
|
||
| // Checks country first. | ||
| guard supportedCountries.contains(countryCode) else { | ||
| return .ineligible(reason: .unsupportedCountry(supportedCountries: supportedCountries)) | ||
| } | ||
|
|
||
| let supportedCurrenciesForCountry = supportedCurrencies[countryCode] ?? [] | ||
| guard supportedCurrenciesForCountry.contains(currencyCode) else { | ||
| return .ineligible(reason: .unsupportedCurrency(supportedCurrencies: supportedCurrenciesForCountry)) | ||
| } | ||
| return .eligible | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Remote Feature Flag Eligibility Check | ||
|
|
||
| private extension POSTabEligibilityCheckerI2 { | ||
| enum RemoteFeatureFlagEligibilityState: Equatable { | ||
| case eligible | ||
| case ineligible(reason: RemoteFeatureFlagIneligibleReason) | ||
| } | ||
|
|
||
| enum RemoteFeatureFlagIneligibleReason: Equatable { | ||
| case selfDeallocated | ||
| case featureFlagDisabled | ||
| } | ||
|
|
||
| @MainActor | ||
| func checkRemoteFeatureEligibility() async -> RemoteFeatureFlagEligibilityState { | ||
| // Only whitelisted accounts in WPCOM have the Point of Sale remote feature flag enabled. These can be found at D159901-code | ||
| // If the account is whitelisted, then the remote value takes preference over the local feature flag configuration | ||
| await withCheckedContinuation { [weak self] continuation in | ||
| guard let self else { | ||
| return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) | ||
| } | ||
| let action = FeatureFlagAction.isRemoteFeatureFlagEnabled(.pointOfSale, defaultValue: false) { [weak self] result in | ||
| guard let self else { | ||
| return continuation.resume(returning: .ineligible(reason: .selfDeallocated)) | ||
| } | ||
| switch result { | ||
| case true: | ||
| // The site is whitelisted. | ||
| continuation.resume(returning: .eligible) | ||
| case false: | ||
| // When the site is not whitelisted, check the local feature flag configuration. | ||
| let localFeatureFlag = featureFlagService.isFeatureFlagEnabled(.pointOfSale) | ||
| continuation.resume(returning: localFeatureFlag ? .eligible : .ineligible(reason: .featureFlagDisabled)) | ||
| } | ||
| } | ||
| self.stores.dispatch(action) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private extension POSTabEligibilityCheckerI2 { | ||
| enum Constants { | ||
| static let wcPluginName = "WooCommerce" | ||
| static let wcPluginMinimumVersion = "9.6.0-beta" | ||
| static let wcPluginMinimumVersionWithFeatureSwitch = "10.0.0" | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we're using Combine here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, removed in b6854d4.