Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Woo POS] MVP analytics: Handle card present events from POS #15138

Merged
merged 12 commits into from
Feb 14, 2025
2 changes: 1 addition & 1 deletion WooCommerce/Classes/Analytics/WooAnalyticsStat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ enum WooAnalyticsStat: String {
case backgroundUpdatesDisabled = "background_updates_disabled"

// MARK: Point of Sale events
case pointOfSaleLoaded = "pos_loaded"
case pointOfSaleLoaded = "loaded"
case pointOfSaleProductsPullToRefresh = "products_pull_to_refresh"
case pointOfSaleVariationsPullToRefresh = "variations_pull_to_refresh"
case pointOfSaleAddItemToCart = "item_added_to_cart"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Yosemite

final class POSCollectOrderPaymentAnalytics: CollectOrderPaymentAnalyticsTracking {
var connectedReaderModel: String?

private var customerInteractionStarted: Double = 0

func preflightResultReceived(_ result: CardReaderPreflightResult?) { }
func trackProcessingCompletion(intent: Yosemite.PaymentIntent) { }

func trackSuccessfulPayment(capturedPaymentData: CardPresentCapturedPaymentData) {
let elapsedTime = calculateElapsedTimeInMilliseconds(start: customerInteractionStarted, end: Date().timeIntervalSince1970)
ServiceLocator.analytics.track(event:
.PointOfSale.cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStated: elapsedTime))
Copy link
Contributor

Choose a reason for hiding this comment

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

We send value such as 16712.467908859253, should we floor it, and send only the millisecond part 16712?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds good to me. Updated here: 4748a30

🔵 Tracked pos_card_present_collect_payment_success, properties: [plan: , site_url: https://indiemelon.mystagingwebsite.com, milliseconds_since_customer_interaction_started: 27589.0, blog_id: -1, is_wpcom_store: false, was_ecommerce_trial: false, store_id: c5bd46cc-1804-4f7b-badb-bb98c449127f]

Since we floor the value and we return a Double, we always get the .0 which I don't think it's necessary, perhaps returning an Int here is enough as the event key already tells is milliseconds. I'll iterate on this when adding the rest of properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree. Lets sync with Android on this.

}

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) { }

func trackCustomerInteractionStarted() {
customerInteractionStarted = Date().timeIntervalSince1970
}

private func calculateElapsedTimeInMilliseconds(start: Double, end: Double) -> Double {
floor((end - start) * 1000)
}
}

