Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Hardware/Hardware/CardReader/UnderlyingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ extension UnderlyingError: LocalizedError {
"the device does not meet minimum requirements.")
case .commandNotAllowedDuringCall:
return NSLocalizedString("The built-in reader cannot be used during a phone call. Please try again after " +
"you finish your call",
"you finish your call.",
comment: "Error message shown when the built-in reader cannot be used because " +
"there is a call in progress")
case .invalidAmount:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel
/// Amount charged
private let amount: String

/// The error returned by the stack
private let error: Error

/// Called when the view is dismissed
private let onDismiss: () -> Void

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

let auxiliaryButtonTitle: String? = nil

var bottomTitle: String? {
error.localizedDescription
}
let bottomTitle: String?

let bottomSubtitle: String? = nil

Expand All @@ -43,12 +38,16 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel
return topTitle + bottomTitle
}

init(amount: String, error: Error, onDismiss: @escaping () -> Void) {
init(amount: String, errorDescription: String?, onDismiss: @escaping () -> Void) {
self.amount = amount
self.error = error
self.bottomTitle = errorDescription
self.onDismiss = onDismiss
}

convenience init(amount: String, error: Error, onDismiss: @escaping () -> Void) {
self.init(amount: amount, errorDescription: error.localizedDescription, onDismiss: onDismiss)
}

func didTapPrimaryButton(in viewController: UIViewController?) {
viewController?.dismiss(animated: true) { [weak self] in
self?.onDismiss()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,16 @@ final class BuiltInCardReaderPaymentAlertsProvider: CardReaderTransactionAlertsP
}

func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
let errorDescription: String?
if let error = error as? CardReaderServiceError {
switch error {
case .connection(let underlyingError),
.discovery(let underlyingError),
.disconnection(let underlyingError),
.intentCreation(let underlyingError),
.paymentMethodCollection(let underlyingError),
.paymentCapture(let underlyingError),
.paymentCancellation(let underlyingError),
.softwareUpdate(let underlyingError, _):
errorDescription = Localization.errorDescription(underlyingError: underlyingError)
default:
errorDescription = error.errorDescription
}
} else {
errorDescription = error.localizedDescription
}
return CardPresentModalError(errorDescription: errorDescription,
return CardPresentModalError(errorDescription: builtInReaderDescription(for: error),
transactionType: .collectPayment,
primaryAction: tryAgain,
dismissCompletion: dismissCompletion)
}

func nonRetryableError(error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion)
CardPresentModalNonRetryableError(amount: amount,
errorDescription: builtInReaderDescription(for: error),
onDismiss: dismissCompletion)
}

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

private extension BuiltInCardReaderPaymentAlertsProvider {
func builtInReaderDescription(for error: Error) -> String? {
if let error = error as? CardReaderServiceError {
switch error {
case .connection(let underlyingError),
.discovery(let underlyingError),
.disconnection(let underlyingError),
.intentCreation(let underlyingError),
.paymentMethodCollection(let underlyingError),
.paymentCapture(let underlyingError),
.paymentCancellation(let underlyingError),
.softwareUpdate(let underlyingError, _):
return Localization.errorDescription(underlyingError: underlyingError)
default:
return error.errorDescription
}
} else {
return error.localizedDescription
}
}

enum Localization {
static func errorDescription(underlyingError: UnderlyingError) -> String? {
switch underlyingError {
Expand All @@ -102,6 +106,10 @@ private extension BuiltInCardReaderPaymentAlertsProvider {
"Sorry, this payment couldn’t be processed",
comment: "Error message when the card reader service experiences an unexpected internal service error."
)
case .notConnectedToReader:
return NSLocalizedString(
"The payment was interrupted and cannot be continued. You can retry the payment from the order screen.",
comment: "Error shown when the built-in card reader payment is interrupted by activity on the phone")
default:
return underlyingError.errorDescription
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protocol CollectOrderPaymentProtocol {
/// - Parameter onCollect: Closure Invoked after the collect process has finished.
/// - Parameter onCompleted: Closure Invoked after the flow has been totally completed.
/// - Parameter onCancel: Closure invoked after the flow is cancelled
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
func collectPayment(onFailure: @escaping (Error) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
}

/// Use case to collect payments from an order.
Expand Down Expand Up @@ -129,15 +129,15 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
/// 7. Tracks payment analytics
///
///
/// - Parameter onCollect: Closure invoked after the collect process has finished.
/// - Parameter onFailure: Closure invoked after the payment process fails.
/// - Parameter onCancel: Closure invoked after the flow is cancelled
/// - Parameter onCompleted: Closure invoked after the flow has been totally completed, currently after merchant has handled the receipt.
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (),
func collectPayment(onFailure: @escaping (Error) -> (),
onCancel: @escaping () -> (),
onCompleted: @escaping () -> ()) {
guard isTotalAmountValid() else {
let error = totalAmountInvalidError()
onCollect(.failure(error))
onFailure(error)
return handleTotalAmountInvalidError(totalAmountInvalidError(), onCompleted: onCancel)
}

Expand All @@ -158,16 +158,14 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
case .failure(CollectOrderPaymentUseCaseError.flowCanceledByUser):
self.rootViewController.presentedViewController?.dismiss(animated: true)
return onCancel()
default:
onCollect(result.map { _ in () }) // Transforms Result<CardPresentCapturedPaymentData, Error> to Result<Void, Error>
}
// Handle payment receipt
guard let paymentData = try? result.get() else {
return onCompleted()
case .failure(let error):
return onFailure(error)
case .success(let paymentData):
// Handle payment receipt
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters,
alertProvider: paymentAlertProvider,
onCompleted: onCompleted)
}
self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters,
alertProvider: paymentAlertProvider,
onCompleted: onCompleted)
})
case .canceled:
self.handlePaymentCancellation()
Expand Down Expand Up @@ -333,33 +331,15 @@ private extension CollectOrderPaymentUseCase {

trackPaymentFailure(with: error)

// Inform about the error
alertsPresenter.present(
viewModel: paymentAlerts.error(error: error,
tryAgain: { [weak self] in

// Cancel current payment
self?.paymentOrchestrator.cancelPayment { [weak self] result in
guard let self = self else { return }

switch result {
case .success:
// Retry payment
self.attemptPayment(alertProvider: paymentAlerts,
onCompletion: onCompletion)

case .failure(let cancelError):
// Inform that payment can't be retried.
self.alertsPresenter.present(
viewModel: paymentAlerts.nonRetryableError(error: cancelError) {
onCompletion(.failure(error))
})
}
}
}, dismissCompletion: {
onCompletion(.failure(error))
})
)
if canRetryPayment(with: error) {
presentRetryableError(error: error,
paymentAlerts: paymentAlerts,
onCompletion: onCompletion)
} else {
presentNonRetryableError(error: error,
paymentAlerts: paymentAlerts,
onCompletion: onCompletion)
}
}

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

private func canRetryPayment(with error: Error) -> Bool {
guard let serviceError = error as? CardReaderServiceError else {
return true
}
switch serviceError {
case .paymentMethodCollection(let underlyingError),
.paymentCapture(let underlyingError),
.paymentCancellation(let underlyingError):
return canRetryPayment(underlyingError: underlyingError)
default:
return true
}
}

private func canRetryPayment(underlyingError: UnderlyingError) -> Bool {
switch underlyingError {
case .notConnectedToReader,
.commandNotAllowedDuringCall,
.featureNotAvailableWithConnectedReader:
return false
default:
return true
}
}

private func presentRetryableError(error: Error,
paymentAlerts: CardReaderTransactionAlertsProviding,
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
alertsPresenter.present(
viewModel: paymentAlerts.error(error: error,
tryAgain: { [weak self] in

// Cancel current payment
self?.paymentOrchestrator.cancelPayment { [weak self] result in
guard let self = self else { return }

switch result {
case .success:
// Retry payment
self.attemptPayment(alertProvider: paymentAlerts,
onCompletion: onCompletion)

case .failure(let cancelError):
// Inform that payment can't be retried.
self.alertsPresenter.present(
viewModel: paymentAlerts.nonRetryableError(error: cancelError) {
onCompletion(.failure(error))
})
}
}
}, dismissCompletion: {
onCompletion(.failure(error))
})
)
}

private func presentNonRetryableError(error: Error,
paymentAlerts: CardReaderTransactionAlertsProviding,
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
alertsPresenter.present(
viewModel: paymentAlerts.nonRetryableError(error: error,
dismissCompletion: {
onCompletion(.failure(error))
}))
}

/// Cancels payment and record analytics.
///
func cancelPayment(onCompleted: @escaping () -> ()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@ import MessageUI
import WooFoundation
import protocol Storage.StorageManagerType


/// Protocol to abstract the `LegacyCollectOrderPaymentUseCase`.
/// Currently only used to facilitate unit tests.
///
protocol LegacyCollectOrderPaymentProtocol {
/// Starts the collect payment flow.
///
///
/// - Parameter onCollect: Closure Invoked after the collect process has finished.
/// - Parameter onCompleted: Closure Invoked after the flow has been totally completed.
/// - Parameter onCancel: Closure invoked after the flow is cancelled
func collectPayment(onCollect: @escaping (Result<Void, Error>) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ())
}

/// Use case to collect payments from an order.
/// Orchestrates reader connection, payment, UI alerts, receipt handling and analytics.
///
final class LegacyCollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
final class LegacyCollectOrderPaymentUseCase: NSObject, LegacyCollectOrderPaymentProtocol {
/// Currency Formatter
///
private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct PaymentMethodsView: View {
Divider()

MethodRow(icon: .creditCardImage, title: Localization.card, accessibilityID: Accessibility.cardMethod) {
viewModel.collectPayment(on: rootViewController, onSuccess: dismiss)
viewModel.collectPayment(on: rootViewController, onSuccess: dismiss, onFailure: dismiss)
}
}

Expand Down
Loading