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

# UIKit integration

> Integrate the Primer Checkout iOS SDK in a UIKit app

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

The Primer Checkout iOS SDK is built with SwiftUI, but it ships a UIKit entry point so you can integrate it from view-controller based apps without rewriting your navigation. [`PrimerCheckoutPresenter`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter) presents the managed checkout as a modal sheet from any `UIViewController` and delivers the result through a delegate.

For most UIKit apps, the presenter is all you need. If you want full control over the layout, you can also embed the SwiftUI composable views ([`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)) inside a `UIHostingController` — see [Embedding the composable views](#embedding-the-composable-views).

> [!NOTE]
>
> `PrimerCheckoutPresenter` is `@available(iOS 15.0, *)` and `@MainActor`. Call its static methods from the main thread — the default for UIKit lifecycle callbacks. For pure SwiftUI apps, use [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) directly instead.

## Present checkout

There are three steps:

1. Assign the [`delegate`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter) on `PrimerCheckoutPresenter.shared`.
2. Call `presentCheckout(...)`, passing the client token and the view controller to present from.
3. Implement `PrimerCheckoutPresenterDelegate` to handle the result.

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

final class CheckoutViewController: UIViewController {

  private let clientToken: String

  init(clientToken: String) {
    self.clientToken = clientToken
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  override func viewDidLoad() {
    super.viewDidLoad()
    PrimerCheckoutPresenter.shared.delegate = self
  }

  @IBAction func payButtonTapped(_ sender: UIButton) {
    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self
    )
  }
}

extension CheckoutViewController: PrimerCheckoutPresenterDelegate {

  func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult) {
    let confirmationVC = ConfirmationViewController(result: result)
    navigationController?.pushViewController(confirmationVC, animated: true)
  }

  func primerCheckoutPresenterDidFailWithError(_ error: PrimerError, checkoutData: PrimerCheckoutData?) {
    print("Payment \(checkoutData?.payment?.id ?? "-") failed: \(error.localizedDescription)")
  }

  func primerCheckoutPresenterDidDismiss() {
    // The shopper closed checkout without completing a payment.
  }
}
```

> [!NOTE]
>
> `PrimerCheckoutPresenter.shared.delegate` is a `weak` reference. Keep the conforming object (here, the view controller) alive for as long as checkout is on screen, otherwise the callbacks will not fire.

Generate the `clientToken` from your backend with the [client session](https://primer.io/docs/checkout/client-session.md) API.

## Presentation overloads

`presentCheckout` has several overloads that differ in how much you configure and whether you supply the presenting view controller yourself. Every overload accepts an optional `completion` closure (trailing-closure syntax works) that runs once the checkout UI has finished presenting.

```swift theme={"dark"}
// Minimal — automatic view controller detection, no settings.
PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken)

// From an explicit view controller.
PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)

// With custom settings.
PrimerCheckoutPresenter.presentCheckout(
  clientToken: clientToken,
  from: self,
  primerSettings: PrimerSettings(uiOptions: PrimerUIOptions(isSuccessScreenEnabled: false))
)

// With custom settings and theme.
PrimerCheckoutPresenter.presentCheckout(
  clientToken: clientToken,
  from: self,
  primerSettings: PrimerSettings(),
  primerTheme: PrimerCheckoutTheme()
)
```

### Parameters

| Parameter | Type | Default | Description |
| - | - | - | - |
| `clientToken` | `String` | — | The client token for the session. Generate it from your backend with the [client session](https://primer.io/docs/checkout/client-session.md) API. |
| `viewController` | `UIViewController` | — | The view controller to present from. Omitted in the automatic-detection overloads. |
| `primerSettings` | `PrimerSettings` | — | Configuration applied to this checkout session. Overloads that omit it use the settings most recently registered in this app session, or `PrimerSettings()` if none were. A previous checkout or `Primer.shared.configure(settings:)` registers settings. See [`PrimerSettings`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings). |
| `primerTheme` | `PrimerCheckoutTheme` | — | Theme configuration for the [design tokens](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/theming/primer-theme.md). Only on the overload that also takes `from:` and `primerSettings`, where it is required. The other overloads use `PrimerCheckoutTheme()`. |
| `completion` | `(() -> Void)?` | `nil` | Optional closure invoked once the checkout UI has finished presenting. If a presentation is already in progress, the call is ignored and the closure runs immediately. It is not called when automatic detection finds no view controller. |
| `onShippingAddressChange` | `ShippingAddressChangeHandler?` | `nil` | Apple Pay Express Checkout: returns the shipping options for the shopper's address. Only on the overloads that take `primerSettings`. |
| `onShippingOptionChange` | `ShippingOptionChangeHandler?` | `nil` | Apple Pay Express Checkout: commits the shipping option the shopper selected. Only on the overloads that take `primerSettings`. |

> [!WARNING]
>
> The automatic-detection overloads (no `from:` parameter) locate the top-most presenting view controller for you. If no presenting view controller can be found, they report a [`PrimerError`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error) through `primerCheckoutPresenterDidFailWithError(_:checkoutData:)`. Assign the `delegate` before calling them.

> [!NOTE]
>
> The overloads that take `primerSettings` or `primerTheme` are **not** `@objc` compatible because of those Swift-only parameter types. From Objective-C, use the `clientToken:` / `clientToken:from:` overloads. They use the same fallback settings as the other overloads that omit `primerSettings`. They also cannot take the shipping handlers.

## Implement the delegate

Conform to `PrimerCheckoutPresenterDelegate` to receive the checkout result. All three methods are required. A failed 3-D Secure challenge is reported like any other failure, through `primerCheckoutPresenterDidFailWithError(_:checkoutData:)`.

| Method | Description |
| - | - |
| `primerCheckoutPresenterDidCompleteWithSuccess(_:)` | Called when payment is successful, with the [`PaymentResult`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state#paymentresult). |
| `primerCheckoutPresenterDidFailWithError(_:checkoutData:)` | Called once per failed attempt, with a [`PrimerError`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error). `checkoutData` carries the payment id and order id when the payment was created before the failure, and is `nil` otherwise. |
| `primerCheckoutPresenterDidDismiss()` | Called when checkout is dismissed without completing a payment. |

```swift theme={"dark"}
func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult)
func primerCheckoutPresenterDidFailWithError(_ error: PrimerError, checkoutData: PrimerCheckoutData?)
func primerCheckoutPresenterDidDismiss()
```

The success callback delivers a `PaymentResult`. Switch on its `status` to route your UI:

```swift theme={"dark"}
func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult) {
  switch result.status {
  case .success:
    showReceipt(paymentId: result.paymentId)
  case .pending:
    showPendingState(paymentId: result.paymentId)
  case .failed:
    showError(message: result.errorMessage)
  default:
    break
  }
}
```

> [!TIP]
>
> The checkout dismisses itself before it delivers `primerCheckoutPresenterDidCompleteWithSuccess(_:)`, so push your own confirmation screen from that callback. `primerCheckoutPresenterDidFailWithError(_:checkoutData:)` fires once per failed attempt, including a failed initialization. The checkout stays open while the SDK error screen offers a retry, and closes on the failure only when `isErrorScreenEnabled` is `false`. For the full payment-result model, see [Handle payment result](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result.md).

> [!NOTE]
>
> The shopper can swipe the sheet down when `PrimerSettings.uiOptions.dismissalMechanism` contains `.gestures` (the default), except while a payment is processing. A swipe on the success screen delivers `primerCheckoutPresenterDidCompleteWithSuccess(_:)`. A swipe anywhere else, including the error screen after a failure was reported, delivers `primerCheckoutPresenterDidDismiss()`.

## Dismiss programmatically

Call `dismiss(animated:completion:)` to close checkout from your own code (for example, after a timeout). This triggers `primerCheckoutPresenterDidDismiss()` on the delegate.

```swift theme={"dark"}
PrimerCheckoutPresenter.dismiss(animated: true) {
  print("Checkout dismissed")
}
```

| Parameter | Type | Default | Description |
| - | - | - | - |
| `animated` | `Bool` | `true` | Accepted for API compatibility. The sheet always dismisses with animation, whatever value you pass. |
| `completion` | `(() -> Void)?` | `nil` | Optional closure invoked after dismissal completes. If no checkout is on screen, it runs immediately and `primerCheckoutPresenterDidDismiss()` is not called. |

## Check availability

Use the static properties to guard presentation:

```swift theme={"dark"}
// Check if checkout can be presented on the current OS version.
guard PrimerCheckoutPresenter.isAvailable else { return }

// Avoid presenting twice.
guard !PrimerCheckoutPresenter.isPresenting else { return }

PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)
```

| Property | Type | Description |
| - | - | - |
| `isAvailable` | `Bool` | `static`. `true` when CheckoutComponents can be presented on the current OS version. |
| `isPresenting` | `Bool` | `static`. `true` while a checkout is being presented or is currently on screen. |

## Embedding the composable views

If the managed sheet does not match your UI, you can embed the SwiftUI composable views directly inside a UIKit screen with `UIHostingController`. Hold a [`PrimerCheckoutSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) as a `@StateObject`, place [`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) under the `.primerCheckoutSession(_:theme:onCompletion:)` modifier, and host that SwiftUI view in your view controller.

First, define the SwiftUI content:

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

@available(iOS 15.0, *)
struct EmbeddedCheckoutView: View {
  @StateObject private var session: PrimerCheckoutSession
  let onCompletion: (PrimerCheckoutState) -> Void

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

  var body: some View {
    ScrollView {
      VStack(spacing: 24) {
        PrimerCardForm()
        PrimerPaymentMethods()
      }
      .padding()
    }
    .primerCheckoutSession(session, onCompletion: onCompletion)
  }
}
```

Then host it from your view controller and add it as a child:

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

@available(iOS 15.0, *)
final class EmbeddedCheckoutViewController: UIViewController {

  private let clientToken: String

  init(clientToken: String) {
    self.clientToken = clientToken
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  override func viewDidLoad() {
    super.viewDidLoad()

    let checkoutView = EmbeddedCheckoutView(clientToken: clientToken) { [weak self] state in
      self?.handle(state)
    }

    let hostingController = UIHostingController(rootView: checkoutView)
    addChild(hostingController)
    hostingController.view.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(hostingController.view)

    NSLayoutConstraint.activate([
      hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
      hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
      hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
      hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])

    hostingController.didMove(toParent: self)
  }

  private func handle(_ state: PrimerCheckoutState) {
    switch state {
    case let .success(result):
      showReceipt(paymentId: result.paymentId)
    case let .failure(error, _):
      showError(message: error.localizedDescription)
    case .dismissed:
      navigationController?.popViewController(animated: true)
    case .initializing, .ready:
      break
    @unknown default:
      break
    }
  }
}
```

> [!WARNING]
>
> The `.primerCheckoutSession(_:theme:onCompletion:)` modifier must sit above the composable views in the hierarchy. It injects the per-feature sessions into the environment and bootstraps the session lifecycle (`start()` on appear, `cancel()` on disappear). Without it, `PrimerCardForm` and `PrimerPaymentMethods` cannot resolve a session and will not function.

> [!TIP]
>
> With embedded composables you receive the outcome as a [`PrimerCheckoutState`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state) in the modifier's `onCompletion`, rather than through the presenter delegate. The state-driven model is identical to a pure SwiftUI integration.

## See also

> [card] [PrimerCheckoutPresenter](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter.md)
>
> The full UIKit entry-point API: overloads, delegate, and result types.

> [card] [PrimerCheckoutState](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state.md)
>
> The state model returned to embedded composable integrations.

> [card] [Handle payment result](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result.md)
>
> Route your UI based on the checkout outcome.

> [card] [SwiftUI navigation](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/guides/swiftui-navigation.md)
>
> Presentation patterns for pure SwiftUI apps.


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