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
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ struct CardPresentCapturedPaymentData {
}

/// Orchestrates the sequence of actions required to capture a payment:
/// 1. Check if there is a card reader connected
/// 2. Launch the reader discovering and pairing UI if there is no reader connected
/// 3. Obtain a Payment Intent from the card reader (i.e., create a payment intent, collect a payment method, and process the payment)
/// 4. Submit the Payment Intent to WCPay to capture a payment
/// Steps 1 and 2 will be implemented as part of https://github.com/woocommerce/woocommerce-ios/issues/4062
/// 1. Triggers the `preparingReader` alert
/// 2. Creates the payment intent parameters
/// 3. Controls (prevents during payment) wallet presentation: we don't want to use the merchant's Apple Pay for their customer's purchase!
/// 4. Obtain a Payment Intent from the card reader (i.e., create a payment intent, collect a payment method, and process the payment)
/// 5. Submit the Payment Intent to WCPay to capture a payment
final class PaymentCaptureOrchestrator {
private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
private let personNameComponentsFormatter = PersonNameComponentsFormatter()
Expand All @@ -39,11 +39,14 @@ final class PaymentCaptureOrchestrator {
paymentGatewayAccount: PaymentGatewayAccount,
paymentMethodTypes: [String],
stripeSmallestCurrencyUnitMultiplier: Decimal,
onPreparingReader: () -> Void,
onWaitingForInput: @escaping (CardReaderInput) -> Void,
onProcessingMessage: @escaping () -> Void,
onDisplayMessage: @escaping (String) -> Void,
onProcessingCompletion: @escaping (PaymentIntent) -> Void,
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> Void) {
onPreparingReader()

/// Set state of CardPresentPaymentStore
///
let setAccount = CardPresentPaymentAction.use(paymentGatewayAccount: paymentGatewayAccount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {

private let transactionType: CardPresentTransactionType

private let alertsProvider: CardReaderTransactionAlertsProviding

init(transactionType: CardPresentTransactionType,
presentingController: UIViewController) {
self.transactionType = transactionType
self.presentingController = presentingController
self.alertsProvider = CardReaderPaymentAlertsProvider(transactionType: transactionType)
}

func presentViewModel(viewModel: CardPresentPaymentsModalViewModel) {
Expand All @@ -60,166 +63,45 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {

// Initial presentation of the modal view controller. We need to provide
// a customer name and an amount.
let viewModel = tapOrInsert(readerInputMethods: inputMethods, onCancel: onCancel)
let viewModel = alertsProvider.tapOrInsertCard(title: title,
amount: amount,
inputMethods: inputMethods,
onCancel: onCancel)
presentViewModel(viewModel: viewModel)
}

func displayReaderMessage(message: String) {
let viewModel = displayMessage(message: message)
let viewModel = alertsProvider.displayReaderMessage(message: message)
presentViewModel(viewModel: viewModel)
}

func processingPayment() {
let viewModel = processing()
let viewModel = alertsProvider.processingTransaction()
presentViewModel(viewModel: viewModel)
}

func success(printReceipt: @escaping () -> Void, emailReceipt: @escaping () -> Void, noReceiptAction: @escaping () -> Void) {
let viewModel = successViewModel(printReceipt: printReceipt,
emailReceipt: emailReceipt,
noReceiptAction: noReceiptAction)
let viewModel = alertsProvider.success(printReceipt: printReceipt,
emailReceipt: emailReceipt,
noReceiptAction: noReceiptAction)
presentViewModel(viewModel: viewModel)
}

func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) {
let viewModel = errorViewModel(error: error, tryAgain: tryAgain, dismissCompletion: dismissCompletion)
let viewModel = alertsProvider.error(error: error,
tryAgain: tryAgain,
dismissCompletion: dismissCompletion)
presentViewModel(viewModel: viewModel)
}

func nonRetryableError(from: UIViewController?, error: Error, dismissCompletion: @escaping () -> Void) {
let viewModel = nonRetryableErrorViewModel(amount: amount, error: error, dismissCompletion: dismissCompletion)
let viewModel = alertsProvider.nonRetryableError(error: error,
dismissCompletion: dismissCompletion)
presentViewModel(viewModel: viewModel)
}

func retryableError(from: UIViewController?, tryAgain: @escaping () -> Void) {
let viewModel = retryableErrorViewModel(tryAgain: tryAgain)
let viewModel = alertsProvider.retryableError(tryAgain: tryAgain)
presentViewModel(viewModel: viewModel)
}
}

private extension OrderDetailsPaymentAlerts {
func tapOrInsert(readerInputMethods: CardReaderInput, onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
CardPresentModalTapCard(name: name,
amount: amount,
transactionType: transactionType,
inputMethods: readerInputMethods,
onCancel: onCancel)
}

func displayMessage(message: String) -> CardPresentPaymentsModalViewModel {
CardPresentModalDisplayMessage(name: name, amount: amount, message: message)
}

func processing() -> CardPresentPaymentsModalViewModel {
CardPresentModalProcessing(name: name, amount: amount, transactionType: transactionType)
}

func successViewModel(printReceipt: @escaping () -> Void,
emailReceipt: @escaping () -> Void,
noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
if MFMailComposeViewController.canSendMail() {
return CardPresentModalSuccess(printReceipt: printReceipt,
emailReceipt: emailReceipt,
noReceiptAction: noReceiptAction)
} else {
return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction)
}
}

func errorViewModel(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),
.refundCreation(let underlyingError),
.refundPayment(let underlyingError, _),
.refundCancellation(let underlyingError),
.softwareUpdate(let underlyingError, _):
errorDescription = Localization.errorDescription(underlyingError: underlyingError, transactionType: transactionType)
default:
errorDescription = error.errorDescription
}
} else {
errorDescription = error.localizedDescription
}
return CardPresentModalError(errorDescription: errorDescription,
transactionType: transactionType,
primaryAction: tryAgain,
dismissCompletion: dismissCompletion)
}

