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

# Migrate to Primer Checkout

> Migrate from Drop-In or Headless to Primer Checkout Components.

This guide covers the key changes when migrating from the legacy SDK to the new Primer Checkout Components. Select your platform to see platform-specific migration steps.

<Note>
  Not every section will apply — skip sections for features you don't use.
</Note>

***

## Package and imports

<Tabs>
  <Tab title="Web">
    For Web-specific framework patterns (React, Vue, Svelte, Angular), see the [Framework Migration Guide](/docs/checkout/primer-checkout/migration/frameworks).

    ### Installation

    ```bash theme={"dark"}
    # Remove legacy package
    npm uninstall @primer-io/checkout-web

    # Install new package
    npm install @primer-io/primer-js
    ```

    ### Import changes

    **Legacy:**

    ```javascript theme={"dark"}
    import { Primer as PrimerSDK } from '@primer-io/checkout-web';
    ```

    **New:**

    ```javascript theme={"dark"}
    import { loadPrimer } from '@primer-io/primer-js';
    ```

    ### Type imports

    If you use TypeScript, the type names have changed:

    ```typescript theme={"dark"}
    import type {
      CheckoutElement,
      PrimerCheckoutOptions,
      PrimerJS,
    } from '@primer-io/primer-js';
    ```

    | Legacy type | New type | Purpose |
    | - | - | - |
    | `PrimerCheckout` | `PrimerJS` | Main SDK instance |
    | `UniversalCheckoutOptions` | `PrimerCheckoutOptions` | Checkout configuration |
    | N/A | `CheckoutElement` | Web component element type |
  </Tab>

  <Tab title="Android">
    <Warning>
      The Android SDK is currently in **beta** (v3.0.0-beta.7). The API is subject to change before stable release.
    </Warning>

    ### No dependency changes required

    Primer Checkout Components is part of the same SDK. No BOM or artifact changes are needed.

    ### Jetpack Compose requirement

    If your project doesn't already use Jetpack Compose, enable it in your module-level `build.gradle.kts`:

    ```kotlin theme={"dark"}
    android {
        buildFeatures {
            compose = true
        }
        composeOptions {
            kotlinCompilerExtensionVersion = "1.5.8"
        }
    }

    dependencies {
        implementation(platform("androidx.compose:compose-bom:2025.12.00"))
        implementation("androidx.compose.ui:ui")
        implementation("androidx.compose.material3:material3")
        implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
    }
    ```

    ### Import

    ```kotlin theme={"dark"}
    import io.primer.android.checkout.*
    ```
  </Tab>

  <Tab title="iOS">
    <Warning>
      The iOS SDK is currently in **beta** (v3.0.0-beta). The API is subject to change before stable release.
    </Warning>

    ### Update the dependency version

    Primer Checkout is part of the same `PrimerSDK` package, but only in the 3.0 beta line. Pin the dependency to `3.0.0-beta.7`:

    * **CocoaPods**: `pod 'PrimerSDK', '3.0.0-beta.7'`
    * **Swift Package Manager**: set the dependency rule to **Exact Version** `3.0.0-beta.7`

    ### Minimum deployment target

    Update your minimum deployment target from iOS 13.0 to **iOS 15.0**:

    * **Xcode**: Target > General > Minimum Deployments > iOS 15.0
    * **Podfile**: `platform :ios, '15.0'`
    * **Package.swift**: `.iOS(.v15)`

    ### Import

    The import stays the same:

    ```swift theme={"dark"}
    import PrimerSDK
    ```
  </Tab>
</Tabs>

***

## Initialization

