---
name: primer-android-checkout
description: Build checkout and payment experiences using Primer's Android CheckoutComponents SDK (io.primer:checkout). Use this skill when implementing payment flows, checkout screens, card forms, or integrating Primer SDK into Jetpack Compose applications. Covers controller pattern, composable APIs, slot-based customization, Material 3 theming, and state management. Verified against 3.0.0-beta.3.
---

# Primer Android Checkout

## Overview

This skill provides comprehensive guidance for building checkout and payment experiences using Primer's Android CheckoutComponents SDK (`io.primer:checkout`). The SDK is a Jetpack Compose-native library that provides composable components for card payments, alternative payment methods, and saved payment methods management.

Use this skill when:

- Implementing checkout screens or payment flows in Android apps
- Building card payment forms with validation and formatting
- Displaying available payment methods from Primer
- Managing saved (vaulted) payment methods
- Customizing checkout UI with design tokens and slot-based composition
- Handling payment lifecycle events and state
- Integrating Primer with Jetpack Compose and Material 3
- Troubleshooting Compose version conflicts, state management, or SDK issues

**SDK status:** Beta (v3.0.0-beta.3). The API may change before stable release.

### Key Architectural Concepts

1. **Controller pattern** -- all stateful components use `remember*Controller()` composable functions that return controller interfaces exposing `StateFlow` and action methods
2. **Slot-based composition** -- top-level composables accept `@Composable` lambda parameters for each UI section, with sensible defaults you can selectively override
3. **Design tokens** -- theming uses a token system (`PrimerTheme`) that automatically maps to Material 3 `ColorScheme`
4. **Two presentation modes** -- `PrimerCheckoutSheet` (modal bottom sheet) and `PrimerCheckoutHost` (inline embedding)

## Import Map

IMPORTANT: Use these exact imports when writing code. The SDK package structure does NOT follow a flat `api.*` convention.

```kotlin
// Checkout presentation (sheet + host + remember function)
import io.primer.checkout.api.checkout.PrimerCheckoutSheet
import io.primer.checkout.api.checkout.PrimerCheckoutHost
import io.primer.checkout.api.checkout.rememberPrimerCheckoutController

// Checkout state, events, controller interface
import io.primer.checkout.api.state.PrimerCheckoutController
import io.primer.checkout.api.state.PrimerCheckoutState
import io.primer.checkout.api.state.PrimerCheckoutEvent
import io.primer.checkout.api.state.formatAmount  // extension on PrimerCheckoutController

// Payment method model (used by PrimerPaymentMethods composable)
import io.primer.checkout.api.checkout.models.PrimerPaymentMethod

// Card form
import io.primer.checkout.components.card.PrimerCardForm
import io.primer.checkout.components.card.CardFormDefaults
import io.primer.checkout.components.card.PrimerCardFormController
import io.primer.checkout.components.card.rememberCardFormController
import io.primer.checkout.components.card.SyncValidationError

// Payment methods list
import io.primer.checkout.components.paymentMethods.PrimerPaymentMethods
import io.primer.checkout.components.paymentMethods.PaymentMethodsDefaults
import io.primer.checkout.components.paymentMethods.PrimerPaymentMethodsController
import io.primer.checkout.components.paymentMethods.rememberPaymentMethodsController

// Vaulted payment methods
import io.primer.checkout.components.paymentMethods.PrimerVaultedPaymentMethods
import io.primer.checkout.components.paymentMethods.VaultedPaymentMethodsDefaults
import io.primer.checkout.components.paymentMethods.PrimerVaultedPaymentMethodsController
import io.primer.checkout.components.paymentMethods.rememberVaultedPaymentMethodsController

// Country selection
import io.primer.checkout.components.country.PrimerCountrySelectionController

// Theming
import io.primer.checkout.PrimerTheme
import io.primer.checkout.LocalPrimerTheme
import io.primer.checkout.internal.tokens.LightColorTokens
import io.primer.checkout.internal.tokens.DarkColorTokens
import io.primer.checkout.internal.tokens.BorderWidthTokens
import io.primer.checkout.internal.tokens.RadiusTokens
import io.primer.checkout.internal.tokens.SizeTokens
import io.primer.checkout.internal.tokens.SpacingTokens
import io.primer.checkout.internal.tokens.TypographyTokens

// Common types from the core SDK (transitive dependencies)
import io.primer.android.components.domain.inputs.models.PrimerInputElementType
import io.primer.android.components.domain.core.models.card.PrimerCardNetwork
import io.primer.android.clientSessionActions.domain.models.PrimerCountry
import io.primer.android.domain.tokenization.models.PrimerVaultedPaymentMethod
import io.primer.android.domain.action.models.PrimerClientSession
```

## Quick Start Guide

### Installation

Prerequisites:

- Android API level 24+ (minSdk)
- Kotlin 2.0+ with the Compose Compiler Gradle plugin
- Jetpack Compose enabled
- A `clientToken` from your server via the Client Session API

Add the SDK dependency to your module-level `build.gradle.kts`:

- **Maven group:** `io.primer`
- **Maven artifact name:** `checkout`
- **TOML entry:** `{ group = "io.primer", name = "checkout", version.ref = "primerCheckout" }`

```kotlin
dependencies {
    implementation("io.primer:checkout:3.0.0-beta.3")
}
```

Ensure Compose is configured:

```kotlin
plugins {
    id("org.jetbrains.kotlin.plugin.compose") version "<your-kotlin-version>"
}

android {
    buildFeatures {
        compose = true
    }
}

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")
}
```

ProGuard/R8: The SDK includes its own rules. No additional configuration needed.

### Minimal Compose Integration (PrimerCheckoutSheet)

The fastest way to add checkout -- a modal bottom sheet with built-in navigation between payment method selection, card form, success, and error screens:

> **Note:** `rememberPrimerCheckoutController` is annotated with `@ExperimentalPrimerApi`. You must opt in with `@OptIn(ExperimentalPrimerApi::class)` on the calling function or `@file:OptIn(ExperimentalPrimerApi::class)` at file level.

```kotlin
@OptIn(ExperimentalPrimerApi::class)
@Composable
fun CheckoutScreen(clientToken: String) {
    val checkout = rememberPrimerCheckoutController(
        clientToken = clientToken,
    )

    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> { /* Navigate to confirmation */ }
                is PrimerCheckoutEvent.Failure -> { /* Error is shown in the UI */ }
            }
        },
    )
}
```

### Inline Integration (PrimerCheckoutHost)

Embed checkout components directly in your layout for full control over navigation and presentation:

```kotlin
@Composable
fun InlineCheckout(clientToken: String) {
    val checkout = rememberPrimerCheckoutController(clientToken)
    val state by checkout.state.collectAsStateWithLifecycle()

    PrimerCheckoutHost(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> { /* Handle success */ }
                is PrimerCheckoutEvent.Failure -> { /* Handle failure */ }
            }
        },
    ) {
        when (state) {
            is PrimerCheckoutState.Loading -> {
                CircularProgressIndicator()
            }
            is PrimerCheckoutState.Ready -> {
                val cardFormController = rememberCardFormController(checkout)
                val paymentMethodsController = rememberPaymentMethodsController(checkout)

                Column {
                    PrimerPaymentMethods(controller = paymentMethodsController)
                    PrimerCardForm(controller = cardFormController)
                }
            }
        }
    }
}
```

## Client Session Requirements

The client token is created by your backend via the Primer Client Session API. Different payment methods require different fields in the session:

| Use Case                       | Required Fields                                               |
| ------------------------------ | ------------------------------------------------------------- |
| Basic checkout                 | `amount`, `currencyCode`, `orderId`                           |
| Payment method filtering       | `countryCode` + `currencyCode` (in `order`)                   |
| Vaulting (save payment method) | `customerId` required                                         |
| Klarna                         | `lineItems` required (item descriptions, amounts, quantities) |
| Google Pay                     | `amount` + `currencyCode`                                     |
| Stripe ACH                     | Customer info (name, email)                                   |

If a required field is missing, the payment method may not appear in the available methods list or the payment flow may fail.

## Architecture: Controller Pattern

### Why Controllers

Every stateful SDK component follows the same pattern: a `remember*Controller()` composable creates a controller that survives recomposition, exposes observable state via `StateFlow`, and provides action methods. This is analogous to Compose's own `rememberScrollState()` or `rememberLazyListState()`.

### remember\*Controller Functions

```kotlin
val checkout = rememberPrimerCheckoutController(clientToken)
val cardFormController = rememberCardFormController(checkout)
val paymentMethodsController = rememberPaymentMethodsController(checkout)
val vaultedMethodsController = rememberVaultedPaymentMethodsController(checkout)
```

All child controllers require the parent `PrimerCheckoutController` and must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot (they need CompositionLocal providers).

### Controller Lifecycle with Compose

Controllers are remembered across recompositions. When the key parameters change (e.g., a new `clientToken`), the controller is recreated. Always use the `remember*` functions -- never construct controllers directly:

```kotlin
// CORRECT -- survives recomposition
val checkout = rememberPrimerCheckoutController(clientToken)

// WRONG -- creates new controller on every recomposition
val checkout = PrimerCheckoutController(clientToken)
```

## PrimerCheckoutController

Central interface for managing a checkout session.

### Creation

```kotlin
@Composable
fun rememberPrimerCheckoutController(
    clientToken: String,
    settings: PrimerSettings = PrimerSettings(),
): PrimerCheckoutController
```

| Parameter     | Type             | Default            | Description                                       |
| ------------- | ---------------- | ------------------ | ------------------------------------------------- |
| `clientToken` | `String`         | Required           | Short-lived token from the Client Session API     |
| `settings`    | `PrimerSettings` | `PrimerSettings()` | SDK configuration including payment handling mode |

### Interface

```kotlin
@Stable
interface PrimerCheckoutController {
    val state: StateFlow<PrimerCheckoutState>
    fun refresh()
    fun dismiss()
}
```

| Member      | Type                             | Description                                                                                              |
| ----------- | -------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `state`     | `StateFlow<PrimerCheckoutState>` | Current checkout lifecycle state. Collect with `collectAsStateWithLifecycle()`.                          |
| `refresh()` | Method                           | Reinitializes the checkout session. State transitions back to `Loading`.                                 |
| `dismiss()` | Method                           | Programmatically dismisses the checkout UI. Closes the bottom sheet or triggers cleanup for inline host. |

### formatAmount Extension

```kotlin
fun PrimerCheckoutController.formatAmount(amountInCents: Int): String
```

Formats an amount in minor units (cents) into a locale-aware currency string based on the current session (e.g., `"$10.00"`, `"10,00 EUR"`).

## PrimerCheckoutState and PrimerCheckoutEvent

### PrimerCheckoutState

Sealed interface representing the lifecycle of a checkout session. Observe via `PrimerCheckoutController.state`.

```kotlin
@Stable
sealed interface PrimerCheckoutState {
    data object Loading : PrimerCheckoutState
    data class Ready(
        val clientSession: PrimerClientSession,
    ) : PrimerCheckoutState
}
```

| State     | Description                                                                                                     |
| --------- | --------------------------------------------------------------------------------------------------------------- |
| `Loading` | SDK is initializing -- fetching configuration and payment methods. Initial state and re-entered on `refresh()`. |
| `Ready`   | Checkout is fully initialized and ready for payment. Contains `clientSession` data.                             |

#### PrimerClientSession

```kotlin
data class PrimerClientSession(
    val customerId: String?,
    val orderId: String?,
    val currencyCode: String?,
    val totalAmount: Int?,
    val lineItems: List<PrimerLineItem>?,
    val orderDetails: PrimerOrder?,
    val customer: PrimerCustomer?,
    val paymentMethod: PrimerPaymentMethod?,
    val fees: List<PrimerFee>?,
)
```

| Property        | Type                    | Description                                                  |
| --------------- | ----------------------- | ------------------------------------------------------------ |
| `customerId`    | `String?`               | Customer identifier from your system                         |
| `orderId`       | `String?`               | Order identifier from your system                            |
| `currencyCode`  | `String?`               | ISO 4217 currency code (e.g., `"USD"`, `"EUR"`)              |
| `totalAmount`   | `Int?`                  | Total amount in minor currency units (e.g., `1000` = $10.00) |
| `lineItems`     | `List<PrimerLineItem>?` | Order line items. Required for Klarna.                       |
| `orderDetails`  | `PrimerOrder?`          | Order details including country code and shipping            |
| `customer`      | `PrimerCustomer?`       | Customer details including addresses                         |
| `paymentMethod` | `PrimerPaymentMethod?`  | Payment method config (e.g., allowed card networks)          |
| `fees`          | `List<PrimerFee>?`      | Fees associated with the session                             |

#### PrimerClientSession Nested Types

```kotlin
data class PrimerCustomer(
    val emailAddress: String?,
    val mobileNumber: String?,
    val firstName: String?,
    val lastName: String?,
    val billingAddress: PrimerAddress?,
    val shippingAddress: PrimerAddress?,
)

data class PrimerOrder(
    val countryCode: CountryCode?,
    val shipping: PrimerShipping?,
)

data class PrimerShipping(
    val amount: Int?,
    val methodId: String?,
    val methodName: String?,
    val methodDescription: String?,
)

data class PrimerLineItem(
    val itemId: String?,
    val itemDescription: String?,
    val amount: Int?,
    val discountAmount: Int?,
    val quantity: Int?,
    val taxCode: String?,
    val taxAmount: Int?,
)

data class PrimerAddress(
    val firstName: String?,
    val lastName: String?,
    val addressLine1: String?,
    val addressLine2: String?,
    val postalCode: String?,
    val city: String?,
    val state: String?,
    val countryCode: CountryCode?,
)

data class PrimerFee(
    val type: String?,
    val amount: Int,
)
```

Note: `PrimerPaymentMethod` inside `PrimerClientSession` is a different type from the top-level `PrimerPaymentMethod` used in payment method lists. This one contains `orderedAllowedCardNetworks: List<CardNetwork.Type>`.

### PrimerCheckoutEvent

Sealed interface for payment outcome events delivered through the `onEvent` callback on `PrimerCheckoutSheet` or `PrimerCheckoutHost`.

```kotlin
@Stable
sealed interface PrimerCheckoutEvent {
    data class Success(
        val checkoutData: PrimerCheckoutData,
    ) : PrimerCheckoutEvent

    data class Failure(
        val error: PrimerError,
    ) : PrimerCheckoutEvent
}
```

| Event     | When                                                     | Property                           |
| --------- | -------------------------------------------------------- | ---------------------------------- |
| `Success` | After successful payment (AUTO) or tokenization (MANUAL) | `checkoutData: PrimerCheckoutData` |
| `Failure` | After a payment error or SDK error                       | `error: PrimerError`               |

Events can fire multiple times per session (e.g., a `Failure` followed by a `Success` on retry).

#### PrimerCheckoutData

```kotlin
data class PrimerCheckoutData(
    val payment: Payment,
    val additionalInfo: PrimerCheckoutAdditionalInfo? = null,
)
```

| Property         | Type                            | Description                                                                          |
| ---------------- | ------------------------------- | ------------------------------------------------------------------------------------ |
| `payment`        | `Payment`                       | Payment details (ID and order ID)                                                    |
| `additionalInfo` | `PrimerCheckoutAdditionalInfo?` | Additional info from the payment method (e.g., multi-banking details, redirect info) |

`PrimerCheckoutAdditionalInfo` is a marker interface implemented by payment method-specific info classes.

#### Payment

```kotlin
data class Payment(
    val id: String,
    val orderId: String,
)
```

| Property  | Type     | Description                                  |
| --------- | -------- | -------------------------------------------- |
| `id`      | `String` | Unique payment identifier assigned by Primer |
| `orderId` | `String` | Order identifier from the client session     |

## PrimerCheckoutSheet

Modal bottom sheet composable with built-in navigation between screens (splash, loading, payment methods, card form, success, error). Each screen is a slot parameter you can override.

### Full Composable Signature

```kotlin
@Composable
fun PrimerCheckoutSheet(
    checkout: PrimerCheckoutController,
    modifier: Modifier = Modifier,
    onDismiss: () -> Unit = {},
    onEvent: (PrimerCheckoutEvent) -> Unit = {},
    theme: PrimerTheme = PrimerTheme(),
    splash: @Composable () -> Unit = { PrimerCheckoutSheetDefaults.Splash() },
    loading: @Composable () -> Unit = { PrimerCheckoutSheetDefaults.Loading() },
    paymentMethodSelection: @Composable () -> Unit = {
        PrimerCheckoutSheetDefaults.PaymentMethodSelection(checkout)
    },
    cardForm: @Composable () -> Unit = {
        val controller = rememberCardFormController(checkout)
        PrimerCardForm(controller = controller)
    },
    success: @Composable (PrimerCheckoutData) -> Unit = { data ->
        PrimerCheckoutSheetDefaults.Success(data)
    },
    error: @Composable (PrimerError) -> Unit = { error ->
        PrimerCheckoutSheetDefaults.Error(error)
    },
)
```

### All Parameters with Defaults

| Parameter                | Type                                       | Default                                     | Description                                                              |
| ------------------------ | ------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------------------ |
| `checkout`               | `PrimerCheckoutController`                 | Required                                    | Checkout controller that drives the sheet lifecycle                      |
| `modifier`               | `Modifier`                                 | `Modifier`                                  | Modifier applied to the bottom sheet container                           |
| `onDismiss`              | `() -> Unit`                               | `{}`                                        | Called on swipe down, back press, or auto-dismiss after result screen    |
| `onEvent`                | `(PrimerCheckoutEvent) -> Unit`            | `{}`                                        | Payment outcome events                                                   |
| `theme`                  | `PrimerTheme`                              | `PrimerTheme()`                             | Design tokens applied to all components within the sheet                 |
| `splash`                 | `@Composable () -> Unit`                   | `Defaults.Splash()`                         | Slot displayed briefly when the sheet first opens                        |
| `loading`                | `@Composable () -> Unit`                   | `Defaults.Loading()`                        | Slot displayed during payment processing                                 |
| `paymentMethodSelection` | `@Composable () -> Unit`                   | `Defaults.PaymentMethodSelection(checkout)` | Slot for payment method selection screen                                 |
| `cardForm`               | `@Composable () -> Unit`                   | `PrimerCardForm(...)`                       | Slot for card payment form screen                                        |
| `success`                | `@Composable (PrimerCheckoutData) -> Unit` | `Defaults.Success(data)`                    | Slot after successful payment. Auto-dismisses after 3 seconds.           |
| `error`                  | `@Composable (PrimerError) -> Unit`        | `Defaults.Error(error)`                     | Slot after payment failure. Shows retry and "try other methods" options. |

### Screen Slots

Override any slot to customize that screen while keeping default behavior for others:

```kotlin
PrimerCheckoutSheet(
    checkout = checkout,
    // Custom card form with rearranged fields
    cardForm = {
        val cardFormController = rememberCardFormController(checkout)
        PrimerCardForm(
            controller = cardFormController,
            cardDetails = {
                Column {
                    CardFormDefaults.CardholderField(cardFormController)
                    CardFormDefaults.CardNumberField(cardFormController)
                    Row {
                        CardFormDefaults.ExpiryField(cardFormController, Modifier.weight(1f))
                        Spacer(Modifier.width(8.dp))
                        CardFormDefaults.CvvField(cardFormController, Modifier.weight(1f))
                    }
                }
            },
        )
    },
    // Custom success screen
    success = { data ->
        Column(
            modifier = Modifier.fillMaxWidth().padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Icon(Icons.Default.CheckCircle, contentDescription = null, tint = Color.Green)
            Text("Payment ${data.payment.id} confirmed!")
        }
    },
)
```

### PrimerCheckoutSheetDefaults

Pre-built composable implementations for each screen slot:

```kotlin
object PrimerCheckoutSheetDefaults {
    @Composable fun Splash()
    @Composable fun Loading()
    @Composable fun Success(checkoutData: PrimerCheckoutData)
    @Composable fun Error(error: PrimerError)
    @Composable fun PaymentMethodSelection(
        checkout: PrimerCheckoutController,
        vaultedMethods: @Composable () -> Unit = { VaultedMethods(checkout) },
        paymentMethods: @Composable () -> Unit = { PaymentMethods(checkout) },
    )
    @Composable fun VaultedMethods(checkout: PrimerCheckoutController)
    @Composable fun PaymentMethods(checkout: PrimerCheckoutController)
}
```

| Function                                                           | Description                                                                                                 |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `Splash()`                                                         | Brief splash screen with Primer logo and loading animation                                                  |
| `Loading()`                                                        | Centered circular progress indicator during payment processing                                              |
| `Success(checkoutData)`                                            | Checkmark animation and payment confirmation. Auto-dismisses after 3 seconds.                               |
| `Error(error)`                                                     | Error message with retry and "try other methods" options                                                    |
| `PaymentMethodSelection(checkout, vaultedMethods, paymentMethods)` | Vaulted methods (if any) + available payment methods. Each section is a slot you can override individually. |
| `VaultedMethods(checkout)`                                         | Saved payment methods section only                                                                          |
| `PaymentMethods(checkout)`                                         | Available payment methods section only (excluding vaulted)                                                  |

## PrimerCheckoutHost

Inline host composable for embedding checkout components directly in your layout.

### Full Composable Signature

```kotlin
@Composable
fun PrimerCheckoutHost(
    checkout: PrimerCheckoutController,
    modifier: Modifier = Modifier,
    onEvent: (PrimerCheckoutEvent) -> Unit = {},
    theme: PrimerTheme = PrimerTheme(),
    content: @Composable () -> Unit,
)
```

