diff --git a/WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift b/WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift index 98ebbb361b1..0d562071e9a 100644 --- a/WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift +++ b/WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift @@ -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() @@ -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) -> Void) { + onPreparingReader() + /// Set state of CardPresentPaymentStore /// let setAccount = CardPresentPaymentAction.use(paymentGatewayAccount: paymentGatewayAccount) diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift index 8162d541280..bbeee14549a 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift @@ -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) { @@ -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 - } - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift index 68d1c1ec96e..2a3a32c0928 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift @@ -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 } diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift index c0d849ce9b8..9fbbaa88d98 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift @@ -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 @@ -146,7 +147,7 @@ final class CardPresentPaymentPreflightController { case .success(let unwrapped): self.readerConnection.send(unwrapped) default: - break + alertsPresenter.dismiss() } } diff --git a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift index ceab72a92f0..0addf755b24 100644 --- a/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift +++ b/WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift @@ -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 } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderPaymentAlertsProvider.swift new file mode 100644 index 00000000000..d875ac76776 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderPaymentAlertsProvider.swift @@ -0,0 +1,149 @@ +import Foundation +import Yosemite +import MessageUI +import enum Hardware.CardReaderServiceError +import enum Hardware.UnderlyingError + +final class CardReaderPaymentAlertsProvider: CardReaderTransactionAlertsProviding { + var name: String = "" + var amount: String = "" + var transactionType: CardPresentTransactionType + + init(transactionType: CardPresentTransactionType) { + self.transactionType = transactionType + } + + func preparingReader(onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalPreparingReader(cancelAction: onCancel) + } + + func tapOrInsertCard(title: String, + amount: String, + inputMethods: Yosemite.CardReaderInput, + onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + name = title + self.amount = amount + return CardPresentModalTapCard(name: title, + amount: amount, + transactionType: transactionType, + inputMethods: inputMethods, + onCancel: onCancel) + } + + func displayReaderMessage(message: String) -> CardPresentPaymentsModalViewModel { + CardPresentModalDisplayMessage(name: name, + amount: amount, + message: message) + } + + func processingTransaction() -> CardPresentPaymentsModalViewModel { + CardPresentModalProcessing(name: name, amount: amount, transactionType: transactionType) + } + + func success(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 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), + .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 nonRetryableError(error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion) + } + + func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel { + CardPresentModalRetryableError(primaryAction: tryAgain) + } +} + +private extension CardReaderPaymentAlertsProvider { + 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 + } + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift new file mode 100644 index 00000000000..9a12c6b7ac4 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/CardReaderTransactionAlertsProviding.swift @@ -0,0 +1,48 @@ +import UIKit +import Yosemite + +/// Defines a protocol for card reader transaction alert providers to conform to - defining what +/// alert viewModels such a provider is expected to provide over the course of performind +/// a card present transaction (payment or refund.) +/// +protocol CardReaderTransactionAlertsProviding { + /// A cancellable alert indicating we are preparing a reader to collect card details + /// + func preparingReader(onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// A cancellable alert indicating the reader is ready to collect card details + /// + func tapOrInsertCard(title: String, + amount: String, + inputMethods: CardReaderInput, + onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// An alert to display a message from a reader + /// + func displayReaderMessage(message: String) -> CardPresentPaymentsModalViewModel + + /// An alert to show that the transaction is being processed + /// + func processingTransaction() -> CardPresentPaymentsModalViewModel + + /// An alert to display successful transaction and provide options related to receipts + /// + func success(printReceipt: @escaping () -> Void, + emailReceipt: @escaping () -> Void, + noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// An alert to display a retriable and cancellable error + /// + func error(error: Error, + tryAgain: @escaping () -> Void, + dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// An alert to display a non-retriable and cancellable error + /// + func nonRetryableError(error: Error, + dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel + + /// An alert to display a retriable error + /// + func retryableError(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 9787b136ebb..b6fedc5a6bc 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -4,6 +4,9 @@ import Yosemite import MessageUI import WooFoundation import protocol Storage.StorageManagerType +//TODO: Move to alertprovider (and ideally, remove from this target or translate through Yosemite) +import enum Hardware.CardReaderServiceError +import enum Hardware.UnderlyingError enum CollectOrderPaymentUseCaseError: Error { case flowCanceledByUser @@ -70,6 +73,10 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { /// private let alertsPresenter: CardPresentPaymentAlertsPresenting + /// Payment alerts provider + /// + private let paymentAlerts: CardReaderTransactionAlertsProviding + /// Stores the card reader listener subscription while trying to connect to one. /// private var readerSubscription: AnyCancellable? @@ -77,17 +84,13 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { /// Stores the connected card reader for analytics. private var connectedReader: CardReader? - /// Alert manager to inform merchants about reader & card actions. - /// - private let alerts: OrderDetailsPaymentAlertsProtocol - /// IPP Configuration. /// private let configuration: CardPresentPaymentsConfiguration /// IPP payments collector. /// - private lazy var paymentOrchestrator = LegacyPaymentCaptureOrchestrator(stores: stores) + private lazy var paymentOrchestrator = PaymentCaptureOrchestrator(stores: stores) /// Coordinates emailing a receipt after payment success. private var receiptEmailCoordinator: CardPresentPaymentReceiptEmailCoordinator? @@ -101,7 +104,6 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { formattedAmount: String, paymentGatewayAccount: PaymentGatewayAccount, rootViewController: UIViewController, - alerts: OrderDetailsPaymentAlertsProtocol, configuration: CardPresentPaymentsConfiguration, stores: StoresManager = ServiceLocator.stores, analytics: Analytics = ServiceLocator.analytics) { @@ -111,7 +113,7 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { self.paymentGatewayAccount = paymentGatewayAccount self.rootViewController = rootViewController self.alertsPresenter = CardPresentPaymentAlertsPresenter(rootViewController: rootViewController) - self.alerts = alerts + self.paymentAlerts = CardReaderPaymentAlertsProvider(transactionType: .collectPayment) self.configuration = configuration self.stores = stores self.analytics = analytics @@ -165,6 +167,7 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol { self.presentReceiptAlert(receiptParameters: paymentData.receiptParameters, onCompleted: onCompleted) }) case .canceled: + self.alertsPresenter.dismiss() self.trackPaymentCancelation() onCancel() case .none: @@ -207,7 +210,8 @@ private extension CollectOrderPaymentUseCase { func handleTotalAmountInvalidError(_ error: Error, onCompleted: @escaping () -> ()) { trackPaymentFailure(with: error) DDLogError("💳 Error: failed to capture payment for order. Order amount is below minimum or not valid") - self.alerts.nonRetryableError(from: self.rootViewController, error: totalAmountInvalidError(), dismissCompletion: onCompleted) + self.alertsPresenter.present(viewModel: paymentAlerts.nonRetryableError(error: totalAmountInvalidError(), + dismissCompletion: onCompleted)) } /// Attempts to collect payment for an order. @@ -215,18 +219,9 @@ private extension CollectOrderPaymentUseCase { func attemptPayment(onCompletion: @escaping (Result) -> ()) { guard let orderTotal = orderTotal else { onCompletion(.failure(NotValidAmountError.other)) - return } - // Show preparing reader alert - // TODO: Move this tho the (New)PaymentCaptureOrchestrator - alerts.preparingReader(onCancel: { [weak self] in - self?.cancelPayment(onCompleted: { - onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) - }) - }) - // Start collect payment process paymentOrchestrator.collectPayment( for: order, @@ -234,23 +229,34 @@ private extension CollectOrderPaymentUseCase { paymentGatewayAccount: paymentGatewayAccount, paymentMethodTypes: configuration.paymentMethods.map(\.rawValue), stripeSmallestCurrencyUnitMultiplier: configuration.stripeSmallestCurrencyUnitMultiplier, + onPreparingReader: { [weak self] in + self?.alertsPresenter.present(viewModel: paymentAlerts.preparingReader(onCancel: { + self?.cancelPayment(onCompleted: { + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) + }) + })) + }, onWaitingForInput: { [weak self] inputMethods in guard let self = self else { return } - self.alerts.tapOrInsertCard(title: Localization.collectPaymentTitle(username: self.order.billingAddress?.firstName), - amount: self.formattedAmount, - inputMethods: inputMethods, - onCancel: { [weak self] in - self?.cancelPayment { - onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) - } - }) - + self.alertsPresenter.present( + viewModel: self.paymentAlerts.tapOrInsertCard( + title: Localization.collectPaymentTitle(username: self.order.billingAddress?.firstName), + amount: self.formattedAmount, + inputMethods: inputMethods, + onCancel: { [weak self] in + self?.cancelPayment { + onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser)) + } + }) + ) }, onProcessingMessage: { [weak self] in + guard let self = self else { return } // Waiting message - self?.alerts.processingPayment() + self.alertsPresenter.present(viewModel: self.paymentAlerts.processingTransaction()) }, onDisplayMessage: { [weak self] message in + guard let self = self else { return } // Reader messages. EG: Remove Card - self?.alerts.displayReaderMessage(message: message) + self.alertsPresenter.present(viewModel: self.paymentAlerts.displayReaderMessage(message: message)) }, onProcessingCompletion: { [weak self] intent in self?.trackProcessingCompletion(intent: intent) self?.markOrderAsPaidIfNeeded(intent: intent) @@ -288,28 +294,31 @@ private extension CollectOrderPaymentUseCase { trackPaymentFailure(with: error) // Inform about the error - alerts.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(onCompletion: onCompletion) - - case .failure(let cancelError): - // Inform that payment can't be retried. - self.alerts.nonRetryableError(from: self.rootViewController, error: cancelError) { - onCompletion(.failure(error)) - } - } - } - }, dismissCompletion: { - onCompletion(.failure(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(onCompletion: onCompletion) + + case .failure(let cancelError): + // Inform that payment can't be retried. + self.alertsPresenter.present( + viewModel: self.paymentAlerts.nonRetryableError(error: cancelError) { + onCompletion(.failure(error)) + }) + } + } + }, dismissCompletion: { + onCompletion(.failure(error)) + }) + ) } private func trackPaymentFailure(with error: Error) { @@ -339,7 +348,7 @@ private extension CollectOrderPaymentUseCase { /// func presentReceiptAlert(receiptParameters: CardPresentReceiptParameters, onCompleted: @escaping () -> ()) { // Present receipt alert - alerts.success(printReceipt: { [order, configuration, weak self] in + alertsPresenter.present(viewModel: paymentAlerts.success(printReceipt: { [order, configuration, weak self] in guard let self = self else { return } // Inform about flow completion. @@ -368,7 +377,7 @@ private extension CollectOrderPaymentUseCase { }, noReceiptAction: { // Inform about flow completion. onCompleted() - }) + })) } /// Presents the native email client with the provided content. diff --git a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift index 5919c0a8c74..79712ace681 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift @@ -240,8 +240,6 @@ final class PaymentMethodsViewModel: ObservableObject { formattedAmount: self.formattedTotal, paymentGatewayAccount: paymentGateway, rootViewController: rootViewController, - alerts: OrderDetailsPaymentAlerts(transactionType: .collectPayment, - presentingController: rootViewController), configuration: CardPresentConfigurationLoader().configuration) self.collectPaymentsUseCase?.collectPayment( diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 7f264db0c68..0655df1e26a 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -503,6 +503,8 @@ 03E471CA293E0A30001A58AD /* CardPresentModalBuiltInConfigurationProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471C9293E0A2F001A58AD /* CardPresentModalBuiltInConfigurationProgress.swift */; }; 03E471CC293E0FB8001A58AD /* CardPresentModalProgressDisplaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471CB293E0FB8001A58AD /* CardPresentModalProgressDisplaying.swift */; }; 03E471CE293F63B4001A58AD /* PaymentCaptureOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471CD293F63B4001A58AD /* PaymentCaptureOrchestrator.swift */; }; + 03E471D0293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471CF293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift */; }; + 03E471D2293FA8B2001A58AD /* CardReaderPaymentAlertsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E471D1293FA8B2001A58AD /* CardReaderPaymentAlertsProvider.swift */; }; 03EF24FA28BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24F928BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift */; }; 03EF24FC28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24FB28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift */; }; 03EF24FE28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03EF24FD28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift */; }; @@ -2517,6 +2519,8 @@ 03E471C9293E0A2F001A58AD /* CardPresentModalBuiltInConfigurationProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardPresentModalBuiltInConfigurationProgress.swift; sourceTree = ""; }; 03E471CB293E0FB8001A58AD /* CardPresentModalProgressDisplaying.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalProgressDisplaying.swift; sourceTree = ""; }; 03E471CD293F63B4001A58AD /* PaymentCaptureOrchestrator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentCaptureOrchestrator.swift; sourceTree = ""; }; + 03E471CF293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderTransactionAlertsProviding.swift; sourceTree = ""; }; + 03E471D1293FA8B2001A58AD /* CardReaderPaymentAlertsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderPaymentAlertsProvider.swift; sourceTree = ""; }; 03EF24F928BF5D21006A033E /* InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryToggleRowViewModel.swift; sourceTree = ""; }; 03EF24FB28BF996F006A033E /* InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsCashOnDeliveryPaymentGatewayHelpers.swift; sourceTree = ""; }; 03EF24FD28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CardPresentPaymentsPlugin+CashOnDelivery.swift"; sourceTree = ""; }; @@ -5647,6 +5651,8 @@ 311F827326CD897900DF5BAD /* CardReaderSettingsAlertsProvider.swift */, 311D21EC264AF0E700102316 /* CardReaderSettingsAlerts.swift */, 03E471BF293A158C001A58AD /* CardReaderConnectionAlertsProviding.swift */, + 03E471CF293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift */, + 03E471D1293FA8B2001A58AD /* CardReaderPaymentAlertsProvider.swift */, 03E471C1293A1F6B001A58AD /* BluetoothReaderConnectionAlertsProvider.swift */, 03E471C3293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift */, 3178C1F626409216000D771A /* CardReaderSettingsConnectedViewModel.swift */, @@ -10022,6 +10028,7 @@ E1325EFB28FD544E00EC9B2A /* InAppPurchasesDebugView.swift in Sources */, 74460D4022289B7600D7316A /* Coordinator.swift in Sources */, B57C743D20F5493300EEFC87 /* AccountHeaderView.swift in Sources */, + 03E471D2293FA8B2001A58AD /* CardReaderPaymentAlertsProvider.swift in Sources */, 03E471CA293E0A30001A58AD /* CardPresentModalBuiltInConfigurationProgress.swift in Sources */, 31AD0B1126E9575F000B6391 /* CardPresentModalConnectingFailed.swift in Sources */, 576EA39425264C9B00AFC0B3 /* RefundConfirmationViewModel.swift in Sources */, @@ -10538,6 +10545,7 @@ 020AF6662923C7ED007760E5 /* StoreNameForm.swift in Sources */, DEC51AFD276AEAE3009F3DF4 /* SystemStatusReportView.swift in Sources */, CECC759C23D61C1400486676 /* AggregateDataHelper.swift in Sources */, + 03E471D0293FA62B001A58AD /* CardReaderTransactionAlertsProviding.swift in Sources */, 02645D7D27BA027B0065DC68 /* Inbox.swift in Sources */, D81D9228222E7F0800FFA585 /* OrderStatusListViewController.swift in Sources */, CEE006082077D14C0079161F /* OrderDetailsViewController.swift in Sources */,