diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index d906df87d19..66830ed43bf 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1276,9 +1276,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" @@ -1287,6 +1288,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" diff --git a/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift b/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift index c8fdfe1095e..fdfc51a0a62 100644 --- a/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift +++ b/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift @@ -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 @@ -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) @@ -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 @@ -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() { } @@ -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. @@ -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 @@ -101,7 +119,7 @@ private extension POSCollectOrderPaymentAnalytics { } private func resetAllCountersOnInteractionStarted() { - orderCreated = 0 + orderSync = 0 cardReaderReady = 0 cardReaderTapped = 0 resetCheckoutTapCountTracker() @@ -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() { } } diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index 218e273db8f..f6db030e25a 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -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 { @@ -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)", + ]) + } } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 79c8a028cdb..08600840dc5 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -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) { + switch result { + case .success(let syncState): + switch syncState { + case .newOrder, .orderUpdated: + collectOrderPaymentAnalyticsTracker.trackOrderSyncSuccess() + default: + break + } + case .failure: + break + } + } } // MARK: - Card payments @@ -242,6 +258,7 @@ extension PointOfSaleAggregateModel { @MainActor func cancelCashPayment() async { + analytics.track(.pointOfSaleBackToCheckoutFromCashTapped) paymentState = .card(.idle) if case .connected = cardReaderConnectionStatus { await collectCardPayment() @@ -250,8 +267,7 @@ extension PointOfSaleAggregateModel { private func cashPaymentSuccess() { paymentState = .cash(.paymentSuccess) - // TODO: Move to trackSuccessfulCashPayment() on #15151 - collectOrderPaymentAnalyticsTracker.resetCheckoutTapCountTracker() + collectOrderPaymentAnalyticsTracker.trackSuccessfulCashPayment() } @MainActor @@ -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() } } diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 0ce08f196b6..22537371961 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -40,6 +40,7 @@ struct POSFloatingControlView: View { } Button { showDocumentation = true + ServiceLocator.analytics.track(.pointOfSaleViewDocsTapped) } label: { Label( title: { Text(Localization.viewDocumentation) }, diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentAnalytics.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentAnalytics.swift index 70d39ecdcaf..12b60825fe1 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentAnalytics.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentAnalytics.swift @@ -9,7 +9,7 @@ protocol CollectOrderPaymentAnalyticsTracking { func trackProcessingCompletion(intent: PaymentIntent) - func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) + func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) func trackPaymentFailure(with error: Error) @@ -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 { @@ -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, diff --git a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift index 714382b25c5..076a9bf9c37 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Collect Payments/CollectOrderPaymentUseCase.swift @@ -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) { diff --git a/WooCommerce/WooCommerceTests/Mocks/MockCollectOrderPaymentAnalyticsTracker.swift b/WooCommerce/WooCommerceTests/Mocks/MockCollectOrderPaymentAnalyticsTracker.swift index 1e46bd5539a..5c6e4aba671 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockCollectOrderPaymentAnalyticsTracker.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockCollectOrderPaymentAnalyticsTracker.swift @@ -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 } @@ -58,7 +58,7 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic // no-op } - func trackOrderCreationSuccess() { + func trackOrderSyncSuccess() { // no-op } @@ -78,4 +78,8 @@ final class MockCollectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalytic func resetCheckoutTapCountTracker() { // no-op } + + func trackSuccessfulCashPayment() { + // no-op + } } diff --git a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift index 7003cf71f75..5ca22be7b87 100644 --- a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift @@ -17,7 +17,7 @@ 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", @@ -25,7 +25,7 @@ struct POSCollectOrderPaymentAnalyticsTests { ] // When - sut.trackSuccessfulPayment(capturedPaymentData: capturedPaymentData) + sut.trackSuccessfulCardPayment(capturedPaymentData: capturedPaymentData) // Then #expect(analyticsProvider.receivedEvents.first(where: { $0 == expectedEvent }) != nil) diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index c61799796d0..bee9b2e27da 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -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) + } } }