| Parameter  | Type                            | Default         | Description                                                     |
| ---------- | ------------------------------- | --------------- | --------------------------------------------------------------- |
| `checkout` | `PrimerCheckoutController`      | Required        | Checkout controller providing session state to child components |
| `modifier` | `Modifier`                      | `Modifier`      | Modifier applied to the host container                          |
| `onEvent`  | `(PrimerCheckoutEvent) -> Unit` | `{}`            | Payment outcome events                                          |
| `theme`    | `PrimerTheme`                   | `PrimerTheme()` | Design tokens applied to all SDK components within the host     |
| `content`  | `@Composable () -> Unit`        | Required        | Your custom layout containing SDK components (trailing lambda)  |

### What It Provides

1. **PrimerTheme** -- applies design tokens so child components inherit your styling
2. **CompositionLocal providers** -- injects checkout context so `remember*Controller()` functions work
3. **Overlay management** -- renders overlays for 3DS challenges and redirect flows automatically

Without `PrimerCheckoutHost`, SDK components like `PrimerCardForm` and `PrimerPaymentMethods` will not function.

### When to Use Host vs Sheet

| Feature               | PrimerCheckoutSheet             | PrimerCheckoutHost           |
| --------------------- | ------------------------------- | ---------------------------- |
| Presentation          | Modal bottom sheet              | Inline in your layout        |
| Navigation            | Built-in screen transitions     | You manage navigation        |
| Dismissal             | Swipe, back press, auto-dismiss | You control visibility       |
| Success/error screens | Built-in slots (customizable)   | You build your own           |
| 3DS/redirect flows    | Handled within the sheet        | Overlay sheet appears on top |

## PrimerSettings

Configuration class for SDK behavior. Pass to `rememberPrimerCheckoutController()`.

### Data Class Definition

```kotlin
data class PrimerSettings(
    var paymentHandling: PrimerPaymentHandling = PrimerPaymentHandling.AUTO,
    var locale: Locale = Locale.getDefault(),
    var paymentMethodOptions: PrimerPaymentMethodOptions = PrimerPaymentMethodOptions(),
    var uiOptions: PrimerUIOptions = PrimerUIOptions(),
    var debugOptions: PrimerDebugOptions = PrimerDebugOptions(),
    var clientSessionCachingEnabled: Boolean = false,
    var apiVersion: PrimerApiVersion = PrimerApiVersion.V2_4,
)
```

| Property                      | Type                         | Default                        | Description                                                                           |
| ----------------------------- | ---------------------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
| `paymentHandling`             | `PrimerPaymentHandling`      | `AUTO`                         | Controls whether the SDK processes payments automatically or delegates to your server |
| `locale`                      | `Locale`                     | `Locale.getDefault()`          | Forces the SDK locale for translations and formatting                                 |
| `paymentMethodOptions`        | `PrimerPaymentMethodOptions` | `PrimerPaymentMethodOptions()` | Payment method-specific configuration                                                 |
| `uiOptions`                   | `PrimerUIOptions`            | `PrimerUIOptions()`            | UI behavior settings                                                                  |
| `debugOptions`                | `PrimerDebugOptions`         | `PrimerDebugOptions()`         | Debug and development options                                                         |
| `clientSessionCachingEnabled` | `Boolean`                    | `false`                        | Caches the client session to reduce network requests                                  |
| `apiVersion`                  | `PrimerApiVersion`           | `V2_4`                         | Primer API version used for backend communication                                     |

### PrimerPaymentHandling

```kotlin
enum class PrimerPaymentHandling {
    AUTO,
    MANUAL,
}
```

| Value    | Description                                                                                                                                                                                               |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AUTO`   | SDK processes payment end-to-end. Emits `Success` or `Failure` events.                                                                                                                                    |
| `MANUAL` | SDK tokenizes the payment method but does not create the payment. Your app receives a token via decision handlers, creates the payment on its own backend, and resumes with a new client token if needed. |

### PrimerSessionIntent

```kotlin
enum class PrimerSessionIntent {
    CHECKOUT,  // One-time payment
    VAULT,     // Save payment method for future use
}
```

| Value      | Description                                       |
| ---------- | ------------------------------------------------- |
| `CHECKOUT` | One-time payment flow                             |
| `VAULT`    | Save payment method for future use without paying |

Exposed on `PrimerPaymentMethod.supportedPrimerSessionIntents` to indicate what intents each payment method supports.

### PrimerUIOptions

```kotlin
data class PrimerUIOptions(
    var isInitScreenEnabled: Boolean = true,
    var isSuccessScreenEnabled: Boolean = true,
    var isErrorScreenEnabled: Boolean = true,
    var dismissalMechanism: List<DismissalMechanism> = listOf(DismissalMechanism.GESTURES),
    var theme: PrimerTheme = PrimerTheme(),
    var cardFormUIOptions: PrimerCardFormUIOptions = PrimerCardFormUIOptions(),
)
```

| Property                 | Type                       | Default                     | Description                                           |
| ------------------------ | -------------------------- | --------------------------- | ----------------------------------------------------- |
| `isInitScreenEnabled`    | `Boolean`                  | `true`                      | Shows a loading screen while the checkout initializes |
| `isSuccessScreenEnabled` | `Boolean`                  | `true`                      | Shows a success screen after payment completes        |
| `isErrorScreenEnabled`   | `Boolean`                  | `true`                      | Shows an error screen when a payment fails            |
| `dismissalMechanism`     | `List<DismissalMechanism>` | `[GESTURES]`                | How the user can dismiss the checkout sheet           |
| `theme`                  | `PrimerTheme`              | `PrimerTheme()`             | Visual theme for all SDK components                   |
| `cardFormUIOptions`      | `PrimerCardFormUIOptions`  | `PrimerCardFormUIOptions()` | Card form-specific UI options                         |

#### DismissalMechanism

| Value          | Description                                |
| -------------- | ------------------------------------------ |
| `GESTURES`     | Dismiss by tapping outside or swiping down |
| `CLOSE_BUTTON` | Shows a close button in the sheet header   |

#### PrimerCardFormUIOptions

| Property              | Type      | Default | Description                                                                   |
| --------------------- | --------- | ------- | ----------------------------------------------------------------------------- |
| `payButtonAddNewCard` | `Boolean` | `false` | When `true`, the card form button shows "Add new card" instead of "Pay $X.XX" |

### PrimerPaymentMethodOptions

```kotlin
data class PrimerPaymentMethodOptions(
    var redirectScheme: String? = null,
    var googlePayOptions: PrimerGooglePayOptions = PrimerGooglePayOptions(),
    var klarnaOptions: PrimerKlarnaOptions = PrimerKlarnaOptions(),
    var threeDsOptions: PrimerThreeDsOptions = PrimerThreeDsOptions(),
    var stripeOptions: PrimerStripeOptions = PrimerStripeOptions(),
)
```

| Property           | Type                     | Default                    | Description                                                                                                                       |
| ------------------ | ------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `redirectScheme`   | `String?`                | `null`                     | Deep link scheme for returning from third-party apps (e.g., `"myapp://primer"`). Required for 3DS, PayPal, Klarna redirect flows. |
| `googlePayOptions` | `PrimerGooglePayOptions` | `PrimerGooglePayOptions()` | Google Pay configuration                                                                                                          |
| `klarnaOptions`    | `PrimerKlarnaOptions`    | `PrimerKlarnaOptions()`    | Klarna configuration                                                                                                              |
| `threeDsOptions`   | `PrimerThreeDsOptions`   | `PrimerThreeDsOptions()`   | 3D Secure configuration                                                                                                           |
| `stripeOptions`    | `PrimerStripeOptions`    | `PrimerStripeOptions()`    | Stripe ACH configuration                                                                                                          |

#### PrimerGooglePayOptions

| Property                        | Type                                     | Default | Description                                                  |
| ------------------------------- | ---------------------------------------- | ------- | ------------------------------------------------------------ |
| `merchantName`                  | `String?`                                | `null`  | Merchant name displayed in the Google Pay sheet              |
| `buttonStyle`                   | `GooglePayButtonStyle`                   | --      | Button appearance: `WHITE` or `BLACK`                        |
| `captureBillingAddress`         | `Boolean`                                | `false` | Requests billing address from Google Pay                     |
| `existingPaymentMethodRequired` | `Boolean`                                | `false` | Only shows Google Pay if the user has a saved payment method |
| `shippingAddressParameters`     | `PrimerGoogleShippingAddressParameters?` | `null`  | Shipping address requirements (has `phoneNumberRequired`)    |
| `requireShippingMethod`         | `Boolean`                                | `false` | Requires a shipping method selection                         |
| `emailAddressRequired`          | `Boolean`                                | `false` | Requests email from Google Pay                               |
| `buttonOptions`                 | `GooglePayButtonOptions`                 | --      | Button customization (`buttonTheme`, `buttonType`)           |

#### PrimerKlarnaOptions

| Property                      | Type      | Default | Description                                     |
| ----------------------------- | --------- | ------- | ----------------------------------------------- |
| `recurringPaymentDescription` | `String?` | `null`  | Description shown for recurring Klarna payments |
| `returnIntentUrl`             | `String?` | `null`  | Return URL after Klarna authorization           |

#### PrimerThreeDsOptions

| Property                 | Type      | Default | Description                               |
| ------------------------ | --------- | ------- | ----------------------------------------- |
| `threeDsAppRequestorUrl` | `String?` | `null`  | App requestor URL for 3DS2 authentication |

#### PrimerStripeOptions

| Property         | Type           | Default | Description                                                                                                                                                               |
| ---------------- | -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mandateData`    | `MandateData?` | `null`  | ACH mandate text. Use `TemplateMandateData(merchantName)` for standard template, or `FullMandateStringData(value)` / `FullMandateData(@StringRes value)` for custom text. |
| `publishableKey` | `String?`      | `null`  | Stripe publishable key                                                                                                                                                    |

### PrimerDebugOptions

| Property                  | Type      | Default | Description                                                          |
| ------------------------- | --------- | ------- | -------------------------------------------------------------------- |
| `is3DSSanityCheckEnabled` | `Boolean` | `true`  | Enables 3DS security sanity checks. Disable only during development. |

### PrimerApiVersion

| Value  | Description                     |
| ------ | ------------------------------- |
| `V2_4` | Primer API version 2.4 (latest) |

### Supported Locales

The SDK ships translations for 57 locales (56 + default English):

ar, az, bg, bs, ca, cs, da, de, el, en (default), es, es-AR, es-MX, et, fa, fi, fil, fr, hi, hr, hu, hy, in (Indonesian), it, iw (Hebrew), ja, ka, kk, ko, ku, ky, lt, lv, mk, ms, nb, nl, nl-BE, pl, pt, pt-BR, ro, ru, sk, sl, sq, sr-Latn, sv, th, tr, uk, ur-PK, uz, vi, zh-CN, zh-HK, zh-TW

RTL languages supported: Arabic (ar), Hebrew (iw), Persian (fa), Urdu (ur-PK).

Set via `PrimerSettings.locale`:

```kotlin
val settings = PrimerSettings(locale = Locale("de")) // German
```

### Settings Usage Example

```kotlin
val settings = PrimerSettings(
    locale = Locale.GERMANY,
    uiOptions = PrimerUIOptions(
        isInitScreenEnabled = false,
        dismissalMechanism = listOf(DismissalMechanism.CLOSE_BUTTON),
    ),
    paymentMethodOptions = PrimerPaymentMethodOptions(
        redirectScheme = "myapp://primer",
        googlePayOptions = PrimerGooglePayOptions(
            merchantName = "My Store",
            captureBillingAddress = true,
        ),
    ),
)

val checkout = rememberPrimerCheckoutController(
    clientToken = clientToken,
    settings = settings,
)
```

## Card Form

### PrimerCardForm Composable

Composable that renders a card payment form with card details, optional billing address, and submit button. Each section is a slot parameter.

```kotlin
@Composable
fun PrimerCardForm(
    controller: PrimerCardFormController,
    modifier: Modifier = Modifier,
    cardDetails: @Composable () -> Unit = { CardFormDefaults.CardDetailsContent(controller) },
    billingAddress: @Composable () -> Unit = { CardFormDefaults.BillingAddressContent(controller) },
    submitButton: @Composable () -> Unit = { CardFormDefaults.SubmitButton(controller) },
)
```

| Parameter        | Type                       | Default                                              | Description                                                              |
| ---------------- | -------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ |
| `controller`     | `PrimerCardFormController` | Required                                             | Controller managing form state, validation, and submission               |
| `modifier`       | `Modifier`                 | `Modifier`                                           | Modifier applied to the root container                                   |
| `cardDetails`    | `@Composable () -> Unit`   | `CardFormDefaults.CardDetailsContent(controller)`    | Slot for card input fields (number, expiry, CVV, cardholder name)        |
| `billingAddress` | `@Composable () -> Unit`   | `CardFormDefaults.BillingAddressContent(controller)` | Slot for billing address fields. Only rendered when required by session. |
| `submitButton`   | `@Composable () -> Unit`   | `CardFormDefaults.SubmitButton(controller)`          | Slot for the submit/pay button                                           |

Default layout order: CardNumberField, ExpiryField + CvvField (side by side), CardholderField, billing address fields, SubmitButton.

### PrimerCardFormController

Controller interface for the card payment form.

#### Creation

```kotlin
@Composable
fun rememberCardFormController(
    checkout: PrimerCheckoutController,
): PrimerCardFormController
```

Must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot.

#### Field Update Methods

| Method                                | Parameter        | Description                                      |
| ------------------------------------- | ---------------- | ------------------------------------------------ |
| `updateCardNumber(value: String)`     | Raw card number  | Updates card number. Triggers network detection. |
| `updateCvv(value: String)`            | CVV digits       | Updates CVV field                                |
| `updateExpiryDate(value: String)`     | Raw expiry input | Updates expiry date. Auto-formats to MM/YY.      |
| `updateCardholderName(value: String)` | Full name        | Updates cardholder name                          |
| `updatePostalCode(value: String)`     | Postal/ZIP code  | Updates billing postal code                      |
| `updateCountryCode(value: String)`    | ISO country code | Updates billing country code                     |
| `updateCity(value: String)`           | City name        | Updates billing city                             |
| `updateState(value: String)`          | State/region     | Updates billing state                            |
| `updateAddressLine1(value: String)`   | Street address   | Updates billing address line 1                   |
| `updateAddressLine2(value: String)`   | Apt, suite, etc. | Updates billing address line 2                   |
| `updatePhoneNumber(value: String)`    | Phone number     | Updates billing phone number                     |
| `updateFirstName(value: String)`      | First name       | Updates billing first name                       |
| `updateLastName(value: String)`       | Last name        | Updates billing last name                        |

#### Action Methods

| Method                                                                | Description                                                                                                 |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `submit()`                                                            | Submits the form for tokenization and payment. Sets `state.isLoading` to `true` during submission.          |
| `selectCardNetwork(network: PrimerCardNetwork)`                       | Selects a network for co-badged cards. Only applicable when `state.networkSelection` has multiple networks. |
| `onFieldFocusChange(type: PrimerInputElementType, hasFocus: Boolean)` | Notifies controller of field focus changes. Triggers on-blur validation.                                    |
| `requestCountrySelection()`                                           | Opens the country selection picker UI                                                                       |
| `setVaultOnSuccess(vault: Boolean)`                                   | Configures whether to save the payment method to vault after success. Requires `customerId` in session.     |

### Controller State Data Class

```kotlin
data class State(
    val cardFields: List<PrimerInputElementType>,
    val billingFields: List<PrimerInputElementType>,
    val fieldErrors: List<SyncValidationError>?,
    val data: Map<PrimerInputElementType, String>,
    val isLoading: Boolean,
    val isFormEnabled: Boolean,
    val selectedCountry: PrimerCountry?,
    val networkSelection: NetworkSelection?,
    val fieldFocusStates: Map<PrimerInputElementType, FieldState>,
    val isFormValid: Boolean,
    val vaultOnSuccess: Boolean,
)
```

| Property           | Type                                      | Description                                                            |
| ------------------ | ----------------------------------------- | ---------------------------------------------------------------------- |
| `cardFields`       | `List<PrimerInputElementType>`            | Card input fields required by the session                              |
| `billingFields`    | `List<PrimerInputElementType>`            | Billing address fields required by the session. Empty if not required. |
| `fieldErrors`      | `List<SyncValidationError>?`              | Validation errors per field. `null` before first validation.           |
| `data`             | `Map<PrimerInputElementType, String>`     | Current field values keyed by input element type                       |
| `isLoading`        | `Boolean`                                 | `true` while submitting a payment                                      |
| `isFormEnabled`    | `Boolean`                                 | `true` when the form accepts input. `false` during submission.         |
| `selectedCountry`  | `PrimerCountry?`                          | Selected billing country, or `null`                                    |
| `networkSelection` | `NetworkSelection?`                       | Co-badge network selection data. `null` when card is not co-badged.    |
| `fieldFocusStates` | `Map<PrimerInputElementType, FieldState>` | Focus state per field (see FieldState below)                           |
| `isFormValid`      | `Boolean`                                 | `true` when all required fields pass validation. Updates in real-time. |
| `vaultOnSuccess`   | `Boolean`                                 | Whether payment method will be saved to vault on success               |

#### NetworkSelection

```kotlin
data class NetworkSelection(
    val selectedNetwork: PrimerCardNetwork?,
    val availableNetworks: List<PrimerCardNetwork>,
    val isNetworkSelectable: Boolean,
    val isUserSelected: Boolean,
)
```

#### FieldState

```kotlin
data class FieldState(
    val hasFocus: Boolean,
    val hasBeenFocused: Boolean,
    val shouldShowError: Boolean,
)
```

| Property          | Type      | Description                                           |
| ----------------- | --------- | ----------------------------------------------------- |
| `hasFocus`        | `Boolean` | Whether the field currently has focus                 |
| `hasBeenFocused`  | `Boolean` | Whether the field has ever received focus             |
| `shouldShowError` | `Boolean` | Whether the field should display its validation error |

### CardFormDefaults

Object providing pre-built composable functions for each part of the card form.

#### Card Detail Fields

All field functions accept `controller: PrimerCardFormController` and `modifier: Modifier = Modifier` (except `CardNetworkField` which has no modifier parameter).

| Function                                | Description                                                                          |
| --------------------------------------- | ------------------------------------------------------------------------------------ |
| `CardNumberField(controller, modifier)` | Card number input with auto-formatting (spaces), Luhn validation, and network icon   |
| `ExpiryField(controller, modifier)`     | Expiry date input with MM/YY auto-formatting                                         |
| `CvvField(controller, modifier)`        | CVV input with dynamic length based on card network, password masking                |
| `CardholderField(controller, modifier)` | Cardholder name input. Only rendered when required by session.                       |
| `CardNetworkField(controller)`          | Co-badge network selector. Only rendered for co-badged cards. No modifier parameter. |

#### Billing Address Fields

Only rendered when required by session configuration.

| Function                                  | Description                                 |
| ----------------------------------------- | ------------------------------------------- |
| `CountryCodeField(controller, modifier)`  | Country selector that opens a picker dialog |
| `FirstNameField(controller, modifier)`    | Billing first name input                    |
| `LastNameField(controller, modifier)`     | Billing last name input                     |
| `AddressLine1Field(controller, modifier)` | Street address line 1                       |
| `AddressLine2Field(controller, modifier)` | Street address line 2 (optional)            |
| `CityField(controller, modifier)`         | City name input                             |
| `StateField(controller, modifier)`        | State or region input                       |
| `PostalCodeField(controller, modifier)`   | Postal or ZIP code input                    |

### Content Composables

#### CardDetailsContent

```kotlin
@Composable
fun CardFormDefaults.CardDetailsContent(
    cardFormState: PrimerCardFormController,
    cardNumber: @Composable () -> Unit = { CardNumberField(cardFormState) },
    expiryDate: @Composable () -> Unit = { ExpiryField(cardFormState) },
    cvv: @Composable () -> Unit = { CvvField(cardFormState) },
    cardholderName: @Composable () -> Unit = { CardholderField(cardFormState) },
)
```

Renders the default card details layout. Each field can be individually replaced via slot parameters.

#### BillingAddressContent

```kotlin
@Composable
fun CardFormDefaults.BillingAddressContent(
    cardFormState: PrimerCardFormController,
    countryCode: @Composable () -> Unit = { CountryCodeField(cardFormState) },
    firstName: @Composable () -> Unit = { FirstNameField(cardFormState) },
    lastName: @Composable () -> Unit = { LastNameField(cardFormState) },
    addressLine1: @Composable () -> Unit = { AddressLine1Field(cardFormState) },
    addressLine2: @Composable () -> Unit = { AddressLine2Field(cardFormState) },
    city: @Composable () -> Unit = { CityField(cardFormState) },
    stateField: @Composable () -> Unit = { StateField(cardFormState) },
    postalCode: @Composable () -> Unit = { PostalCodeField(cardFormState) },
)
```

Renders the default billing address layout. Only renders fields required by the session.

#### SubmitButton

```kotlin
@Composable
fun CardFormDefaults.SubmitButton(
    controller: PrimerCardFormController,
    modifier: Modifier = Modifier,
)
```

Default pay button that displays the formatted amount, disables when invalid or submitting, and shows a loading indicator during submission.

### Card Field Components Detail

