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

# Migration Guide

> 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](/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.2). 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>

    ### No package changes required

    Primer Checkout Components is part of the same `PrimerSDK` package. No dependency changes are needed — if you already have the SDK installed via CocoaPods or Swift Package Manager, you're ready to go.

    ### 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](/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. Render checkout sheet
        PrimerCheckoutSheet(
            checkout = checkout,
            onEvent = { event ->
                when (event) {
                    is PrimerCheckoutEvent.Success -> {
                        val paymentId = event.checkoutData.payment.id
                        navigateToConfirmation(paymentId)
                    }
                    is PrimerCheckoutEvent.Failure -> {
                        Log.e("Checkout", "Failed: ${event.error.description}")
                    }
                }
            },
            onDismiss = { navigateBack() },
        )
    }
    ```

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

    ```kotlin theme={"dark"}
    PrimerCheckoutHost(checkout = checkout, onEvent = { /* ... */ }) {
        // 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              | `PrimerCheckoutEvent` via `onEvent` lambda                     |
    | 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 and theme
    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self,
      primerSettings: PrimerSettings(paymentHandling: .auto),
      primerTheme: PrimerCheckoutTheme()
    )

    // Or with full scope customization
    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self,
      primerSettings: PrimerSettings(paymentHandling: .auto),
      primerTheme: PrimerCheckoutTheme(),
      scope: { checkoutScope in
        // Customize screens, observe state
      }
    )
    ```

    `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 `PrimerCheckout` view directly:

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

    ### Key differences

    | Aspect        | Drop-In                                       | CheckoutComponents (UIKit)                  | CheckoutComponents (SwiftUI) |
    | ------------- | --------------------------------------------- | ------------------------------------------- | ---------------------------- |
    | Entry point   | `Primer.shared.showUniversalCheckout()`       | `PrimerCheckoutPresenter.presentCheckout()` | `PrimerCheckout()` view      |
    | Configuration | `Primer.shared.configure(settings:delegate:)` | Parameters on `presentCheckout()`           | Parameters on initializer    |
    | Delegate      | `PrimerDelegate`                              | `PrimerCheckoutPresenterDelegate`           | `onCompletion` closure       |
    | Min iOS       | 13.0                                          | 15.0                                        | 15.0                         |
    | UI framework  | UIKit                                         | UIKit (wraps SwiftUI internally)            | SwiftUI                      |
    | Dismiss       | `Primer.shared.dismiss()`                     | `PrimerCheckoutPresenter.dismiss()`         | View lifecycle               |
  </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.Error -> { /* Show error */ }
    }
    ```

    The controller also provides:

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

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

  <Tab title="iOS">
    In Primer Checkout Components, SDK access is through the `scope` closure on `PrimerCheckout` (SwiftUI) or `PrimerCheckoutPresenter.presentCheckout()` (UIKit). The scope provides access to state observation and UI customization.

    ```swift theme={"dark"}
    scope: { checkoutScope in
      // Observe state
      Task {
        for await state in checkoutScope.state {
          switch state {
          case .ready(let totalAmount, let currencyCode):
            print("Ready: \(totalAmount) \(currencyCode)")
          default:
            break
          }
        }
      }

      // Customize screens
      checkoutScope.splashScreen = { AnyView(MyLoadingView()) }

      // Access payment method scopes
      if let cardScope: PrimerCardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self) {
        // Interact with the card form
      }
    }
    ```

    See the [PrimerCheckoutScope reference](/sdk/ios-checkout/v3.0.0-beta/api-reference/primer-checkout-scope) for the full scope 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 `PrimerCheckoutEvent` delivered via the `onEvent` lambda, and `StateFlow<PrimerCheckoutState>` for state observation.

    ### Callback migration

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

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> {
                    val paymentId = event.checkoutData.payment.id
                    navigateToConfirmation(paymentId)
                }
                is PrimerCheckoutEvent.Failure -> {
                    Log.e("Checkout", "Error: ${event.error.description}")
                }
            }
        },
        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.Error -> { /* Handle error */ }
    }
    ```

    See [PrimerCheckoutEvent](/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-event) and [PrimerCheckoutState](/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-state) for complete reference.
  </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)`          |
    | `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) {
        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)`   |
    | `primerDidDismiss()`                          | `onCompletion: .dismissed`              |
    | `primerWillCreatePaymentWithData(_:handler:)` | `scope.onBeforePaymentCreate` handler   |

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        Task {
          for await state in checkoutScope.state {
            switch state {
            case .initializing:
              print("Loading...")
            case .ready(let totalAmount, let currencyCode):
              print("Ready: \(totalAmount) \(currencyCode)")
            case .success(let result):
              print("Payment ID: \(result.paymentId)")
            case .failure(let error):
              print("Error: \(error.errorId)")
            case .dismissed:
              print("Dismissed")
            }
          }
        }
      },
      onCompletion: { state in
        switch state {
        case .success(let result):
          navigateToConfirmation(paymentId: result.paymentId)
        case .failure(let error):
          logError(error)
        case .dismissed:
          navigateBack()
        default:
          break
        }
      }
    )
    ```

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

    See [State and events](/sdk/ios-checkout/v3.0.0-beta/configuration/state-events) 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](/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,
            ),
        ),
        onEvent = { /* ... */ },
    )
    ```

    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 = { error -> MyCustomError(error) },
        onEvent = { /* ... */ },
    )
    ```

    ### Payment method configuration

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

    See [SDK Options](/checkout/primer-checkout/configuration/sdk-options), [Styling customization](/checkout/primer-checkout/build-your-ui/styling-customization), and [Layout customization](/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 | Same type, different delivery   |
    | `PrimerTheme` (UIKit color/font overrides) | `PrimerCheckoutTheme` (design tokens)    | New token-based system          |
    | `PrimerUIOptions` screen flags             | Scope closures for custom screens        | Replace flags with custom views |

    ### Theming

    The legacy `PrimerTheme` (colors and fonts) is replaced by `PrimerCheckoutTheme`, which uses a design token system with override structs:

    ```swift theme={"dark"}
    PrimerCheckoutTheme(
      colors: ColorOverrides(
        primerColorBrand: .systemBlue,
        primerColorTextPrimary: .label
      ),
      radius: RadiusOverrides(
        primerRadiusMedium: 12
      ),
      typography: TypographyOverrides(
        primerTypographyBrand: .custom("Inter", size: 16, weight: .semibold)
      )
    )
    ```

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

    ### Layout customization

    In Drop-In, customization was limited to theme properties. In CheckoutComponents, you can replace entire screens via the `scope` closure:

    ```swift theme={"dark"}
    scope: { checkoutScope in
      checkoutScope.splashScreen = { AnyView(MyCustomSplashView()) }
      checkoutScope.loadingScreen = { AnyView(MyCustomLoadingView()) }
      checkoutScope.errorScreen = { error in AnyView(MyCustomErrorView(error: error)) }
    }
    ```

    ### Payment method configuration

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

    See [SDK Options](/checkout/primer-checkout/configuration/sdk-options), [Styling customization](/checkout/primer-checkout/build-your-ui/styling-customization), and [Layout customization](/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 scope architecture and composable 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](/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](/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](/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](/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](/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](/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](/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](/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** — Using `PrimerCheckoutEvent` via `onEvent` lambda 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`
    * **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
    * **No package changes** — Same PrimerSDK dependency

    <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="/checkout/primer-checkout/migration/frameworks">
        React, Vue, Svelte, and Angular patterns
      </Card>

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

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

      <Card title="Styling Variables" icon="palette" href="/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="/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-sheet">
        Modal checkout composable
      </Card>

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

      <Card title="PrimerCheckoutEvent" icon="bolt" href="/sdk/android-checkout/v3.0.0-beta/api/primer-checkout-event">
        Payment outcome events
      </Card>

      <Card title="Theming" icon="palette" href="/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="/sdk/ios-checkout/v3.0.0-beta/api-reference/primer-checkout">
        SwiftUI view API reference
      </Card>

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

      <Card title="State and Events" icon="bolt" href="/sdk/ios-checkout/v3.0.0-beta/configuration/state-events">
        Checkout state lifecycle
      </Card>

      <Card title="Scopes Overview" icon="sitemap" href="/sdk/ios-checkout/v3.0.0-beta/configuration/scopes-overview">
        Scope-based customization
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
