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

# Best practices

> Read about best practices when configuring your checkout and ways to troubleshoot issues

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

<Tabs>
  <Tab title="Web">
    ### Define options outside functions

    Create options objects once and reuse them to avoid unnecessary re-initialization:

    <Tip title="Performance Optimization">
      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.
    </Tip>

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

    <Info title="Deep Comparison">
      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.
    </Info>
  </Tab>

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

  <Tab title="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 title="Performance Optimization">
      SwiftUI re-evaluates `body` frequently. Defining configuration objects as constants ensures they are created once and reused.
    </Tip>

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

    ### Use `.task` for async state observation

    Prefer the `.task` modifier over `onAppear` with `Task` for observing `AsyncStream` state, as `.task` is automatically cancelled when the view disappears:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        // ✅ Start observing state
        Task {
          for await state in checkoutScope.state {
            handleStateChange(state)
          }
        }
      }
    )
    ```
  </Tab>
</Tabs>

## Error handling

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

<Tabs>
  <Tab title="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 title="Debugging SDK Options">
      When options aren't working as expected, check these common issues first:
    </Tip>

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

  <Tab title="Android">
    ### Always handle events

    The Android Components SDK always uses `AUTO` mode — there is no manual mode. Even with default screens, listen to events for logging and analytics:

    ```kotlin theme={"dark"}
    PrimerCheckoutSheet(
        checkout = checkout,
        onEvent = { event ->
            when (event) {
                is PrimerCheckoutEvent.Success -> {
                    analytics.trackPurchase(event.checkoutData)
                }
                is PrimerCheckoutEvent.Failure -> {
                    analytics.trackError(event.error.diagnosticsId)
                }
            }
        },
    )
    ```

    ### Log diagnostics IDs

    Every `PrimerError` includes a `diagnosticsId`. Log it for troubleshooting with Primer support:

    ```kotlin theme={"dark"}
    is PrimerCheckoutEvent.Failure -> {
        Log.e("Checkout", "Error: ${event.error.diagnosticsId}")
    }
    ```
  </Tab>

  <Tab title="iOS">
    ### Handle all completion states

    Always handle all cases in the `onCompletion` callback to avoid unexpected behavior:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      onCompletion: { state in
        switch state {
        case .success(let result):
          handleSuccess(result)
        case .failure(let error):
          handleFailure(error)
        case .dismissed:
          handleDismissal()
        case .initializing, .ready:
          break
        }
      }
    )
    ```

    <Warning>
      Not handling the `.dismissed` case can leave your app in an inconsistent state if the user swipes to dismiss the checkout.
    </Warning>

    ### Log diagnostics IDs

    Every error includes a `diagnosticsId`. Log it for troubleshooting with Primer support, and use `recoverySuggestion` for user-facing messages:

    ```swift theme={"dark"}
    case .failure(let error):
      print("Error diagnosticsId: \(error.diagnosticsId)")
      if let suggestion = error.recoverySuggestion {
        showUserMessage(suggestion)
      }
    ```
  </Tab>
</Tabs>

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

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

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

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

    ### Use `.task` for AsyncStream observation

    Prefer `.task` over `onAppear` with `Task {}`, because `.task` is automatically cancelled when the view disappears:

    ```swift theme={"dark"}
    PrimerCheckout(
      clientToken: clientToken,
      scope: { checkoutScope in
        Task {
          for await state in checkoutScope.state {
            handleStateChange(state)
          }
        }
      }
    )
    ```

    ### Don't force-unwrap optionals in scope closures

    Scope closures capture context at initialization time. Use safe unwrapping to avoid crashes:

    ```swift theme={"dark"}
    scope: { [weak viewModel] checkoutScope in
      Task {
        for await state in checkoutScope.state {
          await viewModel?.handleState(state)
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Security

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

<Tabs>
  <Tab title="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 title="Properties vs Options">
      Mixing these up will cause silent failures. Component properties must use `setAttribute()`. SDK options must be assigned directly to `.options`.
    </Warning>

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

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

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

## Testing

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

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

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

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