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

# Integration best practices

> State management, error diagnostics, performance, security and testing patterns for Web, iOS and Android integrations.

## State management

Manage checkout state carefully to avoid re-initialization and stale data. Keep configuration objects stable and observe state changes with lifecycle-aware collectors.

> [tab] Web
>
> ### Define options outside functions
>
> Create options objects once and reuse them to avoid unnecessary re-initialization:
>
> > [!TIP]
> >
> > Define static options outside your function scope. The SDK uses deep comparison to detect actual changes, but stable object references reduce comparison overhead and improve performance.
>
> ```javascript theme={"dark"}
> // ✅ GOOD: Created once
> const SDK_OPTIONS = {
>   locale: 'en-GB',
> };
>
> function initCheckout() {
>   const checkout = document.querySelector('primer-checkout');
>   checkout.options = SDK_OPTIONS; // Same reference every time
> }
>
> // ❌ AVOID: Created every time function runs
> function initCheckout() {
>   const checkout = document.querySelector('primer-checkout');
>   checkout.options = { locale: 'en-GB' }; // New object every execution
> }
> ```
>
> > [!NOTE]
> >
> > The SDK performs deep comparison to detect actual changes in the `options` object. Using stable references (the GOOD pattern above) minimizes comparison overhead and remains the recommended best practice for optimal performance.

