Skip to main content
Primer Checkout iOS SDK is currently in beta (v3.0.0-beta.1). The API is subject to change before the stable release.
Primer Checkout is built entirely in SwiftUI, so it slots into any navigation pattern you already use. There are two integration shapes:
  • Modal flow — drop PrimerCheckout into a .sheet, .fullScreenCover, or NavigationStack destination and let the SDK render its prebuilt screens.
  • Inline embedding — hold a PrimerCheckoutSession as @StateObject, apply the .primerCheckoutSession(_:onCompletion:) modifier, and place composable views such as PrimerCardForm and PrimerPaymentMethods directly in your own layout.
Both shapes deliver outcomes through a PrimerCheckoutState callback, so you drive navigation from the same .success / .dismissed / .failure cases regardless of how checkout is presented.
Primer Checkout requires iOS 15.0 or later — all checkout views and sessions are annotated @available(iOS 15.0, *) and @MainActor. Some SwiftUI navigation hosts shown below require a newer OS than the SDK floor: NavigationStack and .navigationDestination(for:) require iOS 16.0+, and the .navigationDestination(item:) overload requires iOS 17.0+. On an iOS 15 deployment target, present checkout with .sheet or .fullScreenCover instead.

Reading the completion state

Every entry point reports its terminal outcome through PrimerCheckoutState. Switch on it to decide how navigation should react.
CaseAssociated valueDrives
initializingLoading; no navigation change
readytotalAmount: Int, currencyCode: StringCheckout is interactive
successPaymentResultDismiss and push a confirmation
dismissedUser closed checkout; restore prior screen
failurePrimerErrorError is shown in-UI; usually keep checkout open
PrimerCheckoutState is an evolving enum. Always include a default case when switching on it so future cases compile without breaking your navigation logic.
PrimerCheckout is a self-contained View that renders the SDK’s default screens. Use it whenever you want the fastest integration and do not need to recompose the UI.
public init(
  clientToken: String,
  primerSettings: PrimerSettings = PrimerSettings(),
  primerTheme: PrimerCheckoutTheme = PrimerCheckoutTheme(),
  onCompletion: ((PrimerCheckoutState) -> Void)? = nil
)

Sheet presentation

struct CartView: View {
  @State private var showCheckout = false
  let clientToken: String

  var body: some View {
    Button("Pay now") { showCheckout = true }
      .sheet(isPresented: $showCheckout) {
        PrimerCheckout(
          clientToken: clientToken,
          onCompletion: { state in
            switch state {
            case .success, .dismissed:
              showCheckout = false
            default:
              break
            }
          }
        )
      }
  }
}

Pushing onto a NavigationStack

Use a NavigationStack when checkout is one step inside a longer flow and you want a back button rather than a modal. NavigationStack(path:) and .navigationDestination(for:) require iOS 16.0+; on an iOS 15 target, use the sheet presentation above.
struct ShopFlow: View {
  @State private var path = NavigationPath()
  let clientToken: String

  var body: some View {
    NavigationStack(path: $path) {
      CartScreen(onCheckout: { path.append(Route.checkout) })
        .navigationDestination(for: Route.self) { route in
          switch route {
          case .checkout:
            PrimerCheckout(
              clientToken: clientToken,
              onCompletion: { state in
                if case .success(let result) = state {
                  path.append(Route.confirmation(result.paymentId))
                }
              }
            )
            .navigationTitle("Payment")
            .navigationBarTitleDisplayMode(.inline)
          case .confirmation(let paymentId):
            ConfirmationScreen(paymentId: paymentId)
          }
        }
    }
  }

  enum Route: Hashable {
    case checkout
    case confirmation(String)
  }
}
On a .success, append a confirmation route and pass result.paymentId along — PaymentResult also exposes status, amount, currencyCode, and paymentMethodType for your receipt screen.

Inline embedding with PrimerCheckoutSession

To recompose the UI — for example, to place PrimerCardForm inside your own scroll view alongside an order summary — hold a PrimerCheckoutSession and wire it in with the .primerCheckoutSession(_:onCompletion:) modifier. The modifier injects the session into the environment, so any Primer composable placed under it resolves automatically.
@available(iOS 15.0, *)
public init(
  clientToken: String,
  settings: PrimerSettings = PrimerSettings(),
  theme: PrimerCheckoutTheme = PrimerCheckoutTheme(),
  idempotencyKey: @escaping @Sendable () -> String? = { nil }
)
@available(iOS 15.0, *)
func primerCheckoutSession(
  _ session: PrimerCheckoutSession,
  onCompletion: ((PrimerCheckoutState) -> Void)? = nil
) -> some View
Hold the session with @StateObject so it survives view updates. The modifier bootstraps it on appear and tears it down on disappear.
struct InlineCheckoutScreen: View {
  @StateObject private var session: PrimerCheckoutSession
  let onPaid: (PaymentResult) -> Void

  init(clientToken: String, onPaid: @escaping (PaymentResult) -> Void) {
    _session = StateObject(wrappedValue: PrimerCheckoutSession(clientToken: clientToken))
    self.onPaid = onPaid
  }

  var body: some View {
    ScrollView {
      VStack(spacing: 24) {
        OrderSummary()
        PrimerCardForm()
      }
      .padding()
    }
    .primerCheckoutSession(session) { state in
      if case .success(let result) = state { onPaid(result) }
    }
  }
}
The modifier wires session.cancel() to onDisappear, which also fires on transient disappearance (tab switch, push/pop, parent sheet re-present). The session resets to a restartable state and rebuilds itself when the view reappears — no extra handling required.

Embedding the payment-method list

PrimerPaymentMethods resolves the same session from the environment, so you can compose a full selection-plus-card layout under a single modifier.
ScrollView {
  VStack(spacing: 24) {
    PrimerPaymentMethods()
    PrimerCardForm()
  }
  .padding()
}
.primerCheckoutSession(session) { state in handle(state) }

Driving navigation from the completion state

Whether modal or inline, route on the same PrimerCheckoutState cases: dismiss on .dismissed, advance on .success, and leave checkout in place on .failure so the SDK can present its own error UI.
struct CheckoutContainer: View {
  @State private var showCheckout = false
  @State private var confirmation: PaymentResult?
  let clientToken: String

  var body: some View {
    Button("Checkout") { showCheckout = true }
      .sheet(isPresented: $showCheckout) {
        PrimerCheckout(
          clientToken: clientToken,
          onCompletion: { state in
            switch state {
            case .success(let result):
              showCheckout = false
              confirmation = result
            case .dismissed:
              showCheckout = false
            case .failure:
              break
            default:
              break
            }
          }
        )
      }
      .navigationDestination(item: $confirmation) { result in
        ConfirmationScreen(paymentId: result.paymentId)
      }
  }
}
The .navigationDestination(item:) overload used above requires iOS 17.0+. On earlier targets, drive the same confirmation value through an isPresented-based .sheet or a .navigationDestination(isPresented:) binding instead.
For the inline shape, the same onCompletion closure on .primerCheckoutSession(_:onCompletion:) drives the navigation in identical fashion — pop the embedding screen on .dismissed, push a confirmation on .success.
Do not dismiss checkout on .failure. Payment errors are surfaced inside the checkout UI so the customer can retry; dismissing would discard the recoverable state.

See also

PrimerCheckoutSession

Own the inline session lifecycle and sub-sessions

PrimerCheckout

The prebuilt modal checkout view

PrimerCardForm

Compose the card form inline with slots

Handle payment result

React to checkout outcomes