Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
36 changes: 3 additions & 33 deletions WooCommerce/Classes/ViewModels/MainTabViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ final class MainTabViewModel {

private var cancellables = Set<AnyCancellable>()

let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker = TapToPayBadgePromotionChecker()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps for another PR: Should we abstract this class into some PromotionChecker protocol to keep it detached from the specific TTP promotion checker? It could make sense also inject the protocol in the initializer for testability and to use this common protocol in the future for other feature announcements.


init(storesManager: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
self.storesManager = storesManager
Expand Down Expand Up @@ -93,8 +95,7 @@ final class MainTabViewModel {
listenToReviewsBadgeReloadRequired()
retrieveShouldShowReviewsBadgeOnHubMenuTabValue()

listenToNewFeatureBadgeReloadRequired()
retrieveShouldShowNewFeatureBadgeOnHubMenuTabValue()
tapToPayBadgePromotionChecker.$shouldShowTapToPayBadges.share().assign(to: &$shouldShowNewFeatureBadgeOnHubMenuTab)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as HubMenuViewModel: Should we wrap this line in a method? Sort of "bind/observe badge changes"?

}
}

Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand All @@ -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,
Expand All @@ -134,9 +128,6 @@ final class InPersonPaymentsMenuViewModel {
NotificationCenter.default.removeObserver(self,
name: .firstInPersonPaymentsTransactionsWereUpdated,
object: nil)
NotificationCenter.default.removeObserver(self,
name: .setUpTapToPayViewDidAppear,
object: nil)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<AnyCancellable> = []

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()
}
}
}
10 changes: 5 additions & 5 deletions WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
}
}
13 changes: 11 additions & 2 deletions WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,33 @@ final class HubMenuCoordinator: Coordinator {

private let willPresentReviewDetailsFromPushNotification: () async -> Void

private let tapToPayBadgePromotionChecker: TapToPayBadgePromotionChecker
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as with MainTabViewModel regarding abstracting this class into a protocol, as would seem that we're leaking implementation details about the badges into the HubMenu navigation coordinator responsibility.


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)
}

Expand All @@ -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]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HubMenu> {
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()
}
Expand All @@ -31,7 +36,7 @@ final class HubMenuViewController: UIHostingController<HubMenu> {

func showPaymentsMenu(onCompletion: ((InPersonPaymentsMenuViewController) -> Void)? = nil) -> InPersonPaymentsMenuViewController {
let inPersonPaymentsMenuViewController = InPersonPaymentsMenuViewController(
shouldShowBadgeOnSetUpTapToPay: viewModel.shouldShowNewFeatureBadgeOnPayments,
tapToPayBadgePromotionChecker: tapToPayBadgePromotionChecker,
viewDidLoadAction: onCompletion)
show(inPersonPaymentsMenuViewController, sender: self)

Expand Down
Loading