<Tabs>
  <Tab title="Web">
    The initialization pattern changes from a single function call to a multi-step process involving a Web Component and event listeners.

    ### Legacy pattern

    ```javascript theme={"dark"}
    import { Primer as PrimerSDK } from '@primer-io/checkout-web';

    const checkout = await PrimerSDK.showUniversalCheckout(clientToken, {
      container: '#checkout-container',
      onCheckoutComplete: (data) => { /* ... */ },
      onCheckoutFail: (error, data, handler) => { /* ... */ },
    });
    ```

    ### New pattern

    ```javascript theme={"dark"}
    import { loadPrimer } from '@primer-io/primer-js';

    // 1. Load the SDK (registers the <primer-checkout> web component)
    loadPrimer();

    // 2. Get or create the checkout element
    const checkout = document.querySelector('primer-checkout');

    // 3. Set the client token (component property — use setAttribute)
    checkout.setAttribute('client-token', clientToken);

    // 4. Set SDK options (object — assign directly)
    checkout.options = {
      locale: 'en-GB',
    };

    // 5. Listen for payment events
    checkout.addEventListener('primer:payment-success', (event) => {
      const { payment, paymentMethodType } = event.detail;
      window.location.href = `/confirmation?order=${payment.orderId}`;
    });

    checkout.addEventListener('primer:payment-failure', (event) => {
      const { error } = event.detail;
      console.error('Payment failed:', error.message);
    });
    ```

    Or declare the element in HTML:

    ```html theme={"dark"}
    <primer-checkout client-token="your-client-token"></primer-checkout>
    ```

    ### Key differences

    | Aspect | Legacy | New |
    | - | - | - |
    | Entry point | `PrimerSDK.showUniversalCheckout()` | `loadPrimer()` + `<primer-checkout>` element |
    | Container | `container: '#checkout-container'` (CSS selector) | The `<primer-checkout>` element *is* the container |
    | Configuration | Single options object passed to function | Two types: component properties (`setAttribute`) and SDK `options` (direct assignment) |
    | Payment handling | Callbacks in options object | DOM events (`primer:payment-success`, `primer:payment-failure`) |

    <Warning>
      **Component properties vs SDK options** — these are different and cannot be mixed.

      * **Component properties** (`client-token`, `custom-styles`, `loader-disabled`): Use `setAttribute()`.
      * **SDK options** (`locale`, `card`, `vault`, `applePay`, etc.): Assign directly to `.options`.

      Using `setAttribute('options', ...)` will not work. See the [SDK Options guide](/docs/checkout/primer-checkout/configuration/sdk-options) for details.
    </Warning>
  </Tab>

  <Tab title="Android">
    The initialization changes from configuring a singleton to creating a Compose-based controller and rendering a checkout sheet.

    ### Legacy pattern (Drop-In)

    ```kotlin theme={"dark"}
    // Configure singleton + set listener
    Primer.instance.configure(
        settings = PrimerSettings(),
        listener = checkoutListener,
    )

    // Present checkout
    Primer.instance.showUniversalCheckout(context, clientToken)
    ```

    ### New pattern

    ```kotlin theme={"dark"}
    @Composable
    fun CheckoutScreen(clientToken: String) {
        // 1. Create checkout controller
        val checkout = rememberPrimerCheckoutController(
            clientToken = clientToken,
            settings = PrimerSettings(),
        )

        // 2. Observe outcomes via checkout.state
        val state by checkout.state.collectAsStateWithLifecycle()
        LaunchedEffect(state) {
            when (val s = state) {
                is PrimerCheckoutState.Success -> {
                    val paymentId = s.checkoutData.payment.id
                    navigateToConfirmation(paymentId)
                }
                is PrimerCheckoutState.Failure -> {
                    Log.e("Checkout", "Failed: ${s.primerError.description}")
                }
                else -> Unit
            }
        }

        // 3. Render checkout sheet
        PrimerCheckoutSheet(
            checkout = checkout,
            onDismiss = { navigateBack() },
        )
    }
    ```

    For inline layouts instead of a modal sheet, use `PrimerCheckoutHost`:

    ```kotlin theme={"dark"}
    PrimerCheckoutHost(checkout = checkout) {
        // Your custom Compose layout
    }
    ```

    ### Key differences

    | Aspect | Drop-In | CheckoutComponents |
    | - | - | - |
    | Entry point | `Primer.instance.showUniversalCheckout()` | `rememberPrimerCheckoutController()` + `PrimerCheckoutSheet()` |
    | Configuration | `Primer.instance.configure(settings, listener)` | `PrimerSettings` param on controller |
    | Result handling | `PrimerCheckoutListener` callbacks | `PrimerCheckoutState.Success` / `.Failure` via `checkout.state` |
    | UI framework | Android Views | Jetpack Compose |
    | Presentation | Automatic (Activity) | `PrimerCheckoutSheet` (modal) or `PrimerCheckoutHost` (inline) |
    | State | Listener callbacks | `StateFlow<PrimerCheckoutState>` |
    | Dismiss | `Primer.instance.dismiss()` | `checkout.dismiss()` |
  </Tab>

  <Tab title="iOS">
    The initialization changes from configuring a singleton to creating a view (SwiftUI) or calling a static presenter method (UIKit). Configuration, delegate, and presentation are combined into a single step.

    ### Legacy pattern (Drop-In)

    ```swift theme={"dark"}
    // Configure singleton + set delegate
    Primer.shared.configure(settings: settings, delegate: self)

    // Present checkout
    Primer.shared.showUniversalCheckout(clientToken: clientToken)
    ```

    ### New pattern — UIKit

    For UIKit apps, `PrimerCheckoutPresenter` provides the most direct migration path. It wraps the SwiftUI checkout in a `UIHostingController` and handles presentation.

    ```swift theme={"dark"}
    // Set delegate
    PrimerCheckoutPresenter.shared.delegate = self

    // Present checkout — minimal
    PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)

    // Or with settings
    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self,
      primerSettings: PrimerSettings(paymentHandling: .auto)
    )

    // Or with settings and theme
    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self,
      primerSettings: PrimerSettings(paymentHandling: .auto),
      primerTheme: PrimerCheckoutTheme()
    )
    ```

    `PrimerCheckoutPresenter` also provides:

    * `PrimerCheckoutPresenter.isPresenting` — check if checkout is currently visible
    * `PrimerCheckoutPresenter.dismiss()` — programmatically dismiss the checkout

    ### New pattern — SwiftUI

    For SwiftUI apps, use the managed `PrimerCheckout` view directly. It renders Primer's default screens end to end and reports the outcome through `onCompletion`:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      primerSettings: PrimerSettings(paymentHandling: .auto),
      primerTheme: PrimerCheckoutTheme(),
      onCompletion: { state in
        switch state {
        case .success(let result):
          print("Payment succeeded: \(result.paymentId)")
        case let .failure(error, _):
          print("Payment failed: \(error.errorId)")
        case .dismissed:
          print("Dismissed")
        default:
          break
        }
      }
    )
    ```

    To build a custom layout instead of the managed screens, hold a `PrimerCheckoutSession` as a `@StateObject`, place the composable views (`PrimerCardForm`, `PrimerPaymentMethods`) in your hierarchy, and attach the `.primerCheckoutSession(_:theme:onCompletion:)` modifier:

    ```swift theme={"dark"}
    struct CustomCheckoutView: View {
      @StateObject private var session: PrimerCheckoutSession

      init(clientToken: String) {
        _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
      }

      var body: some View {
        ScrollView {
          PrimerPaymentMethods()
          PrimerCardForm()
        }
        .primerCheckoutSession(session) { state in
          if case .success(let result) = state {
            print("Payment succeeded: \(result.paymentId)")
          }
        }
      }
    }
    ```

    ### Key differences

    | Aspect | Drop-In | CheckoutComponents (UIKit) | CheckoutComponents (SwiftUI) |
    | - | - | - | - |
    | Entry point | `Primer.shared.showUniversalCheckout()` | `PrimerCheckoutPresenter.presentCheckout()` | `PrimerCheckout()` view (managed) or `PrimerCheckoutSession` + `.primerCheckoutSession` (custom) |
    | Configuration | `Primer.shared.configure(settings:delegate:)` | Parameters on `presentCheckout()` | Parameters on initializer |
    | Delegate | `PrimerDelegate` | `PrimerCheckoutPresenterDelegate` | `onCompletion` closure |
    | Payment handling | `.auto` or `.manual` | `.auto` only | `.auto` only |
    | Min iOS | 13.0 | 15.0 | 15.0 |
    | UI framework | UIKit | UIKit (wraps SwiftUI internally) | SwiftUI |
    | Dismiss | `Primer.shared.dismiss()` | `PrimerCheckoutPresenter.dismiss()` | View lifecycle |

    <Warning>
      Primer Checkout creates the payment itself. If your Drop-In or Headless integration uses `PrimerSettings(paymentHandling: .manual)` and creates payments from your backend, switch to `.auto`. A checkout started with `.manual` fails immediately with a `PrimerError` whose `errorId` is `invalid-value`. Use `onBeforePaymentCreate` on [`PrimerCheckoutSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) to pass an idempotency key or to abort a payment before the SDK creates it. Backend-driven manual approval is available on Web today and is coming to iOS, see [Migrate the manual flow](/docs/checkout/primer-checkout/migration/manual-flow).
    </Warning>
  </Tab>
</Tabs>

***

## Accessing the SDK

