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.
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.
Define options outside functions
Create options objects once and reuse them to avoid unnecessary re-initialization: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.
// ✅ 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
}
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.
Use lifecycle-aware state collection
Always use collectAsStateWithLifecycle() instead of collectAsState() to prevent unnecessary work when the app is in the background: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.Define settings and theme as constants
Create PrimerSettings and PrimerCheckoutTheme outside your view body to avoid unnecessary re-creation on each SwiftUI render cycle:SwiftUI re-evaluates body frequently. Defining configuration objects as constants ensures they are created once and reused.
// ✅ 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: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:PrimerCheckout(
clientToken: clientToken,
scope: { checkoutScope in
// ✅ Start observing state
Task {
for await state in checkoutScope.state {
handleStateChange(state)
}
}
}
)
Error handling
Log diagnostics IDs and handle failures gracefully. Every error from the SDK includes identifiers that help Primer support diagnose issues.
Monitor SDK initialization
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:When options aren’t working as expected, check these common issues first:
Check object reference stability: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:// 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);
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: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:is PrimerCheckoutEvent.Failure -> {
Log.e("Checkout", "Error: ${event.error.diagnosticsId}")
}
Handle all completion states
Always handle all cases in the onCompletion callback to avoid unexpected behavior: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
}
}
)
Not handling the .dismissed case can leave your app in an inconsistent state if the user swipes to dismiss the checkout.
Log diagnostics IDs
Every error includes a diagnosticsId. Log it for troubleshooting with Primer support, and use recoverySuggestion for user-facing messages:case .failure(let error):
print("Error diagnosticsId: \(error.diagnosticsId)")
if let suggestion = error.recoverySuggestion {
showUserMessage(suggestion)
}
Minimize unnecessary re-renders and keep configuration stable. Initialize the SDK as early as possible so it can prefetch configuration while the user navigates.
Use TypeScript interfaces for type safety
Define TypeScript interfaces for your options objects to catch errors at compile time: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: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
});
});
Initialize early
Create the checkout controller as early as possible so the SDK can fetch configuration while the user navigates to checkout:@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.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:// ✅ 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: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:scope: { [weak viewModel] checkoutScope in
Task {
for await state in checkoutScope.state {
await viewModel?.handleState(state)
}
}
}
Security
Never expose API keys in client code. Keep configuration minimal and handle sensitive data on your server.
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.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
// ✅ 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:// ✅ 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
},
},
};
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.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:// ✅ 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.
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
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:// 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:Modifier.semantics {
contentDescription = "Pay with Visa ending in 4242"
role = Role.Button
}
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 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:#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:Text("Pay with Visa ending in 4242")
.accessibilityLabel("Pay with Visa ending in 4242")
.accessibilityAddTraits(.isButton)