Skip to content

Commit 618b3a0

Browse files
authored
Merge pull request #8369 from woocommerce/issue/8085-cancellation-from-card-readers
[Mobile Payments] Handle cancellation from card readers
2 parents 6b87459 + fc1213a commit 618b3a0

File tree

9 files changed

+79
-20
lines changed

9 files changed

+79
-20
lines changed

Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public final class StripeCardReaderService: NSObject {
3232
/// Keeps track of whether a chip card needs to be removed
3333
private var timerCancellable: Cancellable?
3434
private var isChipCardInserted: Bool = false
35+
36+
/// Stripe don't tell us where a cancellation comes from: if we keep track of when we trigger one,
37+
/// we can infer when it comes from the cancel button on the reader instead
38+
private var cancellationStartedInApp: Bool?
3539
}
3640

3741

@@ -285,6 +289,8 @@ extension StripeCardReaderService: CardReaderService {
285289
return
286290
}
287291

292+
self.cancellationStartedInApp = true
293+
288294
let cancelPaymentIntent = { [weak self] in
289295
Terminal.shared.cancelPaymentIntent(activePaymentIntent) { (intent, error) in
290296
if let error = error {
@@ -296,6 +302,7 @@ extension StripeCardReaderService: CardReaderService {
296302
self?.activePaymentIntent = nil
297303
promise(.success(()))
298304
}
305+
self?.cancellationStartedInApp = nil
299306
}
300307
}
301308
guard let paymentCancellable = self.paymentCancellable,
@@ -502,27 +509,29 @@ private extension StripeCardReaderService {
502509
/// Because we are chaining promises, we need to retain a reference
503510
/// to this cancellable if we want to cancel
504511
self?.paymentCancellable = Terminal.shared.collectPaymentMethod(intent) { (intent, error) in
505-
self?.paymentCancellable = nil
506-
507512
if let error = error {
508-
let underlyingError = UnderlyingError(with: error)
513+
var underlyingError = UnderlyingError(with: error)
509514
/// the completion block for collectPaymentMethod will be called
510515
/// with error Canceled when collectPaymentMethod is canceled
511516
/// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)collectPaymentMethod:delegate:completion:
512-
513-
if underlyingError != .commandCancelled {
517+
if case .commandCancelled(let cancellationSource) = underlyingError {
518+
DDLogWarn("💳 Warning: collect payment cancelled \(error)")
519+
if case .unknown = cancellationSource {
520+
if self?.cancellationStartedInApp != nil {
521+
underlyingError = .commandCancelled(from: .app)
522+
} else {
523+
underlyingError = .commandCancelled(from: .reader)
524+
}
525+
}
526+
} else {
514527
DDLogError("💳 Error: collect payment method \(underlyingError)")
515-
promise(.failure(CardReaderServiceError.paymentMethodCollection(underlyingError: underlyingError)))
516-
}
517-
518-
if underlyingError == .commandCancelled {
519-
DDLogWarn("💳 Warning: collect payment error cancelled. We actively ignore this error \(error)")
520-
promise(.failure(CardReaderServiceError.paymentCancellation(underlyingError: underlyingError)))
521528
}
522-
529+
self?.paymentCancellable = nil
530+
promise(.failure(CardReaderServiceError.paymentMethodCollection(underlyingError: underlyingError)))
523531
}
524532

525533
if let intent = intent {
534+
self?.paymentCancellable = nil
526535
self?.sendReaderEvent(.cardDetailsCollected)
527536
promise(.success(intent))
528537
}

Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension UnderlyingError {
2626
case ErrorCode.Code.featureNotAvailableWithConnectedReader.rawValue:
2727
self = .featureNotAvailableWithConnectedReader
2828
case ErrorCode.Code.canceled.rawValue:
29-
self = .commandCancelled
29+
self = .commandCancelled(from: .unknown)
3030
case ErrorCode.Code.locationServicesDisabled.rawValue:
3131
self = .locationServicesDisabled
3232
case ErrorCode.Code.bluetoothDisabled.rawValue:

Hardware/Hardware/CardReader/UnderlyingError.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@ public enum UnderlyingError: Error, Equatable {
2727
case featureNotAvailableWithConnectedReader
2828

2929
/// A command was cancelled
30-
case commandCancelled
30+
case commandCancelled(from: CancellationSource)
31+
32+
/// A command can be cancelled on the reader, or in the app.
33+
/// Note that this is not produced by Stripe, we have to infer it from commandCancelled, so we start with `.unknown`.
34+
public enum CancellationSource {
35+
case unknown
36+
case app
37+
case reader
38+
}
3139

3240
/// Access to location services is currently disabled. This may be because:
3341
/// - The user disabled location services in the system settings.
@@ -282,9 +290,15 @@ extension UnderlyingError: LocalizedError {
282290
case .featureNotAvailableWithConnectedReader:
283291
return NSLocalizedString("Unable to perform request with the connected reader - unsupported feature - please try again with another reader",
284292
comment: "Error message when the card reader cannot be used to perform the requested task.")
285-
case .commandCancelled:
286-
return NSLocalizedString("The system canceled the command unexpectedly - please try again",
287-
comment: "Error message when the system cancels a command.")
293+
case .commandCancelled(let cancellationSource):
294+
switch cancellationSource {
295+
case .reader:
296+
return NSLocalizedString("The payment was canceled on the reader",
297+
comment: "Error message when the cancel button on the reader is used.")
298+
default:
299+
return NSLocalizedString("The system canceled the command unexpectedly - please try again",
300+
comment: "Error message when the system cancels a command.")
301+
}
288302
case .locationServicesDisabled:
289303
return NSLocalizedString("Unable to access Location Services - please enable Location Services and try again",
290304
comment: "Error message when location services is not enabled for this application.")

Hardware/HardwareTests/ErrorCodesTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class CardReaderServiceErrorTests: XCTestCase {
3939
}
4040

4141
func test_stripe_cancelled_maps_to_expected_error() {
42-
XCTAssertEqual(.commandCancelled, domainError(stripeCode: 2020))
42+
XCTAssertEqual(.commandCancelled(from: .unknown), domainError(stripeCode: 2020))
4343
}
4444

4545
func test_stripe_location_services_disabled_maps_to_expected_error() {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlert
8787
func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
8888
CardPresentModalRetryableError(primaryAction: tryAgain)
8989
}
90+
91+
func cancelledOnReader() -> CardPresentPaymentsModalViewModel? {
92+
CardPresentModalNonRetryableError(amount: amount,
93+
error: CardReaderServiceError.paymentMethodCollection(underlyingError: .commandCancelled(from: .reader)),
94+
onDismiss: { })
95+
}
9096
}
9197

9298
private extension BluetoothCardReaderPaymentAlertsProvider {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP
7878
func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
7979
CardPresentModalRetryableError(primaryAction: tryAgain)
8080
}
81+
82+
func cancelledOnReader() -> CardPresentPaymentsModalViewModel? {
83+
return nil
84+
}
8185
}
8286

8387
private extension BuiltInCardReaderPaymentAlertsProvider {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,8 @@ protocol CardReaderTransactionAlertsProviding {
4545
/// An alert to display a retriable error
4646
///
4747
func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel
48+
49+
/// An alert to notify the merchant that the transaction was cancelled using a button on the reader
50+
///
51+
func cancelledOnReader() -> CardPresentPaymentsModalViewModel?
4852
}

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
170170
onCompleted: onCompleted)
171171
})
172172
case .canceled:
173-
self.alertsPresenter.dismiss()
174-
self.trackPaymentCancelation()
173+
self.handlePaymentCancellation()
175174
onCancel()
176175
case .none:
177176
break
@@ -281,6 +280,13 @@ private extension CollectOrderPaymentUseCase {
281280
switch result {
282281
case .success(let capturedPaymentData):
283282
self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion)
283+
case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled(let cancellationSource))):
284+
switch cancellationSource {
285+
case .reader:
286+
self?.handlePaymentCancellationFromReader(alertProvider: paymentAlerts)
287+
default:
288+
self?.handlePaymentCancellation()
289+
}
284290
case .failure(let error):
285291
self?.handlePaymentFailureAndRetryPayment(error, alertProvider: paymentAlerts, onCompletion: onCompletion)
286292
}
@@ -303,6 +309,19 @@ private extension CollectOrderPaymentUseCase {
303309
onCompletion(.success(capturedPaymentData))
304310
}
305311

312+
func handlePaymentCancellation() {
313+
trackPaymentCancelation()
314+
alertsPresenter.dismiss()
315+
}
316+
317+
func handlePaymentCancellationFromReader(alertProvider paymentAlerts: CardReaderTransactionAlertsProviding) {
318+
trackPaymentCancelation()
319+
guard let dismissedOnReaderAlert = paymentAlerts.cancelledOnReader() else {
320+
return alertsPresenter.dismiss()
321+
}
322+
alertsPresenter.present(viewModel: dismissedOnReaderAlert)
323+
}
324+
306325
/// Log the failure reason, cancel the current payment and retry it if possible.
307326
///
308327
func handlePaymentFailureAndRetryPayment(_ error: Error,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ private extension LegacyCollectOrderPaymentUseCase {
298298
switch result {
299299
case .success(let capturedPaymentData):
300300
self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion)
301+
case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled(_))):
302+
self?.trackPaymentCancelation()
303+
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
301304
case .failure(let error):
302305
self?.handlePaymentFailureAndRetryPayment(error, onCompletion: onCompletion)
303306
}

0 commit comments

Comments
 (0)