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

# SwiftUI navigation

> Present and embed checkout within SwiftUI navigation

> [!WARNING]
>
> Primer Checkout iOS SDK is currently in **beta** (v3.0.0-beta.8).
> 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(_:theme: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.

> [!NOTE]
>
> Primer Checkout requires iOS 15.0 or later — all checkout views and sessions are annotated `@available(iOS 15.0, *)`, and `PrimerCheckout` and the session classes (`PrimerCheckoutSession`, `PrimerCardFormSession`, `PrimerSelectionSession`) are also `@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

Both SwiftUI shapes report their outcome through `onCompletion` with a `PrimerCheckoutState`. UIKit's `PrimerCheckoutPresenter` uses `PrimerCheckoutPresenterDelegate` instead. The callback receives `.failure` once per failed attempt, and `.success` or `.dismissed` once the checkout ends. After `.success` or `.dismissed`, it delivers nothing more. For an inline session, a failure during initialization is the last callback. The callback never receives the lifecycle cases `.initializing` and `.ready(clientSession:)`. For an inline session, read the lifecycle from `PrimerCheckoutSession.phase` and `clientSession`. Switch on the state to decide how navigation should react.

| Case | Associated value | Drives |
| - | - | - |
| `success` | `PaymentResult` | Dismiss and push a confirmation |
| `dismissed` | — | User closed checkout; restore prior screen |
| `failure` | `PrimerError`, `checkoutData: PrimerCheckoutData?` | With the default `isErrorScreenEnabled: true` in `PrimerUIOptions`, the SDK shows its error screen and checkout stays open. This applies to a failed payment attempt, and to a failed initialization in `PrimerCheckout`. After a failed payment attempt, the screen offers a retry. When another payment method is available, it also offers that method. A later attempt delivers another outcome. For an inline `PrimerCheckoutSession`, an initialization failure ends the session and the SDK shows nothing, so show your own error UI. With `isErrorScreenEnabled: false` in `PrimerUIOptions`, the SDK shows nothing, so show your own error UI or close checkout. `checkoutData` carries the payment and order IDs when a payment was created before the failure. It is `nil` when the failure happened earlier (initialization, tokenization, connectivity). Match it with `case let .failure(error, checkoutData)`. |

> [!WARNING]
>
> `PrimerCheckoutState` is an evolving enum. Always include a `default` case when switching on it so future cases compile without breaking your navigation logic.

## Modal flow with `PrimerCheckout`

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

```swift theme={"dark"}
public init(
  clientToken: String,
  primerSettings: PrimerSettings = PrimerSettings(),
  primerTheme: PrimerCheckoutTheme = PrimerCheckoutTheme(),
  onCompletion: ((PrimerCheckoutState) -> Void)? = nil,
  onShippingAddressChange: ShippingAddressChangeHandler? = nil,
  onShippingOptionChange: ShippingOptionChangeHandler? = nil
)
```

### Sheet presentation

```swift theme={"dark"}
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
            }
          }
        )
      }
  }
}
```

While a payment is processing, the customer cannot swipe the sheet away. Apple Pay and Stripe ACH show no processing screen, so the sheet is not locked while they run.

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

```swift theme={"dark"}
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)
  }
}
```

> [!TIP]
>
> 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(_:theme:onCompletion:)` modifier. The modifier injects the session into the environment, so any Primer composable placed under it resolves automatically.

```swift theme={"dark"}
public init(
  clientToken: String,
  settings: PrimerSettings = PrimerSettings(),
  theme: PrimerCheckoutTheme = PrimerCheckoutTheme(),
  idempotencyKey: @escaping @Sendable () -> String? = { nil },
  onShippingAddressChange: ShippingAddressChangeHandler? = nil,
  onShippingOptionChange: ShippingOptionChangeHandler? = nil
)
```

```swift theme={"dark"}
@available(iOS 15.0, *)
public extension View {
  func primerCheckoutSession(
    _ session: PrimerCheckoutSession,
    theme: PrimerCheckoutTheme? = nil,
    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.

```swift theme={"dark"}
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) }
    }
  }
}
```

> [!NOTE]
>
> 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.

```swift theme={"dark"}
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 a payment `.failure` while the SDK error screen is enabled (the default). An inline session that fails to initialize ends, so show your own error UI.

```swift theme={"dark"}
struct CheckoutContainer: View {
  @State private var showCheckout = false
  @State private var confirmation: String?
  let clientToken: String

  var body: some View {
    NavigationStack {
      Button("Checkout") { showCheckout = true }
        .sheet(isPresented: $showCheckout) {
          PrimerCheckout(
            clientToken: clientToken,
            onCompletion: { state in
              switch state {
              case .success(let result):
                showCheckout = false
                confirmation = result.paymentId
              case .dismissed:
                showCheckout = false
              case .failure:
                break
              default:
                break
              }
            }
          )
        }
        .navigationDestination(item: $confirmation) { paymentId in
          ConfirmationScreen(paymentId: paymentId)
        }
    }
  }
}
```

> [!NOTE]
>
> 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(_:theme:onCompletion:)` drives the navigation in identical fashion — pop the embedding screen on `.dismissed`, push a confirmation on `.success`.

> [!WARNING]
>
> Do not dismiss checkout on a payment `.failure` while the SDK error screen is enabled (`isErrorScreenEnabled` in `PrimerUIOptions`, which defaults to `true`). The SDK shows the error inside checkout so the customer can retry or, where available, choose another payment method. Dismissing would discard that recoverable state. If you pass `PrimerSettings(uiOptions: PrimerUIOptions(isErrorScreenEnabled: false))`, the SDK shows no error UI and leaves the failure to you. Show your own error or dismiss checkout. An inline session that fails to initialize shows no SDK error, so handle that failure yourself.

## See also

> [card] [PrimerCheckoutSession](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session.md)
>
> Own the inline session lifecycle and sub-sessions

> [card] [PrimerCheckout](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout.md)
>
> The prebuilt modal checkout view

> [card] [PrimerCardForm](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form.md)
>
> Compose the card form inline with slots

> [card] [Handle payment result](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result.md)
>
> React to checkout outcomes


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.