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
9 changes: 7 additions & 2 deletions WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1275,9 +1275,10 @@ enum WooAnalyticsStat: String {
case pointOfSaleItemRemovedFromCart = "item_removed_from_cart"
case pointOfSaleCheckoutTapped = "checkout_tapped"
case pointOfSaleBackToCartTapped = "back_to_cart_tapped"
case pointOfSaleBackToCheckoutFromCashTapped = "back_to_checkout_from_cash"
case pointOfSaleClearCartTapped = "clear_cart_tapped"
case pointOfSaleExitMenuItemTapped = "exit_pos_menu_item_tapped"
case pointOfSaleExitConfirmed = "exit_pos_confirmed"
case pointOfSaleExitMenuItemTapped = "exit_menu_item_tapped"
case pointOfSaleExitConfirmed = "exit_confirmed"
case pointOfSaleGetSupportTapped = "get_support_tapped"
case pointOfSaleSimpleProductsExplanationDialogShown = "simple_products_explanation_dialog_shown"
case pointOfSaleCreateNewOrderTapped = "create_new_order_tapped"
Expand All @@ -1286,6 +1287,10 @@ enum WooAnalyticsStat: String {
case pointOfSalePaymentsOnboardingShown = "payments_onboarding_shown"
case pointOfSalePaymentsOnboardingDismissed = "payments_onboarding_dismissed"
case pointOfSaleCardReaderConnectionTapped = "card_reader_connection_tapped"
case pointOfSaleInteractionWithCustomerStarted = "interaction_with_customer_started"
case pointOfSaleViewDocsTapped = "view_docs_tapped"
case pointOfSaleReaderReadyForCardPayment = "reader_ready_for_card_payment"
case pointOfSaleCashCollectPaymentSuccess = "cash_collect_payment_success"

// MARK: Custom Fields events
case productDetailCustomFieldsTapped = "product_detail_custom_fields_tapped"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
var connectedReaderModel: String?

private var customerInteractionStarted: Double = 0
private var orderCreated: Double = 0
private var orderSync: Double = 0
private var cardReaderReady: Double = 0
private var cardReaderTapped: Double = 0
private var checkoutTapCount: Int = 0
Expand All @@ -20,12 +20,12 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func preflightResultReceived(_ result: CardReaderPreflightResult?) { }
func trackProcessingCompletion(intent: Yosemite.PaymentIntent) { }

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
// Property: milliseconds_since_customer_interaction_started
let elapsedTimeSinceCustomerInteraction = calculateElapsedTimeInMilliseconds(since: customerInteractionStarted)

// Property: milliseconds_since_order_creation_success
let elapsedTimeSinceOrderCreation = calculateElapsedTimeInMilliseconds(since: orderCreated)
// Property: milliseconds_since_order_sync_success
let elapsedTimeSinceOrderSync = calculateElapsedTimeInMilliseconds(since: orderSync)

// Property: milliseconds_since_reader_ready_to_collect_payment
let elapsedTimeSinceCardReaderReady = calculateElapsedTimeInMilliseconds(since: cardReaderReady)
Expand All @@ -35,7 +35,7 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin

analytics.track(event: .PointOfSale.cardPresentCollectPaymentSuccess(
millisecondsSinceCustomerIteractionStarted: elapsedTimeSinceCustomerInteraction,
millisecondsSinceOrderCreationSuccess: elapsedTimeSinceOrderCreation,
millisecondsSinceOrderSyncSuccess: elapsedTimeSinceOrderSync,
millisecondsSinceReaderReadyToCollect: elapsedTimeSinceCardReaderReady,
millisecondsSinceCardTapped: elapsedTimeSinceCardTapped,
checkoutTapCount: checkoutTapCount
Expand All @@ -45,6 +45,15 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
resetProcessingPaymentTracking()
}

func trackSuccessfulCashPayment() {
let elapsedTimeSinceCustomerInteraction = calculateElapsedTimeInMilliseconds(since: customerInteractionStarted)

analytics.track(event: .PointOfSale.cashCollectPaymentSuccess(
millisecondsSinceCustomerIteractionStarted: elapsedTimeSinceCustomerInteraction
))
resetCheckoutTapCountTracker()
}

func trackPaymentFailure(with error: any Error) { }
func trackPaymentCancelation(cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) { }
func trackEmailTapped() { }
Expand All @@ -56,15 +65,19 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func trackCustomerInteractionStarted() {
// Any action that is considered as user starting an iteraction resets any ongoing counter
resetAllCountersOnInteractionStarted()
analytics.track(.pointOfSaleInteractionWithCustomerStarted)
customerInteractionStarted = Date().timeIntervalSince1970
}

func trackOrderCreationSuccess() {
orderCreated = trackCurrentTime()
func trackOrderSyncSuccess() {
orderSync = trackCurrentTime()
}

func trackCardReaderReady() {
cardReaderReady = trackCurrentTime()

// As a side effect of knowing when the reader is ready, we track the elapsed from order sync (created or updated)
trackElapsedTimeFromOrderSyncToCardReady()
}

// The Stripe SDK returns multiple `.processing` events, but we want to capture the first one in the stream only.
Expand All @@ -83,6 +96,11 @@ final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTrackin
func resetCheckoutTapCountTracker() {
checkoutTapCount = 0
}

private func trackElapsedTimeFromOrderSyncToCardReady() {
let elapsedTime = cardReaderReady - orderSync
analytics.track(event: .PointOfSale.cardReaderReadyForCardPayment(waitingTime: elapsedTime))
}
}

// Helpers
Expand All @@ -101,7 +119,7 @@ private extension POSCollectOrderPaymentAnalytics {
}

private func resetAllCountersOnInteractionStarted() {
orderCreated = 0
orderSync = 0
cardReaderReady = 0
cardReaderTapped = 0
resetCheckoutTapCountTracker()
Expand All @@ -113,9 +131,10 @@ private extension POSCollectOrderPaymentAnalytics {
// https://github.com/woocommerce/woocommerce-ios/issues/15149
extension CollectOrderPaymentAnalytics {
func trackCustomerInteractionStarted() { }
func trackOrderCreationSuccess() { }
func trackOrderSyncSuccess() { }
func trackCardReaderReady() { }
func trackCardReaderTapped() { }
func trackCheckoutTapped() { }
func resetCheckoutTapCountTracker() { }
func trackSuccessfulCashPayment() { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ extension WooAnalyticsEvent {
static let itemType = "product_type"
static let itemsInCart = "items_in_cart"
static let millisecondsSinceCustomerInteractionStarted = "milliseconds_since_customer_interaction_started"
static let millisecondsSinceOrderCreationSuccess = "milliseconds_since_order_creation_success"
static let millisecondsSinceOrderSyncSuccess = "milliseconds_since_order_sync_success"
static let millisecondsSinceReaderReadyToCollect = "milliseconds_since_reader_ready_to_collect_payment"
static let millisecondsSinceCardTapped = "milliseconds_since_card_tapped"
static let checkoutTapCount = "checkout_tap_count"
static let waitingTime = "waiting_time"
}

static func paymentsOnboardingShown() -> WooAnalyticsEvent {
Expand All @@ -37,19 +38,32 @@ extension WooAnalyticsEvent {
properties: [Key.itemsInCart: itemsInCart])
}

/// Tracks the time elapsed preparing reader for payment, after successful order creation
/// - Parameter waitingTime: Elapsed time from Order creation to card ready for payment
///
static func cardReaderReadyForCardPayment(waitingTime: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleReaderReadyForCardPayment, properties: [Key.waitingTime: "\(waitingTime)"])
}

static func cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double,
millisecondsSinceOrderCreationSuccess: Double,
millisecondsSinceOrderSyncSuccess: Double,
millisecondsSinceReaderReadyToCollect: Double,
millisecondsSinceCardTapped: Double,
checkoutTapCount: Int) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .collectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
Key.millisecondsSinceOrderCreationSuccess: "\(millisecondsSinceOrderCreationSuccess)",
Key.millisecondsSinceOrderSyncSuccess: "\(millisecondsSinceOrderSyncSuccess)",
Key.millisecondsSinceReaderReadyToCollect: "\(millisecondsSinceReaderReadyToCollect)",
Key.millisecondsSinceCardTapped: "\(millisecondsSinceCardTapped)",
Key.checkoutTapCount: "\(checkoutTapCount)"
])
}

static func cashCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleCashCollectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically we could also track millisecondsSinceOrderCreationSuccess but maybe it's not needed.

])
}
}
}

