Skip to content

Commit a861fa9

Browse files
authored
Merge pull request #8352 from woocommerce/issue/8289-show-follow-instructions-during-built-in-collect-payment
[Mobile Payments] show follow instructions during built in collect payment
2 parents e3c8dd8 + 8fdecc0 commit a861fa9

File tree

6 files changed

+233
-26
lines changed

6 files changed

+233
-26
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import UIKit
2+
import Yosemite
3+
4+
/// Modal presented under the Apple-provided built in reader modal, while the card is being collected.
5+
/// This may be visible for a moment or two either side of Apple's screen being shown.
6+
final class CardPresentModalBuiltInFollowReaderInstructions: CardPresentPaymentsModalViewModel {
7+
8+
/// Customer name
9+
private let name: String
10+
11+
/// Charge amount
12+
private let amount: String
13+
14+
let textMode: PaymentsModalTextMode = .fullInfo
15+
let actionsMode: PaymentsModalActionsMode = .none
16+
17+
var topTitle: String {
18+
name
19+
}
20+
21+
var topSubtitle: String? {
22+
amount
23+
}
24+
25+
let image: UIImage = .cardPresentImage
26+
27+
let primaryButtonTitle: String? = nil
28+
29+
let secondaryButtonTitle: String? = nil
30+
31+
let auxiliaryButtonTitle: String? = nil
32+
33+
let bottomTitle: String? = Localization.readerIsReady
34+
35+
let bottomSubtitle: String?
36+
37+
let accessibilityLabel: String?
38+
39+
init(name: String,
40+
amount: String,
41+
transactionType: CardPresentTransactionType,
42+
inputMethods: CardReaderInput) {
43+
self.name = name
44+
self.amount = amount
45+
46+
self.bottomSubtitle = Localization.followReaderInstructions
47+
48+
self.accessibilityLabel = Localization.readerIsReady + Localization.followReaderInstructions
49+
}
50+
51+
func didTapPrimaryButton(in viewController: UIViewController?) {
52+
//
53+
}
54+
55+
func didTapSecondaryButton(in viewController: UIViewController?) {
56+
//
57+
}
58+
59+
func didTapAuxiliaryButton(in viewController: UIViewController?) {
60+
//
61+
}
62+
}
63+
64+
private extension CardPresentModalBuiltInFollowReaderInstructions {
65+
enum Localization {
66+
static let readerIsReady = NSLocalizedString(
67+
"iPhone reader is ready",
68+
comment: "Indicates the status of a built in card reader. Presented to users when payment collection starts"
69+
)
70+
71+
static let followReaderInstructions = NSLocalizedString(
72+
"Follow reader instructions to pay",
73+
comment: "Label asking users to follow the built in reader instruction. Presented to users when a " +
74+
"payment is going to be collected using the iPhone's built in reader"
75+
)
76+
}
77+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
3838
presentingController: UIViewController) {
3939
self.transactionType = transactionType
4040
self.presentingController = presentingController
41-
self.alertsProvider = CardReaderPaymentAlertsProvider(transactionType: transactionType)
41+
self.alertsProvider = BluetoothCardReaderPaymentAlertsProvider(transactionType: transactionType)
4242
}
4343

