From ee76bac7ccce536d2aed78c0486cfb27364fae30 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:19:33 +0300 Subject: [PATCH 1/2] Track more detailed with POS card present collect payment success event Track gatewayID, countryCode, paymentMethod, cardReaderModel, and siteID to match IPP and Android implementation --- .../Classes/Analytics/WooAnalyticsEvent.swift | 2 +- .../POSCollectOrderPaymentAnalytics.swift | 35 ++++++++++++++-- .../WooAnalyticsEvent+PointOfSale.swift | 29 ++++++++++++- .../PointOfSaleEntryPointView.swift | 2 +- .../POS/TabBar/POSTabCoordinator.swift | 2 +- .../Classes/POS/Utils/PreviewHelpers.swift | 41 ++++++++++++++++++- .../Hub Menu/HubMenuViewModel.swift | 3 +- ...POSCollectOrderPaymentAnalyticsTests.swift | 13 +++++- 8 files changed, 115 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift index 49096f5512e..75ec2ebd734 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift @@ -2414,7 +2414,7 @@ extension WooAnalyticsEvent { } } -private extension PaymentMethod { +extension PaymentMethod { var analyticsValue: String { switch self { case .card, .cardPresent: diff --git a/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift b/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift index ccf5fde966b..f78506dc336 100644 --- a/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift +++ b/WooCommerce/Classes/POS/Analytics/POSCollectOrderPaymentAnalytics.swift @@ -3,8 +3,6 @@ import protocol WooFoundation.Analytics import Yosemite final class POSCollectOrderPaymentAnalytics: POSCollectOrderPaymentAnalyticsTracking { - var connectedReaderModel: String? - private var customerInteractionStarted: Double = 0 private var orderSync: Double = 0 private var cardReaderReady: Double = 0 @@ -14,11 +12,35 @@ final class POSCollectOrderPaymentAnalytics: POSCollectOrderPaymentAnalyticsTrac private let analytics: Analytics - init(analytics: Analytics = ServiceLocator.analytics) { + private let siteID: Int64 + private var paymentGatewayAccount: PaymentGatewayAccount? + private let configuration: CardPresentPaymentsConfiguration + private var connectedReader: CardReader? + var connectedReaderModel: String? { + connectedReader?.readerType.model + } + + init(siteID: Int64, + analytics: Analytics = ServiceLocator.analytics, + configuration: CardPresentPaymentsConfiguration = CardPresentConfigurationLoader().configuration) { + self.siteID = siteID self.analytics = analytics + self.configuration = configuration + } + + func preflightResultReceived(_ result: CardReaderPreflightResult?) { + switch result { + case .completed(let connectedReader, let paymentGatewayAccount): + self.connectedReader = connectedReader + self.paymentGatewayAccount = paymentGatewayAccount + case .canceled(_, let paymentGatewayAccount): + self.connectedReader = nil + self.paymentGatewayAccount = paymentGatewayAccount + case .none: + break + } } - func preflightResultReceived(_ result: CardReaderPreflightResult?) { } func trackProcessingCompletion(intent: Yosemite.PaymentIntent) { } func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) { @@ -35,6 +57,11 @@ final class POSCollectOrderPaymentAnalytics: POSCollectOrderPaymentAnalyticsTrac let elapsedTimeSinceCardTapped = calculateElapsedTimeInMilliseconds(since: cardReaderTapped) analytics.track(event: .PointOfSale.cardPresentCollectPaymentSuccess( + forGatewayID: paymentGatewayAccount?.gatewayID, + countryCode: configuration.countryCode, + paymentMethod: capturedPaymentData.paymentMethod, + cardReaderModel: connectedReaderModel, + siteID: siteID, millisecondsSinceCustomerIteractionStarted: elapsedTimeSinceCustomerInteraction, millisecondsSinceOrderSyncSuccess: elapsedTimeSinceOrderSync, millisecondsSinceReaderReadyToCollect: elapsedTimeSinceCardReaderReady, diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index d29894d287f..e462276f49c 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -3,6 +3,8 @@ import enum Yosemite.POSItemType import enum Yosemite.POSItem import struct Yosemite.POSSimpleProduct import struct Yosemite.POSVariation +import enum WooFoundation.CountryCode +import enum Yosemite.PaymentMethod extension WooAnalyticsEvent { enum PointOfSale { @@ -25,6 +27,11 @@ extension WooAnalyticsEvent { static let resultsCount = "results_count" static let millisecondsSinceRequestSent = "milliseconds_since_request_sent" static let totalItems = "total_items" + static let cardReaderModel = "card_reader_model" + static let countryCode = "country" + static let paymentMethodType = "payment_method_type" + static let siteID = "site_id" + static let gatewayID = "plugin_slug" } static func paymentsOnboardingShown() -> WooAnalyticsEvent { @@ -100,12 +107,22 @@ extension WooAnalyticsEvent { WooAnalyticsEvent(statName: .pointOfSaleReaderReadyForCardPayment, properties: [Key.waitingTime: "\(waitingTime)"]) } - static func cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double, + static func cardPresentCollectPaymentSuccess(forGatewayID: String?, + countryCode: CountryCode, + paymentMethod: PaymentMethod, + cardReaderModel: String?, + siteID: Int64, + millisecondsSinceCustomerIteractionStarted: Double, millisecondsSinceOrderSyncSuccess: Double, millisecondsSinceReaderReadyToCollect: Double, millisecondsSinceCardTapped: Double, checkoutTapCount: Int) -> WooAnalyticsEvent { WooAnalyticsEvent(statName: .collectPaymentSuccess, properties: [ + Key.cardReaderModel: readerModel(for: cardReaderModel), + Key.countryCode: countryCode.rawValue, + Key.gatewayID: safeGatewayID(for: forGatewayID), + Key.paymentMethodType: paymentMethod.analyticsValue, + Key.siteID: siteID, Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)", Key.millisecondsSinceOrderSyncSuccess: "\(millisecondsSinceOrderSyncSuccess)", Key.millisecondsSinceReaderReadyToCollect: "\(millisecondsSinceReaderReadyToCollect)", @@ -183,6 +200,16 @@ extension WooAnalyticsEvent { } } +private extension WooAnalyticsEvent.PointOfSale { + static func readerModel(for connectedReaderModel: String?) -> String { + connectedReaderModel ?? "none_connected" + } + + static func safeGatewayID(for gatewayID: String?) -> String { + gatewayID ?? "unknown" + } +} + extension WooAnalyticsEvent.PointOfSale { /// Source of the event where the event is triggered /// Views: Product, Variation, and Coupon Lists. Cart view and Checkout error. diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index 6e9f4185cdc..d9f4f50d64f 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -105,7 +105,7 @@ struct PointOfSaleEntryPointView: View { onPointOfSaleModeActiveStateChange: { _ in }, cardPresentPaymentService: CardPresentPaymentPreviewService(), orderController: PointOfSalePreviewOrderController(), - collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics(), + collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentPreviewAnalytics(), searchHistoryService: PointOfSalePreviewHistoryService(), popularPurchasableItemsController: PointOfSalePreviewItemsController(), barcodeScanService: PointOfSalePreviewBarcodeScanService(), diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index 23492bdb61b..1d9d1719c3f 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -88,7 +88,7 @@ private extension POSTabCoordinator { func presentPOSView() { Task { @MainActor [weak self] in guard let self else { return } - let collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics() + let collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics(siteID: siteID) let cardPresentPaymentService = await CardPresentPaymentService(siteID: siteID, stores: storesManager, collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker) diff --git a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift index c45cece775b..5fc3f7a53c9 100644 --- a/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift +++ b/WooCommerce/Classes/POS/Utils/PreviewHelpers.swift @@ -21,6 +21,7 @@ import enum Yosemite.POSItemType import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol import enum Yosemite.PointOfSaleBarcodeScanError import Combine +import struct Yosemite.PaymentIntent // MARK: - PreviewProvider helpers // @@ -212,7 +213,7 @@ struct POSPreviewHelpers { couponsSearchController: PointOfSaleCouponsControllerProtocol = PointOfSalePreviewCouponsController(), cardPresentPaymentService: CardPresentPaymentFacade = CardPresentPaymentPreviewService(), orderController: PointOfSaleOrderControllerProtocol = PointOfSalePreviewOrderController(), - collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = POSCollectOrderPaymentAnalytics(), + collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalyticsTracking = POSCollectOrderPaymentPreviewAnalytics(), searchHistoryService: POSSearchHistoryProviding = PointOfSalePreviewHistoryService(), popularItemsController: PointOfSaleItemsControllerProtocol = PointOfSalePreviewItemsController(), barcodeScanService: PointOfSaleBarcodeScanServiceProtocol = PointOfSalePreviewBarcodeScanService() @@ -239,4 +240,42 @@ final class PointOfSalePreviewBarcodeScanService: PointOfSaleBarcodeScanServiceP } } +final class POSCollectOrderPaymentPreviewAnalytics: POSCollectOrderPaymentAnalyticsTracking { + func trackCustomerInteractionStarted() {} + + func trackOrderSyncSuccess() {} + + func trackCardReaderReady() {} + + func trackCardReaderTapped() {} + + func trackCheckoutTapped() {} + + func resetCheckoutTapCountTracker() {} + + func trackSuccessfulCashPayment() {} + + var connectedReaderModel: String? + + func preflightResultReceived(_ result: CardReaderPreflightResult?) {} + + func trackProcessingCompletion(intent: PaymentIntent) {} + + func trackSuccessfulCardPayment(capturedPaymentData: CardPresentCapturedPaymentData) {} + + func trackPaymentFailure(with error: any Error) {} + + func trackPaymentCancelation(cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) {} + + func trackEmailTapped() {} + + func trackReceiptPrintTapped() {} + + func trackReceiptPrintSuccess() {} + + func trackReceiptPrintCanceled() {} + + func trackReceiptPrintFailed(error: any Error) {} +} + #endif diff --git a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift index 15ed4ce4348..9ed421efb6c 100644 --- a/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Hub Menu/HubMenuViewModel.swift @@ -154,7 +154,7 @@ final class HubMenuViewModel: ObservableObject { }() private(set) var cardPresentPaymentService: CardPresentPaymentFacade? - private(set) var collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics() + private(set) var collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics private let analytics: Analytics init(siteID: Int64, @@ -181,6 +181,7 @@ final class HubMenuViewModel: ObservableObject { currencySettings: ServiceLocator.currencySettings, featureFlagService: featureFlagService) self.analytics = analytics + self.collectOrderPaymentAnalyticsTracker = POSCollectOrderPaymentAnalytics(siteID: siteID) observeSiteForUIUpdates() observePlanName() observeGoogleAdsEntryPointAvailability() diff --git a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift index 5ca22be7b87..b53327ecc6a 100644 --- a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift @@ -1,5 +1,7 @@ @testable import WooCommerce import protocol WooFoundation.Analytics +import struct Yosemite.CardPresentPaymentsConfiguration +import enum WooFoundation.CountryCode import Testing struct POSCollectOrderPaymentAnalyticsTests { @@ -13,7 +15,9 @@ struct POSCollectOrderPaymentAnalyticsTests { @Test func POSCollectOrderPaymentAnalyticsTests_when_successful_payment_then_tracks_event_and_properties() { // Given - let sut = POSCollectOrderPaymentAnalytics(analytics: analytics) + let siteID: Int64 = 123 + let configuration = CardPresentPaymentsConfiguration(country: .US) + let sut = POSCollectOrderPaymentAnalytics(siteID: siteID, analytics: analytics, configuration: configuration) let capturedPaymentData = CardPresentCapturedPaymentData(paymentMethod: .cardPresent(details: .fake()), receiptParameters: nil) let expectedEvent = "card_present_collect_payment_success" let expectedProperties = [ @@ -21,7 +25,12 @@ struct POSCollectOrderPaymentAnalyticsTests { "milliseconds_since_reader_ready_to_collect_payment", "milliseconds_since_card_tapped", "milliseconds_since_customer_interaction_started", - "checkout_tap_count" + "checkout_tap_count", + "card_reader_model", + "country", + "payment_method_type", + "site_id", + "plugin_slug" ] // When From 030b0a554bc7de57988549da666164862f3e7e8a Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 10 Jul 2025 21:54:32 +0300 Subject: [PATCH 2/2] Update WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift Co-authored-by: Gabriel Maldonado --- .../POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift index b53327ecc6a..bf1f425dff5 100644 --- a/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Analytics/POSCollectOrderPaymentAnalyticsTests.swift @@ -13,7 +13,7 @@ struct POSCollectOrderPaymentAnalyticsTests { analytics = WooAnalytics(analyticsProvider: analyticsProvider) } - @Test func POSCollectOrderPaymentAnalyticsTests_when_successful_payment_then_tracks_event_and_properties() { + @Test func analytics_when_successful_payment_then_tracks_event_and_properties() { // Given let siteID: Int64 = 123 let configuration = CardPresentPaymentsConfiguration(country: .US)