Recipe
- Web
- Android
- iOS
document.addEventListener('primer:payment-success', (event) => {
const { paymentSummary, paymentMethodType } = event.detail;
analytics.track('Payment Completed', {
paymentId: paymentSummary.id,
orderId: paymentSummary.orderId,
method: paymentMethodType,
last4: paymentSummary.paymentMethodData?.last4Digits,
});
});
document.addEventListener('primer:payment-failure', (event) => {
const { error, paymentMethodType } = event.detail;
analytics.track('Payment Failed', {
errorCode: error.code,
method: paymentMethodType,
diagnosticsId: error.diagnosticsId,
});
});
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
}
}
How it works
- Web
- Android
- iOS
- Listen for
primer:payment-successandprimer:payment-failureDOM events - Extract relevant data from
event.detail - Send to your analytics platform with meaningful event names and properties
- Observe
checkout.statewithcollectAsStateWithLifecycle()and react in aLaunchedEffect - Map
PrimerCheckoutStateterminal 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
onCompletionclosure toPrimerCheckout(or to the.primerCheckoutSession(_:theme:onCompletion:)modifier when composing views inline). It receives.failureonce for each failed attempt, while the checkout stays open for the shopper to retry. It then receives.successor.dismissedexactly once. - Map
PrimerCheckoutStatecases 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 | — |
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.Variations
Google Analytics 4 / Firebase Analytics
- Web
- Android
- iOS
document.addEventListener('primer:payment-success', (event) => {
const { paymentSummary, paymentMethodType } = event.detail;
gtag('event', 'purchase', {
transaction_id: paymentSummary.orderId,
value: paymentSummary.amount / 100, // Convert from cents
currency: paymentSummary.currencyCode,
payment_type: paymentMethodType,
});
});
Add the Firebase Analytics dependency:Create an analytics tracker:Wire it into your checkout:
// build.gradle.kts
implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
implementation("com.google.firebase:firebase-analytics")
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)
}
}
}
@Composable
fun 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:Wire it into your checkout.
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.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)
}
}
}
Segment / Reusable analytics wrapper
- Web
- Android
- iOS
document.addEventListener('primer:payment-success', (event) => {
const { paymentSummary, paymentMethodType } = event.detail;
analytics.track('Order Completed', {
order_id: paymentSummary.orderId,
total: paymentSummary.amount / 100,
currency: paymentSummary.currencyCode,
payment_method: paymentMethodType,
});
});
For apps that use multiple analytics providers, create an interface that decouples checkout tracking from any specific SDK:Usage:
interface CheckoutTracker {
fun onPaymentSuccess(paymentId: String, orderId: String?)
fun onPaymentFailure(errorCode: String?, diagnosticsId: String)
fun onPaymentMethodSelected(methodType: String)
}
class CompositeCheckoutTracker(
private val trackers: List<CheckoutTracker>,
) : CheckoutTracker {
override fun onPaymentSuccess(paymentId: String, orderId: String?) {
trackers.forEach { it.onPaymentSuccess(paymentId, orderId) }
}
override fun onPaymentFailure(errorCode: String?, diagnosticsId: String) {
trackers.forEach { it.onPaymentFailure(errorCode, diagnosticsId) }
}
override fun onPaymentMethodSelected(methodType: String) {
trackers.forEach { it.onPaymentMethodSelected(methodType) }
}
}
fun handleState(state: PrimerCheckoutState, tracker: CheckoutTracker) {
when (state) {
is PrimerCheckoutState.Success -> {
val payment = state.checkoutData.payment
tracker.onPaymentSuccess(payment.id, payment.orderId)
}
is PrimerCheckoutState.Failure -> {
tracker.onPaymentFailure(state.primerError.errorCode, state.primerError.diagnosticsId)
}
else -> Unit
}
}
For apps with multiple analytics providers, create a protocol that decouples checkout tracking:Usage:
protocol CheckoutTracker {
func onPaymentSuccess(paymentId: String)
func onPaymentFailure(errorId: String)
}
class CompositeCheckoutTracker: CheckoutTracker {
let trackers: [CheckoutTracker]
init(trackers: [CheckoutTracker]) { self.trackers = trackers }
func onPaymentSuccess(paymentId: String) {
trackers.forEach { $0.onPaymentSuccess(paymentId: paymentId) }
}
func onPaymentFailure(errorId: String) {
trackers.forEach { $0.onPaymentFailure(errorId: errorId) }
}
}
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
}
}
Track payment method selection
- Web
- Android
- iOS
document.addEventListener('primer:payment-start', (event) => {
const { paymentMethodType } = event.detail;
analytics.track('Payment Method Selected', {
method: paymentMethodType,
});
});
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()
}
})
}
}
See also
Log errors for debugging
Capture and surface errors during development
Events guide
Handle payment lifecycle events