#### CardNumberField

| Aspect             | Detail                                                                                  |
| ------------------ | --------------------------------------------------------------------------------------- |
| Auto-formatting    | Groups digits with spaces (e.g., `4242 4242 4242 4242`)                                 |
| Max length         | 16-19 digits depending on detected network                                              |
| Keyboard type      | `KeyboardType.Number`                                                                   |
| Validation         | Luhn algorithm check, minimum length per network                                        |
| Validation timing  | On blur (when user taps another field)                                                  |
| Trailing icon      | Detected card network logo(s)                                                           |
| Network detection  | Local detection begins after 1st digit; remote detection after 8th digit                |
| Supported networks | Visa, Mastercard, Amex, Discover, Diners Club, JCB, UnionPay, Maestro, Cartes Bancaires |

#### ExpiryField

| Aspect          | Detail                                                    |
| --------------- | --------------------------------------------------------- |
| Auto-formatting | Inserts `/` automatically (typing `1225` becomes `12/25`) |
| Max length      | 5 characters (MM/YY)                                      |
| Keyboard        | `KeyboardType.Number`                                     |
| Validation      | Valid month (01-12), future date                          |

#### CvvField

| Aspect     | Detail                                           |
| ---------- | ------------------------------------------------ |
| Max length | 4 digits for Amex, 3 for other networks          |
| Keyboard   | `KeyboardType.Number`                            |
| Visual     | Password masking (dots)                          |
| Dynamic    | Max length updates when card network is detected |

#### CardholderField

| Aspect     | Detail                                                        |
| ---------- | ------------------------------------------------------------- |
| Keyboard   | `KeyboardType.Text` with `KeyboardCapitalization.Words`       |
| Validation | Required (non-empty) when session requires cardholder name    |
| Visibility | Only rendered when `CARDHOLDER_NAME` is in `state.cardFields` |

#### CardNetworkField

| Aspect                | Detail                                                       |
| --------------------- | ------------------------------------------------------------ |
| Visibility            | Only rendered when card has multiple networks (co-badged)    |
| Interaction           | Tapping a chip calls `controller.selectCardNetwork(network)` |
| No modifier parameter | Unlike other fields                                          |

#### Billing Address Field Summary

| Field               | Keyboard             | Validation                         | Notes                                                   |
| ------------------- | -------------------- | ---------------------------------- | ------------------------------------------------------- |
| `CountryCodeField`  | --                   | Required                           | Read-only; opens picker via `requestCountrySelection()` |
| `FirstNameField`    | `Text` + `Words` cap | Required                           | --                                                      |
| `LastNameField`     | `Text` + `Words` cap | Required                           | --                                                      |
| `AddressLine1Field` | `Text`               | Required                           | --                                                      |
| `AddressLine2Field` | `Text`               | Optional                           | --                                                      |
| `CityField`         | `Text`               | Required                           | --                                                      |
| `StateField`        | `Text`               | Required                           | Label adapts to selected country                        |
| `PostalCodeField`   | `Text`               | Required, format varies by country | Label adapts to selected country                        |

### Validation Behavior

1. **While typing** -- no error messages shown. `isFormValid` updates in real-time.
2. **On blur** -- field is validated and any error is added to `state.fieldErrors`.
3. **Error display** -- inline error text below the field using theme error color.
4. **Error clearing** -- errors clear when corrected and re-validated on next blur.

Use `isFormValid` for submit button state. Use `fieldErrors` for error messages.

## Payment Methods

### PrimerPaymentMethods Composable

```kotlin
@Composable
fun PrimerPaymentMethods(
    controller: PrimerPaymentMethodsController,
    modifier: Modifier = Modifier,
    header: @Composable () -> Unit = { PaymentMethodsDefaults.SectionHeader() },
    method: @Composable (PrimerPaymentMethod, () -> Unit) -> Unit = { paymentMethod, onClick ->
        PaymentMethodsDefaults.Method(paymentMethod, onClick)
    },
)
```

| Parameter    | Type                                                    | Default                                  | Description                                                                   |
| ------------ | ------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------- |
| `controller` | `PrimerPaymentMethodsController`                        | Required                                 | Controller providing available payment methods                                |
| `modifier`   | `Modifier`                                              | `Modifier`                               | Modifier applied to the root container                                        |
| `header`     | `@Composable () -> Unit`                                | `PaymentMethodsDefaults.SectionHeader()` | Slot for section header above the list                                        |
| `method`     | `@Composable (PrimerPaymentMethod, () -> Unit) -> Unit` | `PaymentMethodsDefaults.Method(...)`     | Slot for each payment method item. Receives method data and onClick callback. |

Default behavior: renders a "Pay with" header, a vertical list of methods with icon, name, and optional surcharge, groups methods by surcharge amount. Shows `PaymentMethodsDefaults.EmptyState()` when no methods are available.

### PrimerPaymentMethodsController

```kotlin
@Composable
fun rememberPaymentMethodsController(
    checkout: PrimerCheckoutController,
): PrimerPaymentMethodsController
```

Must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot.

| Member           | Type                                   | Description                                                                                   |
| ---------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- |
| `methods`        | `StateFlow<List<PrimerPaymentMethod>>` | Available payment methods for the current session. Updates on `checkout.refresh()`.           |
| `select(method)` | Method                                 | Selects a payment method to begin its payment flow (card form, redirect, native wallet, etc.) |

### PrimerPaymentMethod Data Class

```kotlin
data class PrimerPaymentMethod(
    val paymentMethodType: String,
    val paymentMethodName: String?,
    val supportedPrimerSessionIntents: List<PrimerSessionIntent>,
    val paymentMethodManagerCategories: List<PrimerPaymentMethodManagerCategory>,
    val surcharge: Surcharge? = null,
)
```

| Property                         | Type                                       | Description                                                                                       |
| -------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `paymentMethodType`              | `String`                                   | Payment method identifier (e.g., `"PAYMENT_CARD"`, `"PAYPAL"`, `"GOOGLE_PAY"`)                    |
| `paymentMethodName`              | `String?`                                  | Human-readable display name                                                                       |
| `supportedPrimerSessionIntents`  | `List<PrimerSessionIntent>`                | Intents this method supports (`CHECKOUT`, `VAULT`, or both)                                       |
| `paymentMethodManagerCategories` | `List<PrimerPaymentMethodManagerCategory>` | Categories: `NATIVE_UI`, `RAW_DATA`, `KLARNA`, `STRIPE_ACH`, `COMPONENT_WITH_REDIRECT`, `NOL_PAY` |
| `surcharge`                      | `Surcharge?`                               | Surcharge amount, or `null` if none                                                               |

#### Surcharge

```kotlin
sealed interface Surcharge {
    data class PaymentMethodSurcharge(val amount: Int) : Surcharge
    data class CardNetworksSurcharge(val surcharges: Map<String, Int>) : Surcharge
}
```

Common payment method types: `"PAYMENT_CARD"`, `"PAYPAL"`, `"GOOGLE_PAY"`, `"KLARNA"`, `"IDEAL"`, `"BANCONTACT"`, `"SOFORT"`.

### PrimerVaultedPaymentMethodsController

Controller for saved payment methods.

```kotlin
@Composable
fun rememberVaultedPaymentMethodsController(
    checkout: PrimerCheckoutController,
): PrimerVaultedPaymentMethodsController
```

Must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot.

| Member           | Type                                          | Description                                                                             |
| ---------------- | --------------------------------------------- | --------------------------------------------------------------------------------------- |
| `methods`        | `StateFlow<List<PrimerVaultedPaymentMethod>>` | Saved payment methods for the current customer. Empty while loading or when none exist. |
| `select(method)` | Method                                        | Pay with a saved method. Automatically shows CVV recapture if required.                 |
| `delete(method)` | Method                                        | Delete a saved method. Shows a confirmation dialog before deletion.                     |
| `showAll()`      | Method                                        | Open the full vaulted methods management screen.                                        |

#### PrimerVaultedPaymentMethod

```kotlin
data class PrimerVaultedPaymentMethod(
    val id: String,
    val analyticsId: String,
    val paymentInstrumentType: String,
    val paymentMethodType: String,
    val paymentInstrumentData: PaymentInstrumentData?,
    val threeDSecureAuthentication: ThreeDSecureAuthentication?,
)
```

| Property                     | Type                          | Description                               |
| ---------------------------- | ----------------------------- | ----------------------------------------- |
| `id`                         | `String`                      | Unique identifier for this vaulted method |
| `analyticsId`                | `String`                      | Analytics tracking identifier             |
| `paymentInstrumentType`      | `String`                      | Instrument type (e.g., `"PAYMENT_CARD"`)  |
| `paymentMethodType`          | `String`                      | Payment method type identifier            |
| `paymentInstrumentData`      | `PaymentInstrumentData?`      | Card or payment method details            |
| `threeDSecureAuthentication` | `ThreeDSecureAuthentication?` | 3DS authentication data, if available     |

#### PaymentInstrumentData

| Property                   | Type                 | Description                                           |
| -------------------------- | -------------------- | ----------------------------------------------------- |
| `network`                  | `String?`            | Card network (e.g., `"Visa"`, `"Mastercard"`)         |
| `cardholderName`           | `String?`            | Cardholder name                                       |
| `first6Digits`             | `Int?`               | First 6 digits of card (BIN)                          |
| `last4Digits`              | `Int?`               | Last 4 digits of card for display                     |
| `accountNumberLast4Digits` | `Int?`               | Last 4 digits of account number (bank methods)        |
| `expirationMonth`          | `Int?`               | Expiry month                                          |
| `expirationYear`           | `Int?`               | Expiry year                                           |
| `externalPayerInfo`        | `ExternalPayerInfo?` | External payer details (e.g., PayPal email)           |
| `klarnaCustomerToken`      | `String?`            | Klarna-specific customer token                        |
| `sessionData`              | `SessionData?`       | Session data (recurring description, billing address) |
| `paymentMethodType`        | `String?`            | Type of payment method                                |
| `sessionInfo`              | `SessionInfo?`       | Session info (locale, platform, retail outlet)        |
| `binData`                  | `BinData?`           | BIN data for card routing                             |
| `bankName`                 | `String?`            | Bank name for bank-based methods                      |

#### PrimerVaultedPaymentMethods Composable

```kotlin
@Composable
fun PrimerVaultedPaymentMethods(
    controller: PrimerVaultedPaymentMethodsController,
    modifier: Modifier = Modifier,
    header: @Composable (onShowAll: () -> Unit) -> Unit = { onShowAll ->
        VaultedPaymentMethodsDefaults.SectionHeader(onShowAll = onShowAll)
    },
    item: @Composable (
        method: PrimerVaultedPaymentMethod,
        isSelected: Boolean,
        onSelect: () -> Unit,
    ) -> Unit = { method, isSelected, onSelect ->
        VaultedPaymentMethodsDefaults.Method(method, isSelected, onSelect)
    },
    submitButton: @Composable (
        isLoading: Boolean,
        enabled: Boolean,
        onSubmit: () -> Unit,
    ) -> Unit = { isLoading, enabled, onSubmit ->
        // Default submit button
    },
)
```

