Recipe
- Web
- Android
- iOS
checkout.addEventListener('primer:ready', (event) => {
const primer = event.detail;
primer.onPaymentFailure = ({ error }) => {
console.error('Payment failed:', {
code: error.code,
message: error.message,
diagnosticsId: error.diagnosticsId, // Share with Primer support
});
};
});
val state by checkout.state.collectAsStateWithLifecycle()
LaunchedEffect(state) {
when (val s = state) {
is PrimerCheckoutState.Failure -> {
val error = s.error
Log.e("Checkout", "Payment failed: ${error.description}")
Log.e("Checkout", "Error code: ${error.errorCode}")
Log.e("Checkout", "Diagnostics ID: ${error.diagnosticsId}")
Log.e("Checkout", "Recovery: ${error.recoverySuggestion}")
}
else -> Unit
}
}
PrimerCheckoutSheet(checkout = checkout)
PrimerCheckout(
clientToken: clientToken,
onCompletion: { state in
if case .failure(let error) = state {
print("[Primer] Error ID: \(error.errorId)")
print("[Primer] Description: \(error.errorDescription ?? "N/A")")
print("[Primer] Diagnostics: \(error.diagnosticsId)")
print("[Primer] Recovery: \(error.recoverySuggestion ?? "N/A")")
}
}
)
How it works
- Web
- Android
- iOS
- Listen for the
primer:readyevent to access the Primer SDK instance - Set the
onPaymentFailurecallback - Log the error details including
diagnosticsIdwhich helps Primer support investigate issues
- Observe
checkout.statewithcollectAsStateWithLifecycle()and react in aLaunchedEffect - Handle
PrimerCheckoutState.Failureto access thePrimerError - Log the error details including
diagnosticsIdwhich helps Primer support investigate issues
PrimerError includes these properties:| Property | Type | Description |
|---|---|---|
errorId | String | Unique error identifier |
description | String | Human-readable error message |
errorCode | String? | Error code (e.g., "card_declined") |
diagnosticsId | String | Reference ID for Primer support |
recoverySuggestion | String? | Suggested recovery action for the user |
- Receive the terminal
PrimerCheckoutStatethrough theonCompletionclosure onPrimerCheckoutor the.primerCheckoutSession(_:onCompletion:)modifier, and switch on.failure - Access
PrimerErrorproperties for debugging details - Log the
diagnosticsIdwhich helps Primer support investigate issues
PrimerError includes these properties:| Property | Type | Description |
|---|---|---|
errorId | String | Stable identifier for the error category |
errorDescription | String? | Human-readable error message for logging |
diagnosticsId | String | Reference ID for Primer support |
recoverySuggestion | String? | Suggested recovery action for the user |
The
diagnosticsId is a unique identifier for the error. When contacting Primer support, always include this ID to help them quickly locate and diagnose the issue.Variations
Send errors to logging service
- Web
- Android
- iOS
primer.onPaymentFailure = ({ error, paymentMethodType }) => {
// Send to your logging service (Sentry, LogRocket, etc.)
Sentry.captureException(new Error(error.message), {
extra: {
code: error.code,
diagnosticsId: error.diagnosticsId,
paymentMethod: paymentMethodType,
},
});
};
is PrimerCheckoutState.Failure -> {
val error = s.error
Sentry.captureException(Exception(error.description)) { scope ->
scope.setExtra("error_code", error.errorCode.orEmpty())
scope.setExtra("diagnostics_id", error.diagnosticsId)
}
}
if case .failure(let error) = state {
ErrorLogger.log(
errorId: error.errorId,
description: error.errorDescription ?? "Unknown error",
diagnosticsId: error.diagnosticsId
)
}
Custom error display
- Web
- Android
- iOS
const primerEvents = [
'primer:ready',
'primer:state-change',
'primer:methods-update',
'primer:payment-start',
'primer:payment-success',
'primer:payment-failure',
'primer:card-success',
'primer:card-error',
'primer:bin-data-available',
'primer:vault-methods-update',
];
primerEvents.forEach((eventName) => {
document.addEventListener(eventName, (event) => {
console.log(`[${eventName}]`, event.detail);
});
});
Override the error slot in
PrimerCheckoutSheet to display a custom error screen. Observe checkout.state separately to log diagnostics:val state by checkout.state.collectAsStateWithLifecycle()
LaunchedEffect(state) {
when (val s = state) {
is PrimerCheckoutState.Failure -> {
Log.e("Checkout", "Diagnostics: ${s.error.diagnosticsId}")
}
else -> Unit
}
}
PrimerCheckoutSheet(
checkout = checkout,
error = { error ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(48.dp),
)
Spacer(Modifier.height(16.dp))
Text(
text = "Something went wrong",
style = MaterialTheme.typography.titleLarge,
)
Spacer(Modifier.height(8.dp))
Text(
text = error.description,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (error.recoverySuggestion != null) {
Spacer(Modifier.height(4.dp))
Text(
text = error.recoverySuggestion!!,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
Spacer(Modifier.height(24.dp))
Button(
onClick = { checkout.refresh() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Try again")
}
Spacer(Modifier.height(8.dp))
OutlinedButton(
onClick = { checkout.dismiss() },
modifier = Modifier.fillMaxWidth(),
) {
Text("Use a different method")
}
}
},
onDismiss = { },
)
Compose the checkout with
PrimerCheckoutSession and capture the terminal failure from the .primerCheckoutSession modifier’s onCompletion to drive your own error screen:struct CheckoutView: View {
@StateObject private var session: PrimerCheckoutSession
@State private var paymentError: PrimerError?
init(clientToken: String) {
_session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
}
var body: some View {
ScrollView {
if let error = paymentError {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.largeTitle)
.foregroundColor(.red)
Text("Something went wrong")
.font(.title2)
Text(error.recoverySuggestion ?? "Please try a different payment method.")
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Button("Try again") {
paymentError = nil
Task { await session.refresh() }
}
.buttonStyle(.borderedProminent)
}
.padding(24)
} else {
PrimerPaymentMethods()
PrimerCardForm()
}
}
.primerCheckoutSession(session) { state in
if case .failure(let error) = state {
print("[Primer] Error: \(error.errorDescription ?? "N/A")")
print("[Primer] Diagnostics: \(error.diagnosticsId)")
paymentError = error
}
}
}
}
Inline error handling
- Web
- Android
- iOS
checkout.addEventListener('primer:methods-update', (event) => {
const methods = event.detail.toArray();
console.table(methods.map((m) => ({ type: m.type, id: m.id })));
});
For
PrimerCheckoutHost, observe checkout.state and display your own UI:@Composable
fun InlineCheckoutWithErrors(clientToken: String) {
val checkout = rememberPrimerCheckoutController(clientToken)
val state by checkout.state.collectAsStateWithLifecycle()
var paymentError by remember { mutableStateOf<PrimerError?>(null) }
LaunchedEffect(state) {
when (val s = state) {
is PrimerCheckoutState.Failure -> {
paymentError = s.error
}
is PrimerCheckoutState.Success -> {
paymentError = null
}
else -> Unit
}
}
when (state) {
is PrimerCheckoutState.Loading -> {
CircularProgressIndicator()
}
is PrimerCheckoutState.Ready -> {
PrimerCheckoutHost(checkout = checkout) {
val cardFormController = rememberCardFormController(checkout)
Column(Modifier.padding(16.dp)) {
PrimerCardForm(controller = cardFormController)
paymentError?.let { error ->
Spacer(Modifier.height(16.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer,
),
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(
text = error.description,
style = MaterialTheme.typography.bodyMedium,
)
if (error.recoverySuggestion != null) {
Text(
text = error.recoverySuggestion!!,
style = MaterialTheme.typography.bodySmall,
)
}
}
}
}
}
}
}
}
}
}
Read per-field validation errors from
PrimerCardFormSession.state.fieldErrors inside a PrimerCardForm slot, and handle the terminal payment error in onCompletion:struct InlineCheckoutWithErrors: View {
@StateObject private var session: PrimerCheckoutSession
init(clientToken: String) {
_session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
}
var body: some View {
ScrollView {
PrimerCardForm(
cardDetails: { cardSession in
CardDetails(session: cardSession)
}
)
.padding()
}
.primerCheckoutSession(session) { state in
if case .failure(let error) = state {
print("[Primer] Payment error: \(error.errorDescription ?? "N/A")")
}
}
}
}
struct CardDetails: View {
@ObservedObject var session: PrimerCardFormSession
var body: some View {
VStack(alignment: .leading, spacing: 8) {
CardFormDefaults.cardDetails(session)
ForEach(session.state.fieldErrors) { fieldError in
Text("\(fieldError.fieldType): \(fieldError.message)")
.font(.caption)
.foregroundColor(.red)
}
}
}
}
See also
Track payment in analytics
Send payment events to your analytics platform
Error handling
Handle payment failures and display error messages