> [tab] Android
>
> ### Use lifecycle-aware state collection
>
> Always use `collectAsStateWithLifecycle()` instead of `collectAsState()` to prevent unnecessary work when the app is in the background:
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
> ```
>
> ### Avoid wrapping the controller in a ViewModel
>
> The checkout controller is already a ViewModel internally. Don't wrap it in another ViewModel unless you need to coordinate with other app state.

> [tab] iOS
>
> ### Define settings and theme as constants
>
> Create `PrimerSettings` and `PrimerCheckoutTheme` outside your view `body` to avoid unnecessary re-creation on each SwiftUI render cycle:
>
> > [!TIP]
> >
> > SwiftUI re-evaluates `body` frequently. Defining configuration objects as constants ensures they are created once and reused.
>
> ```swift theme={"dark"}
> // ✅ GOOD: Created once
> private let primerSettings = PrimerSettings(paymentHandling: .auto)
> private let primerTheme = PrimerCheckoutTheme(
>   colors: ColorOverrides(primerColorBrand: .blue)
> )
>
> struct CheckoutView: View {
>   let clientToken: String
>
>   var body: some View {
>     PrimerCheckout(
>       clientToken: clientToken,
>       primerSettings: primerSettings,
>       primerTheme: primerTheme
>     )
>   }
> }
>
> // ❌ AVOID: Created every render
> struct CheckoutView: View {
>   let clientToken: String
>
>   var body: some View {
>     PrimerCheckout(
>       clientToken: clientToken,
>       primerSettings: PrimerSettings(paymentHandling: .auto),
>       primerTheme: PrimerCheckoutTheme(colors: ColorOverrides(primerColorBrand: .blue))
>     )
>   }
> }
> ```
>
> ### Use proper @State management
>
> When storing checkout-related values in SwiftUI state, use `@State` for simple values and `@StateObject` for observable objects:
>
> ```swift theme={"dark"}
> struct CheckoutView: View {
>   let clientToken: String
>   @State private var paymentCompleted = false
>   @State private var paymentResult: PaymentResult?
>
>   var body: some View {
>     if paymentCompleted, let result = paymentResult {
>       ConfirmationView(result: result)
>     } else {
>       PrimerCheckout(
>         clientToken: clientToken,
>         onCompletion: { state in
>           if case .success(let result) = state {
>             paymentResult = result
>             paymentCompleted = true
>           }
>         }
>       )
>     }
>   }
> }
> ```
>
> ### Observe the lifecycle through the session
>
> For composable, inline checkout, hold a `PrimerCheckoutSession` as a `@StateObject`. Observe its `@Published` properties for lifecycle and per-feature state, and receive outcomes through `onCompletion`. The session is automatically started on appear and torn down on disappear by the `.primerCheckoutSession(_:theme:onCompletion:)` modifier — you don't manage an `AsyncStream` or a manual `Task`:
>
> ```swift theme={"dark"}
> struct CheckoutView: View {
>   @StateObject private var session: PrimerCheckoutSession
>
>   init(clientToken: String) {
>     _session = StateObject(
>       wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
>     )
>   }
>
>   var body: some View {
>     ScrollView {
>       PrimerCardForm()
>     }
>     .primerCheckoutSession(session) { state in
>       // .failure once per failed attempt, then .success or .dismissed exactly once
>       handleStateChange(state)
>     }
>   }
> }
> ```
>
> Outcomes arrive through `onCompletion`. A `.failure` after the checkout is ready is delivered once per failed attempt, such as a declined payment or a failed `refresh()`. The checkout stays usable so the shopper can retry, so don't tear it down on `.failure`. `.success` and `.dismissed` are delivered exactly once, and nothing is delivered after them. A `.failure` while the checkout is still initializing ends the session and is delivered once. The lifecycle comes from the session's `@Published` `phase` and `clientSession`. The per-feature sessions (`cardForm`, `selection`) become available once `phase` is `.ready`, and their `@Published` `state` drives your custom slots.

## Error handling

Log diagnostics IDs and handle failures gracefully. Every error from the SDK includes identifiers that help Primer support diagnose issues.

> [tab] Web
>
> ### Monitor SDK initialization
>
> ```javascript theme={"dark"}
> checkout.addEventListener('primer:ready', (event) => {
>   const primer = event.detail;
>   console.log('SDK initialized');
>   console.log('Applied locale:', checkout.options?.locale);
>   console.log('Active payment methods:', primer.getPaymentMethods?.());
> });
> ```
>
> ### Debug configuration issues
>
> Common debugging approaches for options-related issues:
>
> > [!TIP]
> >
> > When options aren't working as expected, check these common issues first:
>
> **Check object reference stability:**
>
> ```javascript theme={"dark"}
> const options = { locale: 'en-GB' };
> console.log('Options reference:', options);
>
> // Later in code
> checkout.options = options;
> console.log('Applied options:', checkout.options);
> console.log('References match:', checkout.options === options);
> ```
>
> **Verify component properties vs SDK options:**
>
> ```javascript theme={"dark"}
> // Component properties use setAttribute()
> const checkout = document.querySelector('primer-checkout');
>
> checkout.setAttribute('client-token', 'new-token');
> console.log('Token attribute:', checkout.getAttribute('client-token'));
>
> // SDK options use direct property assignment
> checkout.options = { locale: 'en-GB' };
> console.log('Options object:', checkout.options);
> ```

> [tab] Android
>
> ### Always handle outcomes
>
> The Android Components SDK always uses `AUTO` mode — there is no manual mode. Even with default screens, observe `checkout.state` for logging and analytics:
>
> ```kotlin theme={"dark"}
> val state by checkout.state.collectAsStateWithLifecycle()
>
> LaunchedEffect(state) {
>     when (val s = state) {
>         is PrimerCheckoutState.Success -> {
>             analytics.trackPurchase(s.checkoutData)
>         }
>         is PrimerCheckoutState.Failure -> {
>             analytics.trackError(s.primerError.diagnosticsId)
>         }
>         else -> Unit
>     }
> }
>
> PrimerCheckoutSheet(checkout = checkout)
> ```
>
> ### Log diagnostics IDs
>
> Every `PrimerError` includes a `diagnosticsId`. Log it for troubleshooting with Primer support:
>
> ```kotlin theme={"dark"}
> is PrimerCheckoutState.Failure -> {
>     Log.e("Checkout", "Error: ${s.primerError.diagnosticsId}")
> }
> ```

> [tab] iOS
>
> ### Handle all completion states
>
> Handle every outcome in the `onCompletion` callback. `.failure` arrives once per failed attempt, including a failed initialization. While `uiOptions.isErrorScreenEnabled` is `true` (the default), the checkout stays open so the shopper can retry, so don't close the checkout on `.failure`. `.success` or `.dismissed` arrives exactly once and ends the checkout. `onCompletion` never receives `.initializing` or `.ready`. The `switch` lists them only to stay exhaustive:
>
> ```swift theme={"dark"}
> PrimerCheckout(
>   clientToken: clientToken,
>   onCompletion: { state in
>     switch state {
>     case .success(let result):
>       handleSuccess(result)
>     case let .failure(error, _):
>       handleFailure(error)
>     case .dismissed:
>       handleDismissal()
>     case .initializing, .ready:
>       break // not delivered to onCompletion
>     }
>   }
> )
> ```
>
> > [!WARNING]
> >
> > `.dismissed` arrives when the shopper closes the checkout from the SDK's own screens. If you present `PrimerCheckout` in your own `.sheet`, a swipe-down closes your sheet without calling `onCompletion`. Also reset your presentation state in `.sheet(isPresented:onDismiss:)`.
>
> ### Log diagnostics IDs
>
> Every error includes a `diagnosticsId`. Log it, together with `recoverySuggestion`, for troubleshooting with Primer support. `recoverySuggestion` is developer-facing English text, so show the shopper your own message:
>
> ```swift theme={"dark"}
> case let .failure(error, _):
>   print("Error diagnosticsId: \(error.diagnosticsId)")
>   if let suggestion = error.recoverySuggestion {
>     print("Recovery suggestion: \(suggestion)")
>   }
> ```

## Performance

Minimize unnecessary re-renders and keep configuration stable. Initialize the SDK as early as possible so it can prefetch configuration while the user navigates.

> [tab] Web
>
> ### Use TypeScript interfaces for type safety
>
> Define TypeScript interfaces for your options objects to catch errors at compile time:
>
> ```typescript theme={"dark"}
> interface PrimerSDKOptions {
>   locale: string;
>   paymentMethodOptions?: {
>     PAYMENT_CARD?: {
>       requireCVV?: boolean;
>       requireBillingAddress?: boolean;
>     };
>     APPLE_PAY?: {
>       merchantName?: string;
>       merchantCountryCode?: string;
>     };
>   };
> }
>
> const options: PrimerSDKOptions = {
>   locale: 'en-GB',
>   paymentMethodOptions: {
>     PAYMENT_CARD: {
>       requireCVV: true,
>       requireBillingAddress: true,
>     },
>   },
> };
> ```
>
> ### Test options configuration separately
>
> Create isolated tests for your options configuration:
>
> ```typescript theme={"dark"}
> describe('SDK Options Configuration', () => {
>   it('should create valid options object', () => {
>     const options = {
>       locale: 'en-GB',
>     };
>
>     expect(options.locale).toBe('en-GB');
>   });
>
>   it('should maintain stable reference', () => {
>     const options = { locale: 'en-GB' };
>     const checkout = document.querySelector('primer-checkout');
>     checkout.options = options;
>
>     expect(checkout.options).toBe(options); // Same reference
>   });
> });
> ```

> [tab] Android
>
> ### Initialize early
>
> Create the checkout controller as early as possible so the SDK can fetch configuration while the user navigates to checkout:
>
> ```kotlin theme={"dark"}
> @Composable
> fun CheckoutScreen(clientToken: String) {
>     val checkout = rememberPrimerCheckoutController(clientToken)
>     // SDK starts loading immediately
> }
> ```
>
> ### Avoid recreating the controller
>
> `rememberPrimerCheckoutController` survives recomposition. Don't call it conditionally or inside callbacks.

> [tab] iOS
>
> ### Avoid creating objects in SwiftUI `body`
>
> Define `PrimerSettings` and `PrimerCheckoutTheme` as constants outside the view body. SwiftUI re-evaluates `body` frequently, so inline allocations waste resources:
>
> ```swift theme={"dark"}
> // ✅ GOOD: Defined once outside body
> private let primerSettings = PrimerSettings(paymentHandling: .auto)
>
> struct CheckoutView: View {
>   let clientToken: String
>
>   var body: some View {
>     PrimerCheckout(
>       clientToken: clientToken,
>       primerSettings: primerSettings
>     )
>   }
> }
> ```
>
> ### Let the session own its lifecycle
>
> For composable checkout, hold the `PrimerCheckoutSession` as a `@StateObject` and attach the `.primerCheckoutSession(_:theme:onCompletion:)` modifier. The modifier starts the session on appear and tears it down on disappear, so you never manage a manual `Task` or `AsyncStream`:
>
> ```swift theme={"dark"}
> struct CheckoutView: View {
>   @StateObject private var session: PrimerCheckoutSession
>
>   init(clientToken: String) {
>     _session = StateObject(
>       wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
>     )
>   }
>
>   var body: some View {
>     ScrollView {
>       PrimerCardForm()
>     }
>     .primerCheckoutSession(session) { state in
>       handleStateChange(state)
>     }
>   }
> }
> ```
>
> ### Don't retain self in `onCompletion`
>
> The completion closure can outlive the view that created it. When forwarding the outcome to a long-lived object such as a view model, capture it weakly to avoid retain cycles:
>
> ```swift theme={"dark"}
> .primerCheckoutSession(session) { [weak viewModel] state in
>   Task { await viewModel?.handleState(state) }
> }
> ```

## Security

Never expose API keys in client code. Keep configuration minimal and handle sensitive data on your server.

> [tab] Web
>
> ### Distinguish between component properties and SDK options
>
> **Component Properties** are HTML attributes that configure the component container. **SDK Options** are configuration settings for the SDK itself.
>
> > [!WARNING]
> >
> > Mixing these up will cause silent failures. Component properties must use `setAttribute()`. SDK options must be assigned directly to `.options`.
>
> **Component Properties** (use `setAttribute()`):
>
> * `client-token` - API authentication
> * `custom-styles` - Visual theming
> * `loader-disabled` - Loader behavior
>
> **SDK Options** (use `options` object):
>
> * `locale` - UI language
> * Payment method configuration
> * Feature settings
> * Merchant domain and API settings
>
> ```javascript theme={"dark"}
> // ✅ CORRECT: Clear separation
> const checkout = document.querySelector('primer-checkout');
>
> // Component properties
> checkout.setAttribute('client-token', 'your-token');
> checkout.setAttribute('loader-disabled', 'false');
>
> // SDK options
> checkout.options = {
>   locale: 'en-GB',
>   applePay: { buttonType: 'buy' },
> };
>
> // ❌ WRONG: Mixing them up
> checkout.setAttribute('locale', 'en-GB'); // Won't work - locale is an SDK option
> checkout.options = {
>   clientToken: 'your-token', // Wrong - this is a component property
> };
> ```
>
> ### Keep options simple and focused
>
> Only configure what you need:
>
> ```javascript theme={"dark"}
> // ✅ GOOD: Simple, focused configuration
> checkout.options = {
>   locale: 'en-GB',
> };
>
> // ❌ AVOID: Over-configuration with unused options
> checkout.options = {
>   locale: 'en-GB',
>   paymentMethodOptions: {
>     PAYMENT_CARD: {
>       requireCVV: true,
>       requireBillingAddress: false,
>       // ... 20 more unused options
>     },
>     APPLE_PAY: {
>       // ... configured but not used
>     },
>   },
> };
> ```

> [tab] Android
>
> ### Client token handling
>
> * Generate client tokens on your server, never hardcode them
> * Client tokens are single-use and expire -- generate a fresh one for each checkout session
> * Never log or persist client tokens
>
> ### ProGuard
>
> The SDK includes its own ProGuard rules. Don't add additional rules that might strip required classes.

> [tab] iOS
>
> ### Client token handling
>
> * Generate client tokens on your server, never hardcode them
> * Client tokens are single-use and expire -- generate a fresh one for each checkout session
> * Never log or persist client tokens
>
> ### Keep configuration minimal
>
> Only configure what you need in `PrimerSettings`. The SDK ships with sensible defaults:
>
> ```swift theme={"dark"}
> // ✅ GOOD: Only override what you need
> private let primerSettings = PrimerSettings(paymentHandling: .auto)
>
> // ❌ AVOID: Over-configuration with defaults
> private let primerSettings = PrimerSettings(
>   paymentHandling: .auto,
>   localeData: nil,
>   uiOptions: nil
> )
> ```

## Testing

Use sandbox mode with test cards to verify your integration before going to production.

> [tab] Web
>
> Test your checkout in a sandbox environment. Verify that:
>
> * Options are applied correctly
> * Events fire as expected
> * Error handling works for declined cards
> * All payment methods render properly

> [tab] Android
>
> ### Use sandbox environment
>
> Always test with sandbox client tokens before going to production. Use test card numbers:
>
> | Card | Result |
> | - | - |
> | `4111 1111 1111 1111` | Success |
> | `4000 0000 0000 0002` | Declined |
>
> ### Test dark mode
>
> Verify your custom theme looks correct in both light and dark modes:
>
> ```kotlin theme={"dark"}
> // Force dark mode for testing
> val theme = PrimerTheme(
>     darkColorTokens = object : DarkColorTokens() {
>         override val primerColorBrand: Color = Color(0xFFA29BFE)
>     },
> )
> ```
>
> ### Accessibility
>
> The SDK supports TalkBack and other accessibility services. Default components include:
>
> * Content descriptions for all interactive elements
> * Semantic roles (buttons, inputs, headings)
> * Focus management for screen transitions
>
> When building custom layouts, ensure your custom components also include accessibility annotations:
>
> ```kotlin theme={"dark"}
> Modifier.semantics {
>     contentDescription = "Pay with Visa ending in 4242"
>     role = Role.Button
> }
> ```

> [tab] iOS
>
> ### Use sandbox environment
>
> Always test with sandbox client tokens before going to production. Use test card numbers:
>
> | Card | Result |
> | - | - |
> | `4111 1111 1111 1111` | Success |
> | `4000 0000 0000 0002` | Declined |
>
> Use the [Primer Dashboard](https://sandbox-dashboard.primer.io) to configure test payment methods.
>
> ### Test dark mode
>
> Verify your custom theme looks correct in both light and dark modes using the iOS simulator's appearance settings or by overriding the color scheme in your preview:
>
> ```swift theme={"dark"}
> #Preview {
>   CheckoutView(clientToken: "sandbox-token")
>     .preferredColorScheme(.dark)
> }
> ```
>
> ### Accessibility
>
> The SDK supports VoiceOver and other iOS accessibility services. Default components include:
>
> * Accessibility labels for all interactive elements
> * Trait annotations (button, header, text field)
> * Focus management for screen transitions
>
> When building custom layouts, ensure your custom components include accessibility modifiers:
>
> ```swift theme={"dark"}
> Text("Pay with Visa ending in 4242")
>   .accessibilityLabel("Pay with Visa ending in 4242")
>   .accessibilityAddTraits(.isButton)
> ```


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