> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Display saved payment methods

> Show saved payment methods count and list saved cards.

Display the number of saved payment methods or list saved cards for returning customers.

## Recipe: Show saved methods count

> [tab] Web
>
> ```javascript theme={"dark"}
> 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` : '';
> });
> ```

> [tab] Android
>
> Use `PrimerVaultedPaymentMethods` to render saved methods:
>
> ```kotlin theme={"dark"}
> 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.

> [tab] iOS
>
> Read the count inside the `header` slot of `PrimerVaultedPaymentMethods`, which hands you the `PrimerSelectionSession`. Custom slot content must be wrapped in `AnyView`:
>
> ```swift theme={"dark"}
> PrimerVaultedPaymentMethods(
>   header: { session in
>     let count = session.vaultedPaymentMethods.count
>     return AnyView(Text(count > 0 ? "\(count) saved" : ""))
>   }
> )
> ```

## Recipe: List saved cards

> [tab] Web
>
> ```javascript theme={"dark"}
> 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('');
> });
> ```

> [tab] Android
>
> For full control over how saved methods are displayed, observe the controller's state directly:
>
> ```kotlin theme={"dark"}
> @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,
>                         )
>                     }
>                 }
>             }
>         }
>     }
> }
> ```

> [tab] iOS
>
> 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`:
>
> ```swift theme={"dark"}
> 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

> [tab] Web
>
> 1. Listen for the `primer:vault-methods-update` DOM event
> 2. Access `vaultedPayments` from the event detail
> 3. Use `.size()` to get the count or `.toArray()` to iterate over methods
> 4. Each method contains `paymentMethodType` and `paymentInstrumentData` with card details

> [tab] Android
>
> 1. Create a vaulted payment methods controller using `rememberVaultedPaymentMethodsController()`
>
> 2. Use `PrimerVaultedPaymentMethods` for the default rendering, or observe the controller's state for a custom UI
>
> 3. To save new payment methods, call `cardFormController.setVaultOnSuccess(true)` before payment
>
> > [!NOTE]
> >
> > Vaulting requires a `customerId` in your client session. Pass it when creating the session on your server.

> [tab] iOS
>
> 1. Place `PrimerVaultedPaymentMethods` inside a hierarchy that applies the `.primerCheckoutSession(_:theme:onCompletion:)` modifier. The modifier injects a `PrimerSelectionSession` into the environment, which `PrimerVaultedPaymentMethods` resolves internally
>
> 2. 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`
>
> 3. `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
>
> 4. The view's submit button pays with the selected method
>
> 5. To save new payment methods, call `try await checkoutSession.cardForm?.setVaultOnSuccess(true)` before payment
>
> > [!NOTE]
> >
> > Vaulting requires a `customerId` in your client session. Pass it when creating the session on your server.

## Variations

### Save and display methods

> [tab] Web
>
> ```javascript theme={"dark"}
> 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('');
> });
> ```

> [tab] Android
>
> To save a new payment method after a successful payment, call `setVaultOnSuccess()` on the card form controller:
>
> ```kotlin theme={"dark"}
> val cardFormController = rememberCardFormController(checkout)
>
> LaunchedEffect(cardFormController) {
>     cardFormController.setVaultOnSuccess(true)
> }
> ```

> [tab] iOS
>
> `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:
>
> ```swift theme={"dark"}
> // 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:
>
> ```swift theme={"dark"}
> // `checkoutSession` is the PrimerCheckoutSession you hold as @StateObject
> PrimerCardForm()
>   .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)
>   }
> ```

### Inline saved methods with card form

> [tab] Web
>
> ```javascript theme={"dark"}
> 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}`,
>       );
>     });
> });
> ```

> [tab] Android
>
> This example shows saved methods above the card form, letting the customer choose between a saved method or entering new card details:
>
> ```kotlin theme={"dark"}
> @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)
>                 }
>             }
>         }
>     }
> }
> ```

> [tab] iOS
>
> Compose `PrimerVaultedPaymentMethods` and `PrimerCardForm` under the `.primerCheckoutSession(_:theme:onCompletion:)` modifier, letting the customer choose between a saved method or entering new card details:
>
> ```swift theme={"dark"}
> 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
>       }
>     }
>   }
> }
> ```

### Conditional UI based on saved methods

> [tab] Web
>
> ```javascript theme={"dark"}
> 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';
> });
> ```

> [tab] Android
>
> The bottom sheet shows vaulted methods automatically when they are available. No extra configuration is needed beyond providing a `customerId` in the client session:
>
> ```kotlin theme={"dark"}
> @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.primerError.diagnosticsId}")
>             }
>             else -> Unit
>         }
>     }
>
>     when (state) {
>         is PrimerCheckoutState.Loading -> {
>             CircularProgressIndicator()
>         }
>         is PrimerCheckoutState.Ready -> {
>             PrimerCheckoutSheet(
>                 checkout = checkout,
>                 onDismiss = { },
>             )
>         }
>     }
> }
> ```

> [tab] iOS
>
> The managed `PrimerCheckout` shows vaulted methods automatically when they are available. No extra configuration is needed beyond providing a `customerId` in the client session:
>
> ```swift theme={"dark"}
> 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.
>
> > [!WARNING]
> >
> > 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:
> >
> > ```swift theme={"dark"}
> > // Pass in the PrimerCheckoutSession you hold as @StateObject
> > struct 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 deleted
> > struct 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`:
>
> ```swift theme={"dark"}
> PrimerVaultedPaymentMethods(
>   header: { session in
>     AnyView(
>       HStack {
>         Text("Pay faster")
>         Spacer()
>         Button("Manage") { session.showAll() }
>       }
>     )
>   }
> )
> ```

## See also

> [card] [Vault manager component](https://primer.io/docs/sdk/primer-checkout-web/components/primer-vault-manager.md)
>
> SDK reference for saved payment methods

> [card] [Events guide](https://primer.io/docs/checkout/primer-checkout/configuration/events.md)
>
> Handle payment lifecycle events


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.