<Tabs>
  <Tab title="Web">
    Some SDK methods require access to the `PrimerJS` instance. This instance is provided via the `primer:ready` event, which fires when the checkout is fully initialized.

    ### Getting the instance

    ```javascript theme={"dark"}
    const checkout = document.querySelector('primer-checkout');

    checkout.addEventListener('primer:ready', (event) => {
      const primer = event.detail;

      // Now you can call SDK methods
      await primer.refreshSession();
    });
    ```

    ### Storing for later use

    If you need to call SDK methods outside of the event handler (e.g., in response to user actions or after a session expires), store the reference:

    ```javascript theme={"dark"}
    let primer;

    checkout.addEventListener('primer:ready', (event) => {
      primer = event.detail;
    });

    // Later, when needed
    async function handleSessionRefresh() {
      if (primer) {
        await primer.refreshSession();
      }
    }
    ```

    ### Available methods

    The `PrimerJS` instance provides:

    | Method | Description |
    | - | - |
    | `refreshSession()` | Sync the SDK with server-side session changes. Returns `Promise<void>`. |
    | `getPaymentMethods()` | Returns the list of available payment methods. |
    | `setCardholderName(name)` | Programmatically set the cardholder name field. |
    | `vault.startPayment(id, options?)` | Start payment with a vaulted method. |
    | `vault.createCvvInput(options)` | Create a CVV input for re-capture. |
    | `vault.delete(id)` | Delete a vaulted payment method. |

    <Info>
      Most integrations don't need to access the `PrimerJS` instance directly. The primary use cases are session refresh, programmatic cardholder name updates, and headless vault implementations.
    </Info>
  </Tab>

  <Tab title="Android">
    In Primer Checkout Components, checkout state is available via `PrimerCheckoutController.state`, a `StateFlow` you can collect in your composables:

    ```kotlin theme={"dark"}
    val checkout = rememberPrimerCheckoutController(clientToken, PrimerSettings())
    val state by checkout.state.collectAsStateWithLifecycle()

    when (state) {
        is PrimerCheckoutState.Loading -> CircularProgressIndicator()
        is PrimerCheckoutState.Ready -> { /* Render checkout */ }
        is PrimerCheckoutState.Success -> { /* Payment completed */ }
        is PrimerCheckoutState.Failure -> { /* Show error */ }
        else -> Unit
    }
    ```

    The controller also provides:

    * `checkout.refresh()` — reinitialize the checkout session
    * `checkout.dismiss()` — programmatically dismiss the checkout UI

    See the [PrimerCheckoutController reference](/docs/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-controller) for the full API.
  </Tab>

  <Tab title="iOS">
    In Primer Checkout Components, checkout state is delivered through the terminal `PrimerCheckoutState` in your `onCompletion` closure, and the composable views own their interactive state through observable sessions.

    For custom layouts, hold a `PrimerCheckoutSession` and attach the `.primerCheckoutSession(_:theme:onCompletion:)` modifier. The composable views (`PrimerCardForm`, `PrimerPaymentMethods`) resolve their per-feature session — `PrimerCardFormSession`, `PrimerSelectionSession` — from the environment, and you observe those sessions inside your slot closures:

    ```swift theme={"dark"}
    @StateObject private var session: PrimerCheckoutSession

    init(clientToken: String) {
      _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
    }

    var body: some View {
      ScrollView {
        // Observe the card form session inside its slots
        PrimerCardForm(
          submitButton: { cardForm in
            Button("Pay") { cardForm.submit() }
              .disabled(!cardForm.state.isValid || cardForm.state.isLoading)
          }
        )
      }
      .primerCheckoutSession(session) { state in
        // Terminal PrimerCheckoutState: .success / .failure / .dismissed
      }
    }
    ```

    `PrimerCheckoutSession` also provides:

    * `session.refresh()` — re-fetch configuration and payment methods after a client-session update
    * `session.cancel()` — tear the session down (the modifier wires this to `onDisappear`)

    See the [PrimerCheckoutSession reference](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) for the full session API.
  </Tab>
</Tabs>

***

## Result handling

