Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

11.3
-----

- [*] In-Person Payments: Show spinner while preparing reader for payment, instead of saying it's ready before it is. [https://github.com/woocommerce/woocommerce-ios/pull/8115]

11.2
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import UIKit

/// Modal presented when an error occurs while connecting to a reader due to problems with the address
///
final class CardPresentModalPreparingReader: CardPresentPaymentsModalViewModel {
let cancelAction: (() -> Void)

let textMode: PaymentsModalTextMode = .reducedTopInfo
let actionsMode: PaymentsModalActionsMode = .secondaryOnlyAction

let topTitle: String = Localization.title

var topSubtitle: String? = nil

let image: UIImage = .paymentErrorImage

let showLoadingIndicator = true

var primaryButtonTitle: String? = nil

let secondaryButtonTitle: String? = Localization.cancel

let auxiliaryButtonTitle: String? = nil

var bottomTitle: String? = Localization.bottomTitle

let bottomSubtitle: String? = Localization.bottomSubitle

var accessibilityLabel: String? {
return topTitle
}

init(cancelAction: @escaping () -> Void) {
self.cancelAction = cancelAction
}

func didTapPrimaryButton(in viewController: UIViewController?) {

}

func didTapSecondaryButton(in viewController: UIViewController?) {
cancelAction()
}

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

private extension CardPresentModalPreparingReader {
enum Localization {
static let title = NSLocalizedString(
"Getting ready to collect payment",
comment: "Title of the alert presented with a spinner while the reader is being prepared"
)

static let bottomTitle = NSLocalizedString(
"Connecting to reader",
comment: "Bottom title of the alert presented with a spinner while the reader is being prepared"
)

static let bottomSubitle = NSLocalizedString(
"Please wait...",
comment: "Bottom subtitle of the alert presented with a spinner while the reader is being prepared"
)

static let cancel = NSLocalizedString(
"Cancel",
comment: "Button to dismiss the alert presented while the reader is being prepared."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ protocol CardPresentPaymentsModalViewModel {
/// An illustration accompanying the modal
var image: UIImage { get }

var showLoadingIndicator: Bool { get }

/// Provides a title for a primary action button
var primaryButtonTitle: String? { get }

Expand Down Expand Up @@ -110,4 +112,8 @@ extension CardPresentPaymentsModalViewModel {
var auxiliaryButtonimage: UIImage? {
get { return nil }
}

var showLoadingIndicator: Bool {
get { return false }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,57 +50,48 @@ final class PaymentCaptureOrchestrator {

stores.dispatch(setAccount)

paymentParameters(
order: order,
orderTotal: orderTotal,
country: paymentGatewayAccount.country,
statementDescriptor: paymentGatewayAccount.statementDescriptor,
paymentMethodTypes: paymentMethodTypes,
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier
) { [weak self] result in
guard let self = self else { return }

switch result {
case let .success(parameters):
/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
/// reader begins to collect payment.
///
self.suppressPassPresentation()

let paymentAction = CardPresentPaymentAction.collectPayment(
siteID: order.siteID,
orderID: order.orderID,
parameters: parameters,
onCardReaderMessage: { event in
switch event {
case .waitingForInput:
onWaitingForInput()
case .displayMessage(let message):
onDisplayMessage(message)
case .cardRemovedAfterClientSidePaymentCapture:
onProcessingMessage()
default:
break
}
},
onProcessingCompletion: { intent in
onProcessingCompletion(intent)
},
onCompletion: { [weak self] result in
self?.allowPassPresentation()
self?.completePaymentIntentCapture(
order: order,
captureResult: result,
onCompletion: onCompletion
)
}
let parameters = paymentParameters(order: order,
orderTotal: orderTotal,
country: paymentGatewayAccount.country,
statementDescriptor: paymentGatewayAccount.statementDescriptor,
paymentMethodTypes: paymentMethodTypes,
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier)

/// Briefly suppress pass (wallet) presentation so that the merchant doesn't attempt to pay for the buyer's order when the
/// reader begins to collect payment.
///
suppressPassPresentation()

let paymentAction = CardPresentPaymentAction.collectPayment(
siteID: order.siteID,
orderID: order.orderID,
parameters: parameters,
onCardReaderMessage: { event in
switch event {
case .waitingForInput:
onWaitingForInput()
case .displayMessage(let message):
onDisplayMessage(message)
case .cardRemovedAfterClientSidePaymentCapture:
onProcessingMessage()
default:
break
}
},
onProcessingCompletion: { intent in
onProcessingCompletion(intent)
},
onCompletion: { [weak self] result in
self?.allowPassPresentation()
self?.completePaymentIntentCapture(
order: order,
captureResult: result,
onCompletion: onCompletion
)

self.stores.dispatch(paymentAction)
case let .failure(error):
onCompletion(Result.failure(error))
}
}
)

stores.dispatch(paymentAction)
}

func cancelPayment(onCompletion: @escaping (Result<Void, Error>) -> Void) {
Expand Down Expand Up @@ -204,37 +195,25 @@ private extension PaymentCaptureOrchestrator {
country: String,
statementDescriptor: String?,
paymentMethodTypes: [String],
stripeSmallestCurrencyUnitMultiplier: Decimal,
onCompletion: @escaping ((Result<PaymentParameters, Error>) -> Void)) {
paymentReceiptEmailParameterDeterminer.receiptEmail(from: order) { [weak self] result in
guard let self = self else { return }

var receiptEmail: String?
if case let .success(email) = result {
receiptEmail = email
}

let metadata = PaymentIntent.initMetadata(
store: self.stores.sessionManager.defaultSite?.name,
customerName: self.buildCustomerNameFromBillingAddress(order.billingAddress),
customerEmail: order.billingAddress?.email,
siteURL: self.stores.sessionManager.defaultSite?.url,
orderID: order.orderID,
paymentType: PaymentIntent.PaymentTypes.single
)

let parameters = PaymentParameters(amount: orderTotal as Decimal,
currency: order.currency,
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier,
applicationFee: self.applicationFee(for: orderTotal, country: country),
receiptDescription: self.receiptDescription(orderNumber: order.number),
statementDescription: statementDescriptor,
receiptEmail: receiptEmail,
paymentMethodTypes: paymentMethodTypes,
metadata: metadata)

onCompletion(Result.success(parameters))
}
stripeSmallestCurrencyUnitMultiplier: Decimal) -> PaymentParameters {
let metadata = PaymentIntent.initMetadata(
store: stores.sessionManager.defaultSite?.name,
customerName: buildCustomerNameFromBillingAddress(order.billingAddress),
customerEmail: order.billingAddress?.email,
siteURL: stores.sessionManager.defaultSite?.url,
orderID: order.orderID,
paymentType: PaymentIntent.PaymentTypes.single
)

return PaymentParameters(amount: orderTotal as Decimal,
currency: order.currency,
stripeSmallestCurrencyUnitMultiplier: stripeSmallestCurrencyUnitMultiplier,
applicationFee: applicationFee(for: orderTotal, country: country),
receiptDescription: receiptDescription(orderNumber: order.number),
statementDescription: statementDescriptor,
receiptEmail: paymentReceiptEmailParameterDeterminer.receiptEmail(from: order),
paymentMethodTypes: paymentMethodTypes,
metadata: metadata)
}

private func applicationFee(for orderTotal: NSDecimalNumber, country: String) -> Decimal? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,47 @@ import Yosemite
/// Determines the email to be set (if any) on a receipt
///
protocol ReceiptEmailParameterDeterminer {
func receiptEmail(from order: Order, onCompletion: @escaping ((Result<String?, Error>) -> Void))
func receiptEmail(from order: Order) -> String?
}

/// Determines the email to be set (if any) on a payment receipt depending on the current payment plugins (WCPay, Stripe) configuration
///
struct PaymentReceiptEmailParameterDeterminer: ReceiptEmailParameterDeterminer {
private let cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol
private let stores: StoresManager
private static let defaultConfiguration = CardPresentConfigurationLoader(stores: ServiceLocator.stores).configuration

init(cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol = CardPresentPluginsDataProvider(configuration: Self.defaultConfiguration),
stores: StoresManager = ServiceLocator.stores) {
init(cardPresentPluginsDataProvider: CardPresentPluginsDataProviderProtocol = CardPresentPluginsDataProvider(configuration: Self.defaultConfiguration)) {
self.cardPresentPluginsDataProvider = cardPresentPluginsDataProvider
self.stores = stores
}

/// We do not need to set the receipt email if WCPay is installed and active
/// and its version is higher or equal than 4.0.0, as it does it itself in that case.
///
/// - Parameters:
/// - order: the order associated with the payment
/// - onCompletion: closure invoked with the result of the inquiry, containing the email (if any) or error
/// - Returns:
/// - `String?`: the email for the reciept, if any. Even if there is an email, this will return `nil` for stores which send the receipt server-side.
///
func receiptEmail(from order: Order, onCompletion: @escaping ((Result<String?, Error>) -> Void)) {
synchronizePlugins(from: order.siteID) { result in
switch result {
case .success():
onCompletion(Result.success(receiptEmail(from: order)))
case let .failure(error):
onCompletion(Result.failure(error))
}
}
}

private func receiptEmail(from order: Order) -> String? {
func receiptEmail(from order: Order) -> String? {
let wcPay = cardPresentPluginsDataProvider.getWCPayPlugin()
let stripe = cardPresentPluginsDataProvider.getStripePlugin()
let paymentPluginsInstalledAndActiveStatus = cardPresentPluginsDataProvider.paymentPluginsInstalledAndActiveStatus(wcPay: wcPay, stripe: stripe)
let paymentPluginsStatus = cardPresentPluginsDataProvider.paymentPluginsInstalledAndActiveStatus(wcPay: wcPay, stripe: stripe)

guard paymentPluginsInstalledAndActiveStatus != .bothAreInstalledAndActive else {
guard paymentPluginsStatus != .bothAreInstalledAndActive else {
return nil
}

guard let wcPay = wcPay,
paymentPluginsInstalledAndActiveStatus == .onlyWCPayIsInstalledAndActive else {
paymentPluginsStatus == .onlyWCPayIsInstalledAndActive else {
return order.billingAddress?.email
}

return wcPayPluginSendsReceiptEmail(version: wcPay.version) ? nil : order.billingAddress?.email
}

private func synchronizePlugins(from siteID: Int64, onCompletion: @escaping ((Result<Void, Error>) -> Void)) {
let systemPluginsAction = SystemStatusAction.synchronizeSystemPlugins(siteID: siteID) { result in
if case let .failure(error) = result {
DDLogError("[PaymentReceiptEmailParameterDeterminer] Error syncing system plugins: \(error)")
onCompletion(Result.failure(error))
} else {
onCompletion(Result.success(()))
}
}

stores.dispatch(systemPluginsAction)
}

private func wcPayPluginSendsReceiptEmail(version: String) -> Bool {
let comparisonResult = VersionHelpers.compare(version, Constants.minimumWCPayPluginVersionThatSendsReceiptEmail)

return comparisonResult == .orderedDescending || comparisonResult == .orderedSame
VersionHelpers.isVersionSupported(version: version,
minimumRequired: Constants.minimumWCPayPluginVersionThatSendsReceiptEmail)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
if let controller = _modalController {
return controller
} else {
let controller = CardPresentPaymentsModalViewController(viewModel: readerIsReady(onCancel: {}))
let controller = CardPresentPaymentsModalViewController(
viewModel: CardPresentModalPreparingReader(cancelAction: { [weak self] in
self?.presentingController?.dismiss(animated: true)
}))
_modalController = controller
return controller
}
Expand All @@ -43,6 +46,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
}
}

func preparingReader(onCancel: @escaping () -> Void) {
presentViewModel(viewModel: CardPresentModalPreparingReader(cancelAction: onCancel))
}

func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void) {
self.name = title
self.amount = amount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import UIKit
protocol OrderDetailsPaymentAlertsProtocol {
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel)

func preparingReader(onCancel: @escaping () -> Void)

func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void)

func tapOrInsertCard(onCancel: @escaping () -> Void)
Expand Down
Loading