#### VaultedPaymentMethodsDefaults

| Function                               | Description                                                             |
| -------------------------------------- | ----------------------------------------------------------------------- |
| `SectionHeader(onShowAll: () -> Unit)` | Default section header ("Saved payment methods") with "Show all" action |
| `Method(method, isSelected, onSelect)` | Default method row with card details, network icon, and selection state |

### PaymentMethodsDefaults

```kotlin
object PaymentMethodsDefaults {
    @Composable fun SectionHeader()
    @Composable fun Method(method: PrimerPaymentMethod, onClick: () -> Unit)
    @Composable fun EmptyState(modifier: Modifier = Modifier)
}
```

| Function                  | Description                                                          |
| ------------------------- | -------------------------------------------------------------------- |
| `SectionHeader()`         | Default "Pay with" header styled with the current theme              |
| `Method(method, onClick)` | Payment method item with icon, name, optional surcharge, and chevron |
| `EmptyState(modifier)`    | Message displayed when no payment methods are available              |

## Klarna Controller

For Klarna payments, the SDK provides a dedicated controller with a state machine that walks the user through category selection and payment authorization.

### PrimerKlarnaController

```kotlin
@Stable
interface PrimerKlarnaController {
    val state: StateFlow<State>
    fun selectPaymentCategory(context: Context, categoryId: String)
    fun submitPayment()

    data class Category(
        val id: String,
        val name: String,
        val url: String,
    )

    data class State(
        val step: Step,
        val categories: List<Category>,
        val selectedCategoryId: String?,
        val paymentView: WeakReference<View>?,
    )

    sealed interface Step {
        data object Loading : Step
        data object CategorySelection : Step
        data object ViewReady : Step
        data object AuthorizationStarted : Step
    }
}
```

#### Creation

```kotlin
@Composable
fun rememberKlarnaController(
    checkout: PrimerCheckoutController,
): PrimerKlarnaController
```

Must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot.

#### State Machine Flow

`Loading` → `CategorySelection` → (user selects category) → `ViewReady` → (user submits) → `AuthorizationStarted`

| Step                   | Description                                   | Action Available                     |
| ---------------------- | --------------------------------------------- | ------------------------------------ |
| `Loading`              | Loading Klarna session data                   | None -- show loading indicator       |
| `CategorySelection`    | Categories available for selection            | `selectPaymentCategory(context, id)` |
| `ViewReady`            | Klarna's native payment view is ready         | `submitPayment()`                    |
| `AuthorizationStarted` | Authorization in progress, waiting for result | None -- show loading indicator       |

#### Key Properties

| Member                    | Type               | Description                                    |
| ------------------------- | ------------------ | ---------------------------------------------- |
| `state`                   | `StateFlow<State>` | Observable Klarna payment flow state           |
| `selectPaymentCategory()` | Method             | Select a category. Requires Android `Context`. |
| `submitPayment()`         | Method             | Submit payment after `ViewReady` state.        |

#### State Properties

| Property             | Type                   | Description                                       |
| -------------------- | ---------------------- | ------------------------------------------------- |
| `step`               | `Step`                 | Current step in the Klarna flow                   |
| `categories`         | `List<Category>`       | Available payment categories                      |
| `selectedCategoryId` | `String?`              | Currently selected category ID                    |
| `paymentView`        | `WeakReference<View>?` | Klarna's native Android View for the payment form |

The `paymentView` is a `WeakReference<View>` -- use `AndroidView` composable to embed it in Compose:

```kotlin
val klarnaState by klarna.state.collectAsStateWithLifecycle()
klarnaState.paymentView?.get()?.let { view ->
    AndroidView(factory = { view })
}
```

**Requirements:** Klarna requires `lineItems` in the client session. Configure `PrimerKlarnaOptions` in settings for `returnIntentUrl` and optional `recurringPaymentDescription`.

## QR Code Controller

For payment methods that generate QR codes (e.g., PromptPay, PayNow), the SDK provides a QR code controller.

### PrimerQrCodeController

```kotlin
@Stable
interface PrimerQrCodeController {
    val state: StateFlow<State>

    sealed class State {
        data object Loading : State()
        data class Ready(
            val qrCodeBase64: String,
            val qrCodeUrl: String? = null,
            val expiresAt: String? = null,
        ) : State()
    }
}
```

#### Creation

```kotlin
@Composable
fun rememberQrCodeController(
    checkout: PrimerCheckoutController,
    paymentMethodType: String,
): PrimerQrCodeController
```

Must be called inside `PrimerCheckoutHost` content or a `PrimerCheckoutSheet` slot.

| Parameter           | Type                       | Description                                                 |
| ------------------- | -------------------------- | ----------------------------------------------------------- |
| `checkout`          | `PrimerCheckoutController` | Parent checkout controller                                  |
| `paymentMethodType` | `String`                   | Payment method type (e.g., `"XFERS_PAYNOW"`, `"PROMPTPAY"`) |

#### State

| State     | Properties                                 | Description                |
| --------- | ------------------------------------------ | -------------------------- |
| `Loading` | None                                       | QR code is being generated |
| `Ready`   | `qrCodeBase64`, `qrCodeUrl?`, `expiresAt?` | QR code ready for display  |

- `qrCodeBase64`: Base64-encoded QR code image. Decode and display as a bitmap.
- `qrCodeUrl`: Direct URL to the QR code image (when available).
- `expiresAt`: ISO 8601 expiration timestamp for the QR code. Display a countdown or refresh when expired.

## Payment Method Flows

When `controller.select(method)` is called on a `PrimerPaymentMethodsController`, different payment methods trigger different UI flows:

| Method Type                     | Flow                             | Controller / UI            | Required Config                    |
| ------------------------------- | -------------------------------- | -------------------------- | ---------------------------------- |
| `PAYMENT_CARD`                  | Card form                        | `PrimerCardFormController` | None                               |
| `GOOGLE_PAY`                    | Native Google Pay sheet          | Automatic                  | `PrimerGooglePayOptions`           |
| `PAYPAL`                        | Browser / Custom Tab redirect    | Automatic                  | `redirectScheme`                   |
| `KLARNA`                        | Category selection → Klarna view | `PrimerKlarnaController`   | `PrimerKlarnaOptions`, `lineItems` |
| `STRIPE_ACH`                    | Stripe account collection        | Automatic                  | `PrimerStripeOptions`              |
| iDEAL, Bancontact, Sofort, etc. | Browser / Custom Tab redirect    | Automatic                  | `redirectScheme`                   |
| PromptPay, PayNow, etc.         | QR code display                  | `PrimerQrCodeController`   | None                               |

**In `PrimerCheckoutSheet`:** Navigation between screens (card form, redirect overlay, etc.) is handled automatically by the sheet.

**In `PrimerCheckoutHost`:** Overlays appear automatically for redirects and 3DS challenges. For Klarna and QR code methods, you create the dedicated controller and show the UI manually.

## Theming

### PrimerTheme

Root theme container holding all design token groups.

```kotlin
data class PrimerTheme(
    val lightColorTokens: LightColorTokens = LightColorTokens(),
    val darkColorTokens: DarkColorTokens = DarkColorTokens(),
    val borderWidthTokens: BorderWidthTokens = BorderWidthTokens(),
    val radiusTokens: RadiusTokens = RadiusTokens(),
    val sizeTokens: SizeTokens = SizeTokens(),
    val spacingTokens: SpacingTokens = SpacingTokens(),
    val typographyTokens: TypographyTokens = TypographyTokens(),
)
```

| Property            | Type                | Description                                             |
| ------------------- | ------------------- | ------------------------------------------------------- |
| `lightColorTokens`  | `LightColorTokens`  | Color tokens for light mode                             |
| `darkColorTokens`   | `DarkColorTokens`   | Color tokens for dark mode. Extends `LightColorTokens`. |
| `borderWidthTokens` | `BorderWidthTokens` | Border widths for inputs, focus rings, dividers         |
| `radiusTokens`      | `RadiusTokens`      | Corner radius for cards, inputs, bottom sheets          |
| `sizeTokens`        | `SizeTokens`        | Sizes for icons and touch targets                       |
| `spacingTokens`     | `SpacingTokens`     | Padding and margin values                               |
| `typographyTokens`  | `TypographyTokens`  | Font sizes, weights, and line heights                   |

### Token Groups

#### LightColorTokens and DarkColorTokens

16 base tokens + 26 semantic tokens. `DarkColorTokens` extends `LightColorTokens` and overrides 15 base tokens. Override base tokens to change foundational colors -- all semantic tokens resolve through them.

**Base tokens (16):**

| Token                                 | Light       | Dark        | Purpose                         |
| ------------------------------------- | ----------- | ----------- | ------------------------------- |
| `primerColorBrand`                    | `#2F98FF`   | `#2F98FF`   | Primary brand color             |
| `primerColorGray000`                  | `#FFFFFF`   | `#171619`   | Lightest gray (background base) |
| `primerColorGray100`                  | `#F5F5F5`   | `#292929`   | Very light gray                 |
| `primerColorGray200`                  | `#EEEEEE`   | `#424242`   | Light gray                      |
| `primerColorGray300`                  | `#E0E0E0`   | `#575757`   | Medium-light gray               |
| `primerColorGray400`                  | `#BDBDBD`   | `#858585`   | Medium gray                     |
| `primerColorGray500`                  | `#9E9E9E`   | `#767577`   | Gray                            |
| `primerColorGray600`                  | `#757575`   | `#C7C7C7`   | Medium-dark gray                |
| `primerColorGray900`                  | `#212121`   | `#EFEFEF`   | Darkest gray (text base)        |
| `primerColorRed100`                   | `#FFECEC`   | `#321C20`   | Error background                |
| `primerColorRed500`                   | `#FF7279`   | `#E46D70`   | Error accent                    |
| `primerColorRed900`                   | `#B4324B`   | `#F6BFBF`   | Error text                      |
| `primerColorGreen500`                 | `#3EB68F`   | `#27B17D`   | Success accent                  |
| `primerColorBlue500`                  | `#399DFF`   | `#3F93E4`   | Info accent                     |
| `primerColorBlue900`                  | `#2270F4`   | `#4AAEFF`   | Link color                      |
| `primerColorBorderTransparentDefault` | transparent | transparent | Transparent border base         |

**Key semantic tokens:**

