From c5b577616820bb971b7bcfbc8fbdcdecb2038374 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 12 Jan 2023 17:59:59 +0000 Subject: [PATCH 1/4] 8089 No retry button on disconnection errors When we get an error collecting the payment, caused by the reader disconnecting, any retry attempt will fail. By showing a non-retriable error which dismisses the payment flow, the merchant will be taken back to the order and can see clearly whether the payment has gone through or not, before attempting the payment again. --- .../CollectOrderPaymentUseCase.swift | 102 +++++++++++++----- 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 1ce9cb1cbdd..a9c1ff74bb8 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -333,33 +333,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) { @@ -370,6 +352,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) -> ()) { + 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) -> ()) { + alertsPresenter.present( + viewModel: paymentAlerts.nonRetryableError(error: error, + dismissCompletion: { + onCompletion(.failure(error)) + })) + } + /// Cancels payment and record analytics. /// func cancelPayment(onCompleted: @escaping () -> ()) { From 8c8d46bab8249903e85fd1d95bd78b66cf30eb97 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 12 Jan 2023 18:15:06 +0000 Subject: [PATCH 2/4] 8089 Specific error for built-in reader disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the built in reader disconnects during a payment flow, e.g. because of going in to the background, or recieving a call, we handle the error as non-retryable. This commit updates the error message to specifically tell the user about this interruption without using the word “disconnected”, which is strange in the context of the built in reader, and informs them about how to continue with the payment. --- .../CardPresentModalNonRetryableError.swift | 15 +++--- ...iltInCardReaderPaymentAlertsProvider.swift | 48 +++++++++++-------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalNonRetryableError.swift b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalNonRetryableError.swift index 95c25bd679c..c765cfb55ab 100644 --- a/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalNonRetryableError.swift +++ b/WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalNonRetryableError.swift @@ -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 @@ -29,9 +26,7 @@ final class CardPresentModalNonRetryableError: CardPresentPaymentsModalViewModel let auxiliaryButtonTitle: String? = nil - var bottomTitle: String? { - error.localizedDescription - } + let bottomTitle: String? let bottomSubtitle: String? = nil @@ -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() diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift index 8624a076125..2a5c833ad78 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/CardReadersV2/BuiltInCardReaderPaymentAlertsProvider.swift @@ -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 { @@ -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 { @@ -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 } From 52e12a07a3e3bb0ece27316b3dd9b1b2ca1190eb Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 13 Jan 2023 11:30:33 +0000 Subject: [PATCH 3/4] =?UTF-8?q?8089=20Don=E2=80=99t=20report=20success=20w?= =?UTF-8?q?hen=20payment=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit prevents us from going through the success flow when Tap to Pay on iPhone payments fail. Previously, if you locked the phone while the Apple payment screen was showing, it would still show a success notice even though the payment hadn’t been taken. --- .../CollectOrderPaymentUseCase.swift | 24 ++++----- .../LegacyCollectOrderPaymentUseCase.swift | 16 +++++- .../Payment Methods/PaymentMethodsView.swift | 2 +- .../PaymentMethodsViewModel.swift | 31 +++++++---- .../PaymentMethodsViewModelTests.swift | 51 +++++++++++++++---- .../MockCollectOrderPaymentUseCase.swift | 4 +- 6 files changed, 91 insertions(+), 37 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index a9c1ff74bb8..6ae20d9e45b 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -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) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ()) + func collectPayment(onFailure: @escaping (Error) -> (), onCancel: @escaping () -> (), onCompleted: @escaping () -> ()) } /// Use case to collect payments from an order. @@ -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) -> (), + 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) } @@ -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 to Result - } - // 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() diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift index 48a1562df70..3e8c74f7606 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/LegacyCollectOrderPaymentUseCase.swift @@ -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) -> (), 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) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift index 87dd5db8779..e514af80182 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsView.swift @@ -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) } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift index 79712ace681..e4ec366fa9b 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModel.swift @@ -99,7 +99,7 @@ final class PaymentMethodsViewModel: ObservableObject { /// Retains the use-case so it can perform all of its async tasks. /// - private var legacyCollectPaymentsUseCase: CollectOrderPaymentProtocol? + private var legacyCollectPaymentsUseCase: LegacyCollectOrderPaymentProtocol? private var collectPaymentsUseCase: CollectOrderPaymentProtocol? @@ -112,6 +112,8 @@ final class PaymentMethodsViewModel: ObservableObject { configuration: upsellCardReadersCampaign.configuration) } + private let isTapToPayOnIPhoneEnabled: Bool + struct Dependencies { let presentNoticeSubject: PassthroughSubject let cardPresentPaymentsOnboardingPresenter: CardPresentPaymentsOnboardingPresenting @@ -141,12 +143,14 @@ final class PaymentMethodsViewModel: ObservableObject { paymentLink: URL? = nil, formattedTotal: String, flow: WooAnalyticsEvent.PaymentsFlow.Flow, + isTapToPayOnIPhoneEnabled: Bool = ServiceLocator.generalAppSettings.settings.isTapToPayOnIPhoneSwitchEnabled, dependencies: Dependencies = Dependencies()) { self.siteID = siteID self.orderID = orderID self.paymentLink = paymentLink self.formattedTotal = formattedTotal self.flow = flow + self.isTapToPayOnIPhoneEnabled = isTapToPayOnIPhoneEnabled presentNoticeSubject = dependencies.presentNoticeSubject cardPresentPaymentsOnboardingPresenter = dependencies.cardPresentPaymentsOnboardingPresenter stores = dependencies.stores @@ -199,19 +203,21 @@ final class PaymentMethodsViewModel: ObservableObject { /// - parameter useCase: Assign a custom useCase object for testing purposes. If not provided `CollectOrderPaymentUseCase` will be used. /// func collectPayment(on rootViewController: UIViewController?, - useCase: CollectOrderPaymentProtocol? = nil, - onSuccess: @escaping () -> ()) { - switch ServiceLocator.generalAppSettings.settings.isTapToPayOnIPhoneSwitchEnabled { + useCase: LegacyCollectOrderPaymentProtocol? = nil, + onSuccess: @escaping () -> (), + onFailure: @escaping () -> ()) { + switch isTapToPayOnIPhoneEnabled { case true: - newCollectPayment(on: rootViewController, useCase: useCase, onSuccess: onSuccess) + newCollectPayment(on: rootViewController, onSuccess: onSuccess, onFailure: onFailure) case false: legacyCollectPayment(on: rootViewController, useCase: useCase, onSuccess: onSuccess) } } func newCollectPayment(on rootViewController: UIViewController?, - useCase: CollectOrderPaymentProtocol? = nil, - onSuccess: @escaping () -> ()) { + useCase: CollectOrderPaymentProtocol? = nil, + onSuccess: @escaping () -> (), + onFailure: @escaping () -> ()) { trackCollectIntention(method: .card) guard let rootViewController = rootViewController else { @@ -243,9 +249,12 @@ final class PaymentMethodsViewModel: ObservableObject { configuration: CardPresentConfigurationLoader().configuration) self.collectPaymentsUseCase?.collectPayment( - onCollect: { [weak self] result in - guard result.isFailure else { return } + onFailure: { [weak self] error in self?.trackFlowFailed() + // Update order in case its status and/or other details are updated after a failed in-person payment + self?.updateOrderAsynchronously() + + onFailure() }, onCancel: { // No tracking required because the flow remains on screen to choose other payment methods. @@ -273,8 +282,8 @@ final class PaymentMethodsViewModel: ObservableObject { } func legacyCollectPayment(on rootViewController: UIViewController?, - useCase: CollectOrderPaymentProtocol? = nil, - onSuccess: @escaping () -> ()) { + useCase: LegacyCollectOrderPaymentProtocol? = nil, + onSuccess: @escaping () -> ()) { trackCollectIntention(method: .card) guard let rootViewController = rootViewController else { diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift index c89de7f3157..683d76fc80f 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Payment Methods/PaymentMethodsViewModelTests.swift @@ -29,6 +29,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(stores: stores) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -54,6 +55,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(stores: stores) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -90,6 +92,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { storage: storage) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -118,6 +121,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(stores: stores) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) stores.whenReceivingAction(ofType: OrderAction.self) { action in switch action { @@ -148,6 +152,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(presentNoticeSubject: noticeSubject, stores: stores) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) stores.whenReceivingAction(ofType: OrderAction.self) { action in switch action { @@ -185,6 +190,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(presentNoticeSubject: noticeSubject, stores: stores) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) stores.whenReceivingAction(ofType: OrderAction.self) { action in switch action { @@ -232,6 +238,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -265,10 +272,11 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}) + viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}, onFailure: {}) // Then assertEqual(analytics.receivedEvents.last, WooAnalyticsStat.paymentsFlowCompleted.rawValue) @@ -283,6 +291,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -314,6 +323,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -346,10 +356,11 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}) + viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}, onFailure: {}) // Then assertEqual(analytics.receivedEvents.last, WooAnalyticsStat.paymentsFlowFailed.rawValue) @@ -365,6 +376,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -382,6 +394,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -405,10 +418,11 @@ final class PaymentMethodsViewModelTests: XCTestCase { analytics: WooAnalytics(analyticsProvider: analytics)) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}) + viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}, onFailure: {}) // Then assertEqual(analytics.receivedEvents.last, WooAnalyticsStat.paymentsFlowCollect.rawValue) @@ -433,6 +447,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -456,6 +471,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -479,6 +495,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -502,6 +519,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -539,6 +557,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -576,6 +595,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { orderID: 111, formattedTotal: "$5.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // Then @@ -584,7 +604,10 @@ final class PaymentMethodsViewModelTests: XCTestCase { func test_paymentLinkRow_is_hidden_if_payment_link_is_not_available() { // Given - let viewModel = PaymentMethodsViewModel(paymentLink: nil, formattedTotal: "$12.00", flow: .simplePayment) + let viewModel = PaymentMethodsViewModel(paymentLink: nil, + formattedTotal: "$12.00", + flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false) // Then XCTAssertFalse(viewModel.showPaymentLinkRow) @@ -594,7 +617,10 @@ final class PaymentMethodsViewModelTests: XCTestCase { func test_paymentLinkRow_is_shown_if_payment_link_is_available() { // Given let paymentURL = URL(string: "http://www.automattic.com") - let viewModel = PaymentMethodsViewModel(paymentLink: paymentURL, formattedTotal: "$12.00", flow: .simplePayment) + let viewModel = PaymentMethodsViewModel(paymentLink: paymentURL, + formattedTotal: "$12.00", + flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false) // Then XCTAssertTrue(viewModel.showPaymentLinkRow) @@ -607,6 +633,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { let dependencies = Dependencies(presentNoticeSubject: noticeSubject) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -651,6 +678,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { storage: storage) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -665,7 +693,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { } .store(in: &self.subscriptions) - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}) + viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}, onFailure: {}) } // Then @@ -690,13 +718,17 @@ final class PaymentMethodsViewModelTests: XCTestCase { storage: storage) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When let calledOnSuccess: Bool = waitFor { promise in - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: { + viewModel.collectPayment(on: UIViewController(), + useCase: useCase, + onSuccess: { promise(true) - }) + }, + onFailure: {}) } // Then @@ -723,6 +755,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { storage: storage) let viewModel = PaymentMethodsViewModel(formattedTotal: "$12.00", flow: .simplePayment, + isTapToPayOnIPhoneEnabled: false, dependencies: dependencies) // When @@ -735,7 +768,7 @@ final class PaymentMethodsViewModelTests: XCTestCase { XCTFail("Unexpected action: \(action)") } } - viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}) + viewModel.collectPayment(on: UIViewController(), useCase: useCase, onSuccess: {}, onFailure: {}) } // Then diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/MockCollectOrderPaymentUseCase.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/MockCollectOrderPaymentUseCase.swift index 0e219af1bfc..d918562d656 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/MockCollectOrderPaymentUseCase.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/MockCollectOrderPaymentUseCase.swift @@ -1,9 +1,9 @@ import Foundation @testable import WooCommerce -/// Mock type for `CollectOrderPaymentProtocol` +/// Mock type for `LegacyCollectOrderPaymentProtocol` /// -struct MockCollectOrderPaymentUseCase: CollectOrderPaymentProtocol { +struct MockCollectOrderPaymentUseCase: LegacyCollectOrderPaymentProtocol { /// Assign to be returned on `onCollect` closure. /// From 8703aa2427ce559fb374d4ee895b3d08398ed322 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 13 Jan 2023 11:32:35 +0000 Subject: [PATCH 4/4] =?UTF-8?q?8089=20Fix=20=E2=80=98During=20call?= =?UTF-8?q?=E2=80=99=20error=20message=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Hardware/Hardware/CardReader/UnderlyingError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hardware/Hardware/CardReader/UnderlyingError.swift b/Hardware/Hardware/CardReader/UnderlyingError.swift index 54f8e55383c..0b8c667e759 100644 --- a/Hardware/Hardware/CardReader/UnderlyingError.swift +++ b/Hardware/Hardware/CardReader/UnderlyingError.swift @@ -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: