Migrate from Drop-In or Headless to Primer Checkout Components.
This guide covers the key changes when migrating from the legacy SDK to the new Primer Checkout Components. Select your platform to see platform-specific migration steps.
Not every section will apply — skip sections for features you don’t use.
Primer Checkout Components is part of the same PrimerSDK package. No dependency changes are needed — if you already have the SDK installed via CocoaPods or Swift Package Manager, you’re ready to go.
PrimerCheckoutSheet (modal) or PrimerCheckoutHost (inline)
State
Listener callbacks
StateFlow<PrimerCheckoutState>
Dismiss
Primer.instance.dismiss()
checkout.dismiss()
The initialization changes from configuring a singleton to creating a view (SwiftUI) or calling a static presenter method (UIKit). Configuration, delegate, and presentation are combined into a single step.
For UIKit apps, PrimerCheckoutPresenter provides the most direct migration path. It wraps the SwiftUI checkout in a UIHostingController and handles presentation.
Copy
Ask AI
// Set delegatePrimerCheckoutPresenter.shared.delegate = self// Present checkout — minimalPrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)// Or with settings and themePrimerCheckoutPresenter.presentCheckout( clientToken: clientToken, from: self, primerSettings: PrimerSettings(paymentHandling: .auto), primerTheme: PrimerCheckoutTheme())// Or with full scope customizationPrimerCheckoutPresenter.presentCheckout( clientToken: clientToken, from: self, primerSettings: PrimerSettings(paymentHandling: .auto), primerTheme: PrimerCheckoutTheme(), scope: { checkoutScope in // Customize screens, observe state })
PrimerCheckoutPresenter also provides:
PrimerCheckoutPresenter.isPresenting — check if checkout is currently visible
PrimerCheckoutPresenter.dismiss() — programmatically dismiss the checkout
Some SDK methods require access to the PrimerJS instance. This instance is provided via the primer:ready event, which fires when the checkout is fully initialized.
const checkout = document.querySelector('primer-checkout');checkout.addEventListener('primer:ready', (event) => { const primer = event.detail; // Now you can call SDK methods await primer.refreshSession();});
If you need to call SDK methods outside of the event handler (e.g., in response to user actions or after a session expires), store the reference:
Copy
Ask AI
let primer;checkout.addEventListener('primer:ready', (event) => { primer = event.detail;});// Later, when neededasync function handleSessionRefresh() { if (primer) { await primer.refreshSession(); }}
Sync the SDK with server-side session changes. Returns Promise<void>.
getPaymentMethods()
Returns the list of available payment methods.
setCardholderName(name)
Programmatically set the cardholder name field.
vault.startPayment(id, options?)
Start payment with a vaulted method.
vault.createCvvInput(options)
Create a CVV input for re-capture.
vault.delete(id)
Delete a vaulted payment method.
Most integrations don’t need to access the PrimerJS instance directly. The primary use cases are session refresh, programmatic cardholder name updates, and headless vault implementations.
In Primer Checkout Components, checkout state is available via PrimerCheckoutController.state, a StateFlow you can collect in your composables:
Copy
Ask AI
val checkout = rememberPrimerCheckoutController(clientToken, PrimerSettings())val state by checkout.state.collectAsStateWithLifecycle()when (state) { is PrimerCheckoutState.Loading -> CircularProgressIndicator() is PrimerCheckoutState.Ready -> { /* Render checkout */ } is PrimerCheckoutState.Error -> { /* Show error */ }}
The controller also provides:
checkout.refresh() — reinitialize the checkout session
checkout.dismiss() — programmatically dismiss the checkout UI
In Primer Checkout Components, SDK access is through the scope closure on PrimerCheckout (SwiftUI) or PrimerCheckoutPresenter.presentCheckout() (UIKit). The scope provides access to state observation and UI customization.
Copy
Ask AI
scope: { checkoutScope in // Observe state Task { for await state in checkoutScope.state { switch state { case .ready(let totalAmount, let currencyCode): print("Ready: \(totalAmount) \(currencyCode)") default: break } } } // Customize screens checkoutScope.splashScreen = { AnyView(MyLoadingView()) } // Access payment method scopes if let cardScope: PrimerCardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self) { // Interact with the card form }}
The PaymentSummary contains partial card data (last 4 digits, network, cardholder name) but not full PII like customer email or full card number. Use server-side webhooks for complete payment data.
onCheckoutFail: (error, data, handler) => { handler.showErrorMessage('Your card was declined');}
New:
Copy
Ask AI
checkout.addEventListener('primer:payment-failure', (event) => { const { error, payment, paymentMethodType, timestamp } = event.detail; // error has: code, message, diagnosticsId, data console.error('Failed:', error.message, error.diagnosticsId); // The SDK shows the error in the UI automatically. // Use this event for logging, analytics, or retry logic.});
The legacy onBeforePaymentCreate callback is replaced by the primer:payment-start event. Use event.preventDefault() to intercept the payment flow, then call continuePaymentCreation() or abortPaymentCreation() from event.detail.Legacy:
Copy
Ask AI
onBeforePaymentCreate: (data, handler) => { if (!termsAccepted) { handler.showErrorMessage('Please accept the terms'); handler.abort(); } else { handler.continuePaymentCreation(); }}
New:
Copy
Ask AI
checkout.addEventListener('primer:payment-start', (event) => { const { paymentMethodType, continuePaymentCreation, abortPaymentCreation } = event.detail; // Prevent automatic continuation event.preventDefault(); if (!document.getElementById('terms-checkbox').checked) { // Show your own error UI alert('Please accept the terms'); abortPaymentCreation(); } else { // Optionally pass an idempotency key continuePaymentCreation({ idempotencyKey: 'your-key' }); }});
If you don’t call event.preventDefault(), the payment continues automatically. Only use preventDefault() when you need to run validation or async checks before payment creation.
// Update the client token, then call refreshSession()checkout.setAttribute('client-token', newClientToken);checkout.addEventListener('primer:ready', (event) => { const primer = event.detail; await primer.refreshSession();});
The callback-based PrimerCheckoutListener is replaced by PrimerCheckoutEvent delivered via the onEvent lambda, and StateFlow<PrimerCheckoutState> for state observation.
val state by checkout.state.collectAsStateWithLifecycle()when (state) { is PrimerCheckoutState.Loading -> { /* Show loading */ } is PrimerCheckoutState.Ready -> { /* Checkout is interactive */ } is PrimerCheckoutState.Error -> { /* Handle error */ }}
PrimerCheckout( clientToken: clientToken, scope: { checkoutScope in Task { for await state in checkoutScope.state { switch state { case .initializing: print("Loading...") case .ready(let totalAmount, let currencyCode): print("Ready: \(totalAmount) \(currencyCode)") case .success(let result): print("Payment ID: \(result.paymentId)") case .failure(let error): print("Error: \(error.errorId)") case .dismissed: print("Dismissed") } } } }, onCompletion: { state in switch state { case .success(let result): navigateToConfirmation(paymentId: result.paymentId) case .failure(let error): logError(error) case .dismissed: navigateBack() default: break } })
PaymentResult replaces CheckoutData as the success type. It contains paymentId, status, amount, currencyCode, and paymentMethodType.
See State and events for the complete state lifecycle documentation.
The legacy SDK had a built-in success screen. The new SDK gives you two options:Option 1: Use the checkout-complete slot (declarative)
Copy
Ask AI
<primer-checkout client-token="your-client-token"> <primer-main slot="main"> <div slot="checkout-complete"> <h2>Thank you for your order!</h2> <p>Your payment was processed successfully.</p> </div> </primer-main></primer-checkout>
The conversion rule for the custom-styles attribute: --primer-color-brand → primerColorBrand (remove --, convert kebab-case to camelCase).Option 3: Inline CSS
Don’t attempt a 1:1 mapping from your legacy CSS-in-JS styles. Instead, start with your brand color (--primer-color-brand) and font (--primer-typography-brand), then use browser DevTools to inspect which tokens affect which elements and adjust from there.
The card form options have been simplified. The legacy requireCvv and requireCardholderName top-level options no longer exist. Use the cardholderName sub-object instead.Legacy:
card: { cardholderName: { required: true, visible: true, defaultValue: 'John Doe', // Optional: pre-fill the field }}
CVV is always required by the card form — there is no option to disable it.For runtime updates to the cardholder name after initialization, use primer.setCardholderName('name') instead of updating the options.
The Klarna configuration has changed significantly from the legacy SDK. There is no klarna.locale option — the Klarna locale is derived from the top-level locale option.Legacy:
Copy
Ask AI
klarna: { locale: 'en-GB',}
New:
Copy
Ask AI
klarna: { paymentFlow: 'DEFAULT', // 'DEFAULT' or 'PREFER_VAULT' allowedPaymentCategories: ['pay_now', 'pay_later', 'pay_over_time'], recurringPaymentDescription: '...', // Required for recurring payments buttonOptions: { text: 'Pay with Klarna', },}
vault: { enabled: true, // Required — must be true to use vault headless: false, // New: hide default UI for custom implementations showEmptyState: true, // New: show message when no saved methods}
Key changes:
visible → enabled (renamed, required).
deletionDisabled — no longer a client-side option. Deletion capability is managed through the vault UI or programmatically via primer.vault.delete().
New:headless mode for building fully custom vault UIs.
New:showEmptyState to show a message when the user has no saved methods.
An alternative approach for card form submission: you can call cardForm.submit() directly on the <primer-card-form> element if you have a reference to it.
Added Jetpack Compose — buildFeatures { compose = true } in build config (if not already present)
Replaced entry point — Using rememberPrimerCheckoutController() + PrimerCheckoutSheet() instead of Primer.instance.showUniversalCheckout()
Replaced listener — Using PrimerCheckoutEvent via onEvent lambda instead of PrimerCheckoutListener
Updated theming — Using PrimerTheme data class instead of PrimerSettings.uiOptions (if customizing)
Tested state observation — Collecting StateFlow<PrimerCheckoutState> with collectAsStateWithLifecycle()
No server-side changes — Backend integration remains the same
No dependency changes — Same SDK BOM
Verify your migration is complete:
Updated minimum deployment target — iOS 15.0
UIKit: Replaced Primer.shared.showUniversalCheckout() with PrimerCheckoutPresenter.presentCheckout()
UIKit: Replaced PrimerDelegate with PrimerCheckoutPresenterDelegate
SwiftUI: Using PrimerCheckout view with onCompletion callback
Updated result handling — Using PaymentResult instead of CheckoutData
Updated theming — Using PrimerCheckoutTheme instead of PrimerTheme (if customizing)
Tested 3DS flows — Optional delegate callbacks available for 3DS lifecycle
No server-side changes — Backend integration remains the same
No package changes — Same PrimerSDK dependency
Known behavior changes from v2.x
These changes may affect existing integrations that also use v2.x Drop-In or Headless:
PrimerLocaleData region code: When providing only languageCode (e.g., PrimerLocaleData(languageCode: "de")), the regionCode now defaults to nil instead of the device region. This produces locale code "de" instead of "de-US". To preserve v2.x behavior, explicitly pass regionCode.
Mastercard number formatting: Card number display changed from groups of 4-6-6 (5555 550000 4444) to standard 4-4-4-4 (5555 5500 0044 44), matching industry standard and other platforms.
PrimerPaymentMethodType is now public: All payment method type cases are part of the public API. Merchants can use these for conditional logic (e.g., if type == .paymentCard).