Use PrimerVaultedPaymentMethods to render saved methods:
val checkout = rememberPrimerCheckoutController(clientToken)val vaultedController = rememberVaultedPaymentMethodsController(checkout)PrimerVaultedPaymentMethods(controller = vaultedController)
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.
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" : "")) })
Override the item slot of PrimerVaultedPaymentMethods to render the saved method with custom UI. The view is a one-row shortcut, so the slot is called once — for the selected method, with isSelected: true. It receives the method, that flag, and an onSelect callback; custom content must be wrapped in AnyView:
Listen for the primer:vault-methods-update DOM event
Access vaultedPayments from the event detail
Use .size() to get the count or .toArray() to iterate over methods
Each method contains paymentMethodType and paymentInstrumentData with card details
Create a vaulted payment methods controller using rememberVaultedPaymentMethodsController()
Use PrimerVaultedPaymentMethods for 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 PrimerVaultedPaymentMethods inside a hierarchy that applies the .primerCheckoutSession(_:theme:onCompletion:) modifier. The modifier injects a PrimerSelectionSession into the environment, which PrimerVaultedPaymentMethods resolves internally
The header slot hands your code that PrimerSelectionSession. Read session.vaultedPaymentMethods for the full list of saved methods, or session.state for selectedVaultedPaymentMethod and isVaultPaymentLoading. The item slot receives the method, an isSelected flag and an onSelect callback. The submitButton slot receives isLoading, isEnabled and an onSubmit callback. When a card needs CVV recapture, the SDK raises the CVV recapture screen. Outside the slots, reach the same session as checkoutSession.selection
PrimerVaultedPaymentMethods() renders the selected method as a one-row shortcut and hides itself when nothing is saved; its default header links to the SDK’s saved-methods screen for switching or deleting. Override the item slot to restyle that row, or build your own layout over vaultedPaymentMethods if you want every method visible inline
The view’s submit button pays with the selected method
To save new payment methods, call try await checkoutSession.cardForm?.setVaultOnSuccess(true) before payment
Vaulting requires a customerId in your client session. Pass it when creating the session on your server.
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)}
selectVaulted(_:) is the pay verb, so call it from your pay button rather than from a row tap. Keep which row looks selected in your own view state:
// Pays with the saved method. Returns immediately; the payment runs on.session.selectVaulted(method)
A card that needs CVV recapture raises the CVV recapture screen, and that screen finishes the payment. The outcome arrives through .primerCheckoutSession(_:theme:onCompletion:).Before a saved-method payment starts, the SDK calls the onBeforePaymentCreate handler you set on PrimerCheckoutSession. In that handler, you can abort the payment or supply an idempotency key. An abort arrives as a payment failure.PrimerVaultedPaymentMethods renders one saved method and a submit button that calls selectVaulted(_:) for you.To save a new card after a successful payment, call setVaultOnSuccess(_:) on the card form session. It updates the client session, which needs a customerId. Call it with false to stop saving, for example from a customer-facing “Save card” toggle:
// `checkoutSession` is the PrimerCheckoutSession you hold as @StateObjectPrimerCardForm() .task(id: checkoutSession.phase) { // `cardForm` is nil until the session is ready, so this runs again when `phase` changes try? await checkoutSession.cardForm?.setVaultOnSuccess(true) }
This example shows saved methods above the card form, letting the customer choose between a saved method or entering new card details:
@Composablefun 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(_:theme: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 let .failure(error, _): print("Payment failed: \(error)") default: break } } }}
The bottom sheet shows vaulted methods automatically when they are available. No extra configuration is needed beyond providing a customerId in the client session:
@Composablefun 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.primerError.diagnosticsId}") } else -> Unit } } when (state) { is PrimerCheckoutState.Loading -> { CircularProgressIndicator() } is PrimerCheckoutState.Ready -> { PrimerCheckoutSheet( checkout = checkout, onDismiss = { }, ) } }}
The managed 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}
For inline layouts, PrimerVaultedPaymentMethods hides itself entirely when the customer has no saved methods, so you do not need to branch on the empty case to keep the section from appearing.
That also means you cannot render an empty-state message from inside the header slot — when there is nothing saved, no slot is called at all. Branch in the layout that contains the view instead, reading vaultedPaymentMethods from checkoutSession.selection. Observe that PrimerSelectionSession with @ObservedObject. Saved methods load after the session is ready. A view that observes only the checkout session does not update when they load or change:
// Pass in the PrimerCheckoutSession you hold as @StateObjectstruct SavedMethodsSection: View { @ObservedObject var checkoutSession: PrimerCheckoutSession var body: some View { if let selection = checkoutSession.selection { SavedMethodsHeading(selection: selection) } PrimerVaultedPaymentMethods() }}// Observes the selection session, so the heading follows saved methods as they load or are deletedstruct SavedMethodsHeading: View { @ObservedObject var selection: PrimerSelectionSession var body: some View { Text(selection.vaultedPaymentMethods.isEmpty ? "Payment method" : "Or pay with a new method") }}
To customize the section’s own heading, override the header slot — it receives the PrimerSelectionSession. Keep a “Show all” affordance: it is the route to the SDK screen where a customer deletes a saved method. Custom slot content must be wrapped in AnyView: