Skip to content

Commit 16f46f2

Browse files
authored
Merge pull request #8620 from woocommerce/issue/8089-disconnection-error-message
[Mobile Payments] Built-in reader disconnection during payment error message
2 parents f4a299f + 8703aa2 commit 16f46f2

File tree

9 files changed

+202
-93
lines changed

9 files changed

+202
-93
lines changed

Hardware/Hardware/CardReader/UnderlyingError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ extension UnderlyingError: LocalizedError {
450450
"the device does not meet minimum requirements.")
451451
case .commandNotAllowedDuringCall:
452452
return NSLocalizedString("The built-in reader cannot be used during a phone call. Please try again after " +
453-
"you finish your call",
453+
"you finish your call.",
454454
comment: "Error message shown when the built-in reader cannot be used because " +
455455
"there is a call in progress")
456456
case .invalidAmount:

WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalNonRetryableError.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel
66
/// Amount charged
77
private let amount: String
88

9-
/// The error returned by the stack
10-
private let error: Error
11-
129
/// Called when the view is dismissed
1310
private let onDismiss: () -> Void
1411

@@ -29,9 +26,7 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel
2926

3027
let auxiliaryButtonTitle: String? = nil
3128

32-
var bottomTitle: String? {
33-
error.localizedDescription
34-
}
29+
let bottomTitle: String?
3530

3631
let bottomSubtitle: String? = nil
3732

@@ -43,12 +38,16 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel
4338
return topTitle + bottomTitle
4439
}
4540

46-
init(amount: String, error: Error, onDismiss: @escaping () -> Void) {
41+
init(amount: String, errorDescription: String?, onDismiss: @escaping () -> Void) {
4742
self.amount = amount
48-
self.error = error
43+
self.bottomTitle = errorDescription
4944
self.onDismiss = onDismiss
5045
}
5146

47+
convenience init(amount: String, error: Error, onDismiss: @escaping () -> Void) {
48+
self.init(amount: amount, errorDescription: error.localizedDescription, onDismiss: onDismiss)
49+
}
50+
5251
func didTapPrimaryButton(in viewController: UIViewController?) {
5352
viewController?.dismiss(animated: true) { [weak self] in
5453
self?.onDismiss()

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

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,16 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP
4848
}
4949

5050
func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
51-
let errorDescription: String?
52-
if let error = error as? CardReaderServiceError {
53-
switch error {
54-
case .connection(let underlyingError),
55-
.discovery(let underlyingError),
56-
.disconnection(let underlyingError),
57-
.intentCreation(let underlyingError),
58-
.paymentMethodCollection(let underlyingError),
59-
.paymentCapture(let underlyingError),
60-
.paymentCancellation(let underlyingError),
61-
.softwareUpdate(let underlyingError, _):
62-
errorDescription = Localization.errorDescription(underlyingError: underlyingError)
63-
default:
64-
errorDescription = error.errorDescription
65-
}
66-
} else {
67-
errorDescription = error.localizedDescription
68-
}
69-
return CardPresentModalError(errorDescription: errorDescription,
51+
return CardPresentModalError(errorDescription: builtInReaderDescription(for: error),
7052
transactionType: .collectPayment,
7153
primaryAction: tryAgain,
7254
dismissCompletion: dismissCompletion)
7355
}
7456

7557
func nonRetryableError(error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
76-
CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion)
58+
CardPresentModalNonRetryableError(amount: amount,
59+
errorDescription: builtInReaderDescription(for: error),
60+
onDismiss: dismissCompletion)
7761
}
7862

7963
func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
@@ -86,6 +70,26 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP
8670
}
8771