func retryableErrorViewModel(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
CardPresentModalRetryableError(primaryAction: tryAgain)
}

func nonRetryableErrorViewModel(amount: String, error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion)
}
}

private extension OrderDetailsPaymentAlerts {
enum Localization {
static func errorDescription(underlyingError: UnderlyingError, transactionType: CardPresentTransactionType) -> String? {
switch underlyingError {
case .unsupportedReaderVersion:
switch transactionType {
case .collectPayment:
return NSLocalizedString(
"The card reader software is out-of-date - please update the card reader software before attempting to process payments",
comment: "Error message when the card reader software is too far out of date to process payments."
)
case .refund:
return NSLocalizedString(
"The card reader software is out-of-date - please update the card reader software before attempting to process refunds",
comment: "Error message when the card reader software is too far out of date to process in-person refunds."
)
}
case .paymentDeclinedByCardReader:
switch transactionType {
case .collectPayment:
return NSLocalizedString("The card was declined by the card reader - please try another means of payment",
comment: "Error message when the card reader itself declines the card.")
case .refund:
return NSLocalizedString("The card was declined by the card reader - please try another means of refund",
comment: "Error message when the card reader itself declines the card.")
}
case .processorAPIError:
switch transactionType {
case .collectPayment:
return NSLocalizedString(
"The payment can not be processed by the payment processor.",
comment: "Error message when the payment can not be processed (i.e. order amount is below the minimum amount allowed.)"
)
case .refund:
return NSLocalizedString(
"The refund can not be processed by the payment processor.",
comment: "Error message when the in-person refund can not be processed (i.e. order amount is below the minimum amount allowed.)"
)
}
case .internalServiceError:
switch transactionType {
case .collectPayment:
return NSLocalizedString(
"Sorry, this payment couldn’t be processed",
comment: "Error message when the card reader service experiences an unexpected internal service error."
)
case .refund:
return NSLocalizedString(
"Sorry, this refund couldn’t be processed",
comment: "Error message when the card reader service experiences an unexpected internal service error."
)
}
default:
return underlyingError.errorDescription
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,12 @@ private extension BuiltInCardReaderConnectionController {
///
private func returnSuccess(result: CardReaderConnectionResult) {
onCompletion?(.success(result))
alertsPresenter.dismiss()
state = .idle
}

/// Calls the completion with a failure result
///
private func returnFailure(error: Error) {
alertsPresenter.dismiss()
onCompletion?(.failure(error))
state = .idle
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ final class CardPresentPaymentPreflightController {
observeConnectedReaders()
// If we're already connected to a reader, return it
if let connectedReader = connectedReader {
readerConnection.send(CardReaderConnectionResult.connected(connectedReader))
handleConnectionResult(.success(.connected(connectedReader)))
return
}

// TODO: Run onboarding if needed
Expand Down Expand Up @@ -146,7 +147,7 @@ final class CardPresentPaymentPreflightController {
case .success(let unwrapped):
self.readerConnection.send(unwrapped)
default:
break
alertsPresenter.dismiss()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,14 +735,12 @@ private extension CardReaderConnectionController {
///
private func returnSuccess(result: CardReaderConnectionResult) {
onCompletion?(.success(result))
alertsPresenter.dismiss()
state = .idle
}

/// Calls the completion with a failure result
///
private func returnFailure(error: Error) {
alertsPresenter.dismiss()
onCompletion?(.failure(error))
state = .idle
}
Expand Down
Loading