Skip to content

Commit a477857

Browse files
authored
[Woo POS] MVP analytics: Handle card present events from POS (#15138)
2 parents a406f48 + 1bf71d3 commit a477857

25 files changed

+213
-76
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,7 +1268,7 @@ enum WooAnalyticsStat: String {
12681268
case backgroundUpdatesDisabled = "background_updates_disabled"
12691269

12701270
// MARK: Point of Sale events
1271-
case pointOfSaleLoaded = "pos_loaded"
1271+
case pointOfSaleLoaded = "loaded"
12721272
case pointOfSaleProductsPullToRefresh = "products_pull_to_refresh"
12731273
case pointOfSaleVariationsPullToRefresh = "variations_pull_to_refresh"
12741274
case pointOfSaleAddItemToCart = "item_added_to_cart"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Yosemite
2+
3+
final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
4+
var connectedReaderModel: String?
5+
6+
private var customerInteractionStarted: Double = 0
7+
8+
func preflightResultReceived(_ result: CardReaderPreflightResult?) { }
9+
func trackProcessingCompletion(intent: Yosemite.PaymentIntent) { }
10+
11+
func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
12+
let elapsedTime = calculateElapsedTimeInMilliseconds(start: customerInteractionStarted, end: Date().timeIntervalSince1970)
13+
ServiceLocator.analytics.track(event:
14+
.PointOfSale.cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStated: elapsedTime))
15+
}
16+
17+
func trackPaymentFailure(with error: any Error) { }
18+
func trackPaymentCancelation(cancelationSource: WooAnalyticsEvent.InPersonPayments.CancellationSource) { }
19+
func trackEmailTapped() { }
20+
func trackReceiptPrintTapped() { }
21+
func trackReceiptPrintSuccess() { }
22+
func trackReceiptPrintCanceled() { }
23+
func trackReceiptPrintFailed(error: any Error) { }
24+
25+
func trackCustomerInteractionStarted() {
26+
customerInteractionStarted = Date().timeIntervalSince1970
27+
}
28+
29+
private func calculateElapsedTimeInMilliseconds(start: Double, end: Double) -> Double {
30+
floor((end - start) * 1000)
31+
}
32+
}
33+
34+
// Protocol conformance. These events are not needed for IPP, only for POS.
35+
extension CollectOrderPaymentAnalytics {
36+
func trackCustomerInteractionStarted() { }
37+
}

WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extension WooAnalyticsEvent {
1212
static let paymentsOnboardingState = "onboarding_state"
1313
static let itemType = "product_type"
1414
static let itemsInCart = "items_in_cart"
15+
static let millisecondsSinceCustomerInteractionStarted = "milliseconds_since_customer_interaction_started"
1516
}
1617

1718
static func paymentsOnboardingShown() -> WooAnalyticsEvent {
@@ -31,6 +32,12 @@ extension WooAnalyticsEvent {
3132
WooAnalyticsEvent(statName: .pointOfSaleCheckoutTapped,
3233
properties: [Key.itemsInCart: itemsInCart])
3334
}
35+
36+
static func cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStated: Double) -> WooAnalyticsEvent {
37+
WooAnalyticsEvent(statName: .collectPaymentSuccess, properties: [
38+
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStated)"]
39+
)
40+
}
3441
}
3542
}
3643

WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ final class CardPresentPaymentCollectOrderPaymentUseCaseAdaptor {
1111
private let currencyFormatter: CurrencyFormatter
1212
@Published private var latestPaymentEvent: CardPresentPaymentEvent = .idle
1313
private let stores: StoresManager
14+
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking
1415

1516
init(currencyFormatter: CurrencyFormatter = .init(currencySettings: ServiceLocator.currencySettings),
1617
paymentEventPublisher: AnyPublisher<CardPresentPaymentEvent, Never>,
17-
stores: StoresManager = ServiceLocator.stores) {
18+
stores: StoresManager = ServiceLocator.stores,
19+
collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking) {
1820
self.currencyFormatter = currencyFormatter
1921
self.stores = stores
22+
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
2023
paymentEventPublisher.assign(to: &$latestPaymentEvent)
2124
}
2225

@@ -51,7 +54,8 @@ final class CardPresentPaymentCollectOrderPaymentUseCaseAdaptor {
5154
alertsPresenter: alertsPresenter,
5255
tapToPayAlertsProvider: CardPresentPaymentsTransactionAlertsProvider(),
5356
bluetoothAlertsProvider: CardPresentPaymentsTransactionAlertsProvider(),
54-
preflightController: preflightController)
57+
preflightController: preflightController,
58+
analyticsTracker: collectOrderPaymentAnalyticsTracker)
5559

5660
return try await withTaskCancellationHandler {
5761
return try await withCheckedThrowingContinuation { continuation in

WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {
2525

2626
private let siteID: Int64
2727
private let stores: StoresManager
28+
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking
2829

2930
private var cardPresentPaymentsConfiguration: CardPresentPaymentsConfiguration {
3031
CardPresentConfigurationLoader().configuration
@@ -33,13 +34,14 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {
3334
private var paymentTask: Task<CardPresentPaymentAdaptedCollectOrderPaymentResult, Error>?
3435

3536
@MainActor
36-
init(siteID: Int64, stores: StoresManager = ServiceLocator.stores) async {
37+
init(siteID: Int64, stores: StoresManager = ServiceLocator.stores, collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking) async {
3738
self.siteID = siteID
3839
let onboardingAdaptor = CardPresentPaymentsOnboardingPresenterAdaptor()
3940
self.onboardingAdaptor = onboardingAdaptor
4041
let paymentAlertsPresenterAdaptor = CardPresentPaymentsAlertPresenterAdaptor()
4142
self.paymentAlertsPresenterAdaptor = paymentAlertsPresenterAdaptor
4243
self.stores = stores
44+
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
4345

4446
connectionControllerManager = CardPresentPaymentsConnectionControllerManager(
4547
siteID: siteID,
@@ -138,7 +140,9 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {
138140

139141
// TODO: Update the connected reader subject when we get a connection here.
140142

141-
let paymentTask = CardPresentPaymentCollectOrderPaymentUseCaseAdaptor(paymentEventPublisher: paymentEventPublisher).collectPaymentTask(
143+
let paymentTask = CardPresentPaymentCollectOrderPaymentUseCaseAdaptor(paymentEventPublisher: paymentEventPublisher,
144+
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker
145+
).collectPaymentTask(
142146
for: order,
143147
using: connectionMethod,
144148
siteID: siteID,

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ protocol PointOfSaleAggregateModelProtocol {
6262
private let cardPresentPaymentService: CardPresentPaymentFacade
6363
private let orderController: PointOfSaleOrderControllerProtocol
6464
private let analytics: Analytics
65+
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking
6566

6667
private var startPaymentOnCardReaderConnection: AnyCancellable?
6768
private var cardReaderDisconnection: AnyCancellable?
@@ -72,11 +73,13 @@ protocol PointOfSaleAggregateModelProtocol {
7273
cardPresentPaymentService: CardPresentPaymentFacade,
7374
orderController: PointOfSaleOrderControllerProtocol,
7475
analytics: Analytics = ServiceLocator.analytics,
76+
collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking,
7577
paymentState: PointOfSalePaymentState = .card(.idle)) {
7678
self.itemsController = itemsController
7779
self.cardPresentPaymentService = cardPresentPaymentService
7880
self.orderController = orderController
7981
self.analytics = analytics
82+
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
8083
self.paymentState = paymentState
8184
publishCardReaderConnectionStatus()
8285
publishPaymentMessages()
@@ -121,6 +124,7 @@ private extension POSItem {
121124
@available(iOS 17.0, *)
122125
extension PointOfSaleAggregateModel {
123126
func addToCart(_ item: POSItem) {
127+
trackCustomerInteractionStarted()
124128
guard let cartItem = item.cartItem else { return }
125129
cart.insert(cartItem, at: cart.startIndex)
126130
}
@@ -151,6 +155,18 @@ extension PointOfSaleAggregateModel {
151155
}
152156
}
153157

158+
// MARK: - Track events
159+
@available(iOS 17.0, *)
160+
private extension PointOfSaleAggregateModel {
161+
func trackCustomerInteractionStarted() {
162+
// At the moment we're assumming that an interaction starts simply when the cart is zero
163+
// but a more complex logic will be needed for other cases
164+
if cart.count == 0 {
165+
collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
166+
}
167+
}
168+
}
169+
154170
// MARK: - Card payments
155171
@available(iOS 17.0, *)
156172
extension PointOfSaleAggregateModel {

WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ private extension CardReaderConnectionStatusView {
171171
let posModel = PointOfSaleAggregateModel(
172172
itemsController: PointOfSalePreviewItemsController(),
173173
cardPresentPaymentService: CardPresentPaymentPreviewService(),
174-
orderController: PointOfSalePreviewOrderController()
174+
orderController: PointOfSalePreviewOrderController(),
175+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics()
175176
)
176177
VStack {
177178
CardReaderConnectionStatusView()

WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Reader Messages/PointOfSalePaymentSuccessView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ private extension PointOfSalePaymentSuccessView {
112112
let posModel = PointOfSaleAggregateModel(
113113
itemsController: PointOfSalePreviewItemsController(),
114114
cardPresentPaymentService: CardPresentPaymentPreviewService(),
115-
orderController: PointOfSalePreviewOrderController())
115+
orderController: PointOfSalePreviewOrderController(),
116+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
116117
return PointOfSalePaymentSuccessView(
117118
viewModel: PointOfSalePaymentSuccessViewModel(formattedOrderTotal: "$3.00",
118119
paymentMethod: .card)

WooCommerce/Classes/POS/Presentation/CartView.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ private extension CartView {
302302
let posModel = PointOfSaleAggregateModel(
303303
itemsController: PointOfSalePreviewItemsController(),
304304
cardPresentPaymentService: CardPresentPaymentPreviewService(),
305-
orderController: PointOfSalePreviewOrderController())
305+
orderController: PointOfSalePreviewOrderController(),
306+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
306307
return CartView()
307308
.environment(posModel)
308309
}
@@ -312,7 +313,8 @@ private extension CartView {
312313
let posModel = PointOfSaleAggregateModel(
313314
itemsController: PointOfSalePreviewItemsController(),
314315
cardPresentPaymentService: CardPresentPaymentPreviewService(),
315-
orderController: PointOfSalePreviewOrderController())
316+
orderController: PointOfSalePreviewOrderController(),
317+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
316318
posModel.addToCart(.simpleProduct(.init(id: UUID(),
317319
name: "Sample Product",
318320
formattedPrice: "$10.00",

WooCommerce/Classes/POS/Presentation/Item Selector/ChildItemList.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ private extension ChildItemList {
164164
let posModel = PointOfSaleAggregateModel(
165165
itemsController: itemsController,
166166
cardPresentPaymentService: CardPresentPaymentPreviewService(),
167-
orderController: PointOfSalePreviewOrderController())
167+
orderController: PointOfSalePreviewOrderController(),
168+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
168169
return ChildItemList(parentItem: parentItem, title: parentProduct.name)
169170
.environment(posModel)
170171
}
@@ -188,7 +189,8 @@ private extension ChildItemList {
188189
let posModel = PointOfSaleAggregateModel(
189190
itemsController: itemsController,
190191
cardPresentPaymentService: CardPresentPaymentPreviewService(),
191-
orderController: PointOfSalePreviewOrderController())
192+
orderController: PointOfSalePreviewOrderController(),
193+
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
192194
return ChildItemList(parentItem: parentItem, title: parentProduct.name)
193195
.environment(posModel)
194196
}

0 commit comments

Comments
 (0)