Integrate the Primer Checkout iOS SDK in a UIKit app
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.
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.
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())
Optional 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.
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.
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.
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.
Call dismiss(animated:completion:) to close checkout from your own code (for example, after a timeout). This triggers primerCheckoutPresenterDidDismiss() on the delegate.
// 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.
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:
Then host it from your view controller and add it as a child:
import SwiftUIimport PrimerSDKimport 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.