4444
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel) {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import MessageUI
44
import enum Hardware.CardReaderServiceError
55
import enum Hardware.UnderlyingError
66

7-
final class CardReaderPaymentAlertsProvider: CardReaderTransactionAlertsProviding {
7+
final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsProviding {
88
var name: String = ""
99
var amount: String = ""
1010
var transactionType: CardPresentTransactionType
@@ -89,7 +89,7 @@ final class CardReaderPaymentAlertsProvider: CardReaderTransactionAlertsProvidin
8989
}
9090
}
9191

92-
private extension CardReaderPaymentAlertsProvider {
92+
private extension BluetoothCardReaderPaymentAlertsProvider {
9393
enum Localization {
9494
static func errorDescription(underlyingError: UnderlyingError, transactionType: CardPresentTransactionType) -> String? {
9595
switch underlyingError {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import Foundation
2+
import Yosemite
3+
import MessageUI
4+
import enum Hardware.CardReaderServiceError
5+
import enum Hardware.UnderlyingError
6+
7+
final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsProviding {
8+
var name: String = ""
9+
var amount: String = ""
10+
11+
func preparingReader(onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
12+
CardPresentModalPreparingReader(cancelAction: onCancel)
13+
}
14+
15+
func tapOrInsertCard(title: String,
16+
amount: String,
17+
inputMethods: Yosemite.CardReaderInput,
18+
onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
19+
name = title
20+
self.amount = amount
21+
return CardPresentModalBuiltInFollowReaderInstructions(name: name,
22+
amount: amount,
23+
transactionType: .collectPayment,
24+
inputMethods: inputMethods)
25+
}
26+
27+
func displayReaderMessage(message: String) -> CardPresentPaymentsModalViewModel {
28+
CardPresentModalDisplayMessage(name: name,
29+
amount: amount,
30+
message: message)
31+
}
32+
33+
func processingTransaction() -> CardPresentPaymentsModalViewModel {
34+
CardPresentModalProcessing(name: name, amount: amount, transactionType: .collectPayment)
35+
}
36+
37+
func success(printReceipt: @escaping () -> Void,
38+
emailReceipt: @escaping () -> Void,
39+
noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
40+
if MFMailComposeViewController.canSendMail() {
41+
return CardPresentModalSuccess(printReceipt: printReceipt,
42+
emailReceipt: emailReceipt,
43+
noReceiptAction: noReceiptAction)
44+
} else {
45+
return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction)
46+
}
47+
}
48+
49+
func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
50+
let errorDescription: String?
51+
if let error = error as? CardReaderServiceError {
52+
switch error {
53+
case .connection(let underlyingError),
54+
.discovery(let underlyingError),
55+
.disconnection(let underlyingError),
56+
.intentCreation(let underlyingError),
57+
.paymentMethodCollection(let underlyingError),
58+
.paymentCapture(let underlyingError),
59+
.paymentCancellation(let underlyingError),
60+
.softwareUpdate(let underlyingError, _):
61+
errorDescription = Localization.errorDescription(underlyingError: underlyingError)
62+
default:
63+
errorDescription = error.errorDescription
64+
}
65+
} else {
66+
errorDescription = error.localizedDescription
67+
}
68+
return CardPresentModalError(errorDescription: errorDescription,
69+
transactionType: .collectPayment,
70+
primaryAction: tryAgain,
71+
dismissCompletion: dismissCompletion)
72+
}
73+
74+
func nonRetryableError(error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
75+
CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion)
76+
}
77+
78+
func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
79+
CardPresentModalRetryableError(primaryAction: tryAgain)
80+
}
81+
}
82+
83+
private extension BuiltInCardReaderPaymentAlertsProvider {
84+
enum Localization {
85+
static func errorDescription(underlyingError: UnderlyingError) -> String? {
86+
switch underlyingError {
87+
case .paymentDeclinedByCardReader:
88+
return NSLocalizedString("The card was declined by the iPhone card reader - please try another means of payment",
89+
comment: "Error message when the card reader itself declines the card.")
90+
case .processorAPIError:
91+
return NSLocalizedString(
92+
"The payment can not be processed by the payment processor.",
93+
comment: "Error message when the payment can not be processed (i.e. order amount is below the minimum amount allowed.)"
94+
)
95+
case .internalServiceError:
96+
return NSLocalizedString(
97+
"Sorry, this payment couldn’t be processed",
98+
comment: "Error message when the card reader service experiences an unexpected internal service error."
99+
)
100+
default:
101+
return underlyingError.errorDescription
102+
}
103+
}
104+
}
105+
}

WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,6 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
7373
///
7474
private let alertsPresenter: CardPresentPaymentAlertsPresenting
7575

76-
/// Payment alerts provider
77-
///
78-
private let paymentAlerts: CardReaderTransactionAlertsProviding
79-
8076
/// Stores the card reader listener subscription while trying to connect to one.
8177
///
8278
private var readerSubscription: AnyCancellable?
@@ -117,7 +113,6 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
117113
self.paymentGatewayAccount = paymentGatewayAccount
118114
self.rootViewController = rootViewController
119115
self.alertsPresenter = CardPresentPaymentAlertsPresenter(rootViewController: rootViewController)
120-
self.paymentAlerts = CardReaderPaymentAlertsProvider(transactionType: .collectPayment)
121116
self.configuration = configuration
122117
self.stores = stores
123118
self.paymentCaptureCelebration = paymentCaptureCelebration
@@ -155,7 +150,8 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
155150
switch connectionResult {
156151
case .connected(let reader):
157152
self.connectedReader = reader
158-
self.attemptPayment(onCompletion: { [weak self] result in
153+
let paymentAlertProvider = reader.paymentAlertProvider()
154+
self.attemptPayment(alertProvider: paymentAlertProvider, onCompletion: { [weak self] result in
159155
guard let self = self else { return }
160156
// Inform about the collect payment state
161157
switch result {
@@ -169,7 +165,9 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
169165
guard let paymentData = try? result.get() else {
170166
return onCompleted()
171167
}
172-
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters, onCompleted: onCompleted)
168+
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters,
169+
alertProvider: paymentAlertProvider,
170+
onCompleted: onCompleted)
173171
})
174172
case .canceled:
175173
self.alertsPresenter.dismiss()
@@ -185,6 +183,17 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
185183
}
186184
}
187185

186+
private extension CardReader {
187+
func paymentAlertProvider() -> CardReaderTransactionAlertsProviding {
188+
switch readerType {
189+
case .appleBuiltIn:
190+
return BuiltInCardReaderPaymentAlertsProvider()
191+
default:
192+
return BluetoothCardReaderPaymentAlertsProvider(transactionType: .collectPayment)
193+
}
194+
}
195+
}
196+
188197
// MARK: Private functions
189198
private extension CollectOrderPaymentUseCase {
190199
/// Checks whether the amount to be collected is valid: (not nil, convertible to decimal, higher than minimum amount ...)
@@ -212,16 +221,19 @@ private extension CollectOrderPaymentUseCase {
212221
return NotValidAmountError.belowMinimumAmount(amount: minimum)
213222
}
214223

215-
func handleTotalAmountInvalidError(_ error: Error, onCompleted: @escaping () -> ()) {
224+
func handleTotalAmountInvalidError(_ error: Error,
225+
onCompleted: @escaping () -> ()) {
216226
trackPaymentFailure(with: error)
217227
DDLogError("💳 Error: failed to capture payment for order. Order amount is below minimum or not valid")
218-
self.alertsPresenter.present(viewModel: paymentAlerts.nonRetryableError(error: totalAmountInvalidError(),
219-
dismissCompletion: onCompleted))
228+
alertsPresenter.present(viewModel: CardPresentModalNonRetryableError(amount: formattedAmount,
229+
error: totalAmountInvalidError(),
230+
onDismiss: onCompleted))
220231
}
221232

222233
/// Attempts to collect payment for an order.
223234
///
224-
func attemptPayment(onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
235+
func attemptPayment(alertProvider paymentAlerts: CardReaderTransactionAlertsProviding,
236+
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
225237
guard let orderTotal = orderTotal else {
226238
onCompletion(.failure(NotValidAmountError.other))
227239
return
@@ -244,7 +256,7 @@ private extension CollectOrderPaymentUseCase {
244256
onWaitingForInput: { [weak self] inputMethods in
245257
guard let self = self else { return }
246258
self.alertsPresenter.present(
247-
viewModel: self.paymentAlerts.tapOrInsertCard(
259+
viewModel: paymentAlerts.tapOrInsertCard(
248260
title: Localization.collectPaymentTitle(username: self.order.billingAddress?.firstName),
249261
amount: self.formattedAmount,
250262
inputMethods: inputMethods,
@@ -257,11 +269,11 @@ private extension CollectOrderPaymentUseCase {
257269
}, onProcessingMessage: { [weak self] in
258270
guard let self = self else { return }
259271
// Waiting message
260-
self.alertsPresenter.present(viewModel: self.paymentAlerts.processingTransaction())
272+
self.alertsPresenter.present(viewModel: paymentAlerts.processingTransaction())
261273
}, onDisplayMessage: { [weak self] message in
262274
guard let self = self else { return }
263275
// Reader messages. EG: Remove Card
264-
self.alertsPresenter.present(viewModel: self.paymentAlerts.displayReaderMessage(message: message))
276+
self.alertsPresenter.present(viewModel: paymentAlerts.displayReaderMessage(message: message))
265277
}, onProcessingCompletion: { [weak self] intent in
266278
self?.trackProcessingCompletion(intent: intent)
267279
self?.markOrderAsPaidIfNeeded(intent: intent)
@@ -270,7 +282,7 @@ private extension CollectOrderPaymentUseCase {
270282
case .success(let capturedPaymentData):
271283
self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion)
272284
case .failure(let error):
273-
self?.handlePaymentFailureAndRetryPayment(error, onCompletion: onCompletion)
285+
self?.handlePaymentFailureAndRetryPayment(error, alertProvider: paymentAlerts, onCompletion: onCompletion)
274286
}
275287
}
276288
)
@@ -293,7 +305,9 @@ private extension CollectOrderPaymentUseCase {
293305

294306
/// Log the failure reason, cancel the current payment and retry it if possible.
295307
///
296-
func handlePaymentFailureAndRetryPayment(_ error: Error, onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
308+
func handlePaymentFailureAndRetryPayment(_ error: Error,
309+
alertProvider paymentAlerts: CardReaderTransactionAlertsProviding,
310+
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
297311
DDLogError("Failed to collect payment: \(error.localizedDescription)")
298312

299313
trackPaymentFailure(with: error)
@@ -310,12 +324,13 @@ private extension CollectOrderPaymentUseCase {
310324
switch result {
311325
case .success:
312326
// Retry payment
313-
self.attemptPayment(onCompletion: onCompletion)
327+
self.attemptPayment(alertProvider: paymentAlerts,
328+
onCompletion: onCompletion)
314329

315330
case .failure(let cancelError):
316331
// Inform that payment can't be retried.
317332
self.alertsPresenter.present(
318-
viewModel: self.paymentAlerts.nonRetryableError(error: cancelError) {
333+
viewModel: paymentAlerts.nonRetryableError(error: cancelError) {
319334
onCompletion(.failure(error))
320335
})
321336
}
@@ -351,7 +366,9 @@ private extension CollectOrderPaymentUseCase {
351366

352367
/// Allow merchants to print or email the payment receipt.
353368
///
354-
func presentReceiptAlert(receiptParameters: CardPresentReceiptParameters, onCompleted: @escaping () -> ()) {
369+
func presentReceiptAlert(receiptParameters: CardPresentReceiptParameters,
370+
alertProvider paymentAlerts: CardReaderTransactionAlertsProviding,
371+
onCompleted: @escaping () -> ()) {
355372
// Present receipt alert
356373
alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: { [order, configuration, weak self] in
357374
guard let self = self else { return }

0 commit comments

Comments
 (0)