Skip to content

Commit dedf976

Browse files
authored
Merge pull request #9812 from woocommerce/issue/9582-only-badge-tap-to-pay-on-eligible-devices
[Mobile Payments] Badge tap to pay on eligible devices and stores, until Set up Tap to Pay opened
2 parents d2936a8 + 2eccabb commit dedf976

File tree

19 files changed

+214
-126
lines changed

19 files changed

+214
-126
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
5858
case .tapToPayOnIPhoneMilestone2:
5959
return true
6060
case .tapToPayBadge:
61-
return buildConfig == .localDeveloper || buildConfig == .alpha
61+
return true
6262
case .domainSettings:
6363
return true
6464
case .jetpackSetupWithApplicationPassword:

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [*] 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]
88
- [*] Product List: Added swipe-to-share gesture on product rows. [https://github.com/woocommerce/woocommerce-ios/pull/9799]
99
- [*] 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]
10+
- [*] 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]
1011
- [*] 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]
1112

1213
- [*] 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]

WooCommerce/Classes/Tools/Shared Site Settings/SelectedSiteSettings.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import Yosemite
33
import Storage
44
import WooFoundation
55

6+
7+
extension Notification.Name {
8+
9+
/// Posted whenever the selectedSiteSettings are refreshed.
10+
///
11+
public static let selectedSiteSettingsRefreshed = Notification.Name(rawValue: "selectedSiteSettingsRefreshed")
12+
}
13+
614
/// Settings for the selected Site
715
///
816
final class SelectedSiteSettings: NSObject {
@@ -63,6 +71,8 @@ extension SelectedSiteSettings {
6371
ServiceLocator.currencySettings.updateCurrencyOptions(with: $0)
6472
}
6573

74+
NotificationCenter.default.post(name: .selectedSiteSettingsRefreshed, object: nil)
75+
6676
// Needed to correcly format the widget data.
6777
UserDefaults.group?[.defaultStoreCurrencySettings] = try? JSONEncoder().encode(ServiceLocator.currencySettings)
6878
}

WooCommerce/Classes/ViewModels/MainTabViewModel.swift

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ final class MainTabViewModel {
3838

3939
private var cancellables = Set<AnyCancellable>()
4040

41+
let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker = TapToPayBadgePromotionChecker()
42+
4143
init(storesManager: StoresManager = ServiceLocator.stores,
4244
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
4345
self.storesManager = storesManager
@@ -93,8 +95,7 @@ final class MainTabViewModel {
9395
listenToReviewsBadgeReloadRequired()
9496
retrieveShouldShowReviewsBadgeOnHubMenuTabValue()
9597

96-
listenToNewFeatureBadgeReloadRequired()
97-
retrieveShouldShowNewFeatureBadgeOnHubMenuTabValue()
98+
tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldShowNewFeatureBadgeOnHubMenuTab)
9899
}
99100
}
100101

@@ -200,21 +201,6 @@ private extension MainTabViewModel {
200201
object: nil)
201202
}
202203

203-
204-
func listenToNewFeatureBadgeReloadRequired() {
205-
NotificationCenter.default.addObserver(self,
206-
selector: #selector(setUpTapToPayViewDidAppear),
207-
name: .setUpTapToPayViewDidAppear,
208-
object: nil)
209-
210-
}
211-
212-
/// Updates the badge after the Set up Tap to Pay flow did appear
213-
///
214-
@objc func setUpTapToPayViewDidAppear() {
215-
shouldShowNewFeatureBadgeOnHubMenuTab = false
216-
}
217-
218204
/// Retrieves whether we should show the reviews on the Menu button and updates `shouldShowReviewsBadge`
219205
///
220206
@objc func retrieveShouldShowReviewsBadgeOnHubMenuTabValue() {
@@ -229,22 +215,6 @@ private extension MainTabViewModel {
229215
storesManager.dispatch(notificationCountAction)
230216
}
231217

232-
/// Retrieves whether we should show the new feature badge on the Menu button
233-
///
234-
func retrieveShouldShowNewFeatureBadgeOnHubMenuTabValue() {
235-
let action = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .tapToPayHubMenuBadge) { [weak self] result in
236-
guard let self = self else { return }
237-
switch result {
238-
case .success(let visible):
239-
self.shouldShowNewFeatureBadgeOnHubMenuTab = visible && self.featureFlagService.isFeatureFlagEnabled(.tapToPayBadge)
240-
case .failure:
241-
self.shouldShowNewFeatureBadgeOnHubMenuTab = false
242-
}
243-
}
244-
245-
storesManager.dispatch(action)
246-
}
247-
248218
/// Listens for changes on the menu badge display logic and updates it depending on them
249219
///
250220
func synchronizeShouldShowBadgeOnHubMenuTabLogic() {

WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/SetUpTapToPayInformationViewModel.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ final class SetUpTapToPayInformationViewModel: PaymentSettingsFlowPresentedViewM
130130
}
131131

