From 4791095e4c9741ffa058b6e7b4e7dda9a9e5763b Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 20 May 2024 15:00:38 +0800 Subject: [PATCH 1/3] Create `POSEligibilityChecker` to check if a store is eligible for POS. --- .../Settings/POS/POSEligibilityChecker.swift | 49 +++++++++++++++++++ .../ViewRelated/Hub Menu/HubMenu.swift | 24 +++++++++ .../Hub Menu/HubMenuViewModel.swift | 21 ++++++-- .../WooCommerce.xcodeproj/project.pbxproj | 12 +++++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift new file mode 100644 index 00000000000..c765e9d50a2 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift @@ -0,0 +1,49 @@ +import Foundation +import UIKit +import class WooFoundation.CurrencySettings +import enum WooFoundation.CountryCode +import protocol Experiments.FeatureFlagService +import struct Yosemite.SiteSetting + +/// Determines whether the POS entry point can be shown based on the selected store and feature gates. +final class POSEligibilityChecker { + private let isBetaFeatureEnabled: Bool + private let cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol + private let siteSettings: [SiteSetting] + private let currencySettings: CurrencySettings + private let featureFlagService: FeatureFlagService + + init(isBetaFeatureEnabled: Bool, + cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(), + siteSettings: [SiteSetting], + currencySettings: CurrencySettings, + featureFlagService: FeatureFlagService) { + self.isBetaFeatureEnabled = isBetaFeatureEnabled + self.siteSettings = siteSettings + self.currencySettings = currencySettings + self.cardPresentPaymentsOnboarding = cardPresentPaymentsOnboarding + self.featureFlagService = featureFlagService + } + + /// 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 + } + + let isCountryCodeUS = SiteAddress(siteSettings: siteSettings).countryCode == CountryCode.US + let isCurrencyUSD = currencySettings.currencyCode == .USD + + // Feature switch enabled + return isBetaFeatureEnabled + // Tablet device + && 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 + } +} diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index 361e2d7b392..db38543340a 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -82,6 +82,30 @@ private extension HubMenu { .disabled(!viewModel.switchStoreEnabled) } + // Point of Sale + if let menu = viewModel.posElement { + Section { + Button { + handleTap(menu: menu) + } label: { + Row(title: menu.title, + titleBadge: nil, + iconBadge: menu.iconBadge, + description: menu.description, + icon: .local(menu.icon), + chevron: .leading) + .foregroundColor(Color(menu.iconColor)) + } + .accessibilityIdentifier(menu.accessibilityIdentifier) + .overlay { + NavigationLink(value: menu.id) { + EmptyView() + } + .opacity(0) + } + } + } + // Settings Section Section(Localization.settings) { ForEach(viewModel.settingsElements, id: \.id) { menu in diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 628540699d4..81d45f9ce59 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -41,6 +41,10 @@ final class HubMenuViewModel: ObservableObject { @Published private(set) var woocommerceAdminURL = WooConstants.URLs.blog.asURL() + /// POS Section Element + /// + @Published private(set) var posElement: HubMenuItem? + /// Settings Elements /// @Published private(set) var settingsElements: [HubMenuItem] = [] @@ -122,6 +126,7 @@ final class HubMenuViewModel: ObservableObject { /// Resets the menu elements displayed on the menu. /// func setupMenuElements() { + setupPOSElement() setupSettingsElements() setupGeneralElements() } @@ -132,6 +137,19 @@ final class HubMenuViewModel: ObservableObject { navigationPath.append(HubMenuNavigationDestination.payments) } + private func setupPOSElement() { + let eligibilityChecker = POSEligibilityChecker(isBetaFeatureEnabled: generalAppSettings.betaFeatureEnabled(.pointOfSale), + cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCase(), + siteSettings: ServiceLocator.selectedSiteSettings.siteSettings, + currencySettings: ServiceLocator.currencySettings, + featureFlagService: featureFlagService) + if eligibilityChecker.isEligible() { + posElement = PointOfSaleEntryPoint() + } else { + posElement = nil + } + } + private func setupSettingsElements() { settingsElements = [Settings()] @@ -153,9 +171,6 @@ final class HubMenuViewModel: ObservableObject { if generalAppSettings.betaFeatureEnabled(.inAppPurchases) { generalElements.append(InAppPurchases()) } - if generalAppSettings.betaFeatureEnabled(.pointOfSale) { - generalElements.append(PointOfSaleEntryPoint()) - } let inboxUseCase = InboxEligibilityUseCase(stores: stores, featureFlagService: featureFlagService) inboxUseCase.isEligibleForInbox(siteID: siteID) { [weak self] isInboxMenuShown in diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 3c4545ca86e..73c6a412b74 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -560,6 +560,7 @@ 02E4908929AE49B9005942AE /* TopPerformersEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4908829AE49B9005942AE /* TopPerformersEmptyView.swift */; }; 02E4908D29AF216E005942AE /* TopPerformersPeriodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4908C29AF216E005942AE /* TopPerformersPeriodView.swift */; }; 02E493EF245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E493EE245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift */; }; + 02E4A0832BFB1C4F006D4F87 /* POSEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4A0822BFB1C4F006D4F87 /* POSEligibilityChecker.swift */; }; 02E4AF7126FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4AF7026FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift */; }; 02E4FD7E2306A8180049610C /* StatsTimeRangeBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4FD7D2306A8180049610C /* StatsTimeRangeBarViewModel.swift */; }; 02E4FD812306AA890049610C /* StatsTimeRangeBarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E4FD802306AA890049610C /* StatsTimeRangeBarViewModelTests.swift */; }; @@ -3333,6 +3334,7 @@ 02E4908829AE49B9005942AE /* TopPerformersEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopPerformersEmptyView.swift; sourceTree = ""; }; 02E4908C29AF216E005942AE /* TopPerformersPeriodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopPerformersPeriodView.swift; sourceTree = ""; }; 02E493EE245C1087000AEA9E /* ProductFormBottomSheetListSelectorCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormBottomSheetListSelectorCommandTests.swift; sourceTree = ""; }; + 02E4A0822BFB1C4F006D4F87 /* POSEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSEligibilityChecker.swift; sourceTree = ""; }; 02E4AF7026FC4F16002AD9F4 /* ProductReviewFromNoteParcelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductReviewFromNoteParcelFactory.swift; sourceTree = ""; }; 02E4FD7D2306A8180049610C /* StatsTimeRangeBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeBarViewModel.swift; sourceTree = ""; }; 02E4FD802306AA890049610C /* StatsTimeRangeBarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeBarViewModelTests.swift; sourceTree = ""; }; @@ -6898,6 +6900,14 @@ path = Authentication; sourceTree = ""; }; + 02E4A0842BFB1D1F006D4F87 /* POS */ = { + isa = PBXGroup; + children = ( + 02E4A0822BFB1C4F006D4F87 /* POSEligibilityChecker.swift */, + ); + path = POS; + sourceTree = ""; + }; 02E4FD7F2306AA770049610C /* Dashboard */ = { isa = PBXGroup; children = ( @@ -10974,6 +10984,7 @@ E138D4F2269ED99A006EA5C6 /* In-Person Payments */, CE27257A219249B5002B22EB /* Help */, CE22E3F821714639005A6BEF /* Privacy */, + 02E4A0842BFB1D1F006D4F87 /* POS */, 03191AE828E20C9200670723 /* PluginDetailsRowView.swift */, ); path = Settings; @@ -14683,6 +14694,7 @@ B57C744E20F56E3800EEFC87 /* UITableViewCell+Helpers.swift in Sources */, 0295355B245ADF8100BDC42B /* FilterType+Products.swift in Sources */, 02CA63DA23D1ADD100BBF148 /* CameraCaptureCoordinator.swift in Sources */, + 02E4A0832BFB1C4F006D4F87 /* POSEligibilityChecker.swift in Sources */, DE8BEB962ABC19B100F5E56C /* ProductDetailPreviewView.swift in Sources */, 260C31602524ECA900157BC2 /* IssueRefundViewController.swift in Sources */, 4521397027FF53E400964ED3 /* CouponExpiryDateView.swift in Sources */, From 564dbcd183c9740bdfe220bdbddcbd97f7ef86da Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 20 May 2024 15:12:13 +0800 Subject: [PATCH 2/3] Separate feature switch check and check the eligibility for the feature switch. --- WooCommerce/Classes/Model/BetaFeature.swift | 3 +-- .../Settings/POS/POSEligibilityChecker.swift | 15 +++++---------- .../ViewRelated/Hub Menu/HubMenuViewModel.swift | 6 +++--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/Model/BetaFeature.swift b/WooCommerce/Classes/Model/BetaFeature.swift index d6df3a24523..319c74d4431 100644 --- a/WooCommerce/Classes/Model/BetaFeature.swift +++ b/WooCommerce/Classes/Model/BetaFeature.swift @@ -57,8 +57,7 @@ extension BetaFeature { case .inAppPurchases: return ServiceLocator.featureFlagService.isFeatureFlagEnabled(.inAppPurchasesDebugMenu) case .pointOfSale: - return ServiceLocator.featureFlagService.isFeatureFlagEnabled(.displayPointOfSaleToggle) && - UIDevice.current.userInterfaceIdiom == .pad + return POSEligibilityChecker().isEligible() default: return true } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift index c765e9d50a2..fefbe6d52d0 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/POS/POSEligibilityChecker.swift @@ -7,18 +7,15 @@ import struct Yosemite.SiteSetting /// Determines whether the POS entry point can be shown based on the selected store and feature gates. final class POSEligibilityChecker { - private let isBetaFeatureEnabled: Bool private let cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol private let siteSettings: [SiteSetting] private let currencySettings: CurrencySettings private let featureFlagService: FeatureFlagService - init(isBetaFeatureEnabled: Bool, - cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(), - siteSettings: [SiteSetting], - currencySettings: CurrencySettings, - featureFlagService: FeatureFlagService) { - self.isBetaFeatureEnabled = isBetaFeatureEnabled + init(cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCaseProtocol = CardPresentPaymentsOnboardingUseCase(), + siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings, + currencySettings: CurrencySettings = ServiceLocator.currencySettings, + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { self.siteSettings = siteSettings self.currencySettings = currencySettings self.cardPresentPaymentsOnboarding = cardPresentPaymentsOnboarding @@ -35,10 +32,8 @@ final class POSEligibilityChecker { let isCountryCodeUS = SiteAddress(siteSettings: siteSettings).countryCode == CountryCode.US let isCurrencyUSD = currencySettings.currencyCode == .USD - // Feature switch enabled - return isBetaFeatureEnabled // Tablet device - && UIDevice.current.userInterfaceIdiom == .pad + 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 diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 81d45f9ce59..1f22eb943b6 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -138,12 +138,12 @@ final class HubMenuViewModel: ObservableObject { } private func setupPOSElement() { - let eligibilityChecker = POSEligibilityChecker(isBetaFeatureEnabled: generalAppSettings.betaFeatureEnabled(.pointOfSale), - cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCase(), + let isBetaFeatureEnabled = generalAppSettings.betaFeatureEnabled(.pointOfSale) + let eligibilityChecker = POSEligibilityChecker(cardPresentPaymentsOnboarding: CardPresentPaymentsOnboardingUseCase(), siteSettings: ServiceLocator.selectedSiteSettings.siteSettings, currencySettings: ServiceLocator.currencySettings, featureFlagService: featureFlagService) - if eligibilityChecker.isEligible() { + if isBetaFeatureEnabled && eligibilityChecker.isEligible() { posElement = PointOfSaleEntryPoint() } else { posElement = nil From 47bc24aecf5595cc66811a93a6be57f365777c16 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 20 May 2024 17:11:37 +0800 Subject: [PATCH 3/3] Move menu item view to a function for reuse in 3 sections. --- .../ViewRelated/Hub Menu/HubMenu.swift | 79 ++++++------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index db38543340a..0ac7f3cb5f0 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -85,72 +85,21 @@ private extension HubMenu { // Point of Sale if let menu = viewModel.posElement { Section { - Button { - handleTap(menu: menu) - } label: { - Row(title: menu.title, - titleBadge: nil, - iconBadge: menu.iconBadge, - description: menu.description, - icon: .local(menu.icon), - chevron: .leading) - .foregroundColor(Color(menu.iconColor)) - } - .accessibilityIdentifier(menu.accessibilityIdentifier) - .overlay { - NavigationLink(value: menu.id) { - EmptyView() - } - .opacity(0) - } + menuItemView(menu: menu) } } // Settings Section Section(Localization.settings) { ForEach(viewModel.settingsElements, id: \.id) { menu in - Button { - handleTap(menu: menu) - } label: { - Row(title: menu.title, - titleBadge: nil, - iconBadge: menu.iconBadge, - description: menu.description, - icon: .local(menu.icon), - chevron: .leading) - .foregroundColor(Color(menu.iconColor)) - } - .accessibilityIdentifier(menu.accessibilityIdentifier) - .overlay { - NavigationLink(value: menu.id) { - EmptyView() - } - .opacity(0) - } + menuItemView(menu: menu) } } // General Section Section(Localization.general) { ForEach(viewModel.generalElements, id: \.id) { menu in - Button { - handleTap(menu: menu) - } label: { - Row(title: menu.title, - titleBadge: nil, - iconBadge: menu.iconBadge, - description: menu.description, - icon: .local(menu.icon), - chevron: .leading) - .foregroundColor(Color(menu.iconColor)) - } - .accessibilityIdentifier(menu.accessibilityIdentifier) - .overlay { - NavigationLink(value: menu.id) { - EmptyView() - } - .opacity(0) - } + menuItemView(menu: menu) } } } @@ -159,6 +108,28 @@ private extension HubMenu { .accentColor(Color(.listSelectedBackground)) } + @ViewBuilder + func menuItemView(menu: HubMenuItem) -> some View { + Button { + handleTap(menu: menu) + } label: { + Row(title: menu.title, + titleBadge: nil, + iconBadge: menu.iconBadge, + description: menu.description, + icon: .local(menu.icon), + chevron: .leading) + .foregroundColor(Color(menu.iconColor)) + } + .accessibilityIdentifier(menu.accessibilityIdentifier) + .overlay { + NavigationLink(value: menu.id) { + EmptyView() + } + .opacity(0) + } + } + @ViewBuilder func detailView(menuID: String) -> some View { Group {