<Tabs>
  <Tab title="Web">
    The new SDK uses DOM events as the primary API for payment lifecycle handling. This replaces the callback-based approach from the legacy SDK.

    ### Migration table

    | Legacy callback | New event | Notes |
    | - | - | - |
    | `onCheckoutComplete(data)` | `primer:payment-success` | Unified `payment` property |
    | `onCheckoutFail(error, data, handler)` | `primer:payment-failure` | Simplified — no handler |
    | `onBeforePaymentCreate(data, handler)` | `primer:payment-start` | Use `preventDefault()` + handlers in `event.detail` |
    | `onPaymentCreationStart()` | `primer:payment-start` | Same event, check `paymentMethodType` |
    | `onClientSessionUpdate(session)` | Call `primer.refreshSession()` | Now a method, not event |
    | `submitButton.onDisable(disabled)` | `primer:state-change` | Check `isProcessing` / `isLoading` |

    ### Payment success

    **Legacy:**

    ```javascript theme={"dark"}
    onCheckoutComplete: (data) => {
      console.log('Payment ID:', data.payment.id);
      console.log('Customer email:', data.customer.email);
    }
    ```

    **New:**

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:payment-success', (event) => {
      const { payment, paymentMethodType, timestamp } = event.detail;

      // payment is a PaymentSummary — contains partial card data
      console.log('Last 4:', payment.last4Digits);
      console.log('Network:', payment.network);
      console.log('Method:', paymentMethodType);

      window.location.href = '/confirmation';
    });
    ```

    The `PaymentSummary` contains partial card data (last 4 digits, network, cardholder name) but not full PII like customer email or full card number. Use server-side webhooks for complete payment data.

    ### Payment failure

    **Legacy:**

    ```javascript theme={"dark"}
    onCheckoutFail: (error, data, handler) => {
      handler.showErrorMessage('Your card was declined');
    }
    ```

    **New:**

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:payment-failure', (event) => {
      const { error, payment, paymentMethodType, timestamp } = event.detail;

      // error has: code, message, diagnosticsId, data
      console.error('Failed:', error.message, error.diagnosticsId);

      // The SDK shows the error in the UI automatically.
      // Use this event for logging, analytics, or retry logic.
    });
    ```

    ### Pre-payment validation (intercepting payments)

    The legacy `onBeforePaymentCreate` callback is replaced by the `primer:payment-start` event. Use `event.preventDefault()` to intercept the payment flow, then call `continuePaymentCreation()` or `abortPaymentCreation()` from `event.detail`.

    **Legacy:**

    ```javascript theme={"dark"}
    onBeforePaymentCreate: (data, handler) => {
      if (!termsAccepted) {
        handler.showErrorMessage('Please accept the terms');
        handler.abort();
      } else {
        handler.continuePaymentCreation();
      }
    }
    ```

    **New:**

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:payment-start', (event) => {
      const { paymentMethodType, continuePaymentCreation, abortPaymentCreation } = event.detail;

      // Prevent automatic continuation
      event.preventDefault();

      if (!document.getElementById('terms-checkbox').checked) {
        // Show your own error UI
        alert('Please accept the terms');
        abortPaymentCreation();
      } else {
        // Optionally pass an idempotency key
        continuePaymentCreation({ idempotencyKey: 'your-key' });
      }
    });
    ```

    <Info>
      If you don't call `event.preventDefault()`, the payment continues automatically. Only use `preventDefault()` when you need to run validation or async checks before payment creation.
    </Info>

    ### Session refresh

    This is no longer a callback. It's a method you call when you need to sync the SDK with server-side session changes.

    **Legacy:**

    ```javascript theme={"dark"}
    onClientSessionUpdate: (session) => {
      console.log('Session updated:', session);
    }

    checkout.updateClientSession(newClientToken);
    ```

    **New:**

    ```javascript theme={"dark"}
    // Update the client token, then call refreshSession()
    checkout.setAttribute('client-token', newClientToken);

    checkout.addEventListener('primer:ready', (event) => {
      const primer = event.detail;
      await primer.refreshSession();
    });
    ```
  </Tab>

  <Tab title="Android">
    The callback-based `PrimerCheckoutListener` is replaced by `StateFlow<PrimerCheckoutState>`. Payment outcomes are terminal cases of that single state flow — observe `checkout.state` and match on `PrimerCheckoutState.Success` / `.Failure`.

    ### Callback migration

    | Legacy (`PrimerCheckoutListener`) | New (CheckoutComponents) |
    | - | - |
    | `onCheckoutCompleted(checkoutData)` | `PrimerCheckoutState.Success` (via `checkout.state`) |
    | `onFailed(error, checkoutData, errorHandler)` | `PrimerCheckoutState.Failure` (via `checkout.state`) |
    | Listener callbacks | `StateFlow<PrimerCheckoutState>` |

    ```kotlin theme={"dark"}
    val state by checkout.state.collectAsStateWithLifecycle()

    LaunchedEffect(state) {
        when (val s = state) {
            is PrimerCheckoutState.Success -> {
                val paymentId = s.checkoutData.payment.id
                navigateToConfirmation(paymentId)
            }
            is PrimerCheckoutState.Failure -> {
                Log.e("Checkout", "Error: ${s.primerError.description}")
            }
            else -> Unit
        }
    }

    PrimerCheckoutSheet(
        checkout = checkout,
        onDismiss = {
            navigateBack()
        },
    )
    ```

    ### State observation

    ```kotlin theme={"dark"}
    val state by checkout.state.collectAsStateWithLifecycle()

    when (state) {
        is PrimerCheckoutState.Loading -> { /* Show loading */ }
        is PrimerCheckoutState.Ready -> { /* Checkout is interactive */ }
        is PrimerCheckoutState.Success -> { /* Payment completed */ }
        is PrimerCheckoutState.Failure -> { /* Handle error */ }
        else -> Unit
    }
    ```

    See [PrimerCheckoutState](/docs/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-state) for the complete state reference, including the `Success` and `Failure` outcome cases.
  </Tab>

  <Tab title="iOS">
    The callback-based `PrimerDelegate` is replaced by either `PrimerCheckoutPresenterDelegate` (UIKit) or `onCompletion` + state observation (SwiftUI).

    ### UIKit — PrimerCheckoutPresenterDelegate

    | Legacy (`PrimerDelegate`) | New (`PrimerCheckoutPresenterDelegate`) |
    | - | - |
    | `primerDidCompleteCheckoutWithData(_:)` | `primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult)` |
    | `primerDidFailWithError(_:data:)` | `primerCheckoutPresenterDidFailWithError(_ error: PrimerError, checkoutData: PrimerCheckoutData?)` |
    | `primerDidDismiss()` | `primerCheckoutPresenterDidDismiss()` |

    ```swift theme={"dark"}
    class CheckoutViewController: UIViewController, PrimerCheckoutPresenterDelegate {

      func showCheckout() {
        PrimerCheckoutPresenter.shared.delegate = self
        PrimerCheckoutPresenter.presentCheckout(
          clientToken: clientToken,
          from: self,
          primerSettings: PrimerSettings(paymentHandling: .auto)
        )
      }

      func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult) {
        let confirmationVC = ConfirmationViewController(paymentId: result.paymentId)
        navigationController?.pushViewController(confirmationVC, animated: true)
      }

      func primerCheckoutPresenterDidFailWithError(_ error: PrimerError, checkoutData: PrimerCheckoutData?) {
        print("Payment failed: \(error.errorId)")
      }

      func primerCheckoutPresenterDidDismiss() {
        print("Checkout dismissed")
      }
    }
    ```

    `PrimerCheckoutPresenterDelegate` also provides **optional 3DS lifecycle callbacks**:

    | Callback | Description |
    | - | - |
    | `primerCheckoutPresenterWillPresent3DSChallenge(_:)` | Called before the 3DS challenge UI appears |
    | `primerCheckoutPresenterDidDismiss3DSChallenge()` | Called when 3DS challenge UI is dismissed |
    | `primerCheckoutPresenterDidComplete3DSChallenge(success:resumeToken:error:)` | Called when 3DS completes |

    ### SwiftUI — onCompletion + state observation

    | Legacy (`PrimerDelegate`) | New (CheckoutComponents) |
    | - | - |
    | `primerDidCompleteCheckoutWithData(_:)` | `onCompletion: .success(PaymentResult)` |
    | `primerDidFailWithError(_:data:)` | `onCompletion: .failure(PrimerError, checkoutData: PrimerCheckoutData?)` |
    | `primerDidDismiss()` | `onCompletion: .dismissed` |
    | `primerWillCreatePaymentWithData(_:handler:)` | `PrimerCheckoutSession.onBeforePaymentCreate` handler |

    The managed `PrimerCheckout` view reports the terminal `PrimerCheckoutState` through `onCompletion`. Switch over the case to route your UI:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      onCompletion: { state in
        switch state {
        case .success(let result):
          navigateToConfirmation(paymentId: result.paymentId)
        case let .failure(error, _):
          logError(error)
        case .dismissed:
          navigateBack()
        default:
          break
        }
      }
    )
    ```

    For custom layouts, the same terminal state is delivered through the `.primerCheckoutSession(_:theme:onCompletion:)` modifier. To observe interactive state during the flow, read the `@Published state` on the per-feature session injected into each slot — for example `PrimerCardFormSession.state` inside a `PrimerCardForm` slot.

    <Info>
      `PaymentResult` replaces `CheckoutData` as the success type. It contains `paymentId`, `status`, `token`, `amount`, `currencyCode`, and `paymentMethodType`.
    </Info>

    See [PrimerCheckoutState](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state) for the complete state lifecycle documentation.
  </Tab>
</Tabs>

***

## Configuration and theming

