Skip to main content
Primer Checkout iOS SDK is currently in beta (v3.0.0-beta.8). The API is subject to change before the stable release.
Every checkout ends in one of three outcomes: the payment succeeds, it fails, or the user dismisses the flow. This guide shows how to receive and react to that outcome in both SwiftUI and UIKit. The outcome is always delivered as a PrimerCheckoutState value:
The same PrimerCheckoutState also has lifecycle cases (.initializing, .ready). onCompletion never receives them. With PrimerCheckoutSession, observe its phase for the lifecycle instead. Always include a default case when switching, so future enum additions stay source-compatible.

SwiftUI

Using PrimerCheckout

The prebuilt PrimerCheckout view accepts an onCompletion closure. It receives .failure once per failed attempt, including a failed initialization. While the SDK error screen is enabled (uiOptions.isErrorScreenEnabled, the default), the checkout stays open so the customer can retry. It then receives .success or .dismissed exactly once, and nothing after that.

Using the .primerCheckoutSession modifier

When you embed the composable views (for example PrimerCardForm) inline in your own layout, hold a PrimerCheckoutSession as a @StateObject and receive the outcome through the onCompletion parameter of the .primerCheckoutSession(_:theme:onCompletion:) modifier.
.success and .dismissed are delivered at most once — the session latches the first state that ends it, so a later .dismissed produced by a view-lifecycle teardown is ignored..failure is different. Once checkout is ready, a failed payment attempt does not end the session: the SDK shows its error screen, the customer can retry, and onCompletion is called again with the next attempt’s outcome. Only a failure before the checkout becomes ready is terminal. Keep your checkout UI alive on .failure — otherwise a customer who retries after a decline and then succeeds never reaches your success handling.
Use the PaymentResult to drive navigation. PaymentResult is Equatable but not Hashable, so it cannot be appended to a NavigationPath or used with navigationDestination(for:) directly. Instead, store the result in @State and present the confirmation screen with navigationDestination(isPresented:). NavigationStack and navigationDestination(isPresented:) require iOS 16. On iOS 15, use NavigationView with a NavigationLink(isActive:) instead.

UIKit

In UIKit, present the checkout with PrimerCheckoutPresenter and adopt PrimerCheckoutPresenterDelegate to receive the outcome. The presenter closes its own sheet, so you don’t dismiss it yourself. It finishes dismissing the sheet before it calls primerCheckoutPresenterDidCompleteWithSuccess(_:). primerCheckoutPresenterDidDismiss() is called when the customer closes the checkout without completing a payment. primerCheckoutPresenterDidFailWithError(_:checkoutData:) is called once per failed attempt, including a failed initialization. The sheet stays open on the SDK error screen so the customer can retry. It closes on failure only when uiOptions.isErrorScreenEnabled is false.
Set the delegate before calling presentCheckout. If no delegate is set when checkout completes, the outcome is logged and otherwise dropped.
The delegate maps one-to-one onto the SwiftUI PrimerCheckoutState cases:

Reading the PaymentResult

On success you receive a PaymentResult. Read its properties to build your confirmation UI, send analytics, or reconcile against your backend. amount and currencyCode can be nil, and they are always nil for card, saved and Stripe ACH payment methods. If you need them, read them from your own order or from your backend, using paymentId. The Charged line in the sample below only shows when both values are present.
status is always .success for card, saved and Stripe ACH payment methods in a .success(PaymentResult) outcome. Other payment methods report the status from the Payments API response. It can be .pending until the processor confirms the payment, as with some Klarna, PayPal, web redirect or QR code payments. Check status before you fulfill an order. Primer Checkout creates the payment for you, so PrimerSettings(paymentHandling: .manual) is not supported.

Handling the PrimerError

On failure you receive a PrimerError. The SDK already surfaces failures inside the checkout UI, so you typically use the error for logging, analytics, and support workflows rather than for displaying a message yourself. Together with the error, you receive checkoutData: PrimerCheckoutData?. In SwiftUI, bind it with case let .failure(error, checkoutData). In UIKit, it is the checkoutData parameter of primerCheckoutPresenterDidFailWithError(_:checkoutData:). If a payment was created before the failure, for example on a decline, use checkoutData?.payment?.id and checkoutData?.payment?.orderId to find it on your backend. checkoutData is nil if the failure happened before a payment existed, for example during initialization or tokenization, or after a connectivity error.
Always log diagnosticsId. Primer support uses it to trace the exact failed request when you raise an issue.

See also

PrimerCheckoutState

The full state enum and the PaymentResult model.

PrimerError

Error categories, IDs, and diagnostics.

PrimerCheckoutPresenter

The UIKit entry point and delegate callbacks.

PrimerCheckout

The prebuilt SwiftUI checkout view.