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

# PrimerCheckoutSession

> Session owner for composable, inline checkout with the .primerCheckoutSession modifier

> [!WARNING]
>
> Primer Checkout iOS SDK is currently in **beta** (v3.0.0-beta.8).
> The API is subject to change before the stable release.

`PrimerCheckoutSession` is the owner of a CheckoutComponents session. You hold it (typically as a `@StateObject`) and wire it into your SwiftUI view hierarchy with the [`.primerCheckoutSession(_:theme:onCompletion:)`](#view-modifier) modifier. The session builds the SDK, runs client-token initialization, creates the checkout scope, and bridges per-method scopes into observable session objects that the composable views (such as [`PrimerCardForm`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) and [`PrimerPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods)) consume from the environment.

Any Primer composable view placed under the modifier resolves its session from the environment. The managed modal [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) does not use a session: it builds its own checkout and needs neither `PrimerCheckoutSession` nor the modifier. While the session is `.ready`, the SDK presents its own follow-up screens (processing, success and failure, plus saved-method management and CVV recapture) in a sheet over your layout. See [What the modifier provides](#what-the-modifier-provides).

> [!NOTE]
>
> This is the iOS analog of the Android `PrimerCheckoutHost`. Where Android wraps your layout in a composable host, iOS wires the session into the environment with a view modifier — child composable views read their per-feature session from `EnvironmentValues`.

## Declaration

```swift theme={"dark"}
@available(iOS 15.0, *)
@MainActor
public final class PrimerCheckoutSession: ObservableObject
```

## Initializer

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

| Parameter | Type | Default | Description |
| - | - | - | - |
| `clientToken` | `String` | Required | The client token from your `POST /client-session` call. See [Client session](https://primer.io/docs/checkout/client-session.md). |
| `settings` | [`PrimerSettings`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings) | `PrimerSettings()` | SDK configuration applied to this session. |
| `theme` | [`PrimerCheckoutTheme`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/theming/primer-theme) | `PrimerCheckoutTheme()` | Design tokens applied to all SDK components within the session. |
| `idempotencyKey` | `@Sendable () -> String?` | `{ nil }` | Declarative idempotency-key provider, invoked once per payment attempt just before the SDK creates the payment. Return `nil` to opt out. Used when `onBeforePaymentCreate` is not set, or when its decision continues without a key. |
| `onShippingAddressChange` | `ShippingAddressChangeHandler?` | `nil` | Express Checkout: supplies the shipping options for the address the shopper entered in the Apple Pay sheet. See [Express Checkout shipping](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/express-checkout-shipping.md). |
| `onShippingOptionChange` | `ShippingOptionChangeHandler?` | `nil` | Express Checkout: commits the option the shopper selected, through your backend. Set it together with `onShippingAddressChange`. |

## Phase

The lifecycle phase of the session. Terminal outcomes (success, failure, dismissed) are **not** delivered here — they are delivered through the modifier's `onCompletion`.

```swift theme={"dark"}
public enum Phase: Equatable {
    case initializing
    case ready
}
```

| Case | Description |
| - | - |
| `initializing` | The session is building the SDK and running client-token initialization. Child sessions (`cardForm`, `selection`) are `nil`. |
| `ready` | Initialization completed. Child sessions resolve and the composable views render. |

## Published properties

| Property | Type | Description |
| - | - | - |
| `phase` | `Phase` | The current lifecycle phase. Declared `@Published public private(set)` — observe it, but only the session mutates it. |
| `clientSession` | [`PrimerClientSession?`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state#primerclientsession) | The resolved client session — order total, currency, line items, customer, fees. `nil` until the session reaches `.ready`. It is a snapshot from the last time the session reached `.ready`, and each successful `refresh()` replaces it. A client-session update made mid-checkout, for example a surcharge or a billing address, does not change it. Call `refresh()` to pick up the latest values. `cancel()` sets it to `nil`. The modifier calls `cancel()` when the view disappears, so it is `nil` again until the restarted session reaches `.ready`. Declared `@Published public private(set)`. |

This is how you read the order the customer is paying for:

```swift theme={"dark"}
struct OrderSummary: View {
  @ObservedObject var session: PrimerCheckoutSession

  var body: some View {
    if let clientSession = session.clientSession {
      VStack(alignment: .leading) {
        if let amount = clientSession.totalAmount,
           let formatted = session.formatAmount(amount) {
          Text("Total: \(formatted)")
        }
        ForEach(clientSession.lineItems ?? [], id: \.itemId) { item in
          Text(item.itemDescription ?? "")
        }
      }
    }
  }
}
```

## Properties

| Property | Type | Description |
| - | - | - |
| `onBeforePaymentCreate` | `BeforePaymentCreateHandler?` | Called before a payment is created. Use the decision handler to provide an idempotency key or abort payment creation. Mutations are forwarded to the checkout scope immediately, so assigning after the session reaches `.ready` still takes effect for the next payment attempt. |
| `idempotencyKey` | `@Sendable () -> String?` | Declarative idempotency-key provider, invoked once per payment attempt just before the SDK creates the payment. Return `nil` (default) to opt out. Used when `onBeforePaymentCreate` is not set, or when its decision continues without a key. Mutations are forwarded to the checkout scope immediately, so post-`.ready` assignment still applies. |
| `onShippingAddressChange` | `ShippingAddressChangeHandler?` | Express Checkout: supplies the shipping options while the Apple Pay sheet is open. Mutations are forwarded to the checkout scope immediately, so post-`.ready` assignment still applies. |
| `onShippingOptionChange` | `ShippingOptionChangeHandler?` | Express Checkout: commits the option the shopper selected. Mutations are forwarded to the checkout scope immediately, so post-`.ready` assignment still applies. |
| `cardForm` | [`PrimerCardFormSession?`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session) | The card-form sub-session, lazily created and cached. Non-`nil` only once `phase == .ready`. |
| `selection` | `PrimerSelectionSession?` | The payment-method-selection sub-session, lazily created and cached. Non-`nil` only once `phase == .ready`. |

> [!NOTE]
>
> `onBeforePaymentCreate` uses the `BeforePaymentCreateHandler` type:
>
> ```swift theme={"dark"}
> public typealias BeforePaymentCreateHandler = @Sendable (
>     _ data: PrimerCheckoutPaymentMethodData,
>     _ decisionHandler: @escaping (PrimerPaymentCreationDecision) -> Void
> ) -> Void
> ```
>
> If you set `onBeforePaymentCreate`, the key you pass to `.continuePaymentCreation(withIdempotencyKey:)` wins. When your decision continues without a key, the SDK falls back to the `idempotencyKey` provider.

The SDK calls `onBeforePaymentCreate` before every payment, including payments with a saved payment method. It waits until you call `decisionHandler`, and there is no timeout. If you have not answered after 5 seconds, the SDK logs a warning. While it waits, the SDK ignores Back, Cancel and "Choose other payment methods" on its own screens. Your own layout stays interactive, so disable your pay button and payment-method list until you call `decisionHandler`. If the checkout is closed before you answer, a later `decisionHandler` call starts no payment.

> [!NOTE]
>
> The two shipping handlers are for Express Checkout. They run while the Apple Pay sheet is open and must return within 20 seconds:
>
> ```swift theme={"dark"}
> public typealias ShippingAddressChangeHandler = @Sendable (
>     _ change: PrimerShippingAddressChange
> ) async throws -> [PrimerShippingOption]
>
> public typealias ShippingOptionChangeHandler = @Sendable (
>     _ change: PrimerShippingOptionChange
> ) async throws -> Void
> ```
>
> See [Express Checkout shipping](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/express-checkout-shipping.md) for the full flow.

## Methods

| Method | Description |
| - | - |
| `start() async` | Builds the SDK and drives the session to `.ready`. Calls made once the session is `.ready` are ignored. Do not call it again while the session is still initializing. The modifier calls this for you on appear, and again after `cancel()` when the view reappears. |
| `refresh() async` | Re-fetches configuration and payment methods from the backend and returns the session to `.ready`. No-op unless the session has finished initializing; concurrent calls are ignored. A failure arrives in `onCompletion` as `.failure` but does not end the session. `phase` returns to `.ready`, you can call `refresh()` again, and a later `.success` or `.dismissed` still arrives. |
| `cancel()` | Tears the session down: dismisses the checkout scope, clears resources, drops cached sub-sessions, sets `clientSession` to `nil` and resets `phase` to `.initializing`. The next `start()` rebuilds the session. Idempotent. The modifier wires this to `onDisappear`, which also fires on a transient disappearance, such as a tab switch or a navigation push. The session is then rebuilt when the view reappears. |
| `formatAmount(_ amountInMinorUnits: Int) -> String?` | Formats an amount in minor units the way the SDK's own screens do. `nil` before the session reaches `.ready`, when no currency is known yet. |

### formatAmount(\_:)

```swift theme={"dark"}
public func formatAmount(_ amountInMinorUnits: Int) -> String?
```

Formats an amount in minor units for display, so a total you print yourself matches what the SDK prints on its own screens.

The currency comes from the client session. Its decimal digits come from the backend and cannot be worked out from [`PrimerClientSession.currencyCode`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state#primerclientsession) alone, because some currencies have none and some have three. The locale is the one in [`PrimerSettings.localeData`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings), which defaults to the device.

```swift theme={"dark"}
if let total = session.clientSession?.totalAmount,
   let formatted = session.formatAmount(total) {
  Text("Pay \(formatted)")
}
```

| Parameter | Type | Description |
| - | - | - |
| `amountInMinorUnits` | `Int` | The amount in the currency's minor units, for example cents. |

Returns `nil` before the session reaches `.ready`.

> [!TIP]
>
> You normally never call `start()` or `cancel()` yourself — the `.primerCheckoutSession` modifier handles the lifecycle. Call `refresh()` when you need to reload after a client-session update (for example, an amount or currency change).

## View modifier

The `View.primerCheckoutSession(_:theme:onCompletion:)` modifier wires a `PrimerCheckoutSession` into the SwiftUI environment, bootstraps it on appear, and tears it down on disappear. Apply it once around the Primer composable views you embed inline in your own layout. The managed modal [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) does not use it.

```swift theme={"dark"}
@available(iOS 15.0, *)
public extension View {
    func primerCheckoutSession(
        _ session: PrimerCheckoutSession,
        theme: PrimerCheckoutTheme? = nil,
        onCompletion: ((PrimerCheckoutState) -> Void)? = nil
    ) -> some View
}
```

| Parameter | Type | Default | Description |
| - | - | - | - |
| `session` | `PrimerCheckoutSession` | Required | The session injected into the environment for child composable views to resolve. |
| `theme` | [`PrimerCheckoutTheme?`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/theming/primer-theme) | `nil` | Overrides the theme the session was created with, re-applied whenever the value changes. Use it to follow a theme your own view state owns, for example a brand or colour-scheme switch. |
| `onCompletion` | `((PrimerCheckoutState) -> Void)?` | `nil` | Receives the outcome states. `.success` and `.dismissed` arrive at most once and end the session. A `.failure` while `start()` is initializing the session also ends it. Any other `.failure`, such as a failed payment attempt or a failed `refresh()`, does not end the session, so `onCompletion` can be called again. A session that restarts after `cancel()`, for example when the view reappears, delivers its own outcome. See [`PrimerCheckoutState`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state). |

### What the modifier provides

1. **Environment injection** — exposes the `PrimerCheckoutSession`, the derived `PrimerCardFormSession`, and the `PrimerSelectionSession` through `EnvironmentValues`, so child views like `PrimerCardForm` and `PrimerPaymentMethods` resolve their per-feature session automatically.
2. **Lifecycle bootstrapping** — calls `start()` on appear and `cancel()` on disappear.
3. **Overlay management** — once the session is `.ready`, the modifier presents the SDK's own follow-up screens in a sheet over your layout: processing, success and failure after any payment you start, the screens of payment methods that need one, the saved-methods list opened by `showAll()` with its delete confirmation, and the CVV recapture screen that `selectVaulted(_:)` raises when a saved card needs it. 3DS challenges and web redirects open their own system UI. Do not present a sheet of your own while one of these is up. The customer cannot swipe the SDK's sheet away while the processing screen is showing or while your `onBeforePaymentCreate` handler has not yet called `decisionHandler`. At any other time, swiping the sheet away closes it and returns to your layout without ending the session.

> [!WARNING]
>
> Without the `.primerCheckoutSession` modifier above them in the hierarchy, composable views such as `PrimerCardForm` and `PrimerPaymentMethods` cannot resolve a session and will not function.

## Usage

Hold the session as a `@StateObject`, place the composable views in a `ScrollView`, and attach the modifier with your completion handler.

```swift theme={"dark"}
import SwiftUI
import PrimerSDK

@available(iOS 15.0, *)
struct CheckoutView: View {
    @StateObject private var session: PrimerCheckoutSession

    init(clientToken: String) {
        _session = StateObject(
            wrappedValue: PrimerCheckoutSession(clientToken: clientToken)
        )
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 24) {
                PrimerCardForm()
                PrimerPaymentMethods()
            }
            .padding()
        }
        .primerCheckoutSession(session) { state in
            switch state {
            case .success(let data):
                print("Payment complete: \(data)")
            case let .failure(error, _):
                print("Payment failed: \(error)")
            case .dismissed:
                print("Checkout dismissed")
            default:
                break
            }
        }
    }
}
```

The child composable views resolve their per-feature session from the environment — you do not pass a session into them. Behind the scenes the modifier injects `PrimerCardFormSession` and `PrimerSelectionSession`, which `PrimerCardForm` and `PrimerPaymentMethods` bind to once the session reaches `.ready`.

### Supplying an idempotency key

Provide the declarative `idempotencyKey` provider at construction, or assign `onBeforePaymentCreate` for full control over the payment-creation decision. Either may be assigned after `.ready` — the change applies to the next payment attempt.

```swift theme={"dark"}
let session = PrimerCheckoutSession(
    clientToken: token,
    idempotencyKey: { UUID().uuidString }
)
```

## See also

> [card] [PrimerCheckout](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout.md)
>
> The managed modal entry point. It builds its own checkout and does not use a session.

> [card] [PrimerCardForm](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form.md)
>
> Composable card form that resolves its session from the environment.

> [card] [PrimerPaymentMethods](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods.md)
>
> Composable payment-method selection list for inline layouts.

> [card] [PrimerCheckoutState](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state.md)
>
> The terminal outcome delivered to onCompletion.


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