Skip to content

Commit 35ce249

Browse files
authored
Merge pull request #6639 from woocommerce/feat/5983-usecase-unit-tests
Ensure IPP objects are deallocated after failure and cancellation code paths with unit tests
2 parents fb5b514 + 15585d6 commit 35ce249

File tree

15 files changed

+541
-102
lines changed

15 files changed

+541
-102
lines changed

WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalError.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ final class CardPresentModalError: CardPresentPaymentsModalViewModel {
55
/// A closure to execute when the primary button is tapped
66
private let primaryAction: () -> Void
77

8+
/// A closure to execute after the secondary button is tapped to dismiss the modal
9+
private let dismissCompletion: () -> Void
10+
811
let textMode: PaymentsModalTextMode = .reducedBottomInfo
912
let actionsMode: PaymentsModalActionsMode = .twoAction
1013

@@ -31,20 +34,26 @@ final class CardPresentModalError: CardPresentPaymentsModalViewModel {
3134
return topTitle + bottomTitle
3235
}
3336

34-
init(errorDescription: String?, transactionType: CardPresentTransactionType, primaryAction: @escaping () -> Void) {
37+
init(errorDescription: String?,
38+
transactionType: CardPresentTransactionType,
39+
primaryAction: @escaping () -> Void,
40+
dismissCompletion: @escaping () -> Void) {
3541
self.topTitle = Localization.paymentFailed(transactionType: transactionType)
3642
self.bottomTitle = errorDescription
3743
self.primaryButtonTitle = Localization.tryAgain(transactionType: transactionType)
3844
self.secondaryButtonTitle = Localization.noThanks(transactionType: transactionType)
3945
self.primaryAction = primaryAction
46+
self.dismissCompletion = dismissCompletion
4047
}
4148

4249
func didTapPrimaryButton(in viewController: UIViewController?) {
4350
primaryAction()
4451
}
4552

4653
func didTapSecondaryButton(in viewController: UIViewController?) {
47-
viewController?.dismiss(animated: true, completion: nil)
54+
viewController?.dismiss(animated: true) { [weak self] in
55+
self?.dismissCompletion()
56+
}
4857
}
4958

5059
func didTapAuxiliaryButton(in viewController: UIViewController?) { }

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ final class PaymentCaptureOrchestrator {
2525

2626
private var walletSuppressionRequestToken: PKSuppressionRequestToken?
2727

28+
private let stores: StoresManager
29+
30+
init(stores: StoresManager = ServiceLocator.stores) {
31+
self.stores = stores
32+
}
33+
2834
func collectPayment(for order: Order,
2935
paymentGatewayAccount: PaymentGatewayAccount,
3036
paymentMethodTypes: [String],
@@ -44,7 +50,7 @@ final class PaymentCaptureOrchestrator {
4450
///
4551
let setAccount = CardPresentPaymentAction.use(paymentGatewayAccount: paymentGatewayAccount)
4652

47-
ServiceLocator.stores.dispatch(setAccount)
53+
stores.dispatch(setAccount)
4854

4955
paymentParameters(
5056
order: order,
@@ -85,7 +91,7 @@ final class PaymentCaptureOrchestrator {
8591
}
8692
)
8793

88-
ServiceLocator.stores.dispatch(paymentAction)
94+
self.stores.dispatch(paymentAction)
8995
case let .failure(error):
9096
onCompletion(Result.failure(error))
9197
}
@@ -97,21 +103,21 @@ final class PaymentCaptureOrchestrator {
97103
self?.allowPassPresentation()
98104
onCompletion(result)
99105
}
100-
ServiceLocator.stores.dispatch(action)
106+
stores.dispatch(action)
101107
}
102108

103109
func emailReceipt(for order: Order, params: CardPresentReceiptParameters, onContent: @escaping (String) -> Void) {
104110
let action = ReceiptAction.generateContent(order: order, parameters: params) { emailContent in
105111
onContent(emailContent)
106112
}
107113

108-
ServiceLocator.stores.dispatch(action)
114+
stores.dispatch(action)
109115
}
110116

111117
func saveReceipt(for order: Order, params: CardPresentReceiptParameters) {
112118
let action = ReceiptAction.saveReceipt(order: order, parameters: params)
113119

114-
ServiceLocator.stores.dispatch(action)
120+
stores.dispatch(action)
115121
}
116122
}
117123

@@ -211,7 +217,7 @@ private extension PaymentCaptureOrchestrator {
211217
}
212218
}
213219

214-
ServiceLocator.stores.dispatch(action)
220+
stores.dispatch(action)
215221
}
216222

