Skip to content

Commit 4226e15

Browse files
committed
Merge branch 'trunk' into issue/8189-handle-range-selection-granularity
2 parents 866dc71 + 8f948db commit 4226e15

25 files changed

+514
-239
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,3 +1773,31 @@ extension WooAnalyticsEvent {
17731773
}
17741774
}
17751775
}
1776+
1777+
// MARK: - Analytics Hub
1778+
//
1779+
extension WooAnalyticsEvent {
1780+
enum AnalyticsHub {
1781+
enum Keys: String {
1782+
case option
1783+
}
1784+
1785+
/// Tracks when the "See more" button is tapped in My Store, to open the Analytics Hub.
1786+
///
1787+
static func seeMoreAnalyticsTapped() -> WooAnalyticsEvent {
1788+
WooAnalyticsEvent(statName: .dashboardSeeMoreAnalyticsTapped, properties: [:])
1789+
}
1790+
1791+
/// Tracks when the date range selector button is tapped.
1792+
///
1793+
static func dateRangeButtonTapped() -> WooAnalyticsEvent {
1794+
WooAnalyticsEvent(statName: .analyticsHubDateRangeButtonTapped, properties: [:])
1795+
}
1796+
1797+
/// Tracks when a date range option is selected like “today”, “yesterday”, or “custom”.
1798+
///
1799+
static func dateRangeOptionSelected(_ option: String) -> WooAnalyticsEvent {
1800+
WooAnalyticsEvent(statName: .analyticsHubDateRangeOptionSelected, properties: [Keys.option.rawValue: option])
1801+
}
1802+
}
1803+
}

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public enum WooAnalyticsStat: String {
130130
case dashboardPulledToRefresh = "dashboard_pulled_to_refresh"
131131
case dashboardNewOrdersButtonTapped = "dashboard_unfulfilled_orders_button_tapped"
132132
case dashboardShareStoreButtonTapped = "dashboard_share_your_store_button_tapped"
133+
case dashboardSeeMoreAnalyticsTapped = "dashboard_see_more_analytics_tapped"
133134

134135
// MARK: Dashboard Data/Action Events
135136
//
@@ -149,6 +150,11 @@ public enum WooAnalyticsStat: String {
149150
case dashboardNewStatsRevertedBannerLearnMoreTapped = "dashboard_new_stats_reverted_banner_learn_more_tapped"
150151
case usedAnalytics = "used_analytics"
151152

153+
// MARK: Analytics Hub Events
154+
//
155+
case analyticsHubDateRangeButtonTapped = "analytics_hub_date_range_button_tapped"
156+
case analyticsHubDateRangeOptionSelected = "analytics_hub_date_range_option_selected"
157+
152158
// MARK: Products Onboarding Events
153159
//
154160
case productsOnboardingEligible = "products_onboarding_store_is_eligible"

WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentModalScanningForReader.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ final class CardPresentModalScanningForReader: CardPresentPaymentsModalViewModel
2121

2222
let auxiliaryButtonTitle: String? = nil
2323

24-
let auxiliaryButtonimage: UIImage? = .infoOutlineImage
25-
2624
var auxiliaryAttributedButtonTitle: NSAttributedString? {
2725
let result = NSMutableAttributedString(
2826
string: .localizedStringWithFormat(

WooCommerce/Classes/ViewModels/CardPresentPayments/CardPresentPaymentsModalViewModel.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ protocol CardPresentPaymentsModalViewModel {
3232
/// Provides a title as a NSAttributedString for an auxiliary button
3333
var auxiliaryAttributedButtonTitle: NSAttributedString? { get }
3434

35-
/// Provides an image for the auxiliary button
36-
var auxiliaryButtonimage: UIImage? { get }
37-
3835
/// The title in the bottom section of the modal. Right below the image
3936
var bottomTitle: String? { get }
4037

@@ -109,10 +106,6 @@ extension CardPresentPaymentsModalViewModel {
109106
get { return nil }
110107
}
111108

112-
var auxiliaryButtonimage: UIImage? {
113-
get { return nil }
114-
}
115-
116109
var showLoadingIndicator: Bool {
117110
get { return false }
118111
}

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ struct CardPresentCapturedPaymentData {
1212
}
1313

1414
/// Orchestrates the sequence of actions required to capture a payment:
15-
/// 1. Check if there is a card reader connected
16-
/// 2. Launch the reader discovering and pairing UI if there is no reader connected
17-
/// 3. Obtain a Payment Intent from the card reader (i.e., create a payment intent, collect a payment method, and process the payment)
18-
/// 4. Submit the Payment Intent to WCPay to capture a payment
19-
/// Steps 1 and 2 will be implemented as part of https://github.com/woocommerce/woocommerce-ios/issues/4062
15+
/// 1. Triggers the `preparingReader` alert
16+
/// 2. Creates the payment intent parameters
17+
/// 3. Controls (prevents during payment) wallet presentation: we don't want to use the merchant's Apple Pay for their customer's purchase!
18+
/// 4. Obtain a Payment Intent from the card reader (i.e., create a payment intent, collect a payment method, and process the payment)
19+
/// 5. Submit the Payment Intent to WCPay to capture a payment
2020
final class PaymentCaptureOrchestrator {
2121
private let currencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)
2222
private let personNameComponentsFormatter = PersonNameComponentsFormatter()
@@ -39,11 +39,14 @@ final class PaymentCaptureOrchestrator {
3939
paymentGatewayAccount: PaymentGatewayAccount,
4040
paymentMethodTypes: [String],
4141
stripeSmallestCurrencyUnitMultiplier: Decimal,
42+
onPreparingReader: () -> Void,
4243
onWaitingForInput: @escaping (CardReaderInput) -> Void,
4344
onProcessingMessage: @escaping () -> Void,
4445
onDisplayMessage: @escaping (String) -> Void,
4546
onProcessingCompletion: @escaping (PaymentIntent) -> Void,
4647
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> Void) {
48+
onPreparingReader()
49+
4750
/// Set state of CardPresentPaymentStore
4851
///
4952
let setAccount = CardPresentPaymentAction.use(paymentGatewayAccount: paymentGatewayAccount)

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

Lines changed: 18 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
3232

3333
private let transactionType: CardPresentTransactionType
3434

35+
private let alertsProvider: CardReaderTransactionAlertsProviding
36+
3537
init(transactionType: CardPresentTransactionType,
3638
presentingController: UIViewController) {
3739
self.transactionType = transactionType
3840
self.presentingController = presentingController
41+
self.alertsProvider = CardReaderPaymentAlertsProvider(transactionType: transactionType)
3942
}
4043

4144
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel) {
@@ -60,166 +63,45 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
6063

6164
// Initial presentation of the modal view controller. We need to provide
6265
// a customer name and an amount.
63-
let viewModel = tapOrInsert(readerInputMethods: inputMethods, onCancel: onCancel)
66+
let viewModel = alertsProvider.tapOrInsertCard(title: title,
67+
amount: amount,
68+
inputMethods: inputMethods,
69+
onCancel: onCancel)
6470
presentViewModel(viewModel: viewModel)
6571
}
6672

6773
func displayReaderMessage(message: String) {
68-
let viewModel = displayMessage(message: message)
74+
let viewModel = alertsProvider.displayReaderMessage(message: message)
6975
presentViewModel(viewModel: viewModel)
7076
}
7177

7278
func processingPayment() {
73-
let viewModel = processing()
79+
let viewModel = alertsProvider.processingTransaction()
7480
presentViewModel(viewModel: viewModel)
7581
}
7682

7783
func success(printReceipt: @escaping () -> Void, emailReceipt: @escaping () -> Void, noReceiptAction: @escaping () -> Void) {
78-
let viewModel = successViewModel(printReceipt: printReceipt,
79-
emailReceipt: emailReceipt,
80-
noReceiptAction: noReceiptAction)
84+
let viewModel = alertsProvider.success(printReceipt: printReceipt,
85+
emailReceipt: emailReceipt,
86+
noReceiptAction: noReceiptAction)
8187
presentViewModel(viewModel: viewModel)
8288
}
8389

8490
func error(error: Error, tryAgain: @escaping () -> Void, dismissCompletion: @escaping () -> Void) {
85-
let viewModel = errorViewModel(error: error, tryAgain: tryAgain, dismissCompletion: dismissCompletion)
91+
let viewModel = alertsProvider.error(error: error,
92+
tryAgain: tryAgain,
93+
dismissCompletion: dismissCompletion)
8694
presentViewModel(viewModel: viewModel)
8795
}
8896

8997
func nonRetryableError(from: UIViewController?, error: Error, dismissCompletion: @escaping () -> Void) {
90-
let viewModel = nonRetryableErrorViewModel(amount: amount, error: error, dismissCompletion: dismissCompletion)
98+
let viewModel = alertsProvider.nonRetryableError(error: error,
99+
dismissCompletion: dismissCompletion)
91100
presentViewModel(viewModel: viewModel)
92101
}
93102

94103
func retryableError(from: UIViewController?, tryAgain: @escaping () -> Void) {
95-
let viewModel = retryableErrorViewModel(tryAgain: tryAgain)
104+
let viewModel = alertsProvider.retryableError(tryAgain: tryAgain)
96105
presentViewModel(viewModel: viewModel)
97106
}
98107
}
99-
100-
private extension OrderDetailsPaymentAlerts {
101-
func tapOrInsert(readerInputMethods: CardReaderInput, onCancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
102-
CardPresentModalTapCard(name: name,
103-
amount: amount,
104-
transactionType: transactionType,
105-
inputMethods: readerInputMethods,
106-
onCancel: onCancel)
107-
}
108-
109-
func displayMessage(message: String) -> CardPresentPaymentsModalViewModel {
110-
CardPresentModalDisplayMessage(name: name, amount: amount, message: message)
111-
}
112-
113-
func processing() -> CardPresentPaymentsModalViewModel {
114-
CardPresentModalProcessing(name: name, amount: amount, transactionType: transactionType)
115-
}
116-
117-
func successViewModel(printReceipt: @escaping () -> Void,
118-
emailReceipt: @escaping () -> Void,
119-
noReceiptAction: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
120-
if MFMailComposeViewController.canSendMail() {
121-
return CardPresentModalSuccess(printReceipt: printReceipt,
122-
emailReceipt: emailReceipt,
123-
noReceiptAction: noReceiptAction)
124-
} else {
125-
return CardPresentModalSuccessWithoutEmail(printReceipt: printReceipt, noReceiptAction: noReceiptAction)
126-
}
127-
}
128-
129-
func errorViewModel(error: Error,
130-
tryAgain: @escaping () -> Void,
131-
dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
132-
let errorDescription: String?
133-
if let error = error as? CardReaderServiceError {
134-
switch error {
135-
case .connection(let underlyingError),
136-
.discovery(let underlyingError),
137-
.disconnection(let underlyingError),
138-
.intentCreation(let underlyingError),
139-
.paymentMethodCollection(let underlyingError),
140-
.paymentCapture(let underlyingError),
141-
.paymentCancellation(let underlyingError),
142-
.refundCreation(let underlyingError),
143-
.refundPayment(let underlyingError, _),
144-
.refundCancellation(let underlyingError),
145-
.softwareUpdate(let underlyingError, _):
146-
errorDescription = Localization.errorDescription(underlyingError: underlyingError, transactionType: transactionType)
147-
default:
148-
errorDescription = error.errorDescription
149-
}
150-
} else {
151-
errorDescription = error.localizedDescription
152-
}
153-
return CardPresentModalError(errorDescription: errorDescription,
154-
transactionType: transactionType,
155-
primaryAction: tryAgain,
156-
dismissCompletion: dismissCompletion)
157-
}
158-
159-
func retryableErrorViewModel(tryAgain: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
160-
CardPresentModalRetryableError(primaryAction: tryAgain)
161-
}
162-
163-
func nonRetryableErrorViewModel(amount: String, error: Error, dismissCompletion: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
164-
CardPresentModalNonRetryableError(amount: amount, error: error, onDismiss: dismissCompletion)
165-
}
166-
}
167-
168-
private extension OrderDetailsPaymentAlerts {
169-
enum Localization {
170-
static func errorDescription(underlyingError: UnderlyingError, transactionType: CardPresentTransactionType) -> String? {
171-
switch underlyingError {
172-
case .unsupportedReaderVersion:
173-
switch transactionType {
174-
case .collectPayment:
175-
return NSLocalizedString(
176-
"The card reader software is out-of-date - please update the card reader software before attempting to process payments",
177-
comment: "Error message when the card reader software is too far out of date to process payments."
178-
)
179-
case .refund:
180-
return NSLocalizedString(
181-
"The card reader software is out-of-date - please update the card reader software before attempting to process refunds",
182-
comment: "Error message when the card reader software is too far out of date to process in-person refunds."
183-
)
184-
}
185-
case .paymentDeclinedByCardReader:
186-
switch transactionType {
187-
case .collectPayment:
188-
return NSLocalizedString("The card was declined by the card reader - please try another means of payment",
189-
comment: "Error message when the card reader itself declines the card.")
190-
case .refund:
191-
return NSLocalizedString("The card was declined by the card reader - please try another means of refund",
192-
comment: "Error message when the card reader itself declines the card.")
193-
}
194-
case .processorAPIError:
195-
switch transactionType {
196-
case .collectPayment:
197-
return NSLocalizedString(
198-
"The payment can not be processed by the payment processor.",
199-
comment: "Error message when the payment can not be processed (i.e. order amount is below the minimum amount allowed.)"
200-
)
201-
case .refund:
202-
return NSLocalizedString(
203-
"The refund can not be processed by the payment processor.",
204-
comment: "Error message when the in-person refund can not be processed (i.e. order amount is below the minimum amount allowed.)"
205-
)
206-
}
207-
case .internalServiceError:
208-
switch transactionType {
209-
case .collectPayment:
210-
return NSLocalizedString(
211-
"Sorry, this payment couldn’t be processed",
212-
comment: "Error message when the card reader service experiences an unexpected internal service error."
213-
)
214-
case .refund:
215-
return NSLocalizedString(
216-
"Sorry, this refund couldn’t be processed",
217-
comment: "Error message when the card reader service experiences an unexpected internal service error."
218-
)
219-
}
220-
default:
221-
return underlyingError.errorDescription
222-
}
223-
}
224-
}
225-
}

WooCommerce/Classes/ViewRelated/CardPresentPayments/BuiltInCardReaderConnectionController.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,14 +532,12 @@ private extension BuiltInCardReaderConnectionController {
532532
///
533533
private func returnSuccess(result: CardReaderConnectionResult) {
534534
onCompletion?(.success(result))
535-
alertsPresenter.dismiss()
536535
state = .idle
537536
}
538537

539538
/// Calls the completion with a failure result
540539
///
541540
private func returnFailure(error: Error) {
542-
alertsPresenter.dismiss()
543541
onCompletion?(.failure(error))
544542
state = .idle
545543
}

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentPreflightController.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ final class CardPresentPaymentPreflightController {
8787
observeConnectedReaders()
8888
// If we're already connected to a reader, return it
8989
if let connectedReader = connectedReader {
90-
readerConnection.send(CardReaderConnectionResult.connected(connectedReader))
90+
handleConnectionResult(.success(.connected(connectedReader)))
91+
return
9192
}
9293

9394
// TODO: Run onboarding if needed
@@ -146,7 +147,7 @@ final class CardPresentPaymentPreflightController {
146147
case .success(let unwrapped):
147148
self.readerConnection.send(unwrapped)
148149
default:
149-
break
150+
alertsPresenter.dismiss()
150151
}
151152
}
152153

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardPresentPaymentsModalViewController.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,10 @@ private extension CardPresentPaymentsModalViewController {
318318
UIView.performWithoutAnimation {
319319
auxiliaryButton.setTitle(viewModel.auxiliaryButtonTitle, for: .normal)
320320
auxiliaryButton.setAttributedTitle(viewModel.auxiliaryAttributedButtonTitle, for: .normal)
321-
auxiliaryButton.setImage(viewModel.auxiliaryButtonimage, for: .normal)
322-
if viewModel.auxiliaryButtonimage != nil {
323-
var config = UIButton.Configuration.plain()
324-
config.imagePadding = Constants.buttonTitleAndImageSpacing
325-
auxiliaryButton.configuration = config
326-
}
321+
var config = UIButton.Configuration.plain()
322+
config.contentInsets = Constants.auxiliaryButtonInsets
323+
config.titleAlignment = .leading
324+
auxiliaryButton.configuration = config
327325
view.layoutIfNeeded()
328326
}
329327
}
@@ -418,7 +416,7 @@ private extension CardPresentPaymentsModalViewController {
418416
static let extraInfoCustomInsets = UIEdgeInsets(top: 12, left: 10, bottom: 12, right: 10)
419417
static let modalHeight: CGFloat = 382
420418
static let modalWidth: CGFloat = 280
421-
static let buttonTitleAndImageSpacing: CGFloat = 8
419+
static let auxiliaryButtonInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
422420
}
423421
}
424422

WooCommerce/Classes/ViewRelated/CardPresentPayments/CardReaderConnectionController.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,14 +735,12 @@ private extension CardReaderConnectionController {
735735
///
736736
private func returnSuccess(result: CardReaderConnectionResult) {
737737
onCompletion?(.success(result))
738-
alertsPresenter.dismiss()
739738
state = .idle
740739
}
741740

742741
/// Calls the completion with a failure result
743742
///
744743
private func returnFailure(error: Error) {
745-
alertsPresenter.dismiss()
746744
onCompletion?(.failure(error))
747745
state = .idle
748746
}

0 commit comments

Comments
 (0)