<Tabs>
  <Tab title="Web">
    ### Options changes

    #### Options that work the same

    These options are identical in both SDKs:

    * `locale` — Language/region code (e.g., `'en-GB'`). Falls back to `en-GB` if the locale is not supported.
    * `merchantDomain` — Your website domain (used for Apple Pay validation).
    * `submitButton.useBuiltInButton` — Show or hide the built-in submit button.
    * `submitButton.amountVisible` — Display the payment amount on the button.

    #### Options that changed

    | Legacy option | New option | Notes |
    | - | - | - |
    | `container` | Removed | The `<primer-checkout>` element *is* the container |
    | `vault.visible` | `vault.enabled` | Renamed — same purpose |
    | `form.inputLabelsVisible` | Removed | Labels are always visible for accessibility |
    | `successScreen.visible` | Removed | Handle success UI in your application |
    | `successScreen.message` | Removed | Use `primer:payment-success` event or the `checkout-complete` slot |
    | `apiVersion` | `sdkCore` | `sdkCore` defaults to `true`; set to `false` only for legacy compatibility |

    #### New options

    | Option | Description |
    | - | - |
    | `vault.headless` | Hide default vault UI for custom implementations |
    | `vault.showEmptyState` | Show a message when no saved payment methods exist |

    #### Success screen replacement

    The legacy SDK had a built-in success screen. The new SDK gives you two options:

    **Option 1: Use the `checkout-complete` slot** (declarative)

    ```html theme={"dark"}
    <primer-checkout client-token="your-client-token">
      <primer-main slot="main">
        <div slot="checkout-complete">
          <h2>Thank you for your order!</h2>
          <p>Your payment was processed successfully.</p>
        </div>
      </primer-main>
    </primer-checkout>
    ```

    **Option 2: Handle via event** (imperative)

    ```javascript theme={"dark"}
    checkout.addEventListener('primer:payment-success', (event) => {
      const { payment } = event.detail;
      window.location.href = `/confirmation?order=${payment.orderId}`;
    });
    ```

    ### Styling changes

    The styling system has fundamentally changed from JavaScript objects to CSS custom properties.

    #### Legacy: CSS-in-JS objects

    ```javascript theme={"dark"}
    PrimerSDK.showUniversalCheckout(clientToken, {
      style: {
        input: {
          base: { fontSize: 16, color: '#000000' },
          invalid: { color: '#DC3545', borderColor: '#DC3545' }
        },
        button: {
          base: { backgroundColor: '#007BFF', color: '#FFFFFF' }
        }
      }
    });
    ```

    #### New: CSS custom properties

    **Option 1: CSS stylesheet** (recommended)

    ```css theme={"dark"}
    primer-checkout {
      --primer-color-brand: #007BFF;
      --primer-color-text-primary: #333333;
      --primer-typography-brand: 'Inter', sans-serif;
      --primer-radius-medium: 8px;
    }
    ```

    **Option 2: `custom-styles` attribute** (JSON string with camelCase keys)

    ```javascript theme={"dark"}
    checkout.setAttribute('custom-styles', JSON.stringify({
      primerColorBrand: '#007BFF',
      primerColorTextPrimary: '#333333',
      primerTypographyBrand: 'Inter, sans-serif',
      primerRadiusMedium: '8px',
    }));
    ```

    The conversion rule for the `custom-styles` attribute: `--primer-color-brand` → `primerColorBrand` (remove `--`, convert kebab-case to camelCase).

    **Option 3: Inline CSS**

    ```javascript theme={"dark"}
    checkout.style.setProperty('--primer-color-brand', '#007BFF');
    ```

    #### Key differences

    | Aspect | Legacy | New |
    | - | - | - |
    | Location | JavaScript options object | CSS stylesheet, `custom-styles` attribute, or inline styles |
    | Syntax | Nested JS objects | CSS variables with `--primer-` prefix |
    | Theming | Manual recreation for dark mode | Built-in dark theme via `class="primer-dark-theme"` |
    | Debugging | Difficult to inspect | Live editing in browser DevTools |
    | Performance | Runtime style injection | Native CSS cascade |

    #### Design token categories

    The new SDK organizes styling into token categories:

    * **Colors** — `--primer-color-brand`, `--primer-color-text-primary`, `--primer-color-background-primary`, and more
    * **Typography** — `--primer-typography-brand` (font family), `--primer-typography-body-medium-size`, etc.
    * **Spacing** — `--primer-space-xxsmall` (2px) through `--primer-space-xlarge` (20px)
    * **Border radius** — `--primer-radius-xsmall` (2px) through `--primer-radius-large` (12px)
    * **Sizing** — `--primer-size-small` through `--primer-size-xxxlarge`
    * **Animation** — `--primer-animation-duration`, `--primer-animation-easing`
    * **Border width** — `--primer-width-default` and state variants

    > See the [Styling Variables Reference](/docs/sdk/primer-checkout-web/styling-variables) for the complete list of available tokens.

    <Info>
      **Don't attempt a 1:1 mapping** from your legacy CSS-in-JS styles. Instead, start with your brand color (`--primer-color-brand`) and font (`--primer-typography-brand`), then use browser DevTools to inspect which tokens affect which elements and adjust from there.
    </Info>
  </Tab>

  <Tab title="Android">
    ### Settings

    `PrimerSettings` is now passed to `rememberPrimerCheckoutController()` instead of `Primer.instance.configure()`.

    | Legacy | New | Notes |
    | - | - | - |
    | `Primer.instance.configure(settings)` | `PrimerSettings` param on controller | Same pattern |
    | Theme via `PrimerSettings.uiOptions` | `PrimerTheme` data class on composables | Separate from settings |
    | Screen flags | Composable slot parameters | Custom screens via composition |

    ### Theming

    The legacy theme settings are replaced by `PrimerTheme`, a data class with design token overrides:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        theme = PrimerTheme(
            lightColorTokens = object : LightColorTokens() {
                override val primerColorBrand: Color = Color(0xFF007BFF)
            },
            radiusTokens = RadiusTokens(
                medium = 12.dp,
            ),
        ),
    )
    ```

    Token categories: `LightColorTokens`/`DarkColorTokens`, `RadiusTokens`, `SpacingTokens`, `TypographyTokens`, `BorderWidthTokens`, and `SizeTokens`.

    ### Layout customization

    In Drop-In, customization was limited to settings. In CheckoutComponents, every screen is a composable slot you can override:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        splash = { MyCustomSplash() },
        loading = { MyCustomLoading() },
        success = { data -> MyCustomSuccess(data) },
        error = { primerError, _ -> MyCustomError(primerError) },
    )
    ```

    ### Payment method configuration

    Payment method behavior is configured server-side through the [client session API](/docs/checkout/client-session). No client-side payment method option migration is needed.

    See [SDK Options](/docs/checkout/primer-checkout/configuration/sdk-options), [Styling customization](/docs/checkout/primer-checkout/build-your-ui/styling-customization), and [Layout customization](/docs/checkout/primer-checkout/build-your-ui/layout-customization) for details.
  </Tab>

  <Tab title="iOS">
    ### Settings

    `PrimerSettings` is now passed directly to `PrimerCheckout` or `PrimerCheckoutPresenter.presentCheckout()` instead of `Primer.shared.configure()`.

    | Legacy | New | Notes |
    | - | - | - |
    | `Primer.shared.configure(settings:)` | `PrimerSettings` param on view/presenter/session | Same type, different delivery |
    | `PrimerTheme` (UIKit color/font overrides) | `PrimerCheckoutTheme` (design tokens) | New token-based system |
    | `PrimerUIOptions` screen flags | `@ViewBuilder` slots on the composable views | Replace flags with custom views |

    ### Theming

    The legacy `PrimerTheme` (colors and fonts) is replaced by `PrimerCheckoutTheme`, which uses a design token system with optional override structs. Every override is optional — set only the tokens you want to change and the SDK fills in the rest:

    ```swift theme={"dark"}
    PrimerCheckoutTheme(
      colors: ColorOverrides(
        primerColorBrand: Color(red: 0.91, green: 0.51, blue: 0.47),
        primerColorTextPrimary: Color(white: 0.1)
      ),
      radius: RadiusOverrides(
        primerRadiusMedium: 12
      ),
      typography: TypographyOverrides(
        bodyMedium: TypographyOverrides.TypographyStyle(
          font: "Inter",
          weight: .semibold,
          size: 16,
          lineHeight: 20
        )
      )
    )
    ```

    Token categories: colors, radius, spacing, sizes, typography, and border widths.

    ### Layout customization

    In Drop-In, customization was limited to theme properties. The managed `PrimerCheckout` view renders Primer's default screens. For full control, compose the views under `PrimerCheckoutSession` and customize them through their `@ViewBuilder` slots — for example the `PrimerCardForm` slots (`cardDetails`, `billingAddress`, `submitButton`), each of which receives a `PrimerCardFormSession`:

    ```swift theme={"dark"}
    ScrollView {
      PrimerCardForm(
        submitButton: { cardForm in
          Button("Pay now") { cardForm.submit() }
            .disabled(!cardForm.state.isValid || cardForm.state.isLoading)
        }
      )
      PrimerPaymentMethods()
    }
    .primerCheckoutSession(session) { state in
      // React to the terminal state.
    }
    ```

    Use `CardFormDefaults` (and `PaymentMethodsDefaults`) building blocks inside slots when you want Primer's prebuilt field views with a custom arrangement. Which fields appear is Dashboard-driven via `CardFormConfiguration`; visual styling is theme-driven through `PrimerCheckoutTheme` design tokens.

    ### Payment method configuration

    Payment method behavior (Apple Pay, Klarna, etc.) is configured server-side through the [client session API](/docs/checkout/client-session). No client-side payment method option migration is needed.

    See [SDK Options](/docs/checkout/primer-checkout/configuration/sdk-options), [Styling customization](/docs/checkout/primer-checkout/build-your-ui/styling-customization), and [Layout customization](/docs/checkout/primer-checkout/build-your-ui/layout-customization) for details.
  </Tab>
