> ## 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.1).
  The API is subject to change before the stable release.
</Warning>

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.

<Info>
  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.
</Info>

## Reading the completion state

Every entry point reports its terminal outcome through `PrimerCheckoutState`. Switch on it to decide how navigation should react.

| Case           | Associated value                           | Drives                                           |
| -------------- | ------------------------------------------ | ------------------------------------------------ |
| `initializing` | —                                          | Loading; no navigation change                    |
| `ready`        | `totalAmount: Int`, `currencyCode: String` | Checkout is interactive                          |
| `success`      | `PaymentResult`                            | Dismiss and push a confirmation                  |
| `dismissed`    | —                                          | User closed checkout; restore prior screen       |
| `failure`      | `PrimerError`                              | Error is shown in-UI; usually keep checkout open |

<Warning>
  `PrimerCheckoutState` is an evolving enum. Always include a `default` case when switching on it so future cases compile without breaking your navigation logic.
</Warning>

## 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
)
```

### 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
            }
          }
        )
      }
  }
}
```

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

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

```swift theme={"dark"}
@available(iOS 15.0, *)
public init(
  clientToken: String,
  settings: PrimerSettings = PrimerSettings(),
  theme: PrimerCheckoutTheme = PrimerCheckoutTheme(),
  idempotencyKey: @escaping @Sendable () -> String? = { nil }
)
```

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

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

### 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 `.failure` so the SDK can present its own error UI.

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

<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.
</Note>

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

<Warning>
  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.
</Warning>

## See also

<CardGroup cols={2}>
  <Card title="PrimerCheckoutSession" icon="window-restore" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session">
    Own the inline session lifecycle and sub-sessions
  </Card>

  <Card title="PrimerCheckout" icon="credit-card" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout">
    The prebuilt modal checkout view
  </Card>

  <Card title="PrimerCardForm" icon="table-list" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form">
    Compose the card form inline with slots
  </Card>

  <Card title="Handle payment result" icon="arrow-right" href="/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result">
    React to checkout outcomes
  </Card>
</CardGroup>