| Token                               | Resolves to           | Purpose               |
| ----------------------------------- | --------------------- | --------------------- |
| `primerColorTextPrimary`            | `primerColorGray900`  | Main text             |
| `primerColorTextSecondary`          | `primerColorGray600`  | Labels, descriptions  |
| `primerColorTextPlaceholder`        | `primerColorGray500`  | Input placeholders    |
| `primerColorTextDisabled`           | `primerColorGray400`  | Disabled text         |
| `primerColorTextNegative`           | `primerColorRed900`   | Error messages        |
| `primerColorTextLink`               | `primerColorBlue900`  | Clickable links       |
| `primerColorBackground`             | `primerColorGray000`  | Primary background    |
| `primerColorBorderOutlinedDefault`  | `primerColorGray300`  | Default input border  |
| `primerColorBorderOutlinedHover`    | `primerColorGray400`  | Hovered input border  |
| `primerColorBorderOutlinedActive`   | `primerColorGray500`  | Active input border   |
| `primerColorBorderOutlinedFocus`    | `primerColorFocus`    | Focused input border  |
| `primerColorBorderOutlinedDisabled` | `primerColorGray200`  | Disabled input border |
| `primerColorBorderOutlinedError`    | `primerColorRed500`   | Error input border    |
| `primerColorBorderOutlinedLoading`  | `primerColorGray200`  | Loading input border  |
| `primerColorBorderOutlinedSelected` | `primerColorBrand`    | Selected item border  |
| `primerColorIconPrimary`            | `primerColorGray900`  | Default icon          |
| `primerColorIconDisabled`           | `primerColorGray400`  | Disabled icon         |
| `primerColorIconNegative`           | `primerColorRed500`   | Error icon            |
| `primerColorIconPositive`           | `primerColorGreen500` | Success icon          |
| `primerColorFocus`                  | `primerColorBrand`    | Focus ring            |
| `primerColorLoader`                 | `primerColorBrand`    | Loading indicator     |

Override base tokens by extending `LightColorTokens` or `DarkColorTokens`:

```kotlin
val theme = PrimerTheme(
    lightColorTokens = object : LightColorTokens() {
        override val primerColorBrand: Color = Color(0xFF6C5CE7)
    },
    darkColorTokens = object : DarkColorTokens() {
        override val primerColorBrand: Color = Color(0xFFA29BFE)
    },
)
```

#### SpacingTokens

```kotlin
data class SpacingTokens(
    val xxsmall: Dp = 2.dp,
    val xsmall: Dp = 4.dp,
    val small: Dp = 8.dp,
    val medium: Dp = 12.dp,
    val large: Dp = 16.dp,
    val xlarge: Dp = 20.dp,
    val base: Dp = 4.dp,
)
```

| Token     | Default | Usage                         |
| --------- | ------- | ----------------------------- |
| `xxsmall` | 2.dp    | Icon padding, minimal gaps    |
| `xsmall`  | 4.dp    | Chip padding, tight spacing   |
| `small`   | 8.dp    | Between form fields           |
| `medium`  | 12.dp   | Section internal padding      |
| `large`   | 16.dp   | Container padding, major gaps |
| `xlarge`  | 20.dp   | Between major sections        |
| `base`    | 4.dp    | Base unit for calculations    |

#### TypographyTokens

```kotlin
data class TypographyTokens(
    val titleXlarge: TypographyStyle = TypographyStyle(
        size = 24, weight = 550, lineHeight = 32, letterSpacing = -0.6f,
    ),
    val titleLarge: TypographyStyle = TypographyStyle(
        size = 16, weight = 550, lineHeight = 20, letterSpacing = -0.2f,
    ),
    val bodyLarge: TypographyStyle = TypographyStyle(
        size = 16, weight = 400, lineHeight = 20, letterSpacing = -0.2f,
    ),
    val bodyMedium: TypographyStyle = TypographyStyle(
        size = 14, weight = 400, lineHeight = 20, letterSpacing = 0f,
    ),
    val bodySmall: TypographyStyle = TypographyStyle(
        size = 12, weight = 400, lineHeight = 16, letterSpacing = 0f,
    ),
)
```

| Token         | Size | Weight | Usage                |
| ------------- | ---- | ------ | -------------------- |
| `titleXlarge` | 24sp | 550    | Sheet titles         |
| `titleLarge`  | 16sp | 550    | Section headers      |
| `bodyLarge`   | 16sp | 400    | Input text, body     |
| `bodyMedium`  | 14sp | 400    | Labels, descriptions |
| `bodySmall`   | 12sp | 400    | Helper text, errors  |

Default font: **Inter** variable font. Override `font` to use your own font resource.

##### TypographyStyle

```kotlin
data class TypographyStyle(
    @FontRes val font: Int,
    val letterSpacing: Float,
    val weight: Int,
    val size: Int,
    val lineHeight: Int,
)
```

Has a `toTextStyle()` method that converts to a Compose `TextStyle`.

#### RadiusTokens

```kotlin
data class RadiusTokens(
    val xsmall: Dp = 2.dp,
    val small: Dp = 4.dp,
    val medium: Dp = 8.dp,
    val large: Dp = 12.dp,
    val base: Dp = 4.dp,
)
```

| Token    | Default | Usage                |
| -------- | ------- | -------------------- |
| `xsmall` | 2.dp    | Subtle rounding      |
| `small`  | 4.dp    | Chips, tags          |
| `medium` | 8.dp    | Input fields, cards  |
| `large`  | 12.dp   | Bottom sheet corners |
| `base`   | 4.dp    | Base unit            |

#### BorderWidthTokens

```kotlin
data class BorderWidthTokens(
    val thin: Dp = 1.dp,
    val medium: Dp = 2.dp,
    val thick: Dp = 3.dp,
)
```

| Token    | Default | Usage                         |
| -------- | ------- | ----------------------------- |
| `thin`   | 1.dp    | Input borders, dividers       |
| `medium` | 2.dp    | Focus rings, selected borders |
| `thick`  | 3.dp    | Heavy emphasis borders        |

#### SizeTokens

```kotlin
data class SizeTokens(
    val small: Dp = 16.dp,
    val medium: Dp = 20.dp,
    val large: Dp = 24.dp,
    val xlarge: Dp = 32.dp,
    val xxlarge: Dp = 44.dp,
    val xxxlarge: Dp = 56.dp,
    val base: Dp = 4.dp,
)
```

| Token      | Default | Usage                |
| ---------- | ------- | -------------------- |
| `small`    | 16.dp   | Small icons          |
| `medium`   | 20.dp   | Default icons        |
| `large`    | 24.dp   | Large icons          |
| `xlarge`   | 32.dp   | Payment method icons |
| `xxlarge`  | 44.dp   | Touch targets        |
| `xxxlarge` | 56.dp   | Large touch targets  |
| `base`     | 4.dp    | Base unit            |

### Material 3 Integration

`PrimerTheme` automatically maps color tokens to a Material 3 `ColorScheme`. Material components used within the checkout tree (such as `ModalBottomSheet`) inherit Primer tokens without additional configuration.

Both `PrimerCheckoutSheet` and `PrimerCheckoutHost` wrap content in a `MaterialTheme` that applies this mapping internally.

Key M3 color mappings:

| M3 Role              | Primer Token                                               |
| -------------------- | ---------------------------------------------------------- |
| `primary`            | `primerColorBrand`                                         |
| `onPrimary`          | `primerColorGray000` (light) / `primerColorGray900` (dark) |
| `primaryContainer`   | `primerColorBlue900`                                       |
| `secondary`          | `primerColorGray600`                                       |
| `secondaryContainer` | `primerColorGray200`                                       |
| `background`         | `primerColorBackground`                                    |
| `onBackground`       | `primerColorTextPrimary`                                   |
| `surface`            | `primerColorBackground`                                    |
| `onSurface`          | `primerColorTextPrimary`                                   |
| `surfaceVariant`     | `primerColorGray100`                                       |
| `onSurfaceVariant`   | `primerColorTextSecondary`                                 |
| `surfaceTint`        | `primerColorBrand`                                         |
| `error`              | `primerColorRed500`                                        |
| `onError`            | `primerColorGray000` (light) / `primerColorGray900` (dark) |
| `errorContainer`     | `primerColorRed100`                                        |
| `onErrorContainer`   | `primerColorTextNegative`                                  |
| `outline`            | `primerColorBorderOutlinedDefault`                         |
| `outlineVariant`     | `primerColorGray300`                                       |
| `scrim`              | `Color.Black` (alpha 0.32)                                 |

Overriding a Primer token automatically updates every M3 role that references it.

### LocalPrimerTheme

```kotlin
val LocalPrimerTheme = staticCompositionLocalOf { PrimerTheme() }
```

Compose `CompositionLocal` providing the current `PrimerTheme`. Set automatically by `PrimerCheckoutSheet` and `PrimerCheckoutHost`. Access with `LocalPrimerTheme.current` inside the checkout composable tree.

### colorTokens() Method

```kotlin
@Composable
fun PrimerTheme.colorTokens(darkTheme: Boolean = isSystemInDarkTheme()): LightColorTokens
```

Returns `darkColorTokens` when `darkTheme` is `true`, `lightColorTokens` otherwise. Defaults to the system theme.

## Common Objects

### PrimerError

```kotlin
class PrimerError {
    val errorId: String
    val description: String
    val errorCode: String?
    val diagnosticsId: String
    val recoverySuggestion: String?
}
```

| Property             | Type      | Description                                                                                               |
| -------------------- | --------- | --------------------------------------------------------------------------------------------------------- |
| `errorId`            | `String`  | Unique error identifier for programmatic handling                                                         |
| `description`        | `String`  | Human-readable error message (suitable for logging)                                                       |
| `errorCode`          | `String?` | Specific failure reason (e.g., `"card_declined"`, `"insufficient_funds"`). `null` for non-payment errors. |
| `diagnosticsId`      | `String`  | Unique diagnostics ID. Provide to Primer support when investigating issues.                               |
| `recoverySuggestion` | `String?` | Suggested recovery action. `null` when no recovery applies.                                               |

#### Common Error IDs

| Error ID                     | Error Class                   | Description                                     |
| ---------------------------- | ----------------------------- | ----------------------------------------------- |
| `"payment-cancelled"`        | `PaymentMethodCancelledError` | User cancelled the payment method flow          |
| `"failed-to-redirect"`       | `PaymentMethodRedirectError`  | Redirect to external app or browser failed      |
| `"bad-network"`              | `BadNetworkError`             | Network request failed due to poor connectivity |
| `"connectivity-errors"`      | `ConnectivityError`           | Device has no network connectivity              |
| `"failed-to-create-session"` | `SessionCreateError`          | Failed to create the checkout session           |
| `"unauthorized"`             | `UnauthorizedError`           | Authentication failed (invalid client token)    |
| `"client-error"`             | `ClientError`                 | HTTP 4xx client error                           |
| `"server-error"`             | `ServerError` / `HttpError`   | HTTP 5xx server error                           |
| `"invalid-value"`            | `GeneralError`                | Invalid value in SDK configuration or session   |
| `"unknown-error"`            | `PrimerUnknownError`          | Unclassified error                              |