</Tabs>

***

## Web-specific details

<Info>
  The following sections apply to **Web only**. iOS and Android handle these concerns through the composable views and slots described above.
</Info>

### Payment method options

#### Card form

The card form options have been simplified. The legacy `requireCvv` and `requireCardholderName` top-level options no longer exist. Use the `cardholderName` sub-object instead.

**Legacy:**

```javascript theme={"dark"}
card: {
  requireCvv: true,
  requireCardholderName: true,
}
```

**New:**

```javascript theme={"dark"}
card: {
  cardholderName: {
    required: true,
    visible: true,
    defaultValue: 'John Doe', // Optional: pre-fill the field
  }
}
```

CVV is always required by the card form — there is no option to disable it.

For runtime updates to the cardholder name after initialization, use `primer.setCardholderName('name')` instead of updating the options.

#### Apple Pay

The Apple Pay options have been restructured. The legacy `merchantId` and `merchantName` are handled server-side, not in client options.

**Legacy:**

```javascript theme={"dark"}
applePay: {
  merchantId: 'merchant.com.example',
  merchantName: 'Example Store',
  captureBillingAddress: true,
}
```

**New:**

```javascript theme={"dark"}
applePay: {
  buttonOptions: {
    type: 'buy',
    buttonStyle: 'black',
  },
  billingOptions: {
    requiredBillingContactFields: ['postalAddress'],
  },
  shippingOptions: {
    requiredShippingContactFields: ['postalAddress', 'name', 'phone', 'email'],
    requireShippingMethod: true,
  },
}
```

Key changes:

* `merchantId` and `merchantName` — removed from client options (configured server-side).
* `captureBillingAddress` — deprecated. Use `billingOptions.requiredBillingContactFields` instead. The only supported billing field is `'postalAddress'`.
* **Shipping fields** — use `'phone'` (not `'phoneNumber'`) and `'email'` (not `'emailAddress'`).
* **Button styling** — new `buttonOptions` object with `type` and `buttonStyle`.

> See the [Apple Pay reference](/docs/sdk/primer-checkout-web/apple-pay) for all button types and complete configuration.

#### Google Pay

Google Pay options are largely the same with some additions.

**Legacy:**

```javascript theme={"dark"}
googlePay: {
  merchantId: 'merchant-id-from-google',
  merchantName: 'Example Store',
  buttonType: 'buy',
  buttonColor: 'black',
}
```

**New:**

```javascript theme={"dark"}
googlePay: {
  buttonType: 'buy',
  buttonColor: 'black',
  buttonSizeMode: 'fill',
  buttonRadius: 4,
  buttonLocale: 'en',
  captureBillingAddress: true,
  captureShippingAddress: true,
  emailRequired: true,
  requireShippingMethod: true,
  shippingAddressParameters: {
    allowedCountryCodes: ['US', 'CA'],
    phoneNumberRequired: true,
  },
}
```

Key changes:

* `merchantId` and `merchantName` — removed from client options (configured server-side).
* New button options: `buttonSizeMode`, `buttonRadius`, `buttonLocale`.
* New address capture: `captureBillingAddress`, `captureShippingAddress`, `emailRequired`.
* New shipping: `requireShippingMethod`, `shippingAddressParameters`.

> See the [Google Pay reference](/docs/sdk/primer-checkout-web/google-pay) for complete configuration.

#### Klarna

The Klarna configuration has changed significantly from the legacy SDK. There is no `klarna.locale` option — the Klarna locale is derived from the top-level `locale` option.

**Legacy:**

```javascript theme={"dark"}
klarna: {
  locale: 'en-GB',
}
```

**New:**

```javascript theme={"dark"}
klarna: {
  paymentFlow: 'DEFAULT',           // 'DEFAULT' or 'PREFER_VAULT'
  allowedPaymentCategories: ['pay_now', 'pay_later', 'pay_over_time'],
  recurringPaymentDescription: '...', // Required for recurring payments
  buttonOptions: {
    text: 'Pay with Klarna',
  },
}
```

Set the locale at the top level instead:

```javascript theme={"dark"}
checkout.options = {
  locale: 'en-GB',
  klarna: {
    paymentFlow: 'DEFAULT',
  },
};
```