217223
func paymentParameters(order: Order,
@@ -225,17 +231,19 @@ private extension PaymentCaptureOrchestrator {
225231
return
226232
}
227233

228-
paymentReceiptEmailParameterDeterminer.receiptEmail(from: order) { result in
234+
paymentReceiptEmailParameterDeterminer.receiptEmail(from: order) { [weak self] result in
235+
guard let self = self else { return }
236+
229237
var receiptEmail: String?
230238
if case let .success(email) = result {
231239
receiptEmail = email
232240
}
233241

234242
let metadata = PaymentIntent.initMetadata(
235-
store: ServiceLocator.stores.sessionManager.defaultSite?.name,
243+
store: self.stores.sessionManager.defaultSite?.name,
236244
customerName: self.buildCustomerNameFromBillingAddress(order.billingAddress),
237245
customerEmail: order.billingAddress?.email,
238-
siteURL: ServiceLocator.stores.sessionManager.defaultSite?.url,
246+
siteURL: self.stores.sessionManager.defaultSite?.url,
239247
orderID: order.orderID,
240248
paymentType: PaymentIntent.PaymentTypes.single
241249
)
@@ -253,7 +261,7 @@ private extension PaymentCaptureOrchestrator {
253261
}
254262

255263
func receiptDescription(orderNumber: String) -> String? {
256-
guard let storeName = ServiceLocator.stores.sessionManager.defaultSite?.name else {
264+
guard let storeName = stores.sessionManager.defaultSite?.name else {
257265
return nil
258266
}
259267

@@ -309,21 +317,21 @@ private extension PaymentCaptureOrchestrator {
309317
}
310318
}
311319

312-
private extension PaymentCaptureOrchestrator {
313-
private enum NotValidAmountError: Error, LocalizedError {
320+
extension PaymentCaptureOrchestrator {
321+
enum NotValidAmountError: Error, LocalizedError {
314322
case belowMinimumAmount(amount: String)
315323
case other
316324

317-
public var errorDescription: String? {
325+
var errorDescription: String? {
318326
switch self {
319327
case .belowMinimumAmount(let amount):
320-
return String.localizedStringWithFormat(Localizations.belowMinimumAmount, amount)
328+
return String.localizedStringWithFormat(Localization.belowMinimumAmount, amount)
321329
case .other:
322-
return Localizations.defaultMessage
330+
return Localization.defaultMessage
323331
}
324332
}
325333

326-
enum Localizations {
334+
private enum Localization {
327335
static let defaultMessage = NSLocalizedString(
328336
"Unable to process payment. Order total amount is not valid.",
329337
comment: "Error message when the order amount is not valid."

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,8 @@ private extension OrderDetailsDataSource {
532532

533533
private func configureCollectPaymentButton(cell: ButtonTableViewCell, at indexPath: IndexPath) {
534534
cell.configure(style: .primary,
535-
title: Titles.collectPayment) {
536-
self.onCellAction?(.collectPayment, indexPath)
535+
title: Titles.collectPayment) { [weak self] in
536+
self?.onCellAction?(.collectPayment, indexPath)
537537
}
538538
cell.hideSeparator()
539539
}

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsPaymentAlerts.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import enum Hardware.UnderlyingError
77
/// A layer of indirection between OrderDetailsViewController and the modal alerts
88
/// presented to provide user-facing feedback about the progress
99
/// of the payment collection process
10-
final class OrderDetailsPaymentAlerts {
10+
final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
1111
private weak var presentingController: UIViewController?
1212

1313
// Storing this as a weak variable means that iOS should automatically set this to nil
@@ -77,8 +77,8 @@ final class OrderDetailsPaymentAlerts {
7777
presentViewModel(viewModel: viewModel)
7878
}
7979

80-
func error(error: Error, tryAgain: @escaping () -> Void) {
81-
let viewModel = errorViewModel(error: error, tryAgain: tryAgain)
80+
func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) {
81+
let viewModel = errorViewModel(error: error, tryAgain: tryAgain, dismissCompletion: dismissCompletion)
8282
presentViewModel(viewModel: viewModel)
8383
}
8484

@@ -127,7 +127,9 @@ private extension OrderDetailsPaymentAlerts {
127127
}
128128
}
129129

130-
func errorViewModel(error: Error, tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
130+
func errorViewModel(error: Error,
131+
tryAgain: @escaping () -> Void,
132+
dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
131133
let errorDescription: String?
132134
if let error = error as? CardReaderServiceError {
133135
switch error {
@@ -149,7 +151,10 @@ private extension OrderDetailsPaymentAlerts {
149151
} else {
150152
errorDescription = error.localizedDescription
151153
}
152-
return CardPresentModalError(errorDescription: errorDescription, transactionType: transactionType, primaryAction: tryAgain)
154+
return CardPresentModalError(errorDescription: errorDescription,
155+
transactionType: transactionType,
156+
primaryAction: tryAgain,
157+
dismissCompletion: dismissCompletion)
153158
}
154159

155160
func retryableErrorViewModel(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import UIKit
2+
3+
/// Protocol for `OrderDetailsPaymentAlerts` to enable unit testing.
4+
protocol OrderDetailsPaymentAlertsProtocol {
5+
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel)
6+
7+
func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void)
8+
9+
func tapOrInsertCard(onCancel: @escaping () -> Void)
10+
11+
func displayReaderMessage(message: String)
12+
13+
func processingPayment()
14+
15+
func success(printReceipt: @escaping () -> Void, emailReceipt: @escaping () -> Void, noReceiptTitle: String, noReceiptAction: @escaping () -> Void)
16+
17+
func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void)
18+
19+
func nonRetryableError(from: UIViewController?, error: Error)
20+
21+
func retryableError(from: UIViewController?, tryAgain: @escaping () -> Void)
22+
}

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,10 @@ extension OrderDetailsViewModel {
538538
order: order,
539539
formattedAmount: formattedTotal,
540540
paymentGatewayAccount: paymentGateway,
541-
rootViewController: rootViewController)
541+
rootViewController: rootViewController,
542+
alerts: OrderDetailsPaymentAlerts(transactionType: .collectPayment,
543+
presentingController: rootViewController),
544+
configuration: configurationLoader.configuration)
542545
collectPaymentsUseCase?.collectPayment(backButtonTitle: backButtonTitle, onCollect: onCollect, onCompleted: { [weak self] in
543546
// Make sure we free all the resources
544547
self?.collectPaymentsUseCase = nil

0 commit comments

Comments
 (0)