val state by checkout.state.collectAsStateWithLifecycle()LaunchedEffect(state) { when (val s = state) { is PrimerCheckoutState.Success -> { val payment = s.checkoutData.payment trackPaymentSuccess(payment.id, payment.orderId) } is PrimerCheckoutState.Failure -> { val error = s.primerError trackPaymentFailure(error.errorCode, error.diagnosticsId) } else -> Unit }}PrimerCheckoutSheet(checkout = checkout)
PrimerCheckout(clientToken: clientToken) { state in switch state { case let .success(result): Analytics.track("payment_success", properties: [ "payment_id": result.paymentId ]) case let .failure(error, _): Analytics.track("payment_failure", properties: [ "error_id": error.errorId, "diagnostics_id": error.diagnosticsId ]) default: break }}
Listen for primer:payment-success and primer:payment-failure DOM events
Extract relevant data from event.detail
Send to your analytics platform with meaningful event names and properties
Observe checkout.state with collectAsStateWithLifecycle() and react in a LaunchedEffect
Map PrimerCheckoutState terminal cases to analytics events
Extract relevant data from each state and send to your analytics platform
Checkout State
Analytics Event
Key Properties
Success
purchase_completed
payment_id, order_id
Failure
purchase_failed
error_code, diagnostics_id
Pass an onCompletion closure to PrimerCheckout (or to the .primerCheckoutSession(_:theme:onCompletion:) modifier when composing views inline). It receives .failure once for each failed attempt, while the checkout stays open for the shopper to retry. It then receives .success or .dismissed exactly once.
Map PrimerCheckoutState cases to analytics events
Extract relevant data from each state and send to your analytics platform
Checkout State
Analytics Event
Key Properties
.success(result)
payment_success
payment_id
.failure(error, checkoutData)
payment_failure
error_id, diagnostics_id, plus payment_id and order_id from checkoutData?.payment
.dismissed
checkout_dismissed
—
If no payment was created before the failure, checkoutData is nil. This happens for initialization, tokenization and connectivity failures..initializing and .ready are never delivered. With PrimerCheckout, a failed initialization arrives as .failure. The error screen then offers a retry. With the inline modifier, a failed initialization is the last outcome delivered..failure can arrive more than once in one checkout, one per failed attempt. A payment_failure count is therefore per attempt, not per checkout.
class CheckoutAnalytics( private val firebaseAnalytics: FirebaseAnalytics,) { fun trackPaymentSuccess(paymentId: String, orderId: String?) { firebaseAnalytics.logEvent("purchase_completed") { param("payment_id", paymentId) param("order_id", orderId.orEmpty()) } } fun trackPaymentFailure(errorCode: String?, diagnosticsId: String) { firebaseAnalytics.logEvent("purchase_failed") { param("error_code", errorCode.orEmpty()) param("diagnostics_id", diagnosticsId) } } fun trackPaymentMethodSelected(methodType: String) { firebaseAnalytics.logEvent("payment_method_selected") { param("payment_method_type", methodType) } }}
Wire it into your checkout:
@Composablefun CheckoutScreen( clientToken: String, analytics: CheckoutAnalytics,) { val checkout = rememberPrimerCheckoutController(clientToken) val state by checkout.state.collectAsStateWithLifecycle() LaunchedEffect(state) { when (val s = state) { is PrimerCheckoutState.Success -> { val payment = s.checkoutData.payment analytics.trackPaymentSuccess(payment.id, payment.orderId) } is PrimerCheckoutState.Failure -> { analytics.trackPaymentFailure( s.primerError.errorCode, s.primerError.diagnosticsId, ) } else -> Unit } } when (state) { is PrimerCheckoutState.Loading -> CircularProgressIndicator() is PrimerCheckoutState.Ready -> { PrimerCheckoutSheet(checkout = checkout) } }}
Add the Firebase Analytics dependency via SPM or CocoaPods, then create a tracker:
class CheckoutAnalytics { let firebaseAnalytics: Analytics.Type = Analytics.self func trackState(_ state: PrimerCheckoutState) { switch state { case let .success(result): firebaseAnalytics.logEvent("purchase_completed", parameters: [ "payment_id": result.paymentId ]) case let .failure(error, _): firebaseAnalytics.logEvent("purchase_failed", parameters: [ "error_id": error.errorId, "diagnostics_id": error.diagnosticsId ]) default: break } }}
PaymentResult.amount and currencyCode are optional and are nil for card and saved-method payments. Take the purchase value and currency from your own order.
Wire it into your checkout. PrimerCheckout calls onCompletion with .failure for each failed attempt, then once with .success or .dismissed:
struct CheckoutView: View { let clientToken: String let analytics = CheckoutAnalytics() var body: some View { PrimerCheckout(clientToken: clientToken) { state in analytics.trackState(state) } }}
func handleState(_ state: PrimerCheckoutState, tracker: CheckoutTracker) { switch state { case let .success(result): tracker.onPaymentSuccess(paymentId: result.paymentId) case let .failure(error, _): tracker.onPaymentFailure(errorId: error.errorId) default: break }}
Payment method selection tracking is not available through PrimerCheckoutState. The SDK manages payment method selection internally within its UI components.
PrimerPaymentMethods passes each row’s CheckoutPaymentMethod (with type and name) and an onSelect action to its method slot. Wrap PaymentMethodsDefaults.method and track the tap before calling onSelect. PrimerPaymentMethods is a composable view, so place it inside .primerCheckoutSession(_:theme:onCompletion:). The managed PrimerCheckout modal has no selection callback.
struct TrackedPaymentMethods: View { var body: some View { PrimerPaymentMethods(method: { method, onSelect in PaymentMethodsDefaults.method(method) { Analytics.track("payment_method_selected", properties: [ "type": method.type, "name": method.name ]) onSelect() } }) }}