-
Notifications
You must be signed in to change notification settings - Fork 117
[Woo POS] Fix POS menu item visibility issues with unit tests #12808
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
Changes from all commits
cca539e
ea46320
c2ed3e4
79d3f03
be7d813
31d8342
2bff92c
0e2c11c
61b04d9
59c1682
8352ec8
fd4d561
94aebf7
7221165
5ffdc0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import SwiftUI | ||
import protocol Experiments.FeatureFlagService | ||
import struct Storage.GeneralAppSettingsStorage | ||
|
||
final class BetaFeaturesConfigurationViewModel: ObservableObject { | ||
@Published private(set) var availableFeatures: [BetaFeature] = [] | ||
private let appSettings: GeneralAppSettingsStorage | ||
private let featureFlagService: FeatureFlagService | ||
private let posEligibilityChecker: POSEligibilityCheckerProtocol | ||
|
||
init(appSettings: GeneralAppSettingsStorage = ServiceLocator.generalAppSettings, | ||
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, | ||
posEligibilityChecker: POSEligibilityCheckerProtocol = POSEligibilityChecker( | ||
cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCase(), | ||
siteSettings: ServiceLocator.selectedSiteSettings, | ||
currencySettings: ServiceLocator.currencySettings, | ||
featureFlagService: ServiceLocator.featureFlagService | ||
)) { | ||
self.appSettings = appSettings | ||
self.featureFlagService = featureFlagService | ||
self.posEligibilityChecker = posEligibilityChecker | ||
observePOSEligibilityForAvailableFeatures() | ||
} | ||
|
||
func isOn(feature: BetaFeature) -> Binding<Bool> { | ||
appSettings.betaFeatureEnabledBinding(feature) | ||
} | ||
} | ||
|
||
private extension BetaFeaturesConfigurationViewModel { | ||
func observePOSEligibilityForAvailableFeatures() { | ||
posEligibilityChecker.isEligible | ||
.map { [weak self] isEligibleForPOS in | ||
guard let self else { | ||
return [] | ||
} | ||
return BetaFeature.allCases.filter { betaFeature in | ||
switch betaFeature { | ||
case .viewAddOns: | ||
return true | ||
case .inAppPurchases: | ||
return self.featureFlagService.isFeatureFlagEnabled(.inAppPurchasesDebugMenu) | ||
case .pointOfSale: | ||
return isEligibleForPOS | ||
} | ||
} | ||
} | ||
.assign(to: &$availableFeatures) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,70 @@ | ||
import Combine | ||
import Foundation | ||
import UIKit | ||
import class WooFoundation.CurrencySettings | ||
import enum WooFoundation.CountryCode | ||
import protocol Experiments.FeatureFlagService | ||
import struct Yosemite.SiteSetting | ||
|
||
protocol POSEligibilityCheckerProtocol { | ||
/// As POS eligibility can change from site settings and card payment onboarding state, it's recommended to observe the eligibility value. | ||
var isEligible: AnyPublisher<Bool, Never> { get } | ||
} | ||
|
||
/// Determines whether the POS entry point can be shown based on the selected store and feature gates. | ||
final class POSEligibilityChecker { | ||
final class POSEligibilityChecker: POSEligibilityCheckerProtocol { | ||
var isEligible: AnyPublisher<Bool, Never> { | ||
$isEligibleValue.eraseToAnyPublisher() | ||
} | ||
|
||
@Published private var isEligibleValue: Bool = false | ||
|
||
private let userInterfaceIdiom: UIUserInterfaceIdiom | ||
private let cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol | ||
private let siteSettings: [SiteSetting] | ||
private let siteSettings: SelectedSiteSettings | ||
private let currencySettings: CurrencySettings | ||
private let featureFlagService: FeatureFlagService | ||
|
||
init(cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(), | ||
siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings, | ||
init(userInterfaceIdiom: UIUserInterfaceIdiom = UIDevice.current.userInterfaceIdiom, | ||
cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(), | ||
siteSettings: SelectedSiteSettings = ServiceLocator.selectedSiteSettings, | ||
currencySettings: CurrencySettings = ServiceLocator.currencySettings, | ||
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { | ||
self.userInterfaceIdiom = userInterfaceIdiom | ||
self.siteSettings = siteSettings | ||
self.currencySettings = currencySettings | ||
self.cardPresentPaymentsOnboarding = cardPresentPaymentsOnboarding | ||
self.featureFlagService = featureFlagService | ||
observeOnboardingStateForEligibilityCheck() | ||
} | ||
} | ||
|
||
private extension POSEligibilityChecker { | ||
/// Returns whether the selected store is eligible for POS. | ||
func isEligible() -> Bool { | ||
// Always checks the main POS feature flag before any other checks. | ||
guard featureFlagService.isFeatureFlagEnabled(.displayPointOfSaleToggle) else { | ||
return false | ||
func observeOnboardingStateForEligibilityCheck() { | ||
// Conditions that are fixed for its lifetime. | ||
let isTablet = userInterfaceIdiom == .pad | ||
let isFeatureFlagEnabled = featureFlagService.isFeatureFlagEnabled(.displayPointOfSaleToggle) | ||
guard isTablet && isFeatureFlagEnabled else { | ||
isEligibleValue = false | ||
return | ||
} | ||
|
||
let isCountryCodeUS = SiteAddress(siteSettings: siteSettings).countryCode == CountryCode.US | ||
let isCurrencyUSD = currencySettings.currencyCode == .USD | ||
cardPresentPaymentsOnboarding.statePublisher | ||
.filter { [weak self] _ in | ||
self?.isEligibleFromSiteChecks() ?? false | ||
} | ||
.map { onboardingState in | ||
// Woo Payments plugin enabled and user setup complete | ||
onboardingState == .completed(plugin: .wcPayOnly) || onboardingState == .completed(plugin: .wcPayPreferred) | ||
} | ||
.assign(to: &$isEligibleValue) | ||
} | ||
|
||
// Tablet device | ||
return UIDevice.current.userInterfaceIdiom == .pad | ||
// Woo Payments plugin enabled and user setup complete | ||
&& (cardPresentPaymentsOnboarding.state == .completed(plugin: .wcPayOnly) || cardPresentPaymentsOnboarding.state == .completed(plugin: .wcPayPreferred)) | ||
// USD currency | ||
&& isCurrencyUSD | ||
// US store location | ||
&& isCountryCodeUS | ||
func isEligibleFromSiteChecks() -> Bool { | ||
// Conditions that can change if site settings are synced during the lifetime. | ||
let isCountryCodeUS = SiteAddress(siteSettings: siteSettings.siteSettings).countryCode == .US | ||
let isCurrencyUSD = currencySettings.currencyCode == .USD | ||
return isCountryCodeUS && isCurrencyUSD | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -67,6 +67,8 @@ final class HubMenuViewModel: ObservableObject { | |||||||||||||||||||||||||||
private let stores: StoresManager | ||||||||||||||||||||||||||||
private let featureFlagService: FeatureFlagService | ||||||||||||||||||||||||||||
private let generalAppSettings: GeneralAppSettingsStorage | ||||||||||||||||||||||||||||
private let cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol | ||||||||||||||||||||||||||||
private let posEligibilityChecker: POSEligibilityCheckerProtocol | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
private(set) var productReviewFromNoteParcel: ProductReviewFromNoteParcel? | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
|
@@ -114,6 +116,11 @@ final class HubMenuViewModel: ObservableObject { | |||||||||||||||||||||||||||
self.generalAppSettings = generalAppSettings | ||||||||||||||||||||||||||||
self.switchStoreEnabled = stores.isAuthenticatedWithoutWPCom == false | ||||||||||||||||||||||||||||
self.blazeEligibilityChecker = blazeEligibilityChecker | ||||||||||||||||||||||||||||
self.cardPresentPaymentsOnboarding = CardPresentPaymentsOnboardingUseCase() | ||||||||||||||||||||||||||||
self.posEligibilityChecker = POSEligibilityChecker(cardPresentPaymentsOnboarding: cardPresentPaymentsOnboarding, | ||||||||||||||||||||||||||||
siteSettings: ServiceLocator.selectedSiteSettings, | ||||||||||||||||||||||||||||
currencySettings: ServiceLocator.currencySettings, | ||||||||||||||||||||||||||||
featureFlagService: featureFlagService) | ||||||||||||||||||||||||||||
observeSiteForUIUpdates() | ||||||||||||||||||||||||||||
observePlanName() | ||||||||||||||||||||||||||||
tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldShowNewFeatureBadgeOnPayments) | ||||||||||||||||||||||||||||
|
@@ -138,16 +145,16 @@ final class HubMenuViewModel: ObservableObject { | |||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
private func setupPOSElement() { | ||||||||||||||||||||||||||||
let isBetaFeatureEnabled = generalAppSettings.betaFeatureEnabled(.pointOfSale) | ||||||||||||||||||||||||||||
let eligibilityChecker = POSEligibilityChecker(cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCase(), | ||||||||||||||||||||||||||||
siteSettings: ServiceLocator.selectedSiteSettings.siteSettings, | ||||||||||||||||||||||||||||
currencySettings: ServiceLocator.currencySettings, | ||||||||||||||||||||||||||||
featureFlagService: featureFlagService) | ||||||||||||||||||||||||||||
if isBetaFeatureEnabled && eligibilityChecker.isEligible() { | ||||||||||||||||||||||||||||
posElement = PointOfSaleEntryPoint() | ||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||
posElement = nil | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
cardPresentPaymentsOnboarding.refreshIfNecessary() | ||||||||||||||||||||||||||||
Publishers.CombineLatest(generalAppSettings.betaFeatureEnabledPublisher(.pointOfSale), posEligibilityChecker.isEligible) | ||||||||||||||||||||||||||||
.map { isBetaFeatureEnabled, isEligibleForPOS in | ||||||||||||||||||||||||||||
if isBetaFeatureEnabled && isEligibleForPOS { | ||||||||||||||||||||||||||||
return PointOfSaleEntryPoint() | ||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||
return nil | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
Comment on lines
+150
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it might be clearer to use a filter then a map:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the downstream value is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested in Playground today, if we apply the filter, the view model doesn't get reset back to |
||||||||||||||||||||||||||||
.assign(to: &$posElement) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
private func setupSettingsElements() { | ||||||||||||||||||||||||||||
|
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.
What benefit do we get from moving this to the viewModel?
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 question - as I had to create a view model to observe POS eligibility check (from the onboarding state observation), I thought moving the
appSettings
dependency to the view model would enable unit testing in the future even though we don't have unit tests on it right now.