132132
func viewDidAppear() {
133-
let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .tapToPayHubMenuBadge, remindAfterDays: nil, onCompletion: nil)
134-
stores.dispatch(action)
135133
NotificationCenter.default.post(name: .setUpTapToPayViewDidAppear, object: nil)
136134
}
137135

WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewController.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ final class InPersonPaymentsMenuViewController: UIViewController {
6868
/// - viewDidLoadAction: Provided as a one-time callback on viewDidLoad, originally to handle universal link navigation correctly.
6969
init(stores: StoresManager = ServiceLocator.stores,
7070
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
71-
shouldShowBadgeOnSetUpTapToPay: Bool,
71+
tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker,
7272
viewDidLoadAction: ((InPersonPaymentsMenuViewController) -> Void)? = nil) {
7373
self.stores = stores
7474
self.featureFlagService = featureFlagService
75-
self.viewModel = InPersonPaymentsMenuViewModel(shouldShowBadgeOnSetUpTapToPay: shouldShowBadgeOnSetUpTapToPay)
75+
self.viewModel = InPersonPaymentsMenuViewModel(dependencies: .init(tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker))
7676
self.cardPresentPaymentsOnboardingUseCase = CardPresentPaymentsOnboardingUseCase()
7777
self.cashOnDeliveryToggleRowViewModel = InPersonPaymentsCashOnDeliveryToggleRowViewModel()
7878
self.setUpFlowOnlyEnabledAfterOnboardingComplete = !featureFlagService.isFeatureFlagEnabled(.tapToPayOnIPhoneMilestone2)
@@ -767,10 +767,10 @@ private extension InPersonPaymentsMenuViewController {
767767
/// SwiftUI wrapper for CardReaderSettingsPresentingViewController
768768
///
769769
struct InPersonPaymentsMenu: UIViewControllerRepresentable {
770-
let shouldShowBadgeOnSetUpTapToPay: Bool
770+
let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker
771771

772772
func makeUIViewController(context: Context) -> some UIViewController {
773-
InPersonPaymentsMenuViewController(shouldShowBadgeOnSetUpTapToPay: shouldShowBadgeOnSetUpTapToPay)
773+
InPersonPaymentsMenuViewController(tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker)
774774
}
775775

776776
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

WooCommerce/Classes/ViewRelated/Dashboard/Settings/In-Person Payments/InPersonPaymentsMenuViewModel.swift

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ final class InPersonPaymentsMenuViewModel {
77
struct Dependencies {
88
let stores: StoresManager
99
let analytics: Analytics
10+
let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker
1011

1112
init(stores: StoresManager = ServiceLocator.stores,
12-
analytics: Analytics = ServiceLocator.analytics) {
13+
analytics: Analytics = ServiceLocator.analytics,
14+
tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker? = nil) {
1315
self.stores = stores
1416
self.analytics = analytics
17+
self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker ?? TapToPayBadgePromotionChecker(stores: stores)
1518
}
1619
}
1720

@@ -44,11 +47,10 @@ final class InPersonPaymentsMenuViewModel {
4447
let cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration
4548

4649
init(dependencies: Dependencies = Dependencies(),
47-
cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration,
48-
shouldShowBadgeOnSetUpTapToPay: Bool) {
50+
cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration) {
4951
self.dependencies = dependencies
5052
self.cardPresentPaymentsConfiguration = cardPresentPaymentsConfiguration
51-
self.shouldBadgeTapToPayOnIPhone = shouldShowBadgeOnSetUpTapToPay
53+
dependencies.tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldBadgeTapToPayOnIPhone)
5254
}
5355

5456
func viewDidLoad() {
@@ -100,10 +102,6 @@ final class InPersonPaymentsMenuViewModel {
100102
selector: #selector(refreshTapToPayFeedbackVisibility),
101103
name: .firstInPersonPaymentsTransactionsWereUpdated,
102104
object: nil)
103-
NotificationCenter.default.addObserver(self,
104-
selector: #selector(setUpTapToPayViewDidAppear),
105-
name: .setUpTapToPayViewDidAppear,
106-
object: nil)
107105
}
108106

109107
@objc func refreshTapToPayFeedbackVisibility() {
@@ -113,10 +111,6 @@ final class InPersonPaymentsMenuViewModel {
113111
checkShouldShowTapToPayFeedbackRow(siteID: siteID)
114112
}
115113

116-
@objc private func setUpTapToPayViewDidAppear() {
117-
self.shouldBadgeTapToPayOnIPhone = false
118-
}
119-
120114
func orderCardReaderPressed() {
121115
analytics.track(.paymentsMenuOrderCardReaderTapped)
122116
showWebView = PurchaseCardReaderWebViewViewModel(configuration: cardPresentPaymentsConfiguration,
@@ -134,9 +128,6 @@ final class InPersonPaymentsMenuViewModel {
134128
NotificationCenter.default.removeObserver(self,
135129
name: .firstInPersonPaymentsTransactionsWereUpdated,
136130
object: nil)
137-
NotificationCenter.default.removeObserver(self,
138-
name: .setUpTapToPayViewDidAppear,
139-
object: nil)
140131
}
141132
}
142133

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Foundation
2+
import Yosemite
3+
import Experiments
4+
import Combine
5+
6+
final class TapToPayBadgePromotionChecker {
7+
private let featureFlagService: FeatureFlagService
8+
private let stores: StoresManager
9+
10+
@Published private(set) var shouldShowTapToPayBadges: Bool = false
11+
12+
private var cancellables: Set<AnyCancellable> = []
13+
14+
init(featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
15+
stores: StoresManager = ServiceLocator.stores) {
16+
self.featureFlagService = featureFlagService
17+
self.stores = stores
18+
19+
listenToTapToPayBadgeReloadRequired()
20+
Task {
21+
await checkTapToPayBadgeVisibility()
22+
}
23+
}
24+
25+
func hideTapToPayBadge() {
26+
guard shouldShowTapToPayBadges else {
27+
return
28+
}
29+
let action = AppSettingsAction.setFeatureAnnouncementDismissed(campaign: .tapToPayHubMenuBadge, remindAfterDays: nil, onCompletion: nil)
30+
stores.dispatch(action)
31+
shouldShowTapToPayBadges = false
32+
}
33+
34+
@MainActor
35+
private func checkTapToPayBadgeVisibility() async {
36+
guard let siteID = stores.sessionManager.defaultStoreID else {
37+
return shouldShowTapToPayBadges = false
38+
}
39+
40+
let supportDeterminer = CardReaderSupportDeterminer(siteID: siteID)
41+
guard supportDeterminer.siteSupportsLocalMobileReader(),
42+
await supportDeterminer.deviceSupportsLocalMobileReader(),
43+
await !supportDeterminer.hasPreviousTapToPayUsage() else {
44+
return shouldShowTapToPayBadges = false
45+
}
46+
47+
do {
48+
let visible = try await withCheckedThrowingContinuation({ [weak self] continuation in
49+
let action = AppSettingsAction.getFeatureAnnouncementVisibility(campaign: .tapToPayHubMenuBadge) { result in
50+
continuation.resume(with: result)
51+
}
52+
self?.stores.dispatch(action)
53+
})
54+
shouldShowTapToPayBadges = visible && featureFlagService.isFeatureFlagEnabled(.tapToPayBadge)
55+
} catch {
56+
DDLogError("Could not fetch feature announcement visibility \(error)")
57+
}
58+
}
59+
60+
private func listenToTapToPayBadgeReloadRequired() {
61+
NotificationCenter.default.addObserver(self,
62+
selector: #selector(setUpTapToPayViewDidAppear),
63+
name: .setUpTapToPayViewDidAppear,
64+
object: nil)
65+
// It's not ideal that we need this, and the notification should be removed when we remove this badge.
66+
// Changing the store recreates this class, so we check for support again... however, the store country is
67+
// fetched by the CardPresentPaymentsConfigurationLoader, from the `ServiceLocator.selectedSiteSettings`.
68+
// The site settings are not updated until slightly later, so we need to refresh the badge logic when they are.
69+
// Ideally, we would improve the CardPresentConfigurationLoader to accurately get the current country.
70+
NotificationCenter.default.addObserver(self,
71+
selector: #selector(refreshBadgeVisibility),
72+
name: .selectedSiteSettingsRefreshed,
73+
object: nil)
74+
}
75+
76+
@objc private func setUpTapToPayViewDidAppear() {
77+
hideTapToPayBadge()
78+
}
79+
80+
@objc private func refreshBadgeVisibility() {
81+
Task {
82+
await checkTapToPayBadgeVisibility()
83+
}
84+
}
85+
}

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ struct HubMenu: View {
105105
EmptyView()
106106
}.hidden()
107107
NavigationLink(destination:
108-
InPersonPaymentsMenu(shouldShowBadgeOnSetUpTapToPay: viewModel.shouldShowNewFeatureBadgeOnPayments)
108+
InPersonPaymentsMenu(tapToPayBadgePromotionChecker: viewModel.tapToPayBadgePromotionChecker)
109109
.navigationTitle(InPersonPaymentsView.Localization.title),
110110
isActive: $showingPayments) {
111111
EmptyView()
@@ -334,17 +334,17 @@ private extension HubMenu {
334334

335335
struct HubMenu_Previews: PreviewProvider {
336336
static var previews: some View {
337-
HubMenu(viewModel: .init(siteID: 123))
337+
HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker()))
338338
.environment(\.colorScheme, .light)
339339

340-
HubMenu(viewModel: .init(siteID: 123))
340+
HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker()))
341341
.environment(\.colorScheme, .dark)
342342

343-
HubMenu(viewModel: .init(siteID: 123))
343+
HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker()))
344344
.previewLayout(.fixed(width: 312, height: 528))
345345
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
346346

347-
HubMenu(viewModel: .init(siteID: 123))
347+
HubMenu(viewModel: .init(siteID: 123, tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker()))
348348
.previewLayout(.fixed(width: 1024, height: 768))
349349
}
350350
}

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,33 @@ final class HubMenuCoordinator: Coordinator {
2222

2323
private let willPresentReviewDetailsFromPushNotification: () async -> Void
2424

25+
private let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker
26+
2527
init(navigationController: UINavigationController,
2628
pushNotificationsManager: PushNotesManager = ServiceLocator.pushNotesManager,
2729
storesManager: StoresManager = ServiceLocator.stores,
2830
noticePresenter: NoticePresenter = ServiceLocator.noticePresenter,
2931
switchStoreUseCase: SwitchStoreUseCaseProtocol,
32+
tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker,
3033
willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) {
3134

3235
self.pushNotificationsManager = pushNotificationsManager
3336
self.storesManager = storesManager
3437
self.noticePresenter = noticePresenter
3538
self.switchStoreUseCase = switchStoreUseCase
39+
self.tapToPayBadgePromotionChecker = tapToPayBadgePromotionChecker
3640
self.willPresentReviewDetailsFromPushNotification = willPresentReviewDetailsFromPushNotification
3741
self.navigationController = navigationController
3842
}
3943

40-
convenience init(navigationController: UINavigationController, willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) {
44+
convenience init(navigationController: UINavigationController,
45+
tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker,
46+
willPresentReviewDetailsFromPushNotification: @escaping () async -> Void) {
4147
let storesManager = ServiceLocator.stores
4248
self.init(navigationController: navigationController,
4349
storesManager: storesManager,
4450
switchStoreUseCase: SwitchStoreUseCase(stores: storesManager),
51+
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker,
4552
willPresentReviewDetailsFromPushNotification: willPresentReviewDetailsFromPushNotification)
4653
}
4754

@@ -55,7 +62,9 @@ final class HubMenuCoordinator: Coordinator {
5562

5663
/// Replaces `start()` because the menu tab's navigation stack could be updated multiple times when site ID changes.
5764
func activate(siteID: Int64) {
58-
hubMenuController = HubMenuViewController(siteID: siteID, navigationController: navigationController)
65+
hubMenuController = HubMenuViewController(siteID: siteID,
66+
navigationController: navigationController,
67+
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker)
5968
if let hubMenuController = hubMenuController {
6069
navigationController.viewControllers = [hubMenuController]
6170
}

0 commit comments

Comments
 (0)