Skip to content

Commit 83f1d4c

Browse files
authored
Merge pull request #8115 from woocommerce/issue/8079-spinner-while-awaiting-reader-readiness
[Mobile Payments] Show spinner while awaiting reader readiness
2 parents 64979b3 + c0b6e12 commit 83f1d4c

18 files changed

+335
-252
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
11.3
44
-----
5-
5+
- [*] In-Person Payments: Show spinner while preparing reader for payment, instead of saying it's ready before it is. [https://github.com/woocommerce/woocommerce-ios/pull/8115]
66

77
11.2
88
-----
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import UIKit
2+
3+
/// Modal presented when an error occurs while connecting to a reader due to problems with the address
4+
///
5+
final class CardPresentModalPreparingReader: CardPresentPaymentsModalViewModel {
6+
let cancelAction: (() -> Void)
7+
8+
let textMode: PaymentsModalTextMode = .reducedTopInfo
9+
let actionsMode: PaymentsModalActionsMode = .secondaryOnlyAction
10+
11+
let topTitle: String = Localization.title
12+
13+
var topSubtitle: String? = nil
14+
15+
let image: UIImage = .paymentErrorImage
16+
17+
let showLoadingIndicator = true
18+
19+
var primaryButtonTitle: String? = nil
20+
21+
let secondaryButtonTitle: String? = Localization.cancel
22+
23+
let auxiliaryButtonTitle: String? = nil
24+
25+
var bottomTitle: String? = Localization.bottomTitle
26+
27+
let bottomSubtitle: String? = Localization.bottomSubitle
28+
29+
var accessibilityLabel: String? {
30+
return topTitle
31+
}
32+
33+
init(cancelAction: @escaping () -> Void) {
34+
self.cancelAction = cancelAction
35+
}
36+
37+
func didTapPrimaryButton(in viewController: UIViewController?) {
38+
39+
}
40+
41+
func didTapSecondaryButton(in viewController: UIViewController?) {
42+
cancelAction()
43+
}
44+
45+
func didTapAuxiliaryButton(in viewController: UIViewController?) { }
46+
}
47+
48+
private extension CardPresentModalPreparingReader {
49+
enum Localization {
50+
static let title = NSLocalizedString(
51+
"Getting ready to collect payment",
52+
comment: "Title of the alert presented with a spinner while the reader is being prepared"
53+
)
54+
55+
static let bottomTitle = NSLocalizedString(
56+
"Connecting to reader",
57+
comment: "Bottom title of the alert presented with a spinner while the reader is being prepared"
58+
)
59+
60+
static let bottomSubitle = NSLocalizedString(
61+
"Please wait...",
62+
comment: "Bottom subtitle of the alert presented with a spinner while the reader is being prepared"
63+
)
64+
65+
static let cancel = NSLocalizedString(
66+
"Cancel",
67+
comment: "Button to dismiss the alert presented while the reader is being prepared."
68+
)
69+
}
70+
}

WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentPaymentsModalViewModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ protocol CardPresentPaymentsModalViewModel {
1818
/// An illustration accompanying the modal
1919
var image: UIImage { get }
2020

21+
var showLoadingIndicator: Bool { get }
22+
2123
/// Provides a title for a primary action button
2224
var primaryButtonTitle: String? { get }
2325

@@ -110,4 +112,8 @@ extension CardPresentPaymentsModalViewModel {
110112
var auxiliaryButtonimage: UIImage? {
111113
get { return nil }
112114
}
115+
116+
var showLoadingIndicator: Bool {
117+
get { return false }
118+
}
113119
}

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift

Lines changed: 59 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -50,57 +50,48 @@ final class PaymentCaptureOrchestrator {
5050

5151
stores.dispatch(setAccount)
5252

53-
paymentParameters(
54-
order: order,
55-
orderTotal: orderTotal,
56-
country: paymentGatewayAccount.country,
57-
statementDescriptor: paymentGatewayAccount.statementDescriptor,
58-
paymentMethodTypes: paymentMethodTypes,
59-
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier
60-
) { [weak self] result in
61-
guard let self = self else { return }
62-
63-
switch result {
64-
case let .success(parameters):
65-
/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
66-
/// reader begins to collect payment.
67-
///
68-
self.suppressPassPresentation()
69-
70-
let paymentAction = CardPresentPaymentAction.collectPayment(
71-
siteID: order.siteID,
72-
orderID: order.orderID,
73-
parameters: parameters,
74-
onCardReaderMessage: { event in
75-
switch event {
76-
case .waitingForInput:
77-
onWaitingForInput()
78-
case .displayMessage(let message):
79-
onDisplayMessage(message)
80-
case .cardRemovedAfterClientSidePaymentCapture:
81-
onProcessingMessage()
82-
default:
83-
break
84-
}
85-
},
86-
onProcessingCompletion: { intent in
87-
onProcessingCompletion(intent)
88-
},
89-
onCompletion: { [weak self] result in
90-
self?.allowPassPresentation()
91-
self?.completePaymentIntentCapture(
92-
order: order,
93-
captureResult: result,
94-
onCompletion: onCompletion
95-
)
96-
}
53+
let parameters = paymentParameters(order: order,
54+
orderTotal: orderTotal,
55+
country: paymentGatewayAccount.country,
56+
statementDescriptor: paymentGatewayAccount.statementDescriptor,
57+
paymentMethodTypes: paymentMethodTypes,
58+
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier)
59+
60+
/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
61+
/// reader begins to collect payment.
62+
///
63+
suppressPassPresentation()
64+
65+
let paymentAction = CardPresentPaymentAction.collectPayment(
66+
siteID: order.siteID,
67+
orderID: order.orderID,
68+
parameters: parameters,
69+
onCardReaderMessage: { event in
70+
switch event {
71+
case .waitingForInput:
72+
onWaitingForInput()
73+
case .displayMessage(let message):
74+
onDisplayMessage(message)
75+
case .cardRemovedAfterClientSidePaymentCapture:
76+
onProcessingMessage()
77+
default:
78+
break
79+
}
80+
},
81+
onProcessingCompletion: { intent in
82+
onProcessingCompletion(intent)
83+
},
84+
onCompletion: { [weak self] result in
85+
self?.allowPassPresentation()
86+
self?.completePaymentIntentCapture(
87+
order: order,
88+
captureResult: result,
89+
onCompletion: onCompletion
9790
)
98-
99-
self.stores.dispatch(paymentAction)
100-
case let .failure(error):
101-
onCompletion(Result.failure(error))
10291
}
103-
}
92+
)
93+
94+
stores.dispatch(paymentAction)
10495
}
10596

10697
func cancelPayment(onCompletion: @escaping (Result<Void, Error>) -> Void) {
@@ -204,37 +195,25 @@ private extension PaymentCaptureOrchestrator {
204195
country: String,
205196
statementDescriptor: String?,
206197
paymentMethodTypes: [String],
207-
stripeSmallestCurrencyUnitMultiplier: Decimal,
208-
onCompletion: @escaping ((Result<PaymentParameters, Error>) -> Void)) {
209-
paymentReceiptEmailParameterDeterminer.receiptEmail(from: order) { [weak self] result in
210-
guard let self = self else { return }
211-
212-
var receiptEmail: String?
213-
if case let .success(email) = result {
214-
receiptEmail = email
215-
}
216-
217-
let metadata = PaymentIntent.initMetadata(
218-
store: self.stores.sessionManager.defaultSite?.name,
219-
customerName: self.buildCustomerNameFromBillingAddress(order.billingAddress),
220-
customerEmail: order.billingAddress?.email,
221-
siteURL: self.stores.sessionManager.defaultSite?.url,
222-
orderID: order.orderID,
223-
paymentType: PaymentIntent.PaymentTypes.single
224-
)
225-
226-
let parameters = PaymentParameters(amount: orderTotal as Decimal,
227-
currency: order.currency,
228-
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier,
229-
applicationFee: self.applicationFee(for: orderTotal, country: country),
230-
receiptDescription: self.receiptDescription(orderNumber: order.number),
231-
statementDescription: statementDescriptor,
232-
receiptEmail: receiptEmail,
233-
paymentMethodTypes: paymentMethodTypes,
234-
metadata: metadata)
235-
236-
onCompletion(Result.success(parameters))
237-
}
198+
stripeSmallestCurrencyUnitMultiplier: Decimal) -> PaymentParameters {
199+
let metadata = PaymentIntent.initMetadata(
200+
store: stores.sessionManager.defaultSite?.name,
201+
customerName: buildCustomerNameFromBillingAddress(order.billingAddress),
202+
customerEmail: order.billingAddress?.email,
203+
siteURL: stores.sessionManager.defaultSite?.url,
204+
orderID: order.orderID,
205+
paymentType: PaymentIntent.PaymentTypes.single
206+
)
207+
208+
return PaymentParameters(amount: orderTotal as Decimal,
209+
currency: order.currency,
210+
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier,
211+
applicationFee: applicationFee(for: orderTotal, country: country),
212+
receiptDescription: receiptDescription(orderNumber: order.number),
213+
statementDescription: statementDescriptor,
214+
receiptEmail: paymentReceiptEmailParameterDeterminer.receiptEmail(from: order),
215+
paymentMethodTypes: paymentMethodTypes,
216+
metadata: metadata)
238217
}
239218

240219
private func applicationFee(for orderTotal: NSDecimalNumber, country: String) -> Decimal? {

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentReceiptEmailParameterDeterminer.swift

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,47 @@ import Yosemite
44
/// Determines the email to be set (if any) on a receipt
55
///
66
protocol ReceiptEmailParameterDeterminer {
7-
func receiptEmail(from order: Order, onCompletion: @escaping ((Result<String?, Error>) -> Void))
7+
func receiptEmail(from order: Order) -> String?
88
}
99

1010
/// Determines the email to be set (if any) on a payment receipt depending on the current payment plugins (WCPay, Stripe) configuration
1111
///
1212
struct PaymentReceiptEmailParameterDeterminer: ReceiptEmailParameterDeterminer {
1313
private let cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol
14-
private let stores: StoresManager
1514
private static let defaultConfiguration = CardPresentConfigurationLoader(stores: ServiceLocator.stores).configuration
1615

17-
init(cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol = CardPresentPluginsDataProvider(configuration: Self.defaultConfiguration),
18-
stores: StoresManager = ServiceLocator.stores) {
16+
init(cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol = CardPresentPluginsDataProvider(configuration: Self.defaultConfiguration)) {
1917
self.cardPresentPluginsDataProvider = cardPresentPluginsDataProvider
20-
self.stores = stores
2118
}
2219

2320
/// We do not need to set the receipt email if WCPay is installed and active
2421
/// and its version is higher or equal than 4.0.0, as it does it itself in that case.
2522
///
2623
/// - Parameters:
2724
/// - order: the order associated with the payment
28-
/// - onCompletion: closure invoked with the result of the inquiry, containing the email (if any) or error
25+
/// - Returns:
26+
/// - `String?`: the email for the reciept, if any. Even if there is an email, this will return `nil` for stores which send the receipt server-side.
2927
///
30-
func receiptEmail(from order: Order, onCompletion: @escaping ((Result<String?, Error>) -> Void)) {
31-
synchronizePlugins(from: order.siteID) { result in
32-
switch result {
33-
case .success():
34-
onCompletion(Result.success(receiptEmail(from: order)))
35-
case let .failure(error):
36-
onCompletion(Result.failure(error))
37-
}
38-
}
39-
}
40-
41-
private func receiptEmail(from order: Order) -> String? {
28+
func receiptEmail(from order: Order) -> String? {
4229
let wcPay = cardPresentPluginsDataProvider.getWCPayPlugin()
4330
let stripe = cardPresentPluginsDataProvider.getStripePlugin()
44-
let paymentPluginsInstalledAndActiveStatus = cardPresentPluginsDataProvider.paymentPluginsInstalledAndActiveStatus(wcPay: wcPay, stripe: stripe)
31+
let paymentPluginsStatus = cardPresentPluginsDataProvider.paymentPluginsInstalledAndActiveStatus(wcPay: wcPay, stripe: stripe)
4532

46-
guard paymentPluginsInstalledAndActiveStatus != .bothAreInstalledAndActive else {
33+
guard paymentPluginsStatus != .bothAreInstalledAndActive else {
4734
return nil
4835
}
4936

5037
guard let wcPay = wcPay,
51-
paymentPluginsInstalledAndActiveStatus == .onlyWCPayIsInstalledAndActive else {
38+
paymentPluginsStatus == .onlyWCPayIsInstalledAndActive else {
5239
return order.billingAddress?.email
5340
}
5441

5542
return wcPayPluginSendsReceiptEmail(version: wcPay.version) ? nil : order.billingAddress?.email
5643
}
5744

58-
private func synchronizePlugins(from siteID: Int64, onCompletion: @escaping ((Result<Void, Error>) -> Void)) {
59-
let systemPluginsAction = SystemStatusAction.synchronizeSystemPlugins(siteID: siteID) { result in
60-
if case let .failure(error) = result {
61-
DDLogError("[PaymentReceiptEmailParameterDeterminer] Error syncing system plugins: \(error)")
62-
onCompletion(Result.failure(error))
63-
} else {
64-
onCompletion(Result.success(()))
65-
}
66-
}
67-
68-
stores.dispatch(systemPluginsAction)
69-
}
70-
7145
private func wcPayPluginSendsReceiptEmail(version: String) -> Bool {
72-
let comparisonResult = VersionHelpers.compare(version, Constants.minimumWCPayPluginVersionThatSendsReceiptEmail)
73-
74-
return comparisonResult == .orderedDescending || comparisonResult == .orderedSame
46+
VersionHelpers.isVersionSupported(version: version,
47+
minimumRequired: Constants.minimumWCPayPluginVersionThatSendsReceiptEmail)
7548
}
7649
}
7750

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
1717
if let controller = _modalController {
1818
return controller
1919
} else {
20-
let controller = CardPresentPaymentsModalViewController(viewModel: readerIsReady(onCancel: {}))
20+
let controller = CardPresentPaymentsModalViewController(
21+
viewModel: CardPresentModalPreparingReader(cancelAction: { [weak self] in
22+
self?.presentingController?.dismiss(animated: true)
23+
}))
2124
_modalController = controller
2225
return controller
2326
}
@@ -43,6 +46,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
4346
}
4447
}
4548

49+
func preparingReader(onCancel: @escaping () -> Void) {
50+
presentViewModel(viewModel: CardPresentModalPreparingReader(cancelAction: onCancel))
51+
}
52+
4653
func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void) {
4754
self.name = title
4855
self.amount = amount

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlertsProtocol.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import UIKit
44
protocol OrderDetailsPaymentAlertsProtocol {
55
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel)
66

7+
func preparingReader(onCancel: @escaping () -> Void)
8+
79
func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void)
810

911
func tapOrInsertCard(onCancel: @escaping () -> Void)

0 commit comments

Comments
 (0)