Skip to main content
Primer Checkout iOS SDK is currently in beta (v3.0.0-beta.1). 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 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 and PrimerPaymentMethods) inside a UIHostingController — see Embedding the composable views.
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 directly instead.

Present checkout

There are three steps:
  1. Assign the delegate on PrimerCheckoutPresenter.shared.
  2. Call presentCheckout(...), passing the client token and the view controller to present from.
  3. Implement PrimerCheckoutPresenterDelegate to handle the result.
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.
  }
}
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 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.
// 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

ParameterTypeDefaultDescription
clientTokenStringThe client token for the session. Generate it from your backend with the client session API.
viewControllerUIViewControllerThe view controller to present from. Omitted in the automatic-detection overloads.
primerSettingsPrimerSettingsPrimerSettings.currentConfiguration applied to this checkout session. See PrimerSettings.
primerThemePrimerCheckoutThemePrimerCheckoutTheme()Theme configuration for the design tokens.
completion(() -> Void)?nilOptional closure invoked once the checkout UI has finished presenting.
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 through primerCheckoutPresenterDidFailWithError(_:). Assign the delegate before calling them.
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.

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

MethodDescription
primerCheckoutPresenterDidCompleteWithSuccess(_:)Called when payment is successful, with the PaymentResult.
primerCheckoutPresenterDidFailWithError(_:)Called when payment fails, with a PrimerError.
primerCheckoutPresenterDidDismiss()Called when checkout is dismissed without completing a payment.
func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult)
func primerCheckoutPresenterDidFailWithError(_ error: PrimerError)
func primerCheckoutPresenterDidDismiss()
The success callback delivers a PaymentResult. Switch on its status to route your UI:
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
  }
}
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.

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.
MethodDescription
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).
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.
PrimerCheckoutPresenter.dismiss(animated: true) {
  print("Checkout dismissed")
}
ParameterTypeDefaultDescription
animatedBooltrueWhether to animate the dismissal.
completion(() -> Void)?nilOptional closure invoked after dismissal completes.

Check availability

Use the static properties to guard presentation:
// 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)
PropertyTypeDescription
isAvailableBoolstatic. true when CheckoutComponents can be presented on the current OS version.
isPresentingBoolstatic. 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 as a @StateObject, place PrimerCardForm and PrimerPaymentMethods under the .primerCheckoutSession(_:onCompletion:) modifier, and host that SwiftUI view in your view controller. First, define the SwiftUI content:
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:
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
    }
  }
}
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.
With embedded composables you receive the outcome as a PrimerCheckoutState in the modifier’s onCompletion, rather than through the presenter delegate. The state-driven model is identical to a pure SwiftUI integration.

See also

PrimerCheckoutPresenter

The full UIKit entry-point API: overloads, delegate, and result types.

PrimerCheckoutState

The state model returned to embedded composable integrations.

Handle payment result

Route your UI based on the checkout outcome.

SwiftUI navigation

Presentation patterns for pure SwiftUI apps.