diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 16db3e0bd24..2102b26851d 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -58,7 +58,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService { case .tapToPayOnIPhoneMilestone2: return true case .tapToPayBadge: - return buildConfig == .localDeveloper || buildConfig == .alpha + return true case .domainSettings: return true case .jetpackSetupWithApplicationPassword: diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index b2d34e29d5e..b525e309957 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -7,6 +7,7 @@ - [*] Add Products: A new view is display to celebrate when the first product is created in a store. [https://github.com/woocommerce/woocommerce-ios/pull/9790] - [*] Product List: Added swipe-to-share gesture on product rows. [https://github.com/woocommerce/woocommerce-ios/pull/9799] - [*] Product form: a share action is shown in the navigation bar if the product can be shared and no more than one action is displayed, in addition to the more menu > Share. [https://github.com/woocommerce/woocommerce-ios/pull/9789] +- [*] Payments: show badges leading to Set up Tap to Pay on iPhone for eligible stores and devices [https://github.com/woocommerce/woocommerce-ios/pull/9812] - [*] Orders: Fixes a bug where the Orders list would not load if an order had a non-integer gift card amount applied to the order (with the Gift Cards extension). [https://github.com/woocommerce/woocommerce-ios/pull/9795] - [*] My Store: A new button to share the current store is added on the top right of the screen. [https://github.com/woocommerce/woocommerce-ios/pull/9796] diff --git a/WooCommerce/Classes/Tools/Shared Site Settings/SelectedSiteSettings.swift b/WooCommerce/Classes/Tools/Shared Site Settings/SelectedSiteSettings.swift index e352f62451d..d68f7fbfcf6 100644 --- a/WooCommerce/Classes/Tools/Shared Site Settings/SelectedSiteSettings.swift +++ b/WooCommerce/Classes/Tools/Shared Site Settings/SelectedSiteSettings.swift @@ -3,6 +3,14 @@ import Yosemite import Storage import WooFoundation + +extension Notification.Name { + + /// Posted whenever the selectedSiteSettings are refreshed. + /// + public static let selectedSiteSettingsRefreshed = Notification.Name(rawValue: "selectedSiteSettingsRefreshed") +} + /// Settings for the selected Site /// final class SelectedSiteSettings: NSObject { @@ -63,6 +71,8 @@ extension SelectedSiteSettings { ServiceLocator.currencySettings.updateCurrencyOptions(with: $0) } + NotificationCenter.default.post(name: .selectedSiteSettingsRefreshed, object: nil) + // Needed to correcly format the widget data. UserDefaults.group?[.defaultStoreCurrencySettings] = try? JSONEncoder().encode(ServiceLocator.currencySettings) } diff --git a/WooCommerce/Classes/ViewModels/MainTabViewModel.swift b/WooCommerce/Classes/ViewModels/MainTabViewModel.swift index ab8c171adab..8341fe1ba6d 100644 --- a/WooCommerce/Classes/ViewModels/MainTabViewModel.swift +++ b/WooCommerce/Classes/ViewModels/MainTabViewModel.swift @@ -38,6 +38,8 @@ final class MainTabViewModel { private var cancellables = Set() + let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker = TapToPayBadgePromotionChecker() + init(storesManager: StoresManager = ServiceLocator.stores, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { self.storesManager = storesManager @@ -93,8 +95,7 @@ final class MainTabViewModel { listenToReviewsBadgeReloadRequired() retrieveShouldShowReviewsBadgeOnHubMenuTabValue() - listenToNewFeatureBadgeReloadRequired() - retrieveShouldShowNewFeatureBadgeOnHubMenuTabValue() + tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldShowNewFeatureBadgeOnHubMenuTab) } } @@ -200,21 +201,6 @@ private extension MainTabViewModel { object: nil) } - - func listenToNewFeatureBadgeReloadRequired() { - NotificationCenter.default.addObserver(self, - selector: #selector(setUpTapToPayViewDidAppear), - name: .setUpTapToPayViewDidAppear, - object: nil) - - } - - /// Updates the badge after the Set up Tap to Pay flow did appear - /// - @objc func setUpTapToPayViewDidAppear() { - shouldShowNewFeatureBadgeOnHubMenuTab = false - } - /// Retrieves whether we should show the reviews on the Menu button and updates `shouldShowReviewsBadge` /// @objc func retrieveShouldShowReviewsBadgeOnHubMenuTabValue() { @@ -229,22 +215,6 @@ private extension MainTabViewModel { storesManager.dispatch(notificationCountAction) } - /// Retrieves whether we should show the new feature badge on the Menu button - /// - func retrieveShouldShowNewFeatureBadgeOnHubMenuTabValue() { - let action = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .tapToPayHubMenuBadge) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let visible): - self.shouldShowNewFeatureBadgeOnHubMenuTab = visible && self.featureFlagService.isFeatureFlagEnabled(.tapToPayBadge) - case .failure: - self.shouldShowNewFeatureBadgeOnHubMenuTab = false - } - } - - storesManager.dispatch(action) - } - /// Listens for changes on the menu badge display logic and updates it depending on them /// func synchronizeShouldShowBadgeOnHubMenuTabLogic() { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/SetUpTapToPayInformationViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/SetUpTapToPayInformationViewModel.swift index b3eb4cb65cd..47234043e47 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/SetUpTapToPayInformationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/SetUpTapToPayInformationViewModel.swift @@ -130,8 +130,6 @@ final class SetUpTapToPayInformationViewModel: PaymentSettingsFlowPresentedViewM } func viewDidAppear() { - let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .tapToPayHubMenuBadge, remindAfterDays: nil, onCompletion: nil) - stores.dispatch(action) NotificationCenter.default.post(name: .setUpTapToPayViewDidAppear, object: nil) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift index 10592246a9d..3ee153a330c 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift @@ -68,11 +68,11 @@ final class InPersonPaymentsMenuViewController: UIViewController { /// - viewDidLoadAction: Provided as a one-time callback on viewDidLoad, originally to handle universal link navigation correctly. init(stores: StoresManager = ServiceLocator.stores, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, - shouldShowBadgeOnSetUpTapToPay: Bool, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker, viewDidLoadAction: ((InPersonPaymentsMenuViewController) -> Void)? = nil) { self.stores = stores self.featureFlagService = featureFlagService - self.viewModel = InPersonPaymentsMenuViewModel(shouldShowBadgeOnSetUpTapToPay: shouldShowBadgeOnSetUpTapToPay) + self.viewModel = InPersonPaymentsMenuViewModel(dependencies: .init(tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker)) self.cardPresentPaymentsOnboardingUseCase = CardPresentPaymentsOnboardingUseCase() self.cashOnDeliveryToggleRowViewModel = InPersonPaymentsCashOnDeliveryToggleRowViewModel() self.setUpFlowOnlyEnabledAfterOnboardingComplete = !featureFlagService.isFeatureFlagEnabled(.tapToPayOnIPhoneMilestone2) @@ -767,10 +767,10 @@ private extension InPersonPaymentsMenuViewController { /// SwiftUI wrapper for CardReaderSettingsPresentingViewController /// struct InPersonPaymentsMenu: UIViewControllerRepresentable { - let shouldShowBadgeOnSetUpTapToPay: Bool + let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker func makeUIViewController(context: Context) -> some UIViewController { - InPersonPaymentsMenuViewController(shouldShowBadgeOnSetUpTapToPay: shouldShowBadgeOnSetUpTapToPay) + InPersonPaymentsMenuViewController(tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker) } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift index fd52859da7d..b77e7935327 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift @@ -7,11 +7,14 @@ final class InPersonPaymentsMenuViewModel { struct Dependencies { let stores: StoresManager let analytics: Analytics + let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker init(stores: StoresManager = ServiceLocator.stores, - analytics: Analytics = ServiceLocator.analytics) { + analytics: Analytics = ServiceLocator.analytics, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker? = nil) { self.stores = stores self.analytics = analytics + self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker ?? TapToPayBadgePromotionChecker(stores: stores) } } @@ -44,11 +47,10 @@ final class InPersonPaymentsMenuViewModel { let cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration init(dependencies: Dependencies = Dependencies(), - cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration, - shouldShowBadgeOnSetUpTapToPay: Bool) { + cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration) { self.dependencies = dependencies self.cardPresentPaymentsConfiguration = cardPresentPaymentsConfiguration - self.shouldBadgeTapToPayOnIPhone = shouldShowBadgeOnSetUpTapToPay + dependencies.tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldBadgeTapToPayOnIPhone) } func viewDidLoad() { @@ -100,10 +102,6 @@ final class InPersonPaymentsMenuViewModel { selector: #selector(refreshTapToPayFeedbackVisibility), name: .firstInPersonPaymentsTransactionsWereUpdated, object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(setUpTapToPayViewDidAppear), - name: .setUpTapToPayViewDidAppear, - object: nil) } @objc func refreshTapToPayFeedbackVisibility() { @@ -113,10 +111,6 @@ final class InPersonPaymentsMenuViewModel { checkShouldShowTapToPayFeedbackRow(siteID: siteID) } - @objc private func setUpTapToPayViewDidAppear() { - self.shouldBadgeTapToPayOnIPhone = false - } - func orderCardReaderPressed() { analytics.track(.paymentsMenuOrderCardReaderTapped) showWebView = PurchaseCardReaderWebViewViewModel(configuration: cardPresentPaymentsConfiguration, @@ -134,9 +128,6 @@ final class InPersonPaymentsMenuViewModel { NotificationCenter.default.removeObserver(self, name: .firstInPersonPaymentsTransactionsWereUpdated, object: nil) - NotificationCenter.default.removeObserver(self, - name: .setUpTapToPayViewDidAppear, - object: nil) } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/TapToPayBadgePromotionChecker.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/TapToPayBadgePromotionChecker.swift new file mode 100644 index 00000000000..221cd0b26e4 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/TapToPayBadgePromotionChecker.swift @@ -0,0 +1,85 @@ +import Foundation +import Yosemite +import Experiments +import Combine + +final class TapToPayBadgePromotionChecker { + private let featureFlagService: FeatureFlagService + private let stores: StoresManager + + @Published private(set) var shouldShowTapToPayBadges: Bool = false + + private var cancellables: Set = [] + + init(featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, + stores: StoresManager = ServiceLocator.stores) { + self.featureFlagService = featureFlagService + self.stores = stores + + listenToTapToPayBadgeReloadRequired() + Task { + await checkTapToPayBadgeVisibility() + } + } + + func hideTapToPayBadge() { + guard shouldShowTapToPayBadges else { + return + } + let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .tapToPayHubMenuBadge, remindAfterDays: nil, onCompletion: nil) + stores.dispatch(action) + shouldShowTapToPayBadges = false + } + + @MainActor + private func checkTapToPayBadgeVisibility() async { + guard let siteID = stores.sessionManager.defaultStoreID else { + return shouldShowTapToPayBadges = false + } + + let supportDeterminer = CardReaderSupportDeterminer(siteID: siteID) + guard supportDeterminer.siteSupportsLocalMobileReader(), + await supportDeterminer.deviceSupportsLocalMobileReader(), + await !supportDeterminer.hasPreviousTapToPayUsage() else { + return shouldShowTapToPayBadges = false + } + + do { + let visible = try await withCheckedThrowingContinuation({ [weak self] continuation in + let action = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .tapToPayHubMenuBadge) { result in + continuation.resume(with: result) + } + self?.stores.dispatch(action) + }) + shouldShowTapToPayBadges = visible && featureFlagService.isFeatureFlagEnabled(.tapToPayBadge) + } catch { + DDLogError("Could not fetch feature announcement visibility \(error)") + } + } + + private func listenToTapToPayBadgeReloadRequired() { + NotificationCenter.default.addObserver(self, + selector: #selector(setUpTapToPayViewDidAppear), + name: .setUpTapToPayViewDidAppear, + object: nil) + // It's not ideal that we need this, and the notification should be removed when we remove this badge. + // Changing the store recreates this class, so we check for support again... however, the store country is + // fetched by the CardPresentPaymentsConfigurationLoader, from the `ServiceLocator.selectedSiteSettings`. + // The site settings are not updated until slightly later, so we need to refresh the badge logic when they are. + // Ideally, we would improve the CardPresentConfigurationLoader to accurately get the current country. + NotificationCenter.default.addObserver(self, + selector: #selector(refreshBadgeVisibility), + name: .selectedSiteSettingsRefreshed, + object: nil) + } + + @objc private func setUpTapToPayViewDidAppear() { + hideTapToPayBadge() + } + + @objc private func refreshBadgeVisibility() { + Task { + await checkTapToPayBadgeVisibility() + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift index aa0ef45fc84..109bfaddc9d 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift @@ -105,7 +105,7 @@ struct HubMenu: View { EmptyView() }.hidden() NavigationLink(destination: - InPersonPaymentsMenu(shouldShowBadgeOnSetUpTapToPay: viewModel.shouldShowNewFeatureBadgeOnPayments) + InPersonPaymentsMenu(tapToPayBadgePromotionChecker: viewModel.tapToPayBadgePromotionChecker) .navigationTitle(InPersonPaymentsView.Localization.title), isActive: $showingPayments) { EmptyView() @@ -334,17 +334,17 @@ private extension HubMenu { struct HubMenu_Previews: PreviewProvider { static var previews: some View { - HubMenu(viewModel: .init(siteID: 123)) + HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker())) .environment(\.colorScheme, .light) - HubMenu(viewModel: .init(siteID: 123)) + HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker())) .environment(\.colorScheme, .dark) - HubMenu(viewModel: .init(siteID: 123)) + HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker())) .previewLayout(.fixed(width: 312, height: 528)) .environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge) - HubMenu(viewModel: .init(siteID: 123)) + HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker())) .previewLayout(.fixed(width: 1024, height: 768)) } } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift index bf9f5eae3d7..f2f0f6a5903 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift @@ -22,26 +22,33 @@ final class HubMenuCoordinator: Coordinator { private let willPresentReviewDetailsFromPushNotification: () async -> Void + private let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker + init(navigationController: UINavigationController, pushNotificationsManager: PushNotesManager = ServiceLocator.pushNotesManager, storesManager: StoresManager = ServiceLocator.stores, noticePresenter: NoticePresenter = ServiceLocator.noticePresenter, switchStoreUseCase: SwitchStoreUseCaseProtocol, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker, willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) { self.pushNotificationsManager = pushNotificationsManager self.storesManager = storesManager self.noticePresenter = noticePresenter self.switchStoreUseCase = switchStoreUseCase + self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker self.willPresentReviewDetailsFromPushNotification = willPresentReviewDetailsFromPushNotification self.navigationController = navigationController } - convenience init(navigationController: UINavigationController, willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) { + convenience init(navigationController: UINavigationController, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker, + willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) { let storesManager = ServiceLocator.stores self.init(navigationController: navigationController, storesManager: storesManager, switchStoreUseCase: SwitchStoreUseCase(stores: storesManager), + tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker, willPresentReviewDetailsFromPushNotification: willPresentReviewDetailsFromPushNotification) } @@ -55,7 +62,9 @@ final class HubMenuCoordinator: Coordinator { /// Replaces `start()` because the menu tab's navigation stack could be updated multiple times when site ID changes. func activate(siteID: Int64) { - hubMenuController = HubMenuViewController(siteID: siteID, navigationController: navigationController) + hubMenuController = HubMenuViewController(siteID: siteID, + navigationController: navigationController, + tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker) if let hubMenuController = hubMenuController { navigationController.viewControllers = [hubMenuController] } diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift index 9bec8da7e57..38f9ef48587 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewController.swift @@ -5,10 +5,15 @@ import Yosemite /// Displays a grid view of all available menu in the "Menu" tab (eg. View Store, Reviews, Coupons, etc...) final class HubMenuViewController: UIHostingController { private let viewModel: HubMenuViewModel + private let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker - init(siteID: Int64, navigationController: UINavigationController?) { + init(siteID: Int64, + navigationController: UINavigationController?, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker) { self.viewModel = HubMenuViewModel(siteID: siteID, - navigationController: navigationController) + navigationController: navigationController, + tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker) + self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker super.init(rootView: HubMenu(viewModel: viewModel)) configureTabBarItem() } @@ -31,7 +36,7 @@ final class HubMenuViewController: UIHostingController { func showPaymentsMenu(onCompletion: ((InPersonPaymentsMenuViewController) -> Void)? = nil) -> InPersonPaymentsMenuViewController { let inPersonPaymentsMenuViewController = InPersonPaymentsMenuViewController( - shouldShowBadgeOnSetUpTapToPay: viewModel.shouldShowNewFeatureBadgeOnPayments, + tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker, viewDidLoadAction: onCompletion) show(inPersonPaymentsMenuViewController, sender: self) diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 80e6de07db2..90ef1e0ab3d 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -65,21 +65,24 @@ final class HubMenuViewModel: ObservableObject { private var cancellables: Set = [] + let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker + init(siteID: Int64, navigationController: UINavigationController? = nil, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, stores: StoresManager = ServiceLocator.stores, generalAppSettings: GeneralAppSettingsStorage = ServiceLocator.generalAppSettings) { self.siteID = siteID self.navigationController = navigationController + self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker self.stores = stores self.featureFlagService = featureFlagService self.generalAppSettings = generalAppSettings self.switchStoreEnabled = stores.isAuthenticatedWithoutWPCom == false observeSiteForUIUpdates() observePlanName() - listenToNewFeatureBadgeReloadRequired() - retrieveShouldShowNewFeatureBadgeOnPaymentsValue() + tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldShowNewFeatureBadgeOnPayments) } func viewDidAppear() { @@ -138,36 +141,6 @@ final class HubMenuViewModel: ObservableObject { stores.dispatch(action) } - private func listenToNewFeatureBadgeReloadRequired() { - NotificationCenter.default.addObserver(self, - selector: #selector(setUpTapToPayViewDidAppear), - name: .setUpTapToPayViewDidAppear, - object: nil) - - } - - /// Retrieves whether we should show the new feature badge on the Menu button - /// - func retrieveShouldShowNewFeatureBadgeOnPaymentsValue() { - let action = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .tapToPayHubMenuBadge) { [weak self] result in - guard let self = self else { return } - switch result { - case .success(let visible): - self.shouldShowNewFeatureBadgeOnPayments = visible && self.featureFlagService.isFeatureFlagEnabled(.tapToPayBadge) - case .failure: - self.shouldShowNewFeatureBadgeOnPayments = false - } - } - - stores.dispatch(action) - } - - /// Updates the badge after the Set up Tap to Pay flow did appear - /// - @objc private func setUpTapToPayViewDidAppear() { - self.shouldShowNewFeatureBadgeOnPayments = false - } - /// Present the `StorePickerViewController` using the `StorePickerCoordinator`, passing the navigation controller from the entry point. /// func presentSwitchStore() { diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 1231c0a3add..969992e5266 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -559,6 +559,7 @@ private extension MainTabBarController { func createHubMenuTabCoordinator() -> HubMenuCoordinator { HubMenuCoordinator(navigationController: hubMenuNavigationController, + tapToPayBadgePromotionChecker: viewModel.tapToPayBadgePromotionChecker, willPresentReviewDetailsFromPushNotification: { [weak self] in await withCheckedContinuation { [weak self] continuation in self?.navigateTo(.hubMenu) { diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 0a24d0914a8..a34787008b5 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -565,6 +565,7 @@ 0373A12B2A1D1E4D00731236 /* BadgedLeftImageTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0373A12A2A1D1E4D00731236 /* BadgedLeftImageTableViewCell.xib */; }; 0373A12D2A1D1E6000731236 /* BadgedLeftImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373A12C2A1D1E6000731236 /* BadgedLeftImageTableViewCell.swift */; }; 0373A12F2A1D1F2100731236 /* DotView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373A12E2A1D1F2100731236 /* DotView.swift */; }; + 0373A1312A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0373A1302A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift */; }; 0375799928201F750083F2E1 /* CardPresentPaymentsReadinessUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0375799828201F750083F2E1 /* CardPresentPaymentsReadinessUseCase.swift */; }; 0375799B28227EDE0083F2E1 /* CardPresentPaymentsOnboardingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0375799A28227EDE0083F2E1 /* CardPresentPaymentsOnboardingPresenter.swift */; }; 0375799D2822F9040083F2E1 /* MockCardPresentPaymentsOnboardingPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0375799C2822F9040083F2E1 /* MockCardPresentPaymentsOnboardingPresenter.swift */; }; @@ -2867,6 +2868,7 @@ 0373A12A2A1D1E4D00731236 /* BadgedLeftImageTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BadgedLeftImageTableViewCell.xib; sourceTree = ""; }; 0373A12C2A1D1E6000731236 /* BadgedLeftImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgedLeftImageTableViewCell.swift; sourceTree = ""; }; 0373A12E2A1D1F2100731236 /* DotView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotView.swift; sourceTree = ""; }; + 0373A1302A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayBadgePromotionChecker.swift; sourceTree = ""; }; 0375799828201F750083F2E1 /* CardPresentPaymentsReadinessUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentsReadinessUseCase.swift; sourceTree = ""; }; 0375799A28227EDE0083F2E1 /* CardPresentPaymentsOnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentsOnboardingPresenter.swift; sourceTree = ""; }; 0375799C2822F9040083F2E1 /* MockCardPresentPaymentsOnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCardPresentPaymentsOnboardingPresenter.swift; sourceTree = ""; }; @@ -10177,6 +10179,7 @@ 03B9E5202A13971F005C77F5 /* TapToPayReconnectionController.swift */, 03B9E5282A14F136005C77F5 /* SilenceablePassthroughCardPresentPaymentAlertsPresenter.swift */, 03B9E5262A14EA89005C77F5 /* CardReaderSupportDeterminer.swift */, + 0373A1302A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift */, B90C65CC29ACE2D6004CAB9E /* CardPresentPaymentOnboardingStateCache.swift */, B946880F29B8DD01000646B0 /* InPersonPaymentsMenuViewController+Activity.swift */, ); @@ -12193,6 +12196,7 @@ B932847429A8D6E600B01251 /* CardPresentPaymentsOnboardingIPPUsersRefresher.swift in Sources */, AE3AA889290C303B00BE422D /* WebKitViewController.swift in Sources */, 4535EE7A281ADD56004212B4 /* CouponCodeInputFormatter.swift in Sources */, + 0373A1312A1E3FD700731236 /* TapToPayBadgePromotionChecker.swift in Sources */, DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */, 035DBA45292D0164003E5125 /* CollectOrderPaymentUseCase.swift in Sources */, 0282DD96233C960C006A5FDB /* SearchResultCell.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift index 5e84ff02347..ee2a981c680 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModelTests.swift @@ -28,8 +28,7 @@ class InPersonPaymentsMenuViewModelTests: XCTestCase { configuration = CardPresentPaymentsConfiguration(country: "US") sut = InPersonPaymentsMenuViewModel(dependencies: dependencies, - cardPresentPaymentsConfiguration: configuration, - shouldShowBadgeOnSetUpTapToPay: false) + cardPresentPaymentsConfiguration: configuration) } func test_viewDidLoad_synchronizes_payment_gateways() throws { @@ -92,8 +91,7 @@ class InPersonPaymentsMenuViewModelTests: XCTestCase { stripeSmallestCurrencyUnitMultiplier: 100) sut = InPersonPaymentsMenuViewModel(dependencies: dependencies, - cardPresentPaymentsConfiguration: configuration, - shouldShowBadgeOnSetUpTapToPay: false) + cardPresentPaymentsConfiguration: configuration) // When sut.viewDidLoad() @@ -118,8 +116,7 @@ class InPersonPaymentsMenuViewModelTests: XCTestCase { stripeSmallestCurrencyUnitMultiplier: 100) sut = InPersonPaymentsMenuViewModel(dependencies: dependencies, - cardPresentPaymentsConfiguration: configuration, - shouldShowBadgeOnSetUpTapToPay: false) + cardPresentPaymentsConfiguration: configuration) waitFor { promise in self.stores.whenReceivingAction(ofType: CardPresentPaymentAction.self) { action in diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuCoordinatorTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuCoordinatorTests.swift index eb14c1f9afa..f12d840f0bc 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuCoordinatorTests.swift @@ -125,6 +125,7 @@ private extension HubMenuCoordinatorTests { storesManager: storesManager, noticePresenter: noticePresenter, switchStoreUseCase: switchStoreUseCase, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), willPresentReviewDetailsFromPushNotification: willPresentReviewDetailsFromPushNotification) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift index 978f26149d1..e4c43596d66 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/HubMenu/HubMenuViewModelTests.swift @@ -8,7 +8,7 @@ final class HubMenuViewModelTests: XCTestCase { func test_viewDidAppear_then_posts_notification() { // Given - let viewModel = HubMenuViewModel(siteID: sampleSiteID) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker()) expectation(forNotification: .hubMenuViewDidAppear, object: nil, handler: nil) // When @@ -23,7 +23,9 @@ final class HubMenuViewModelTests: XCTestCase { let featureFlagService = MockFeatureFlagService(isInboxOn: false) // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService) // Then XCTAssertNil(viewModel.generalElements.firstIndex(where: { item in @@ -58,7 +60,10 @@ final class HubMenuViewModelTests: XCTestCase { } // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() // Then both inbox and coupons are in the menu @@ -98,7 +103,10 @@ final class HubMenuViewModelTests: XCTestCase { } // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() // Then @@ -134,7 +142,10 @@ final class HubMenuViewModelTests: XCTestCase { } // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() // Then neither inbox nor coupons is in the menu @@ -161,7 +172,10 @@ final class HubMenuViewModelTests: XCTestCase { break } } - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() // Then @@ -184,7 +198,10 @@ final class HubMenuViewModelTests: XCTestCase { break } } - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() XCTAssertNil(viewModel.generalElements.firstIndex(where: { item in @@ -202,6 +219,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -217,6 +235,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -232,6 +251,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then XCTAssertNotNil(viewModel.storeURL) @@ -249,6 +269,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then XCTAssertNotNil(viewModel.woocommerceAdminURL) @@ -266,6 +287,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then XCTAssertNotNil(viewModel.woocommerceAdminURL) @@ -283,6 +305,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -300,6 +323,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -317,6 +341,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -334,6 +359,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -351,6 +377,7 @@ final class HubMenuViewModelTests: XCTestCase { // When let viewModel = HubMenuViewModel(siteID: site.siteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), stores: stores) // Then @@ -366,7 +393,10 @@ final class HubMenuViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: sessionManager) // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() XCTAssertNotNil(viewModel.settingsElements.firstIndex(where: { item in @@ -383,7 +413,10 @@ final class HubMenuViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: sessionManager) // When - let viewModel = HubMenuViewModel(siteID: sampleSiteID, featureFlagService: featureFlagService, stores: stores) + let viewModel = HubMenuViewModel(siteID: sampleSiteID, + tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker(), + featureFlagService: featureFlagService, + stores: stores) viewModel.setupMenuElements() XCTAssertNil(viewModel.settingsElements.firstIndex(where: { item in diff --git a/Yosemite/Yosemite/Stores/AppSettingsStore.swift b/Yosemite/Yosemite/Stores/AppSettingsStore.swift index 025bf5892f3..c41ec86e8fb 100644 --- a/Yosemite/Yosemite/Stores/AppSettingsStore.swift +++ b/Yosemite/Yosemite/Stores/AppSettingsStore.swift @@ -789,16 +789,18 @@ private extension AppSettingsStore { extension AppSettingsStore { + /// Dismisses a feature announcement campaign, optionally reminding the user after the specified number of days elapses, + /// by marking the campaign as visible again. + /// - Parameters: + /// - campaign: campaign to dismiss + /// - remindAfterDays: optionally remind the user after this many days. If nil is passed, the campaign is permanently dismissed + /// - onCompletion: completion handler func setFeatureAnnouncementDismissed( campaign: FeatureAnnouncementCampaign, remindAfterDays: Int?, onCompletion: ((Result) -> ())?) { do { - guard let remindAfterDays else { - return - } - let remindAfter = Date().addingDays(remindAfterDays) - let newSettings = FeatureAnnouncementCampaignSettings(dismissedDate: Date(), remindAfter: remindAfter) + let newSettings = FeatureAnnouncementCampaignSettings(dismissedDate: Date(), remindAfter: date(adding: remindAfterDays)) let settings = generalAppSettings.settings let settingsToSave = settings.replacing(featureAnnouncementSettings: newSettings, for: campaign) @@ -810,6 +812,13 @@ extension AppSettingsStore { } } + private func date(adding days: Int?) -> Date? { + guard let days else { + return nil + } + return NSCalendar.current.date(byAdding: .day, value: days, to: Date()) + } + func getFeatureAnnouncementVisibility(campaign: FeatureAnnouncementCampaign, onCompletion: (Result) -> ()) { guard let campaignSettings = generalAppSettings.value(for: \.featureAnnouncementCampaignSettings)[campaign] else { return onCompletion(.success(true)) diff --git a/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift b/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift index a5b55e1b716..66fc81b0ce6 100644 --- a/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/AppSettingsStoreTests.swift @@ -841,7 +841,7 @@ final class AppSettingsStoreTests: XCTestCase { extension AppSettingsStoreTests { - func test_setFeatureAnnouncementDismissed_for_campaign_when_remindAfterDays_is_nil_then_does_not_store_current_date() throws { + func test_setFeatureAnnouncementDismissed_for_campaign_when_remindAfterDays_is_nil_then_dismissal_is_stored_with_no_reminder_date() throws { // Given try fileStorage?.deleteFile(at: expectedGeneralStoreSettingsFileURL) // When @@ -850,12 +850,13 @@ extension AppSettingsStoreTests { // Then let savedSettings: GeneralAppSettings? = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) - XCTAssertNil(savedSettings) guard let savedSettings else { - return + return XCTFail("Expected settings to be saved, but none were found") } - let savedDate: Date? = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) - XCTAssertNil(savedDate) + let dismissedDate: Date = try XCTUnwrap(savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) + XCTAssert(Calendar.current.isDateInToday(dismissedDate)) + let remindAfterDate: Date? = savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter + XCTAssertNil(remindAfterDate) } func test_setFeatureAnnouncementDismissed_for_campaign_stores_current_date() throws { @@ -871,7 +872,7 @@ extension AppSettingsStoreTests { // Then let savedSettings: GeneralAppSettings = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) - let actualDismissDate = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) + let actualDismissDate = try XCTUnwrap(savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) XCTAssert(Calendar.current.isDate(actualDismissDate, inSameDayAs: currentTime)) } @@ -890,7 +891,7 @@ extension AppSettingsStoreTests { // Then let savedSettings: GeneralAppSettings = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) - let actualRemindAfter = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter) + let actualRemindAfter = try XCTUnwrap(savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter) XCTAssert(Calendar.current.isDate(actualRemindAfter, inSameDayAs: twoWeeksTime)) } @@ -909,7 +910,7 @@ extension AppSettingsStoreTests { // Then let savedSettings: GeneralAppSettings = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) - let actualRemindAfter = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter) + let actualRemindAfter = try XCTUnwrap(savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.remindAfter) XCTAssert(Calendar.current.isDate(actualRemindAfter, inSameDayAs: oneWeekTime)) } @@ -930,7 +931,7 @@ extension AppSettingsStoreTests { // Then let savedSettings: GeneralAppSettings = try XCTUnwrap(fileStorage?.data(for: expectedGeneralAppSettingsFileURL)) - let actualDismissDate = try XCTUnwrap( savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) + let actualDismissDate = try XCTUnwrap(savedSettings.featureAnnouncementCampaignSettings[.upsellCardReaders]?.dismissedDate) XCTAssert(Calendar.current.isDate(actualDismissDate, inSameDayAs: currentTime))