From 8b607e08f06e827287217181306181590f8b1085 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 9 Dec 2022 12:14:10 +0000 Subject: [PATCH 1/6] 8085 Handle cancelation from a card reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously (prior to 5233d70b ) we didn’t dismiss any alerts when cancel was tapped on the reader (or built in reader modal) because we ignored the error. This corrects the comments, warnings, and errors produced (in the StripeCardReaderService) now that we don’t ignore the error any more. It also stops showing an alert (with a try again button) when the error is recieved in the CollectOrderPaymentUseCase, and instead dismisses the payment alerts. Now, in the new flow, we handle cancelations from card readers and from buttons in the app equally: we dismiss the alerts and track the cancelation events. --- .../StripeCardReader/StripeCardReaderService.swift | 11 ++++------- .../Collect Payments/CollectOrderPaymentUseCase.swift | 10 ++++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift index edad952d282..e90d9a917fd 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift @@ -509,16 +509,13 @@ private extension StripeCardReaderService { /// the completion block for collectPaymentMethod will be called /// with error Canceled when collectPaymentMethod is canceled /// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)collectPaymentMethod:delegate:completion: - - if underlyingError != .commandCancelled { + if underlyingError == .commandCancelled { + DDLogWarn("💳 Warning: collect payment cancelled \(error)") + } else { DDLogError("💳 Error: collect payment method \(underlyingError)") - promise(.failure(CardReaderServiceError.paymentMethodCollection(underlyingError: underlyingError))) } - if underlyingError == .commandCancelled { - DDLogWarn("💳 Warning: collect payment error cancelled. We actively ignore this error \(error)") - promise(.failure(CardReaderServiceError.paymentCancellation(underlyingError: underlyingError))) - } + promise(.failure(CardReaderServiceError.paymentMethodCollection(underlyingError: underlyingError))) } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 286abef11e1..de34c929a52 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -164,8 +164,7 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { onCompleted: onCompleted) }) case .canceled: - self.alertsPresenter.dismiss() - self.trackPaymentCancelation() + self.handlePaymentCancellation() onCancel() case .none: break @@ -275,6 +274,8 @@ private extension CollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): + self?.handlePaymentCancellation() case .failure(let error): self?.handlePaymentFailureAndRetryPayment(error, alertProvider: paymentAlerts, onCompletion: onCompletion) } @@ -297,6 +298,11 @@ private extension CollectOrderPaymentUseCase { onCompletion(.success(capturedPaymentData)) } + func handlePaymentCancellation() { + trackPaymentCancelation() + alertsPresenter.dismiss() + } + /// Log the failure reason, cancel the current payment and retry it if possible. /// func handlePaymentFailureAndRetryPayment(_ error: Error, From 4b9db0cf3ce9e40bd67299d9cf6dec4d64d9a1e0 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 9 Dec 2022 12:55:03 +0000 Subject: [PATCH 2/6] 8085 Handle cancellation from a reader in legacy flow --- .../Collect Payments/LegacyCollectOrderPaymentUseCase.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift index 21bcbf9246e..30f2d9a0b51 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift @@ -293,6 +293,9 @@ private extension LegacyCollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): + self?.trackPaymentCancelation() + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) case .failure(let error): self?.handlePaymentFailureAndRetryPayment(error, onCompletion: onCompletion) } From 186f7a60ec360d74075aabab4b8e3cbfba1a27a1 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 9 Dec 2022 13:45:40 +0000 Subject: [PATCH 3/6] 8085 Inform the merchant about on-reader cancels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the user cancels the payment on the reader, it can help the merchant to specifically notify them that the payment has not been taken, and they still need to capture payment. We do not do this with the built-in reader because it’s clear when everything happens on the same device. --- .../StripeCardReader/StripeCardReaderService.swift | 12 +++++++----- Hardware/Hardware/CardReader/UnderlyingError.swift | 7 +++++++ .../BluetoothCardReaderPaymentAlertsProvider.swift | 6 ++++++ .../BuiltInCardReaderPaymentAlertsProvider.swift | 4 ++++ .../CardReaderTransactionAlertsProviding.swift | 4 ++++ .../CollectOrderPaymentUseCase.swift | 10 ++++++++++ .../LegacyCollectOrderPaymentUseCase.swift | 3 ++- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift index e90d9a917fd..13d98fe244b 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift @@ -502,24 +502,26 @@ private extension StripeCardReaderService { /// Because we are chaining promises, we need to retain a reference /// to this cancellable if we want to cancel self?.paymentCancellable = Terminal.shared.collectPaymentMethod(intent) { (intent, error) in - self?.paymentCancellable = nil - if let error = error { - let underlyingError = UnderlyingError(with: error) + var underlyingError = UnderlyingError(with: error) /// the completion block for collectPaymentMethod will be called /// with error Canceled when collectPaymentMethod is canceled /// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)collectPaymentMethod:delegate:completion: if underlyingError == .commandCancelled { DDLogWarn("💳 Warning: collect payment cancelled \(error)") + /// If we've not used the cancellable in the app, the cancellation must have come from the reader + if self?.paymentCancellable != nil { + underlyingError = .commandCancelledOnReader + } } else { DDLogError("💳 Error: collect payment method \(underlyingError)") } - + self?.paymentCancellable = nil promise(.failure(CardReaderServiceError.paymentMethodCollection(underlyingError: underlyingError))) - } if let intent = intent { + self?.paymentCancellable = nil self?.sendReaderEvent(.cardDetailsCollected) promise(.success(intent)) } diff --git a/Hardware/Hardware/CardReader/UnderlyingError.swift b/Hardware/Hardware/CardReader/UnderlyingError.swift index 902f38c6183..dcc57232465 100644 --- a/Hardware/Hardware/CardReader/UnderlyingError.swift +++ b/Hardware/Hardware/CardReader/UnderlyingError.swift @@ -29,6 +29,10 @@ public enum UnderlyingError: Error, Equatable { /// A command was cancelled case commandCancelled + /// A command was cancelled on the reader. + /// Note that this is not produced by Stripe, we have to infer it from commandCancelled. + case commandCancelledOnReader + /// Access to location services is currently disabled. This may be because: /// - The user disabled location services in the system settings. /// - The user denied access to location services for your app. @@ -285,6 +289,9 @@ extension UnderlyingError: LocalizedError { case .commandCancelled: return NSLocalizedString("The system canceled the command unexpectedly - please try again", comment: "Error message when the system cancels a command.") + case .commandCancelledOnReader: + return NSLocalizedString("The payment was canceled on the reader", + comment: "Error message when the cancel button on the reader is used.") case .locationServicesDisabled: return NSLocalizedString("Unable to access Location Services - please enable Location Services and try again", comment: "Error message when location services is not enabled for this application.") diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift index 5a83bb3140e..99b0435beb7 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift @@ -87,6 +87,12 @@ final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlert func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { CardPresentModalRetryableError(primaryAction: tryAgain) } + + func cancelledOnReader() -> CardPresentPaymentsModalViewModel? { + CardPresentModalNonRetryableError(amount: amount, + error: CardReaderServiceError.paymentMethodCollection(underlyingError: .commandCancelledOnReader), + onDismiss: { }) + } } private extension BluetoothCardReaderPaymentAlertsProvider { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift index 92181ab7677..31657b4629c 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift @@ -79,6 +79,10 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { CardPresentModalRetryableError(primaryAction: tryAgain) } + + func cancelledOnReader() -> CardPresentPaymentsModalViewModel? { + return nil + } } private extension BuiltInCardReaderPaymentAlertsProvider { diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift index 9a12c6b7ac4..67af4238aff 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift @@ -45,4 +45,8 @@ protocol CardReaderTransactionAlertsProviding { /// An alert to display a retriable error /// func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// An alert to notify the merchant that the transaction was cancelled using a button on the reader + /// + func cancelledOnReader() -> CardPresentPaymentsModalViewModel? } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index de34c929a52..c143954cfba 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -274,6 +274,8 @@ private extension CollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelledOnReader)): + self?.handlePaymentCancellationFromReader(alertProvider: paymentAlerts) case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): self?.handlePaymentCancellation() case .failure(let error): @@ -303,6 +305,14 @@ private extension CollectOrderPaymentUseCase { alertsPresenter.dismiss() } + func handlePaymentCancellationFromReader(alertProvider paymentAlerts: CardReaderTransactionAlertsProviding) { + trackPaymentCancelation() + guard let dismissedOnReaderAlert = paymentAlerts.cancelledOnReader() else { + return alertsPresenter.dismiss() + } + alertsPresenter.present(viewModel: dismissedOnReaderAlert) + } + /// Log the failure reason, cancel the current payment and retry it if possible. /// func handlePaymentFailureAndRetryPayment(_ error: Error, diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift index 30f2d9a0b51..241e7589b85 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift @@ -293,7 +293,8 @@ private extension LegacyCollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) - case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelledOnReader)), + .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): self?.trackPaymentCancelation() onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) case .failure(let error): From bc89e2b6ffc1e8c9ba11b267a3e1720f9027979f Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 12 Dec 2022 13:19:38 +0000 Subject: [PATCH 4/6] 8085 Combine cancellation reasons in underlyingerror --- .../StripeCardReaderService.swift | 10 +++++-- .../UnderlyingError+Stripe.swift | 2 +- .../Hardware/CardReader/UnderlyingError.swift | 29 ++++++++++++------- ...toothCardReaderPaymentAlertsProvider.swift | 2 +- .../CollectOrderPaymentUseCase.swift | 11 ++++--- .../LegacyCollectOrderPaymentUseCase.swift | 3 +- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift index 13d98fe244b..6d764c1f06a 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift @@ -507,11 +507,15 @@ private extension StripeCardReaderService { /// the completion block for collectPaymentMethod will be called /// with error Canceled when collectPaymentMethod is canceled /// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)collectPaymentMethod:delegate:completion: - if underlyingError == .commandCancelled { + if case .commandCancelled(let cancellationSource) = underlyingError { DDLogWarn("💳 Warning: collect payment cancelled \(error)") /// If we've not used the cancellable in the app, the cancellation must have come from the reader - if self?.paymentCancellable != nil { - underlyingError = .commandCancelledOnReader + if case .unknown = cancellationSource { + if self?.paymentCancellable != nil { + underlyingError = .commandCancelled(from: .reader) + } else { + underlyingError = .commandCancelled(from: .app) + } } } else { DDLogError("💳 Error: collect payment method \(underlyingError)") diff --git a/Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift b/Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift index 915699280d0..6a17582593c 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift @@ -26,7 +26,7 @@ extension UnderlyingError { case ErrorCode.Code.featureNotAvailableWithConnectedReader.rawValue: self = .featureNotAvailableWithConnectedReader case ErrorCode.Code.canceled.rawValue: - self = .commandCancelled + self = .commandCancelled(from: .unknown) case ErrorCode.Code.locationServicesDisabled.rawValue: self = .locationServicesDisabled case ErrorCode.Code.bluetoothDisabled.rawValue: diff --git a/Hardware/Hardware/CardReader/UnderlyingError.swift b/Hardware/Hardware/CardReader/UnderlyingError.swift index dcc57232465..54f8e55383c 100644 --- a/Hardware/Hardware/CardReader/UnderlyingError.swift +++ b/Hardware/Hardware/CardReader/UnderlyingError.swift @@ -27,11 +27,15 @@ public enum UnderlyingError: Error, Equatable { case featureNotAvailableWithConnectedReader /// A command was cancelled - case commandCancelled - - /// A command was cancelled on the reader. - /// Note that this is not produced by Stripe, we have to infer it from commandCancelled. - case commandCancelledOnReader + case commandCancelled(from: CancellationSource) + + /// A command can be cancelled on the reader, or in the app. + /// Note that this is not produced by Stripe, we have to infer it from commandCancelled, so we start with `.unknown`. + public enum CancellationSource { + case unknown + case app + case reader + } /// Access to location services is currently disabled. This may be because: /// - The user disabled location services in the system settings. @@ -286,12 +290,15 @@ extension UnderlyingError: LocalizedError { case .featureNotAvailableWithConnectedReader: return NSLocalizedString("Unable to perform request with the connected reader - unsupported feature - please try again with another reader", comment: "Error message when the card reader cannot be used to perform the requested task.") - case .commandCancelled: - return NSLocalizedString("The system canceled the command unexpectedly - please try again", - comment: "Error message when the system cancels a command.") - case .commandCancelledOnReader: - return NSLocalizedString("The payment was canceled on the reader", - comment: "Error message when the cancel button on the reader is used.") + case .commandCancelled(let cancellationSource): + switch cancellationSource { + case .reader: + return NSLocalizedString("The payment was canceled on the reader", + comment: "Error message when the cancel button on the reader is used.") + default: + return NSLocalizedString("The system canceled the command unexpectedly - please try again", + comment: "Error message when the system cancels a command.") + } case .locationServicesDisabled: return NSLocalizedString("Unable to access Location Services - please enable Location Services and try again", comment: "Error message when location services is not enabled for this application.") diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift index 99b0435beb7..ef32090f74d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BluetoothCardReaderPaymentAlertsProvider.swift @@ -90,7 +90,7 @@ final class BluetoothCardReaderPaymentAlertsProvider: CardReaderTransactionAlert func cancelledOnReader() -> CardPresentPaymentsModalViewModel? { CardPresentModalNonRetryableError(amount: amount, - error: CardReaderServiceError.paymentMethodCollection(underlyingError: .commandCancelledOnReader), + error: CardReaderServiceError.paymentMethodCollection(underlyingError: .commandCancelled(from: .reader)), onDismiss: { }) } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index c143954cfba..89f4d87a982 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -274,10 +274,13 @@ private extension CollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) - case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelledOnReader)): - self?.handlePaymentCancellationFromReader(alertProvider: paymentAlerts) - case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): - self?.handlePaymentCancellation() + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled(let cancellationSource))): + switch cancellationSource { + case .reader: + self?.handlePaymentCancellationFromReader(alertProvider: paymentAlerts) + default: + self?.handlePaymentCancellation() + } case .failure(let error): self?.handlePaymentFailureAndRetryPayment(error, alertProvider: paymentAlerts, onCompletion: onCompletion) } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift index 241e7589b85..2bbbfadf1e5 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift @@ -293,8 +293,7 @@ private extension LegacyCollectOrderPaymentUseCase { switch result { case .success(let capturedPaymentData): self?.handleSuccessfulPayment(capturedPaymentData: capturedPaymentData, onCompletion: onCompletion) - case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelledOnReader)), - .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled)): + case .failure(CardReaderServiceError.paymentMethodCollection(.commandCancelled(_))): self?.trackPaymentCancelation() onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) case .failure(let error): From bee90e94b993ff9c24528ed0ba29a6b33e8b1feb Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 12 Dec 2022 13:42:54 +0000 Subject: [PATCH 5/6] 8085 Explicit flag for cancellation source --- .../StripeCardReader/StripeCardReaderService.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift index 6d764c1f06a..7cb5c1ca31c 100644 --- a/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift +++ b/Hardware/Hardware/CardReader/StripeCardReader/StripeCardReaderService.swift @@ -32,6 +32,10 @@ public final class StripeCardReaderService: NSObject { /// Keeps track of whether a chip card needs to be removed private var timerCancellable: Cancellable? private var isChipCardInserted: Bool = false + + /// Stripe don't tell us where a cancellation comes from: if we keep track of when we trigger one, + /// we can infer when it comes from the cancel button on the reader instead + private var cancellationStartedInApp: Bool? } @@ -285,6 +289,8 @@ extension StripeCardReaderService: CardReaderService { return } + self.cancellationStartedInApp = true + let cancelPaymentIntent = { [weak self] in Terminal.shared.cancelPaymentIntent(activePaymentIntent) { (intent, error) in if let error = error { @@ -296,6 +302,7 @@ extension StripeCardReaderService: CardReaderService { self?.activePaymentIntent = nil promise(.success(())) } + self?.cancellationStartedInApp = nil } } guard let paymentCancellable = self.paymentCancellable, @@ -509,12 +516,11 @@ private extension StripeCardReaderService { /// https://stripe.dev/stripe-terminal-ios/docs/Classes/SCPTerminal.html#/c:objc(cs)SCPTerminal(im)collectPaymentMethod:delegate:completion: if case .commandCancelled(let cancellationSource) = underlyingError { DDLogWarn("💳 Warning: collect payment cancelled \(error)") - /// If we've not used the cancellable in the app, the cancellation must have come from the reader if case .unknown = cancellationSource { - if self?.paymentCancellable != nil { - underlyingError = .commandCancelled(from: .reader) - } else { + if self?.cancellationStartedInApp != nil { underlyingError = .commandCancelled(from: .app) + } else { + underlyingError = .commandCancelled(from: .reader) } } } else { From fc1213a692dec0edb71a9d8ed9c9242cbc200be6 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Mon, 12 Dec 2022 18:09:29 +0000 Subject: [PATCH 6/6] Fix test --- Hardware/HardwareTests/ErrorCodesTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hardware/HardwareTests/ErrorCodesTests.swift b/Hardware/HardwareTests/ErrorCodesTests.swift index b97d14ea6ba..80a35854f36 100644 --- a/Hardware/HardwareTests/ErrorCodesTests.swift +++ b/Hardware/HardwareTests/ErrorCodesTests.swift @@ -39,7 +39,7 @@ final class CardReaderServiceErrorTests: XCTestCase { } func test_stripe_cancelled_maps_to_expected_error() { - XCTAssertEqual(.commandCancelled, domainError(stripeCode: 2020)) + XCTAssertEqual(.commandCancelled(from: .unknown), domainError(stripeCode: 2020)) } func test_stripe_location_services_disabled_maps_to_expected_error() {