Skip to content

Commit 5d2398b

Browse files
authored
Merge pull request #5786 from woocommerce/issue/5655-reuse-payments-use-case
2 parents da09d05 + 15c0796 commit 5d2398b

File tree

2 files changed

+36
-234
lines changed

2 files changed

+36
-234
lines changed

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

Lines changed: 27 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import Combine
77
import enum Networking.DotcomError
88

99
final class OrderDetailsViewModel {
10-
private let paymentOrchestrator = PaymentCaptureOrchestrator()
10+
/// Retains the use-case so it can perform all of its async tasks.
11+
///
12+
private var collectPaymentsUseCase: CollectOrderPaymentUseCase?
13+
1114
private let stores: StoresManager
1215

1316
private(set) var order: Order
@@ -85,16 +88,6 @@ final class OrderDetailsViewModel {
8588
}
8689
}
8790

88-
/// Name of the user we will be collecting car present payments from
89-
///
90-
var collectPaymentFrom: String {
91-
guard let name = order.billingAddress?.firstName else {
92-
return "Collect payment"
93-
}
94-
95-
return "Collect payment from \(name)"
96-
}
97-
9891
/// Closure to be executed when the UI needs to be reloaded.
9992
/// That could happen, for example, when new incoming data is detected
10093
///
@@ -134,15 +127,6 @@ final class OrderDetailsViewModel {
134127
order.billingAddress?.email
135128
}
136129

137-
/// Subject for the email containing a receipt generated after a card present payment has been captured
138-
///
139-
var paymentReceiptEmailSubject: String {
140-
guard let storeName = stores.sessionManager.defaultSite?.name else {
141-
return Localization.emailSubjectWithoutStoreName
142-
}
143-
144-
return String.localizedStringWithFormat(Localization.emailSubjectWithStoreName, storeName)
145-
}
146130

147131
private var cardPresentPaymentGatewayAccounts: [PaymentGatewayAccount] {
148132
return dataSource.cardPresentPaymentGatewayAccounts()
@@ -519,66 +503,30 @@ extension OrderDetailsViewModel {
519503
stores.dispatch(deleteTrackingAction)
520504
}
521505

522-
/// Returns a publisher that emits an initial value if there is no reader connected and completes as soon as a
523-
/// reader connects.
524-
func cardReaderAvailable() -> AnyPublisher<[CardReader], Never> {
525-
Future<AnyPublisher<[CardReader], Never>, Never> { [stores] promise in
526-
let action = CardPresentPaymentAction.checkCardReaderConnected(onCompletion: { publisher in
527-
promise(.success(publisher))
528-
})
529-
530-
stores.dispatch(action)
531-
}
532-
.switchToLatest()
533-
.eraseToAnyPublisher()
534-
}
535-
536-
/// We are passing the ReceiptParameters as part of the completon block
537-
/// We do so at this point for testing purposes.
538-
/// When we implement persistance, the receipt metadata would be persisted
539-
/// to Storage, associated to an order. We would not need to propagate
540-
/// that object outside of Yosemite.
541-
func collectPayment(onWaitingForInput: @escaping () -> Void, // i.e. waiting for buyer to swipe/insert/tap card
542-
onProcessingMessage: @escaping () -> Void, // i.e. payment is processing
543-
onDisplayMessage: @escaping (String) -> Void, // e.g. "Remove Card"
544-
onCompletion: @escaping (Result<CardPresentReceiptParameters, Error>) -> Void) { // used to tell user payment completed (or not)
545-
/// We don't have a concept of priority yet, so use the first paymentGatewayAccount for now
546-
/// since we can't yet have multiple accounts
547-
///
548-
if self.cardPresentPaymentGatewayAccounts.count != 1 {
549-
DDLogWarn("Expected one card present gateway account. Got something else.")
506+
/// Collects payments for the current order.
507+
/// Tries to connect to a reader if necessary.
508+
/// Handles receipt sharing.
509+
///
510+
func collectPayment(rootViewController: UIViewController, backButtonTitle: String, onCollect: @escaping (Result<Void, Error>) -> Void) {
511+
guard let paymentGateway = cardPresentPaymentGatewayAccounts.first else {
512+
return DDLogError("⛔️ Payment Gateway not found, can't collect payment.")
550513
}
551514

552-
let statementDescriptor = cardPresentPaymentGatewayAccounts.first?.statementDescriptor
553-
554-
paymentOrchestrator.collectPayment(for: self.order,
555-
statementDescriptor: statementDescriptor,
556-
onWaitingForInput: onWaitingForInput,
557-
onProcessingMessage: onProcessingMessage,
558-
onDisplayMessage: onDisplayMessage,
559-
onCompletion: onCompletion)
560-
561-
}
562-
563-
func cancelPayment(onCompletion: @escaping (Result<Void, Error>) -> Void) {
564-
paymentOrchestrator.cancelPayment(onCompletion: onCompletion)
565-
}
566-
567-
func printReceipt(params: CardPresentReceiptParameters) {
568-
ReceiptActionCoordinator.printReceipt(for: order, params: params)
569-
}
570-
571-
func emailReceipt(params: CardPresentReceiptParameters, onContent: @escaping (String) -> Void) {
572-
ServiceLocator.analytics.track(.receiptEmailTapped)
573-
paymentOrchestrator.emailReceipt(for: order, params: params, onContent: onContent)
574-
}
575-
}
576-
577-
private extension OrderDetailsViewModel {
578-
enum Localization {
579-
static let emailSubjectWithStoreName = NSLocalizedString("Your receipt from %1$@",
580-
comment: "Subject of email sent with a card present payment receipt")
581-
static let emailSubjectWithoutStoreName = NSLocalizedString("Your receipt",
582-
comment: "Subject of email sent with a card present payment receipt")
515+
let formattedTotal: String = {
516+
let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
517+
let currencyCode = ServiceLocator.currencySettings.currencyCode
518+
let unit = ServiceLocator.currencySettings.symbol(from: currencyCode)
519+
return currencyFormatter.formatAmount(order.total, with: unit) ?? ""
520+
}()
521+
522+
collectPaymentsUseCase = CollectOrderPaymentUseCase(siteID: order.siteID,
523+
order: order,
524+
formattedAmount: formattedTotal,
525+
paymentGatewayAccount: paymentGateway,
526+
rootViewController: rootViewController)
527+
collectPaymentsUseCase?.collectPayment(backButtonTitle: backButtonTitle, onCollect: onCollect, onCompleted: { [weak self] in
528+
// Make sure we free all the resources
529+
self?.collectPaymentsUseCase = nil
530+
})
583531
}
584532
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift

Lines changed: 9 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -56,31 +56,6 @@ final class OrderDetailsViewController: UIViewController {
5656

5757
private let notices = OrderDetailsNotices()
5858

59-
/// Orchestrates what needs to be presented in the modal views
60-
/// that provide user-facing feedback about the card present payment process.
61-
private lazy var paymentAlerts: OrderDetailsPaymentAlerts = {
62-
OrderDetailsPaymentAlerts(presentingController: self)
63-
}()
64-
65-
/// Subscription that listens for connected readers while we are trying to connect to one to capture payment
66-
/// We need to cancel that subscription if the process is canceled by the user or when we connect to a reader.
67-
///
68-
private var cardReaderAvailableSubscription: Combine.Cancellable? = nil
69-
70-
/// Connection Controller (helps connect readers)
71-
///
72-
private lazy var connectionController: CardReaderConnectionController? = {
73-
guard let siteID = viewModel?.order.siteID else {
74-
return nil
75-
}
76-
77-
return CardReaderConnectionController(
78-
forSiteID: siteID,
79-
knownReaderProvider: CardReaderSettingsKnownReaderStorage(),
80-
alertsProvider: CardReaderSettingsAlerts()
81-
)
82-
}()
83-
8459
// MARK: - View Lifecycle
8560

8661
/// Create an instance of `Self` from its corresponding storyboard.
@@ -513,10 +488,10 @@ private extension OrderDetailsViewController {
513488
case .issueRefund:
514489
issueRefundWasPressed()
515490
case .collectPayment:
516-
guard let indexPath = indexPath else {
491+
guard indexPath != nil else {
517492
break
518493
}
519-
collectPayment(at: indexPath)
494+
collectPayment()
520495
case .reprintShippingLabel(let shippingLabel):
521496
guard let navigationController = navigationController else {
522497
assertionFailure("Cannot reprint a shipping label because `navigationController` is nil")
@@ -710,139 +685,24 @@ private extension OrderDetailsViewController {
710685
present(navigationController, animated: true, completion: nil)
711686
}
712687

713-
@objc private func collectPayment(at: IndexPath) {
714-
cardReaderAvailableSubscription = viewModel.cardReaderAvailable()
715-
.sink(
716-
receiveCompletion: { [weak self] result in
717-
self?.dismiss(animated: false, completion: {
718-
self?.collectPaymentForCurrentOrder()
719-
})
720-
self?.cardReaderAvailableSubscription = nil
721-
},
722-
receiveValue: { [weak self] _ in
723-
self?.connectToCardReader()
724-
})
725-
}
726-
727-
private func collectPaymentForCurrentOrder() {
728-
let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
729-
let currencyCode = ServiceLocator.currencySettings.currencyCode
730-
let unit = ServiceLocator.currencySettings.symbol(from: currencyCode)
731-
let value = currencyFormatter.formatAmount(viewModel.order.total, with: unit) ?? ""
732-
733-
paymentAlerts.readerIsReady(title: viewModel.collectPaymentFrom,
734-
amount: value)
735-
736-
ServiceLocator.analytics.track(.collectPaymentTapped)
737-
viewModel.collectPayment(
738-
onWaitingForInput: { [weak self] in
739-
self?.paymentAlerts.tapOrInsertCard(onCancel: {
740-
self?.viewModel.cancelPayment(onCompletion: { _ in
741-
ServiceLocator.analytics.track(.collectPaymentCanceled)
742-
})
743-
})
744-
},
745-
onProcessingMessage: { [weak self] in
746-
self?.paymentAlerts.processingPayment()
747-
},
748-
onDisplayMessage: { [weak self] message in // display a message from the reader, e.g. "Remove Card"
749-
self?.paymentAlerts.displayReaderMessage(message: message)
750-
},
751-
onCompletion: { [weak self] result in
752-
guard let self = self else {
753-
return
754-
}
755-
756-
switch result {
757-
case .failure(let error):
758-
ServiceLocator.analytics.track(.collectPaymentFailed, withError: error)
759-
DDLogError("Failed to collect payment: \(error.localizedDescription)")
760-
self.paymentAlerts.error(error: error, tryAgain: {
761-
self.retryCollectPayment()
762-
})
763-
case .success(let receiptParameters):
764-
ServiceLocator.analytics.track(.collectPaymentSuccess)
765-
self.syncOrderAfterPaymentCollection {
766-
self.refreshCardPresentPaymentEligibility()
767-
}
768-
769-
self.paymentAlerts.success(printReceipt: {
770-
self.viewModel.printReceipt(params: receiptParameters)
771-
}, emailReceipt: {
772-
self.viewModel.emailReceipt(params: receiptParameters, onContent: { emailContent in
773-
self.emailReceipt(emailContent)
774-
})
775-
}, noReceiptTitle: Localization.Payments.backToOrder,
776-
noReceiptAction: {})
688+
@objc private func collectPayment() {
689+
viewModel.collectPayment(rootViewController: self, backButtonTitle: Localization.Payments.backToOrder) { [weak self] result in
690+
guard let self = self else { return }
691+
// Refresh date & view once payment has been collected.
692+
if result.isSuccess {
693+
self.syncOrderAfterPaymentCollection {
694+
self.refreshCardPresentPaymentEligibility()
777695
}
778696
}
779-
)
780-
}
781-
782-
private func retryCollectPayment() {
783-
viewModel.cancelPayment { [weak self] result in
784-
switch result {
785-
case .failure(let error):
786-
self?.paymentAlerts.nonRetryableError(from: self, error: error)
787-
case .success:
788-
self?.collectPaymentForCurrentOrder()
789-
}
790697
}
791698
}
792699

793-
private func connectToCardReader() {
794-
connectionController?.searchAndConnect(from: self) { _ in
795-
/// No need for logic here. Once connected, the connected reader will publish
796-
/// through the `cardReaderAvailableSubscription`
797-
}
798-
}
799-
800-
private func cancelObservingCardReader() {
801-
cardReaderAvailableSubscription?.cancel()
802-
cardReaderAvailableSubscription = nil
803-
}
804-
805700
private func itemAddOnsButtonTapped(addOns: [OrderItemAttribute]) {
806701
let addOnsViewModel = OrderAddOnListI1ViewModel(attributes: addOns)
807702
let addOnsController = OrderAddOnsListViewController(viewModel: addOnsViewModel)
808703
let navigationController = WooNavigationController(rootViewController: addOnsController)
809704
present(navigationController, animated: true, completion: nil)
810705
}
811-
812-
private func emailReceipt(_ content: String) {
813-
guard MFMailComposeViewController.canSendMail() else {
814-
DDLogError("⛔️ Failed to submit email receipt for order: \(viewModel.order.orderID). Email is not configured")
815-
return
816-
}
817-
818-
let mail = MFMailComposeViewController()
819-
mail.mailComposeDelegate = self
820-
821-
mail.setSubject(viewModel.paymentReceiptEmailSubject)
822-
mail.setMessageBody(content, isHTML: true)
823-
824-
if let customerEmail = viewModel.order.billingAddress?.email {
825-
mail.setToRecipients([customerEmail])
826-
}
827-
828-
present(mail, animated: true)
829-
}
830-
}
831-
832-
extension OrderDetailsViewController: MFMailComposeViewControllerDelegate {
833-
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
834-
switch result {
835-
case .cancelled:
836-
ServiceLocator.analytics.track(.receiptEmailCanceled)
837-
case .sent, .saved:
838-
ServiceLocator.analytics.track(.receiptEmailSuccess)
839-
case .failed:
840-
ServiceLocator.analytics.track(.receiptEmailFailed, withError: error ?? UnknownEmailError())
841-
@unknown default:
842-
assertionFailure("MFMailComposeViewController finished with an unknown result type")
843-
}
844-
controller.dismiss(animated: true)
845-
}
846706
}
847707

848708
// MARK: - UITableViewDelegate Conformance
@@ -909,12 +769,6 @@ extension OrderDetailsViewController: UITableViewDelegate {
909769
}
910770
}
911771

912-
extension OrderDetailsViewController: UIAdaptivePresentationControllerDelegate {
913-
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
914-
cancelObservingCardReader()
915-
}
916-
}
917-
918772
// MARK: - Trackings alert
919773
// Track / delete tracking alert
920774
private extension OrderDetailsViewController {

0 commit comments

Comments
 (0)