#### Programmatic Error Handling

```kotlin
onEvent = { event ->
    when (event) {
        is PrimerCheckoutEvent.Failure -> {
            when (event.error.errorId) {
                "payment-cancelled" -> {
                    // User cancelled -- no action needed, they can retry
                }
                "bad-network", "connectivity-errors" -> {
                    showSnackbar("No internet connection. Please try again.")
                }
                "unauthorized", "failed-to-create-session" -> {
                    // Token expired or invalid -- fetch a new one
                    fetchNewClientToken()
                }
                "server-error" -> {
                    showSnackbar("Server error. Please try again later.")
                }
                else -> {
                    Log.e("Checkout", "${event.error.errorId}: ${event.error.description}")
                    Log.e("Checkout", "diagnosticsId: ${event.error.diagnosticsId}")
                }
            }
        }
        is PrimerCheckoutEvent.Success -> { /* handle success */ }
    }
}
```

### PrimerCardNetwork

```kotlin
data class PrimerCardNetwork(
    val network: CardNetwork.Type,
    val displayName: String,
    val allowed: Boolean,
)
```

| Property      | Type               | Description                                                 |
| ------------- | ------------------ | ----------------------------------------------------------- |
| `network`     | `CardNetwork.Type` | The card network type                                       |
| `displayName` | `String`           | Human-readable network name                                 |
| `allowed`     | `Boolean`          | Whether this network is allowed for the current transaction |

#### CardNetwork.Type

```kotlin
enum class CardNetwork.Type {
    OTHER, VISA, MASTERCARD, AMEX, DINERS_CLUB, DISCOVER,
    JCB, UNIONPAY, MAESTRO, ELO, MIR, HIPER, HIPERCARD,
    CARTES_BANCAIRES, DANKORT, EFTPOS,
}
```

### PrimerCountry

```kotlin
data class PrimerCountry(
    val name: String,
    val code: CountryCode,
)
```

`CountryCode` is an ISO 3166-1 alpha-2 enum with 249 values (e.g., `CountryCode.US`, `CountryCode.GB`, `CountryCode.DE`).

#### Country Selection Controller

```kotlin
@Composable
fun rememberCountrySelectionController(): PrimerCountrySelectionController
```

| Member                          | Type               | Description                             |
| ------------------------------- | ------------------ | --------------------------------------- |
| `state`                         | `StateFlow<State>` | Current state of the country picker     |
| `onCountrySelected(code, name)` | Method             | Select a country                        |
| `onSearch(query)`               | Method             | Filter the country list by search query |

State contains: `countries: List<PrimerCountry>`, `filteredCountries: List<PrimerCountry>`, `searchQuery: String`, `isLoading: Boolean`.

### PrimerInputElementType

```kotlin
enum class PrimerInputElementType {
    CARD_NUMBER, EXPIRY_DATE, CVV, CARDHOLDER_NAME,
    COUNTRY_CODE, FIRST_NAME, LAST_NAME,
    ADDRESS_LINE_1, ADDRESS_LINE_2, CITY, STATE, POSTAL_CODE,
}
```

| Value             | Validation                                      |
| ----------------- | ----------------------------------------------- |
| `CARD_NUMBER`     | Luhn check + card network detection             |
| `EXPIRY_DATE`     | MM/YY format, future date                       |
| `CVV`             | Correct length for card network (3 or 4 digits) |
| `CARDHOLDER_NAME` | Non-empty when required                         |
| `COUNTRY_CODE`    | ISO 3166-1 alpha-2                              |
| `FIRST_NAME`      | Non-empty                                       |
| `LAST_NAME`       | Non-empty                                       |
| `ADDRESS_LINE_1`  | Non-empty                                       |
| `ADDRESS_LINE_2`  | Optional                                        |
| `CITY`            | Non-empty                                       |
| `STATE`           | Non-empty                                       |
| `POSTAL_CODE`     | Format varies by country                        |

### SyncValidationError

```kotlin
data class SyncValidationError(
    val inputElementType: PrimerInputElementType,
    val errorId: String,
    @StringRes val fieldId: Int,
    @StringRes val errorResId: Int?,
    @StringRes val errorFormatId: Int?,
)
```

| Property           | Type                     | Description                                          |
| ------------------ | ------------------------ | ---------------------------------------------------- |
| `inputElementType` | `PrimerInputElementType` | Which field has the error                            |
| `errorId`          | `String`                 | Programmatic error identifier                        |
| `fieldId`          | `@StringRes Int`         | String resource ID for the field label               |
| `errorResId`       | `@StringRes Int?`        | String resource ID for the error message, or `null`  |
| `errorFormatId`    | `@StringRes Int?`        | String resource ID for format-style error, or `null` |

## Troubleshooting

### Quick Diagnosis

| Symptom                      | Likely Cause                  | Solution                                                          |
| ---------------------------- | ----------------------------- | ----------------------------------------------------------------- |
| Compose version conflict     | Compiler version mismatch     | Align `kotlinCompilerExtensionVersion`                            |
| Checkout stays in Loading    | Invalid/expired client token  | Check Logcat for errors, regenerate token                         |
| Payment methods empty        | Dashboard misconfiguration    | Verify Dashboard settings and client session fields               |
| Card form fields missing     | Dashboard settings            | Only fields marked required in Dashboard appear                   |
| Recomposition causes re-init | Controller outside `remember` | Use `rememberPrimerCheckoutController()`                          |
| State not updating in UI     | Not lifecycle-aware collect   | Use `collectAsStateWithLifecycle()`                               |
| 3DS challenge stuck          | WebView destroyed on rotation | Add `android:configChanges="orientation\|screenSize"` to Activity |

### Compose Version Conflict

Build fails with Compose compiler version mismatch:

```kotlin
android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }
}
```

### Checkout Stays in Loading State

`PrimerCheckoutState.Loading` never transitions to `Ready`:

- Check Logcat for SDK error messages
- Verify the client token is fresh and generated correctly
- Confirm network connectivity
- Ensure API key is correct in client session creation

### Payment Methods Not Showing

`PrimerPaymentMethods` shows empty list:

- Verify payment methods are configured in Primer Dashboard
- Ensure client session includes correct `currencyCode` and `countryCode`
- Check that methods are enabled for the currency/country combination

### Card Form Fields Not Appearing

Some fields (cardholder name, billing address) do not appear -- this is expected. Fields are configured by your Primer Dashboard settings. Only required fields appear.

### 3DS Challenge Not Completing

Ensure your Activity has `android:configChanges="orientation|screenSize"` to prevent the WebView from being destroyed during configuration changes.

### Recomposition Causes Re-initialization

```kotlin
// CORRECT
val checkout = rememberPrimerCheckoutController(clientToken)

// WRONG -- creates new controller on recomposition
val checkout = PrimerCheckoutController(clientToken)
```

### State Not Updating in UI

Use lifecycle-aware state collection:

```kotlin
val state by checkout.state.collectAsStateWithLifecycle()
```

### Validation vs Payment Errors

| Error Type        | When                                             | Handling                                           |
| ----------------- | ------------------------------------------------ | -------------------------------------------------- |
| Validation errors | During input (invalid format, missing fields)    | Automatic by input components; prevents submission |
| Payment failures  | After submission (declined card, network issues) | Explicit handling via `onEvent` callback           |

### Debugging State Changes

```kotlin
LaunchedEffect(checkout) {
    checkout.state.collect { state ->
        Log.d("PrimerCheckout", "State: $state")
    }
}
```

### Logging All Events

```kotlin
PrimerCheckoutSheet(
    checkout = checkout,
    onEvent = { event ->
        Log.d("PrimerCheckout", "Event: $event")
        when (event) {
            is PrimerCheckoutEvent.Success -> { }
            is PrimerCheckoutEvent.Failure -> {
                Log.e("PrimerCheckout", "diagnosticsId: ${event.error.diagnosticsId}")
            }
        }
    },
)
```

### Getting Help

When contacting Primer support, include:

1. The `diagnosticsId` from any error callbacks
2. Your Android API level, Compose version, and SDK version
3. Steps to reproduce the issue

## Critical Best Practices

### Always Use remember\* Functions

All controllers must be created with their `remember*` composable function. Direct construction will not survive recomposition and will cause repeated initialization.

### Always Use collectAsStateWithLifecycle()

When observing `StateFlow` from controllers, always use `collectAsStateWithLifecycle()` from `androidx.lifecycle:lifecycle-runtime-compose`. This ensures state collection respects the Activity/Fragment lifecycle and avoids memory leaks.

```kotlin
val checkoutState by checkout.state.collectAsStateWithLifecycle()
val cardFormState by cardFormController.state.collectAsStateWithLifecycle()
val methods by paymentMethodsController.methods.collectAsStateWithLifecycle()
```

### Set redirectScheme for Redirect Flows

Any payment method that redirects to a third-party app (3DS, PayPal, Klarna) requires a `redirectScheme` in `PrimerPaymentMethodOptions`. Without it, the user cannot return to your app.

```kotlin
val settings = PrimerSettings(
    paymentMethodOptions = PrimerPaymentMethodOptions(
        redirectScheme = "myapp://primer",
    ),
)
```

Register the scheme in your `AndroidManifest.xml`:

```xml
<activity android:name=".CheckoutActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="primer" />
    </intent-filter>
</activity>
```

### Place Controllers at the Right Scope

Create `rememberPrimerCheckoutController` at the screen level. Create child controllers (`rememberCardFormController`, `rememberPaymentMethodsController`) inside `PrimerCheckoutHost` content or `PrimerCheckoutSheet` slots where `CompositionLocal` providers are available.

### Handle Both Success and Failure Events

Always handle both `PrimerCheckoutEvent.Success` and `PrimerCheckoutEvent.Failure` in `onEvent`. Events can fire multiple times per session (retry after failure).

### Use PrimerCheckoutHost for Custom Layouts

If you need full control over layout, navigation, and presentation, use `PrimerCheckoutHost` instead of `PrimerCheckoutSheet`. The host provides the same `CompositionLocal` providers and overlay management but lets you build your own UI structure.

### Prevent 3DS WebView Destruction

Add to your Activity in `AndroidManifest.xml`:

```xml
<activity
    android:name=".CheckoutActivity"
    android:configChanges="orientation|screenSize" />
```

This prevents the WebView from being destroyed during configuration changes, which would cause 3DS challenges to fail.

### Controller Cleanup and Lifecycle

v3 controllers auto-cleanup when their enclosing composition exits -- there is no explicit `cleanup()` method to call. Key lifecycle behaviors:

- `dismiss()` closes the checkout UI (sheet or host), but the controller remains valid
- `refresh()` reinitializes the session, transitioning state back to `Loading`
- Child controllers (`rememberCardFormController`, `rememberKlarnaController`, etc.) are tied to the composition scope where they are created