Expand Down
24 changes: 19 additions & 5 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ private extension PointOfSaleAggregateModel {
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
}
}

// Tracks when the order is created or updated successfully
// pdfdoF-6hn#comment-7625-p2
func trackOrderSyncState(_ result: Result<SyncOrderState, Error>) {
switch result {
case .success(let syncState):
switch syncState {
case .newOrder, .orderUpdated:
collectOrderPaymentAnalyticsTracker.trackOrderSyncSuccess()
default:
break
}
case .failure:
break
}
}
}

// MARK: - Card payments
Expand Down Expand Up @@ -242,6 +258,7 @@ extension PointOfSaleAggregateModel {

@MainActor
func cancelCashPayment() async {
analytics.track(.pointOfSaleBackToCheckoutFromCashTapped)
paymentState = .card(.idle)
if case .connected = cardReaderConnectionStatus {
await collectCardPayment()
Expand All @@ -250,8 +267,7 @@ extension PointOfSaleAggregateModel {

private func cashPaymentSuccess() {
paymentState = .cash(.paymentSuccess)
// TODO: Move to trackSuccessfulCashPayment() on #15151
collectOrderPaymentAnalyticsTracker.resetCheckoutTapCountTracker()
collectOrderPaymentAnalyticsTracker.trackSuccessfulCashPayment()
}

@MainActor
Expand Down Expand Up @@ -453,9 +469,7 @@ extension PointOfSaleAggregateModel {
let syncOrderResult = await orderController.syncOrder(for: cart, retryHandler: { [weak self] in
await self?.checkOut()
})
if case .success(.newOrder) = syncOrderResult {
collectOrderPaymentAnalyticsTracker.trackOrderCreationSuccess()
}
trackOrderSyncState(syncOrderResult)
await startPaymentWhenCardReaderConnected()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct POSFloatingControlView: View {
}
Button {
showDocumentation = true
ServiceLocator.analytics.track(.pointOfSaleViewDocsTapped)
} label: {
Label(
title: { Text(Localization.viewDocumentation) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ protocol CollectOrderPaymentAnalyticsTracking {

func trackProcessingCompletion(intent: PaymentIntent)

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData)
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData)

func trackPaymentFailure(with error: Error)

Expand All @@ -26,11 +26,12 @@ protocol CollectOrderPaymentAnalyticsTracking {
func trackReceiptPrintFailed(error: Error)

func trackCustomerInteractionStarted()
func trackOrderCreationSuccess()
func trackOrderSyncSuccess()
func trackCardReaderReady()
func trackCardReaderTapped()
func trackCheckoutTapped()
func resetCheckoutTapCountTracker()
func trackSuccessfulCashPayment()
}

final class CollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
Expand Down Expand Up @@ -89,7 +90,7 @@ final class CollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
}
}

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
analytics.track(event: WooAnalyticsEvent.InPersonPayments
.collectPaymentSuccess(forGatewayID: paymentGatewayAccount?.gatewayID,
countryCode: configuration.countryCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private extension CollectOrderPaymentUseCase {
/// Tracks the successful payments
///
func handleSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
analyticsTracker.trackSuccessfulPayment(capturedPaymentData: capturedPaymentData)
analyticsTracker.trackSuccessfulCardPayment(capturedPaymentData: capturedPaymentData)
}

func handlePaymentCancellation(from cancellationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic

var didCallTrackSuccessfulPayment = false
var spyTrackSuccessfulPaymentCapturedPaymentData: CardPresentCapturedPaymentData? = nil
func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
didCallTrackSuccessfulPayment = true
spyTrackSuccessfulPaymentCapturedPaymentData = capturedPaymentData
}
Expand Down Expand Up @@ -58,7 +58,7 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic
// no-op
}

func trackOrderCreationSuccess() {
func trackOrderSyncSuccess() {
// no-op
}

Expand All @@ -78,4 +78,8 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic
func resetCheckoutTapCountTracker() {
// no-op
}

func trackSuccessfulCashPayment() {
// no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ struct POSCollectOrderPaymentAnalyticsTests {
let capturedPaymentData = CardPresentCapturedPaymentData(paymentMethod: .cardPresent(details: .fake()), receiptParameters: nil)
let expectedEvent = "card_present_collect_payment_success"
let expectedProperties = [
"milliseconds_since_order_creation_success",
"milliseconds_since_order_sync_success",
"milliseconds_since_reader_ready_to_collect_payment",
"milliseconds_since_card_tapped",
"milliseconds_since_customer_interaction_started",
"checkout_tap_count"
]

// When
sut.trackSuccessfulPayment(capturedPaymentData: capturedPaymentData)
sut.trackSuccessfulCardPayment(capturedPaymentData: capturedPaymentData)

// Then
#expect(analyticsProvider.receivedEvents.first(where: { $0 == expectedEvent }) != nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,21 @@ struct PointOfSaleAggregateModelTests {
// Then
#expect(analyticsTracker.didCallTrackCheckoutTapped == true)
}

@available(iOS 17.0, *)
@Test func cancelCashPayment_when_invoked_then_tracks_expected_event() async throws {
// Given
let analyticsTracker = MockCollectOrderPaymentAnalyticsTracker()
let sut = PointOfSaleAggregateModel(itemsController: MockPointOfSaleItemsController(),
cardPresentPaymentService: MockCardPresentPaymentService(),
orderController: MockPointOfSaleOrderController(),
collectOrderPaymentAnalyticsTracker: analyticsTracker)
// When
await sut.cancelCashPayment()

// Then
#expect(analyticsProvider.receivedEvents.first(where: { $0 == "back_to_checkout_from_cash" }) != nil)
}
}
}

Expand Down