Recipe: Show saved methods count
- Web
- Android
- iOS
document.addEventListener('primer:vault-methods-update', (event) => {
const { vaultedPayments } = event.detail;
const count = vaultedPayments.size();
document.getElementById('saved-methods-badge').textContent =
count > 0 ? `${count} saved` : '';
});
Use This renders a list of saved payment methods with card brand icons, masked card numbers, and expiry dates. The customer can select a saved method to pay with it.
PrimerVaultedPaymentMethods to render saved methods:val checkout = rememberPrimerCheckoutController(clientToken)
val vaultedController = rememberVaultedPaymentMethodsController(checkout)
PrimerVaultedPaymentMethods(controller = vaultedController)
Read the count inside the
header slot of PrimerVaultedPaymentMethods, which hands you the PrimerSelectionSession. Custom slot content must be wrapped in AnyView:PrimerVaultedPaymentMethods(
header: { session in
let count = session.vaultedPaymentMethods.count
return AnyView(Text(count > 0 ? "\(count) saved" : ""))
}
)
Recipe: List saved cards
- Web
- Android
- iOS
document.addEventListener('primer:vault-methods-update', (event) => {
const methods = event.detail.vaultedPayments.toArray();
const container = document.getElementById('saved-cards');
container.innerHTML = methods
.filter((m) => m.paymentMethodType === 'PAYMENT_CARD')
.map((m) => `
<div class="saved-card">
${m.paymentInstrumentData.network} •••• ${m.paymentInstrumentData.last4Digits}
</div>
`)
.join('');
});
For full control over how saved methods are displayed, observe the controller’s state directly:
@Composable
fun CustomVaultedMethods(
checkout: PrimerCheckoutController,
) {
val vaultedController = rememberVaultedPaymentMethodsController(checkout)
val vaultedState by vaultedController.state.collectAsStateWithLifecycle()
Column {
vaultedState.paymentMethods.forEach { method ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
onClick = { vaultedController.select(method) },
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(Modifier.weight(1f)) {
Text(
text = method.paymentMethodType,
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = method.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (vaultedState.selectedMethod == method) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
}
Override the
item slot of PrimerVaultedPaymentMethods to render saved methods with custom UI. The slot receives the method, whether it is currently selected, and an onSelect callback; custom content must be wrapped in AnyView:PrimerVaultedPaymentMethods(
item: { method, isSelected, onSelect in
AnyView(
Button(action: onSelect) {
HStack {
VStack(alignment: .leading) {
Text(method.paymentMethodType)
.font(.body)
if method.paymentMethodType == "PAYMENT_CARD" {
Text("Saved card")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
if isSelected {
Image(systemName: "checkmark.circle.fill")
}
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
}
.buttonStyle(.plain)
)
}
)
How it works
- Web
- Android
- iOS
- Listen for the
primer:vault-methods-updateDOM event - Access
vaultedPaymentsfrom the event detail - Use
.size()to get the count or.toArray()to iterate over methods - Each method contains
paymentMethodTypeandpaymentInstrumentDatawith card details
- Create a vaulted payment methods controller using
rememberVaultedPaymentMethodsController() - Use
PrimerVaultedPaymentMethodsfor the default rendering, or observe the controller’s state for a custom UI - To save new payment methods, call
cardFormController.setVaultOnSuccess(true)before payment
Vaulting requires a
customerId in your client session. Pass it when creating the session on your server.- Place
PrimerVaultedPaymentMethodsinside a hierarchy that applies the.primerCheckoutSession(_:onCompletion:)modifier. The modifier injects aPrimerSelectionSessioninto the environment, whichPrimerVaultedPaymentMethodsresolves internally - Your custom code receives that
PrimerSelectionSessiononly through the slot closures the view hands you (header,item,submitButton) — readsession.vaultedPaymentMethodsfor the list of saved methods, orsession.statefor selection and CVV state, inside a slot - Use
PrimerVaultedPaymentMethods()for the default list, or override itsitemslot for a custom UI - Tapping a row calls
session.selectVaulted(_:); the view’s submit button pays with the selected method
Vaulting requires a
customerId in your client session. Pass it when creating the session on your server.Variations
Save and display methods
- Web
- Android
- iOS
document.addEventListener('primer:vault-methods-update', (event) => {
const methods = event.detail.vaultedPayments.toArray();
const container = document.getElementById('saved-methods');
container.innerHTML = methods
.map((method) => {
if (method.paymentMethodType === 'PAYMENT_CARD') {
const { network, last4Digits } = method.paymentInstrumentData;
return `
<div class="saved-method">
<img src="/icons/${network.toLowerCase()}.svg" alt="${network}">
<span>•••• ${last4Digits}</span>
</div>
`;
}
return `
<div class="saved-method">
<img src="/icons/${method.paymentMethodType.toLowerCase()}.svg" alt="${method.paymentMethodType}">
<span>${method.paymentMethodType}</span>
</div>
`;
})
.join('');
});
To save a new payment method after a successful payment, call
setVaultOnSuccess() on the card form controller:val cardFormController = rememberCardFormController(checkout)
LaunchedEffect(cardFormController) {
cardFormController.setVaultOnSuccess(true)
}
Select a saved method with
selectVaulted(_:); the submit button then pays with it. When the card requires CVV recapture, state.requiresCvvInput becomes true and you feed the input through updateCvvInput(_:):// Mark a saved method as the active selection
session.selectVaulted(method)
// If CVV recapture is required, drive the managed CVV field
if session.state.requiresCvvInput {
session.updateCvvInput("123")
}
PrimerVaultedPaymentMethods renders the submit button and inserts the managed CVV field automatically when recapture is required.Inline saved methods with card form
- Web
- Android
- iOS
document.addEventListener('primer:vault-methods-update', (event) => {
const methods = event.detail.vaultedPayments.toArray();
methods
.filter((m) => m.paymentMethodType === 'PAYMENT_CARD')
.forEach((method) => {
const { network, last4Digits, expirationMonth, expirationYear } =
method.paymentInstrumentData;
console.log(
`${network} •••• ${last4Digits} - Expires ${expirationMonth}/${expirationYear}`,
);
});
});
This example shows saved methods above the card form, letting the customer choose between a saved method or entering new card details:
@Composable
fun InlineCheckoutWithVault(clientToken: String) {
val checkout = rememberPrimerCheckoutController(clientToken)
val state by checkout.state.collectAsStateWithLifecycle()
LaunchedEffect(state) {
when (state) {
is PrimerCheckoutState.Success -> { }
is PrimerCheckoutState.Failure -> { }
else -> Unit
}
}
when (state) {
is PrimerCheckoutState.Loading -> {
CircularProgressIndicator()
}
is PrimerCheckoutState.Ready -> {
PrimerCheckoutHost(
checkout = checkout,
) {
val vaultedController = rememberVaultedPaymentMethodsController(checkout)
val cardFormController = rememberCardFormController(checkout)
LaunchedEffect(cardFormController) {
cardFormController.setVaultOnSuccess(true)
}
Column(Modifier.padding(16.dp)) {
Text(
text = "Saved payment methods",
style = MaterialTheme.typography.titleMedium,
)
Spacer(Modifier.height(12.dp))
PrimerVaultedPaymentMethods(controller = vaultedController)
Spacer(Modifier.height(24.dp))
HorizontalDivider()
Spacer(Modifier.height(24.dp))
Text(
text = "Or pay with a new card",
style = MaterialTheme.typography.titleMedium,
)
Spacer(Modifier.height(12.dp))
PrimerCardForm(controller = cardFormController)
}
}
}
}
}
Compose
PrimerVaultedPaymentMethods and PrimerCardForm under the .primerCheckoutSession(_:onCompletion:) modifier, letting the customer choose between a saved method or entering new card details:struct InlineCheckoutWithVault: View {
@StateObject private var session: PrimerCheckoutSession
init(clientToken: String) {
_session = StateObject(
wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
)
}
var body: some View {
ScrollView {
VStack(spacing: 24) {
Text("Saved payment methods")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
PrimerVaultedPaymentMethods()
Divider()
Text("Or pay with a new card")
.font(.title3.weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
PrimerCardForm()
}
.padding()
}
.primerCheckoutSession(session) { state in
switch state {
case .success(let data):
print("Payment complete: \(data)")
case .failure(let error):
print("Payment failed: \(error)")
default:
break
}
}
}
}
Conditional UI based on saved methods
- Web
- Android
- iOS
document.addEventListener('primer:vault-methods-update', (event) => {
const { vaultedPayments } = event.detail;
const hasSavedMethods = vaultedPayments.size() > 0;
document.getElementById('saved-methods-section').style.display = hasSavedMethods
? 'block'
: 'none';
document.getElementById('new-payment-section').querySelector('h3').textContent =
hasSavedMethods ? 'Or pay with a new method' : 'Payment method';
});
The bottom sheet shows vaulted methods automatically when they are available. No extra configuration is needed beyond providing a
customerId in the client session:@Composable
fun CheckoutWithSavedMethods(clientToken: String) {
val checkout = rememberPrimerCheckoutController(clientToken)
val state by checkout.state.collectAsStateWithLifecycle()
LaunchedEffect(state) {
when (val s = state) {
is PrimerCheckoutState.Success -> { }
is PrimerCheckoutState.Failure -> {
Log.e("Checkout", "Failed: ${s.error.diagnosticsId}")
}
else -> Unit
}
}
when (state) {
is PrimerCheckoutState.Loading -> {
CircularProgressIndicator()
}
is PrimerCheckoutState.Ready -> {
PrimerCheckoutSheet(
checkout = checkout,
onDismiss = { },
)
}
}
}
The managed For inline layouts, drive conditional UI from a slot that receives the If you split the header into its own subview, pass the session in as an
PrimerCheckout shows vaulted methods automatically when they are available. No extra configuration is needed beyond providing a customerId in the client session:PrimerCheckout(clientToken: clientToken) { state in
// Handle the terminal payment result
}
PrimerSelectionSession — for example the header slot of PrimerVaultedPaymentMethods, which hands you the session. Custom slot content must be wrapped in AnyView:PrimerVaultedPaymentMethods(
header: { session in
AnyView(
Text(session.vaultedPaymentMethods.isEmpty
? "Payment method"
: "Or pay with a new method")
)
}
)
@ObservedObject:struct SavedMethodsHeader: View {
@ObservedObject var session: PrimerSelectionSession
var body: some View {
if session.vaultedPaymentMethods.isEmpty {
Text("Payment method")
} else {
Text("Or pay with a new method")
}
}
}
See also
Vault manager component
SDK reference for saved payment methods
Events guide
Handle payment lifecycle events