// Protocol conformance. These events are not needed for IPP, only for POS.
extension CollectOrderPaymentAnalytics {
func trackCustomerInteractionStarted() { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extension WooAnalyticsEvent {
static let paymentsOnboardingState = "onboarding_state"
static let itemType = "product_type"
static let itemsInCart = "items_in_cart"
static let millisecondsSinceCustomerInteractionStarted = "milliseconds_since_customer_interaction_started"
}

static func paymentsOnboardingShown() -> WooAnalyticsEvent {
Expand All @@ -31,6 +32,12 @@ extension WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleCheckoutTapped,
properties: [Key.itemsInCart: itemsInCart])
}

static func cardPresentCollectPaymentSuccess(millisecondsSinceCustomerIteractionStated: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .collectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStated)"]
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ final class CardPresentPaymentCollectOrderPaymentUseCaseAdaptor {
private let currencyFormatter: CurrencyFormatter
@Published private var latestPaymentEvent: CardPresentPaymentEvent = .idle
private let stores: StoresManager
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking

init(currencyFormatter: CurrencyFormatter = .init(currencySettings: ServiceLocator.currencySettings),
paymentEventPublisher: AnyPublisher<CardPresentPaymentEvent, Never>,
stores: StoresManager = ServiceLocator.stores) {
stores: StoresManager = ServiceLocator.stores,
collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking) {
self.currencyFormatter = currencyFormatter
self.stores = stores
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
paymentEventPublisher.assign(to: &$latestPaymentEvent)
}

Expand Down Expand Up @@ -51,7 +54,8 @@ final class CardPresentPaymentCollectOrderPaymentUseCaseAdaptor {
alertsPresenter: alertsPresenter,
tapToPayAlertsProvider: CardPresentPaymentsTransactionAlertsProvider(),
bluetoothAlertsProvider: CardPresentPaymentsTransactionAlertsProvider(),
preflightController: preflightController)
preflightController: preflightController,
analyticsTracker: collectOrderPaymentAnalyticsTracker)

return try await withTaskCancellationHandler {
return try await withCheckedThrowingContinuation { continuation in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {

private let siteID: Int64
private let stores: StoresManager
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking

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

@MainActor
init(siteID: Int64, stores: StoresManager = ServiceLocator.stores) async {
init(siteID: Int64, stores: StoresManager = ServiceLocator.stores, collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking) async {
self.siteID = siteID
let onboardingAdaptor = CardPresentPaymentsOnboardingPresenterAdaptor()
self.onboardingAdaptor = onboardingAdaptor
let paymentAlertsPresenterAdaptor = CardPresentPaymentsAlertPresenterAdaptor()
self.paymentAlertsPresenterAdaptor = paymentAlertsPresenterAdaptor
self.stores = stores
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker

connectionControllerManager = CardPresentPaymentsConnectionControllerManager(
siteID: siteID,
Expand Down Expand Up @@ -138,7 +140,9 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {

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

let paymentTask = CardPresentPaymentCollectOrderPaymentUseCaseAdaptor(paymentEventPublisher: paymentEventPublisher).collectPaymentTask(
let paymentTask = CardPresentPaymentCollectOrderPaymentUseCaseAdaptor(paymentEventPublisher: paymentEventPublisher,
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker
).collectPaymentTask(
for: order,
using: connectionMethod,
siteID: siteID,
Expand Down
16 changes: 16 additions & 0 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ protocol PointOfSaleAggregateModelProtocol {
private let cardPresentPaymentService: CardPresentPaymentFacade
private let orderController: PointOfSaleOrderControllerProtocol
private let analytics: Analytics
private let collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking

private var startPaymentOnCardReaderConnection: AnyCancellable?
private var cardReaderDisconnection: AnyCancellable?
Expand All @@ -72,11 +73,13 @@ protocol PointOfSaleAggregateModelProtocol {
cardPresentPaymentService: CardPresentPaymentFacade,
orderController: PointOfSaleOrderControllerProtocol,
analytics: Analytics = ServiceLocator.analytics,
collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking,
paymentState: PointOfSalePaymentState = .card(.idle)) {
self.itemsController = itemsController
self.cardPresentPaymentService = cardPresentPaymentService
self.orderController = orderController
self.analytics = analytics
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
self.paymentState = paymentState
publishCardReaderConnectionStatus()
publishPaymentMessages()
Expand Down Expand Up @@ -121,6 +124,7 @@ private extension POSItem {
@available(iOS 17.0, *)
extension PointOfSaleAggregateModel {
func addToCart(_ item: POSItem) {
trackCustomerInteractionStarted()
guard let cartItem = item.cartItem else { return }
cart.insert(cartItem, at: cart.startIndex)
}
Expand Down Expand Up @@ -151,6 +155,18 @@ extension PointOfSaleAggregateModel {
}
}

// MARK: - Track events
@available(iOS 17.0, *)
private extension PointOfSaleAggregateModel {
func trackCustomerInteractionStarted() {
// At the moment we're assumming that an interaction starts simply when the cart is zero
// but a more complex logic will be needed for other cases
if cart.count == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should clearing the cart reset the customer interaction time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question, I don't think we're clear on that yet. Initially it would seem we would, but I have some doubts about it. Context: pdfdoF-6hn#comment-7582-p2

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. Lets sync with Android. Otherwise, in some cases we could have inconclusive data.

collectOrderPaymentAnalyticsTracker.trackCustomerInteractionStarted()
}
}
}

// MARK: - Card payments
@available(iOS 17.0, *)
extension PointOfSaleAggregateModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ private extension CardReaderConnectionStatusView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController()
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics()
)
VStack {
CardReaderConnectionStatusView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ private extension PointOfSalePaymentSuccessView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return PointOfSalePaymentSuccessView(
viewModel: PointOfSalePaymentSuccessViewModel(formattedOrderTotal: "$3.00",
paymentMethod: .card)
Expand Down
6 changes: 4 additions & 2 deletions WooCommerce/Classes/POS/Presentation/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ private extension CartView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return CartView()
.environment(posModel)
}
Expand All @@ -312,7 +313,8 @@ private extension CartView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
posModel.addToCart(.simpleProduct(.init(id: UUID(),
name: "Sample Product",
formattedPrice: "$10.00",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ private extension ChildItemList {
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return ChildItemList(parentItem: parentItem, title: parentProduct.name)
.environment(posModel)
}
Expand All @@ -188,7 +189,8 @@ private extension ChildItemList {
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return ChildItemList(parentItem: parentItem, title: parentProduct.name)
.environment(posModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ private extension ItemListRow {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
ItemList(state: .loading([]))
.environment(posModel)
}
Expand Down
6 changes: 4 additions & 2 deletions WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ private extension ItemListView {
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return ItemListView()
.environment(posModel)
}
Expand All @@ -267,7 +268,8 @@ private extension ItemListView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return ItemListView()
.environment(posModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ private extension POSFloatingControlView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
POSFloatingControlView(showExitPOSModal: .constant(false), showSupport: .constant(false), showDocumentation: .constant(false))
.environment(\.posBackgroundAppearance, .primary)
.environment(posModel)
Expand All @@ -144,8 +145,9 @@ private extension POSFloatingControlView {
let paymentService = CardPresentPaymentPreviewService()
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: paymentService,
orderController: PointOfSalePreviewOrderController())
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
paymentService.readerConnectionStatus = .connected(.init(name: "", batteryLevel: 0.6))
return POSFloatingControlView(showExitPOSModal: .constant(false), showSupport: .constant(false), showDocumentation: .constant(false))
.environment(\.posBackgroundAppearance, .primary)
Expand All @@ -157,7 +159,8 @@ private extension POSFloatingControlView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
POSFloatingControlView(showExitPOSModal: .constant(false), showSupport: .constant(false), showDocumentation: .constant(false))
.environment(\.posBackgroundAppearance, .secondary)
.environment(posModel)
Expand Down
3 changes: 2 additions & 1 deletion WooCommerce/Classes/POS/Presentation/PaymentButtons.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ private extension PaymentsActionButtons {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
PaymentsActionButtons(isShowingSendReceiptView: .constant(false), isShowingReceiptNotEligibleBanner: .constant(true))
.environment(posModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ private extension PointOfSaleCollectCashView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
PointOfSaleCollectCashView(orderTotal: "$1.23")
.environment(posModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ private extension PointOfSaleDashboardView {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
return NavigationStack {
PointOfSaleDashboardView()
.environment(posModel)
Expand All @@ -204,7 +205,8 @@ private extension PointOfSaleDashboardView {
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
itemsController.itemsViewState = .init(containerState: .content, itemsStack: .init(root: .loading([]), itemStates: [:]))
return NavigationStack {
PointOfSaleDashboardView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ struct PointOfSaleEntryPointView: View {
init(itemsController: PointOfSaleItemsControllerProtocol,
onPointOfSaleModeActiveStateChange: @escaping ((Bool) -> Void),
cardPresentPaymentService: CardPresentPaymentFacade,
orderController: PointOfSaleOrderControllerProtocol) {
orderController: PointOfSaleOrderControllerProtocol,
collectOrderPaymentAnalyticsTracker: CollectOrderPaymentAnalyticsTracking) {
self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange

let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
cardPresentPaymentService: cardPresentPaymentService,
orderController: orderController)
orderController: orderController,
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker)

self._posModel = State(wrappedValue: posModel)
}
Expand All @@ -42,6 +44,7 @@ struct PointOfSaleEntryPointView: View {
PointOfSaleEntryPointView(itemsController: PointOfSalePreviewItemsController(),
onPointOfSaleModeActiveStateChange: { _ in },
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController())
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
}
#endif
Loading