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

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`](/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`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) and [`PrimerPaymentMethods`](/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).

<Info>
  `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`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) directly instead.
</Info>

## Present checkout

There are three steps:

1. Assign the [`delegate`](/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) {
    print("Payment 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.
</Note>

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

## Presentation overloads

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

```swift theme={"dark"}
// Minimal — automatic view controller detection, default 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(paymentHandling: .manual)
)

// 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](/docs/checkout/client-session) API.          |
| `viewController` | `UIViewController`    | —                        | The view controller to present from. Omitted in the automatic-detection overloads.                                                |
| `primerSettings` | `PrimerSettings`      | `PrimerSettings.current` | Configuration applied to this checkout session. See [`PrimerSettings`](/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings). |
| `primerTheme`    | `PrimerCheckoutTheme` | `PrimerCheckoutTheme()`  | Theme configuration for the [design tokens](/docs/sdk/ios-checkout/v3.0.0-beta/api/theming/primer-theme).                              |
| `completion`     | `(() -> Void)?`       | `nil`                    | Optional closure invoked once the checkout UI has finished presenting.                                                            |

<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`](/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error) through `primerCheckoutPresenterDidFailWithError(_:)`. Assign the `delegate` before calling them.
</Warning>

<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, or configure settings globally through `PrimerSettings`.
</Note>

## Implement the delegate

Conform to `PrimerCheckoutPresenterDelegate` to receive results and lifecycle events. The three result methods are required; the 3DS lifecycle methods are optional thanks to default implementations in a protocol extension.

### Required methods

| Method                                              | Description                                                                                                                           |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `primerCheckoutPresenterDidCompleteWithSuccess(_:)` | Called when payment is successful, with the [`PaymentResult`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state#paymentresult). |
| `primerCheckoutPresenterDidFailWithError(_:)`       | Called when payment fails, with a [`PrimerError`](/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error).                             |
| `primerCheckoutPresenterDidDismiss()`               | Called when checkout is dismissed without completing a payment.                                                                       |

```swift theme={"dark"}
func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult)
func primerCheckoutPresenterDidFailWithError(_ error: PrimerError)
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 automatically before delivering `primerCheckoutPresenterDidCompleteWithSuccess(_:)` or `primerCheckoutPresenterDidFailWithError(_:)`. Use those callbacks to push your own confirmation or error screen. For the full payment-result model, see [Handle payment result](/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result).
</Tip>

### Optional 3DS lifecycle methods

Implement these only if you need to observe the 3-D Secure challenge lifecycle. Each has a default empty implementation, so you can omit any you do not need.

| Method                                                                       | Description                                                                                                                      |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `primerCheckoutPresenterWillPresent3DSChallenge(_:)`                         | Called when a 3DS challenge is about to be presented, with the `PrimerPaymentMethodTokenData` requiring 3DS.                     |
| `primerCheckoutPresenterDidDismiss3DSChallenge()`                            | Called when the 3DS challenge UI is dismissed.                                                                                   |
| `primerCheckoutPresenterDidComplete3DSChallenge(success:resumeToken:error:)` | Called when the 3DS challenge completes, with the success flag, the resume token (when successful), and the error (when failed). |

```swift theme={"dark"}
extension CheckoutViewController {
  func primerCheckoutPresenterWillPresent3DSChallenge(
    _ paymentMethodTokenData: PrimerPaymentMethodTokenData
  ) {
    print("3DS challenge starting")
  }

  func primerCheckoutPresenterDidDismiss3DSChallenge() {
    print("3DS challenge dismissed")
  }

  func primerCheckoutPresenterDidComplete3DSChallenge(
    success: Bool,
    resumeToken: String?,
    error: Error?
  ) {
    print("3DS result: \(success)")
  }
}
```

## 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`  | Whether to animate the dismissal.                   |
| `completion` | `(() -> Void)?` | `nil`   | Optional closure invoked after dismissal completes. |

## 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`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) as a `@StateObject`, place [`PrimerCardForm`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) and [`PrimerPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods) under the `.primerCheckoutSession(_: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(_: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.
</Warning>

<Tip>
  With embedded composables you receive the outcome as a [`PrimerCheckoutState`](/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.
</Tip>

## See also

<CardGroup cols={2}>
  <Card title="PrimerCheckoutPresenter" icon="mobile" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter">
    The full UIKit entry-point API: overloads, delegate, and result types.
  </Card>

  <Card title="PrimerCheckoutState" icon="diagram-project" href="/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state">
    The state model returned to embedded composable integrations.
  </Card>

  <Card title="Handle payment result" icon="receipt" href="/docs/sdk/ios-checkout/v3.0.0-beta/guides/handle-payment-result">
    Route your UI based on the checkout outcome.
  </Card>

  <Card title="SwiftUI navigation" icon="arrow-right" href="/docs/sdk/ios-checkout/v3.0.0-beta/guides/swiftui-navigation">
    Presentation patterns for pure SwiftUI apps.
  </Card>
</CardGroup>