> See the [SDK Options Reference](/docs/sdk/primer-checkout-web/sdk-options-reference#klarna-options) for all Klarna options.

#### PayPal

PayPal options have been restructured with a `style` sub-object.

**Legacy:**

```javascript theme={"dark"}
paypal: {
  buttonColor: 'blue',
  buttonShape: 'rect',
  buttonHeight: 45,
  buttonLabel: 'pay',
  buttonTagline: false,
  paymentFlow: 'PREFER_VAULT',
}
```

**New:**

```javascript theme={"dark"}
paypal: {
  style: {
    color: 'gold',    // Default is 'gold', not 'blue'
    shape: 'rect',
    height: 45,
    label: 'pay',
    tagline: false,
    layout: 'vertical',
    borderRadius: 4,
  },
  vault: true,  // Replaces paymentFlow: 'PREFER_VAULT'
}
```

Key changes:

* Button properties moved into `style` sub-object and shortened (`buttonColor` → `style.color`).
* Default color is `'gold'`, not `'blue'`.
* `buttonSize` removed — use `style.height` with a pixel value.
* `paymentFlow: 'PREFER_VAULT'` → `vault: true`.

> See the [PayPal reference](/docs/sdk/primer-checkout-web/paypal) for all options.

### Vault changes

#### Configuration

**Legacy:**

```javascript theme={"dark"}
vault: {
  visible: true,
  deletionDisabled: false,
}
```

**New:**

```javascript theme={"dark"}
vault: {
  enabled: true,       // Required — must be true to use vault
  headless: false,     // New: hide default UI for custom implementations
  showEmptyState: true, // New: show message when no saved methods
}
```

Key changes:

* `visible` → `enabled` (renamed, required).
* `deletionDisabled` — no longer a client-side option. Deletion capability is managed through the vault UI or programmatically via `primer.vault.delete()`.
* **New:** `headless` mode for building fully custom vault UIs.
* **New:** `showEmptyState` to show a message when the user has no saved methods.

#### Vault API methods

The new SDK provides programmatic vault management:

```javascript theme={"dark"}
checkout.addEventListener('primer:ready', async (event) => {
  const primer = event.detail;

  // Create CVV input for re-capture
  const cvvInput = await primer.vault.createCvvInput({
    cardNetwork: 'VISA',         // Card network, not paymentMethodId
    container: '#cvv-container',
    placeholder: 'CVV',
  });

  // Start payment with a saved method
  await primer.vault.startPayment(paymentMethodId);

  // Or with CVV for cards requiring re-capture
  await primer.vault.startPayment(paymentMethodId, { cvv: '123' });

  // Delete a saved method
  await primer.vault.delete(paymentMethodId);
});
```

<Warning>
  `vault.createCvvInput()` takes a `cardNetwork` string (e.g., `'VISA'`, `'MASTERCARD'`), not a `paymentMethodId`. It returns a `Promise` — always use `await`.
</Warning>

#### Vault events

| Event | Payload | Description |
| - | - | - |
| `primer:vault-methods-update` | `{ vaultedPayments, cvvRecapture, timestamp }` | Vaulted methods loaded or changed |
| `primer:vault-selection-change` | `{ paymentMethodId, timestamp }` | User selected or deselected a saved method |
| `primer:vault-submit` | `{ source? }` | Dispatch this to trigger vault payment submission |

The `vaultedPayments` payload is an array of `VaultedPaymentMethodSummary` objects.

```javascript theme={"dark"}
checkout.addEventListener('primer:vault-methods-update', (event) => {
  const { vaultedPayments, cvvRecapture, timestamp } = event.detail;

  // vaultedPayments is already an array
  console.log(`${vaultedPayments.length} saved methods`);

  vaultedPayments.forEach(method => {
    console.log(method.paymentInstrumentData?.last4Digits);
  });

  if (cvvRecapture) {
    // CVV re-entry is required for vaulted card payments
  }
});
```

#### Headless vault mode

Build a completely custom vault UI while retaining full vault functionality:

```javascript theme={"dark"}
checkout.options = {
  vault: {
    enabled: true,
    headless: true,
  },
};

checkout.addEventListener('primer:vault-methods-update', async (event) => {
  const { vaultedPayments, cvvRecapture } = event.detail;

  // Render your custom vault UI
  vaultedPayments.forEach(method => {
    if (method.paymentInstrumentType === 'PAYMENT_CARD') {
      const { network, last4Digits } = method.paymentInstrumentData;
      // Render: Visa •••• 4242
    }
  });

  // Create CVV input if re-capture is required
  if (cvvRecapture) {
    const primer = /* get from primer:ready */;
    const cvvInput = await primer.vault.createCvvInput({
      cardNetwork: vaultedPayments[0].paymentInstrumentData.network,
      container: '#cvv-container',
    });
  }
});
```

> See the [Vault Manager component reference](/docs/sdk/primer-checkout-web/components/primer-vault-manager) for the complete headless vault implementation guide.

### Custom submit button

The mechanism for triggering payment submission from an external button has changed from an imperative method call to a dispatched event.

#### Card form submission

**Legacy:**

```javascript theme={"dark"}
document.getElementById('my-button').addEventListener('click', () => {
  checkout.submit();
});
```

**New:**

```javascript theme={"dark"}
document.getElementById('my-button').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('primer:card-submit', {
    bubbles: true,
    composed: true,
  }));
});
```

#### Vault payment submission

For vault payments, dispatch `primer:vault-submit` instead:

```javascript theme={"dark"}
document.getElementById('vault-pay-button').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('primer:vault-submit', {
    bubbles: true,
    composed: true,
  }));
});
```

<Info>
  An alternative approach for card form submission: you can call `cardForm.submit()` directly on the `<primer-card-form>` element if you have a reference to it.

  ```javascript theme={"dark"}
  const cardForm = document.querySelector('primer-card-form');
  await cardForm.submit();
  ```
</Info>

#### Button state management

Monitor the `primer:state-change` event to enable/disable your custom button:

```javascript theme={"dark"}
checkout.addEventListener('primer:state-change', (event) => {
  const { isProcessing, isLoading } = event.detail;
  document.getElementById('my-button').disabled = isProcessing || isLoading;
});
```

For card form validation state, listen to `primer:card-success` and `primer:card-error`:

```javascript theme={"dark"}
checkout.addEventListener('primer:card-success', () => {
  // Card form is valid — enable submit
});

checkout.addEventListener('primer:card-error', (event) => {
  // Card form has validation errors
  console.log('Errors:', event.detail.errors);
});
```

> See the [External Submit Button recipe](/docs/checkout/primer-checkout/guides-and-recipes/external-submit-button) for a complete example.

### Events

The new SDK uses a comprehensive DOM event system as the primary API for payment lifecycle handling.

#### Complete event reference

##### Lifecycle events

| Event | Payload | Description |
| - | - | - |
| `primer:ready` | `PrimerJS` instance | SDK initialized and ready |
| `primer:state-change` | `{ isProcessing, isLoading, isSuccessful, primerJsError, paymentFailure }` | State changes during payment lifecycle |
| `primer:methods-update` | `InitializedPaymentMethod[]` | Available payment methods loaded or changed |

##### Payment events

| Event | Payload | Description |
| - | - | - |
| `primer:payment-start` | `{ paymentMethodType, abortPaymentCreation, continuePaymentCreation, timestamp }` | Payment creation begins — use `preventDefault()` to intercept |
| `primer:payment-success` | `{ payment, paymentMethodType, timestamp }` | Payment completed successfully |
| `primer:payment-failure` | `{ error, payment?, paymentMethodType, timestamp }` | Payment failed |

##### Card form events

| Event | Payload | Description |
| - | - | - |
| `primer:card-network-change` | `CardNetworksContextType` | Card network detected from user input |
| `primer:card-success` | `{ result }` | Card form validation succeeded |
| `primer:card-error` | `{ errors }` | Card form has validation errors |

##### Vault events

| Event | Payload | Description |
| - | - | - |
| `primer:vault-methods-update` | `{ vaultedPayments, cvvRecapture, timestamp }` | Vaulted methods loaded or changed |
| `primer:vault-selection-change` | `{ paymentMethodId, timestamp }` | User selected a saved method |
| `primer:show-other-payments-toggle` | `{ action?, source? }` | Dispatch to toggle the "other payments" section |
| `primer:show-other-payments-toggled` | `{ expanded }` | "Other payments" toggle state changed |

##### Triggerable events

You can dispatch these to control the SDK. Both require `bubbles: true` and `composed: true`.

| Event | Purpose |
| - | - |
| `primer:card-submit` | Trigger card form submission |
| `primer:vault-submit` | Trigger vault payment submission |
| `primer:show-other-payments-toggle` | Toggle the "other payment methods" section |

> See the [Events Reference](/docs/sdk/primer-checkout-web/events-reference) for full payload types and usage examples.

***

## Migration checklist

<Tabs>
  <Tab title="Web">
    Verify your migration is complete:

    * **Replaced package** — Uninstalled `@primer-io/checkout-web`, installed `@primer-io/primer-js`
    * **Updated imports** — Using `loadPrimer` and new type names
    * **Updated initialization** — Using `<primer-checkout>` element with `setAttribute('client-token', ...)` and `.options = {}`
    * **Updated to events** — Using `primer:payment-success`, `primer:payment-failure` events
    * **Updated pre-payment validation** — Using `primer:payment-start` with `preventDefault()`
    * **Updated styling** — Replaced CSS-in-JS objects with `--primer-*` CSS custom properties
    * **Updated payment method config** — Corrected Apple Pay, PayPal, Klarna options
    * **Updated vault config** — Using `vault.enabled` and vault events
    * **Updated custom submit** — Dispatching `primer:card-submit` / `primer:vault-submit` events
    * **Tested in your framework** — See [Framework Migration Guide](/docs/checkout/primer-checkout/migration/frameworks)
  </Tab>

  <Tab title="Android">
    Verify your migration is complete:

    * **Added Jetpack Compose** — `buildFeatures { compose = true }` in build config (if not already present)
    * **Replaced entry point** — Using `rememberPrimerCheckoutController()` + `PrimerCheckoutSheet()` instead of `Primer.instance.showUniversalCheckout()`
    * **Replaced listener** — Observing `PrimerCheckoutState.Success` / `.Failure` on `checkout.state` instead of `PrimerCheckoutListener`
    * **Updated theming** — Using `PrimerTheme` data class instead of `PrimerSettings.uiOptions` (if customizing)
    * **Tested state observation** — Collecting `StateFlow<PrimerCheckoutState>` with `collectAsStateWithLifecycle()`
    * **No server-side changes** — Backend integration remains the same
    * **No dependency changes** — Same SDK BOM
  </Tab>

  <Tab title="iOS">
    Verify your migration is complete:

    * **Updated minimum deployment target** — iOS 15.0
    * **UIKit**: Replaced `Primer.shared.showUniversalCheckout()` with `PrimerCheckoutPresenter.presentCheckout()`
    * **UIKit**: Replaced `PrimerDelegate` with `PrimerCheckoutPresenterDelegate`
    * **SwiftUI**: Using `PrimerCheckout` view with `onCompletion` callback
    * **Updated result handling** — Using `PaymentResult` instead of `CheckoutData`
    * **Payment handling** — `PrimerSettings` uses the default `.auto`. `.manual` is not supported and fails the checkout at start
    * **Updated theming** — Using `PrimerCheckoutTheme` instead of `PrimerTheme` (if customizing)
    * **Tested 3DS flows** — Optional delegate callbacks available for 3DS lifecycle
    * **No server-side changes** — Backend integration remains the same
    * **Updated the dependency** — `PrimerSDK` pinned to `3.0.0-beta.7`

    <Accordion title="Known behavior changes from v2.x">
      These changes may affect existing integrations that also use v2.x Drop-In or Headless:

      * **`PrimerLocaleData` region code**: When providing only `languageCode` (e.g., `PrimerLocaleData(languageCode: "de")`), the `regionCode` now defaults to `nil` instead of the device region. This produces locale code `"de"` instead of `"de-US"`. To preserve v2.x behavior, explicitly pass `regionCode`.
      * **Mastercard number formatting**: Card number display changed from groups of 4-6-6 (`5555 550000 4444`) to standard 4-4-4-4 (`5555 5500 0044 44`), matching industry standard and other platforms.
      * **`PrimerPaymentMethodType` is now public**: All payment method type cases are part of the public API. Merchants can use these for conditional logic (e.g., `if type == .paymentCard`).
    </Accordion>
  </Tab>
</Tabs>

***

## Next steps

<Tabs>
  <Tab title="Web">
    <CardGroup cols={2}>
      <Card title="Framework Migration" icon="code" href="/docs/checkout/primer-checkout/migration/frameworks">
        React, Vue, Svelte, and Angular patterns
      </Card>

      <Card title="SDK Options Reference" icon="gear" href="/docs/sdk/primer-checkout-web/sdk-options-reference">
        Complete configuration reference
      </Card>

      <Card title="Events Reference" icon="bolt" href="/docs/sdk/primer-checkout-web/events-reference">
        Full event payloads and types
      </Card>

      <Card title="Styling Variables" icon="palette" href="/docs/sdk/primer-checkout-web/styling-variables">
        Complete design token reference
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Android">
    <CardGroup cols={2}>
      <Card title="PrimerCheckoutSheet" icon="mobile" href="/docs/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-sheet">
        Modal checkout composable
      </Card>

      <Card title="PrimerCheckoutController" icon="gear" href="/docs/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-controller">
        Session controller API
      </Card>

      <Card title="PrimerCheckoutState" icon="bolt" href="/docs/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-state">
        Checkout state and payment outcomes
      </Card>

      <Card title="Theming" icon="palette" href="/docs/sdk/android-checkout/v3.0.0-beta/api/theming/primer-theme">
        Design token system
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="iOS">
    <CardGroup cols={2}>
      <Card title="PrimerCheckout" icon="mobile" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout">
        SwiftUI view API reference
      </Card>

      <Card title="UIKit Integration" icon="window" href="/docs/sdk/ios-checkout/v3.0.0-beta/guides/uikit-integration">
        PrimerCheckoutPresenter guide
      </Card>

      <Card title="PrimerCheckoutState" icon="bolt" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state">
        Checkout state lifecycle
      </Card>

      <Card title="API Overview" icon="sitemap" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/overview">
        Composable views and sessions
      </Card>
    </CardGroup>
  </Tab>
</Tabs>


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