> ## 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

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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" : ""))
      }
    )
    ```
  </Tab>
</Tabs>

## Recipe: List saved cards

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="iOS">
    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`:

    ```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)
        )
      }
    )
    ```
  </Tab>
</Tabs>

## How it works

<Tabs>
  <Tab title="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>

  <Tab title="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.
    </Note>
  </Tab>

  <Tab title="iOS">
    1. Place `PrimerVaultedPaymentMethods` inside a hierarchy that applies the `.primerCheckoutSession(_:onCompletion:)` modifier. The modifier injects a `PrimerSelectionSession` into the environment, which `PrimerVaultedPaymentMethods` resolves internally
    2. Your custom code receives that `PrimerSelectionSession` only through the slot closures the view hands you (`header`, `item`, `submitButton`) — read `session.vaultedPaymentMethods` for the list of saved methods, or `session.state` for selection and CVV state, inside a slot
    3. Use `PrimerVaultedPaymentMethods()` for the default list, or override its `item` slot for a custom UI
    4. Tapping a row calls `session.selectVaulted(_:)`; the view's submit button pays with the selected method

    <Note>
      Vaulting requires a `customerId` in your client session. Pass it when creating the session on your server.
    </Note>
  </Tab>
</Tabs>

## Variations

### Save and display methods

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="iOS">
    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(_:)`:

    ```swift theme={"dark"}
    // 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.
  </Tab>
</Tabs>

### Inline saved methods with card form

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="iOS">
    Compose `PrimerVaultedPaymentMethods` and `PrimerCardForm` under the `.primerCheckoutSession(_: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 .failure(let error):
            print("Payment failed: \(error)")
          default:
            break
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Conditional UI based on saved methods

<Tabs>
  <Tab title="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>

  <Tab title="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.error.diagnosticsId}")
                }
                else -> Unit
            }
        }

        when (state) {
            is PrimerCheckoutState.Loading -> {
                CircularProgressIndicator()
            }
            is PrimerCheckoutState.Ready -> {
                PrimerCheckoutSheet(
                    checkout = checkout,
                    onDismiss = { },
                )
            }
        }
    }
    ```
  </Tab>

  <Tab title="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, drive conditional UI from a slot that receives the `PrimerSelectionSession` — for example the `header` slot of `PrimerVaultedPaymentMethods`, which hands you the session. Custom slot content must be wrapped in `AnyView`:

    ```swift theme={"dark"}
    PrimerVaultedPaymentMethods(
      header: { session in
        AnyView(
          Text(session.vaultedPaymentMethods.isEmpty
            ? "Payment method"
            : "Or pay with a new method")
        )
      }
    )
    ```

    If you split the header into its own subview, pass the session in as an `@ObservedObject`:

    ```swift theme={"dark"}
    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")
        }
      }
    }
    ```
  </Tab>
</Tabs>

## See also

<CardGroup cols={2}>
  <Card title="Vault manager component" icon="bookmark" href="/docs/sdk/primer-checkout-web/components/primer-vault-manager">
    SDK reference for saved payment methods
  </Card>

  <Card title="Events guide" icon="bolt" href="/docs/checkout/primer-checkout/configuration/events">
    Handle payment lifecycle events
  </Card>
</CardGroup>