8872
private extension BuiltInCardReaderPaymentAlertsProvider {
73+
func builtInReaderDescription(for error: Error) -> String? {
74+
if let error = error as? CardReaderServiceError {
75+
switch error {
76+
case .connection(let underlyingError),
77+
.discovery(let underlyingError),
78+
.disconnection(let underlyingError),
79+
.intentCreation(let underlyingError),
80+
.paymentMethodCollection(let underlyingError),
81+
.paymentCapture(let underlyingError),
82+
.paymentCancellation(let underlyingError),
83+
.softwareUpdate(let underlyingError, _):
84+
return Localization.errorDescription(underlyingError: underlyingError)
85+
default:
86+
return error.errorDescription
87+
}
88+
} else {
89+
return error.localizedDescription
90+
}
91+
}
92+
8993
enum Localization {
9094
static func errorDescription(underlyingError: UnderlyingError) -> String? {
9195
switch underlyingError {
@@ -102,6 +106,10 @@ private extension BuiltInCardReaderPaymentAlertsProvider {
102106
"Sorry, this payment couldn’t be processed",
103107
comment: "Error message when the card reader service experiences an unexpected internal service error."
104108
)
109+
case .notConnectedToReader:
110+
return NSLocalizedString(
111+
"The payment was interrupted and cannot be continued. You can retry the payment from the order screen.",
112+
comment: "Error shown when the built-in card reader payment is interrupted by activity on the phone")
105113
default:
106114
return underlyingError.errorDescription
107115
}

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

Lines changed: 86 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protocol CollectOrderPaymentProtocol {
2222
/// - Parameter onCollect: Closure Invoked after the collect process has finished.
2323
/// - Parameter onCompleted: Closure Invoked after the flow has been totally completed.
2424
/// - Parameter onCancel: Closure invoked after the flow is cancelled
25-
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
25+
func collectPayment(onFailure: @escaping (Error) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
2626
}
2727

2828
/// Use case to collect payments from an order.
@@ -129,15 +129,15 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
129129
/// 7. Tracks payment analytics
130130
///
131131
///
132-
/// - Parameter onCollect: Closure invoked after the collect process has finished.
132+
/// - Parameter onFailure: Closure invoked after the payment process fails.
133133
/// - Parameter onCancel: Closure invoked after the flow is cancelled
134134
/// - Parameter onCompleted: Closure invoked after the flow has been totally completed, currently after merchant has handled the receipt.
135-
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (),
135+
func collectPayment(onFailure: @escaping (Error) -> (),
136136
onCancel: @escaping () -> (),
137137
onCompleted: @escaping () -> ()) {
138138
guard isTotalAmountValid() else {
139139
let error = totalAmountInvalidError()
140-
onCollect(.failure(error))
140+
onFailure(error)
141141
return handleTotalAmountInvalidError(totalAmountInvalidError(), onCompleted: onCancel)
142142
}
143143

@@ -158,16 +158,14 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
158158
case .failure(CollectOrderPaymentUseCaseError.flowCanceledByUser):
159159
self.rootViewController.presentedViewController?.dismiss(animated: true)
160160
return onCancel()
161-
default:
162-
onCollect(result.map { _ in () }) // Transforms Result<CardPresentCapturedPaymentData, Error> to Result<Void, Error>
163-
}
164-
// Handle payment receipt
165-
guard let paymentData = try? result.get() else {
166-
return onCompleted()
161+
case .failure(let error):
162+
return onFailure(error)
163+
case .success(let paymentData):
164+
// Handle payment receipt
165+
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters,
166+
alertProvider: paymentAlertProvider,
167+
onCompleted: onCompleted)
167168
}
168-
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters,
169-
alertProvider: paymentAlertProvider,
170-
onCompleted: onCompleted)
171169
})
172170
case .canceled:
173171
self.handlePaymentCancellation()
@@ -333,33 +331,15 @@ private extension CollectOrderPaymentUseCase {
333331

334332
trackPaymentFailure(with: error)
335333

336-
// Inform about the error
337-
alertsPresenter.present(
338-
viewModel: paymentAlerts.error(error: error,
339-
tryAgain: { [weak self] in
340-
341-
// Cancel current payment
342-
self?.paymentOrchestrator.cancelPayment { [weak self] result in
343-
guard let self = self else { return }
344-
345-
switch result {
346-
case .success:
347-
// Retry payment
348-
self.attemptPayment(alertProvider: paymentAlerts,
349-
onCompletion: onCompletion)
350-
351-
case .failure(let cancelError):
352-
// Inform that payment can't be retried.
353-
self.alertsPresenter.present(
354-
viewModel: paymentAlerts.nonRetryableError(error: cancelError) {
355-
onCompletion(.failure(error))
356-
})
357-
}
358-
}
359-
}, dismissCompletion: {
360-
onCompletion(.failure(error))
361-
})
362-
)
334+
if canRetryPayment(with: error) {
335+
presentRetryableError(error: error,
336+
paymentAlerts: paymentAlerts,
337+
onCompletion: onCompletion)
338+
} else {
339+
presentNonRetryableError(error: error,
340+
paymentAlerts: paymentAlerts,
341+
onCompletion: onCompletion)
342+
}
363343
}
364344

365345
private func trackPaymentFailure(with error: Error) {
@@ -370,6 +350,72 @@ private extension CollectOrderPaymentUseCase {
370350
cardReaderModel: connectedReader?.readerType.model))
371351
}
372352

353+
private func canRetryPayment(with error: Error) -> Bool {
354+
guard let serviceError = error as? CardReaderServiceError else {
355+
return true
356+
}
357+
switch serviceError {
358+
case .paymentMethodCollection(let underlyingError),
359+
.paymentCapture(let underlyingError),
360+
.paymentCancellation(let underlyingError):
361+
return canRetryPayment(underlyingError: underlyingError)
362+
default:
363+
return true
364+
}
365+
}
366+
367+
private func canRetryPayment(underlyingError: UnderlyingError) -> Bool {
368+
switch underlyingError {
369+
case .notConnectedToReader,
370+
.commandNotAllowedDuringCall,
371+
.featureNotAvailableWithConnectedReader:
372+
return false
373+
default:
374+
return true
375+
}
376+
}
377+
378+
private func presentRetryableError(error: Error,
379+
paymentAlerts: CardReaderTransactionAlertsProviding,
380+
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
381+
alertsPresenter.present(
382+
viewModel: paymentAlerts.error(error: error,
383+
tryAgain: { [weak self] in
384+
385+
// Cancel current payment
386+
self?.paymentOrchestrator.cancelPayment { [weak self] result in
387+
guard let self = self else { return }
388+
389+
switch result {
390+
case .success:
391+
// Retry payment
392+
self.attemptPayment(alertProvider: paymentAlerts,
393+
onCompletion: onCompletion)
394+
395+
case .failure(let cancelError):
396+
// Inform that payment can't be retried.
397+
self.alertsPresenter.present(
398+
viewModel: paymentAlerts.nonRetryableError(error: cancelError) {
399+
onCompletion(.failure(error))
400+
})
401+
}
402+
}
403+
}, dismissCompletion: {
404+
onCompletion(.failure(error))
405+
})
406+
)
407+
}
408+
409+
private func presentNonRetryableError(error: Error,
410+
paymentAlerts: CardReaderTransactionAlertsProviding,
411+
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
412+
alertsPresenter.present(
413+
viewModel: paymentAlerts.nonRetryableError(error: error,
414+
dismissCompletion: {
415+
onCompletion(.failure(error))
416+
}))
417+
}
418+
373419
/// Cancels payment and record analytics.
374420
///
375421
func cancelPayment(onCompleted: @escaping () -> ()) {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import MessageUI
55
import WooFoundation
66
import protocol Storage.StorageManagerType
77

8+
9+
/// Protocol to abstract the `LegacyCollectOrderPaymentUseCase`.
10+
/// Currently only used to facilitate unit tests.
11+
///
12+
protocol LegacyCollectOrderPaymentProtocol {
13+
/// Starts the collect payment flow.
14+
///
15+
///
16+
/// - Parameter onCollect: Closure Invoked after the collect process has finished.
17+
/// - Parameter onCompleted: Closure Invoked after the flow has been totally completed.
18+
/// - Parameter onCancel: Closure invoked after the flow is cancelled
19+
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
20+
}
21+
822
/// Use case to collect payments from an order.
923
/// Orchestrates reader connection, payment, UI alerts, receipt handling and analytics.
1024
///
11-
final class LegacyCollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
25+
final class LegacyCollectOrderPaymentUseCase: NSObject, LegacyCollectOrderPaymentProtocol {
1226
/// Currency Formatter
1327
///
1428
private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)

WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ struct PaymentMethodsView: View {
6060
Divider()
6161

6262
MethodRow(icon: .creditCardImage, title: Localization.card, accessibilityID: Accessibility.cardMethod) {
63-
viewModel.collectPayment(on: rootViewController, onSuccess: dismiss)
63+
viewModel.collectPayment(on: rootViewController, onSuccess: dismiss, onFailure: dismiss)
6464
}
6565
}
6666

0 commit comments

Comments
 (0)