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.
Every checkout ends in one of three outcomes: the payment succeeds, it fails, or the user dismisses the flow. This guide shows how to receive and react to that outcome in both SwiftUI and UIKit. The outcome is always delivered as a PrimerCheckoutState value:
CaseAssociated valueMeaning
.successPaymentResultThe payment completed.
.failurePrimerErrorThe payment or checkout failed.
.dismissedThe user closed the checkout without completing a payment.
The same PrimerCheckoutState also has lifecycle cases (.initializing, .ready) that you do not need to handle when reacting to the final outcome. Always include a default case when switching, so future enum additions stay source-compatible.

SwiftUI

Using PrimerCheckout

The prebuilt PrimerCheckout view accepts an onCompletion closure that fires once with the final state.
import SwiftUI
import PrimerSDK

struct CheckoutView: View {
  let clientToken: String
  @State private var paymentResult: PaymentResult?

  var body: some View {
    if let paymentResult {
      ConfirmationView(result: paymentResult)
    } else {
      PrimerCheckout(clientToken: clientToken) { state in
        switch state {
        case let .success(result):
          paymentResult = result
        case let .failure(error):
          print("Checkout failed: \(error.errorId)\(error.diagnosticsId)")
        case .dismissed:
          break
        default:
          break
        }
      }
    }
  }
}

Using the .primerCheckoutSession modifier

When you embed the composable views (for example PrimerCardForm) inline in your own layout, hold a PrimerCheckoutSession as a @StateObject and receive the outcome through the onCompletion parameter of the .primerCheckoutSession(_:onCompletion:) modifier.
import SwiftUI
import PrimerSDK

struct InlineCheckoutView: View {
  @StateObject private var session: PrimerCheckoutSession
  @State private var paymentResult: PaymentResult?

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

  var body: some View {
    Group {
      if let paymentResult {
        ConfirmationView(result: paymentResult)
      } else {
        ScrollView {
          PrimerCardForm()
        }
      }
    }
    .primerCheckoutSession(session) { state in
      switch state {
      case let .success(result):
        paymentResult = result
      case let .failure(error):
        print("Checkout failed: \(error.errorId)\(error.diagnosticsId)")
      case .dismissed:
        break
      default:
        break
      }
    }
  }
}
onCompletion is delivered exactly once. The session latches the first terminal state, so any later .dismissed produced by a view-lifecycle teardown is ignored.
Use the PaymentResult to drive navigation. PaymentResult is Equatable but not Hashable, so it cannot be appended to a NavigationPath or used with navigationDestination(for:) directly. Instead, store the result in @State and present the confirmation screen with navigationDestination(isPresented:).
import SwiftUI
import PrimerSDK

struct CheckoutFlowView: View {
  let clientToken: String
  @State private var paymentResult: PaymentResult?

  private var showsConfirmation: Binding<Bool> {
    Binding(
      get: { paymentResult != nil },
      set: { if !$0 { paymentResult = nil } }
    )
  }

  var body: some View {
    NavigationStack {
      PrimerCheckout(clientToken: clientToken) { state in
        if case let .success(result) = state {
          paymentResult = result
        }
      }
      .navigationDestination(isPresented: showsConfirmation) {
        if let paymentResult {
          ConfirmationView(result: paymentResult)
        }
      }
    }
  }
}

UIKit

In UIKit, present the checkout with PrimerCheckoutPresenter and adopt PrimerCheckoutPresenterDelegate to receive the outcome. The presenter dismisses its own modal before calling the relevant delegate method.
import UIKit
import PrimerSDK

final class CheckoutViewController: UIViewController {
  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") }

  func startCheckout() {
    PrimerCheckoutPresenter.shared.delegate = self
    PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)
  }
}

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

  func primerCheckoutPresenterDidFailWithError(_ error: PrimerError) {
    print("Checkout failed: \(error.errorId)\(error.diagnosticsId)")
  }

  func primerCheckoutPresenterDidDismiss() {
    // User closed the checkout without completing a payment.
  }
}
Set the delegate before calling presentCheckout. If no delegate is set when checkout completes, the outcome is logged and otherwise dropped.
The delegate maps one-to-one onto the SwiftUI PrimerCheckoutState cases:
Delegate methodEquivalent state
primerCheckoutPresenterDidCompleteWithSuccess(_:).success(PaymentResult)
primerCheckoutPresenterDidFailWithError(_:).failure(PrimerError)
primerCheckoutPresenterDidDismiss().dismissed

Reading the PaymentResult

On success you receive a PaymentResult. Read its properties to build your confirmation UI, send analytics, or reconcile against your backend.
PropertyTypeDescription
paymentIdStringThe Primer payment ID.
statusPaymentStatusThe payment status: .pending, .success, or .failed.
tokenString?The payment method token, when available.
redirectUrlString?A redirect URL, when the flow requires one.
errorMessageString?A human-readable error message, when present.
amountInt?The amount in minor units (for example, cents for USD).
currencyCodeString?The ISO 4217 currency code (for example, "USD").
paymentMethodTypeString?The payment method type used (for example, "PAYMENT_CARD").
import SwiftUI
import PrimerSDK

struct ConfirmationView: View {
  let result: PaymentResult

  var body: some View {
    VStack(spacing: 12) {
      Text("Payment confirmed")
        .font(.title2.bold())
      Text("Payment ID: \(result.paymentId)")
      if let amount = result.amount, let currency = result.currencyCode {
        Text("Charged: \(amount) \(currency)")
      }
    }
  }
}
status is usually .success for a .success(PaymentResult) outcome. In manual payment handling, the payment may still be .pending until your backend confirms it.

Handling the PrimerError

On failure you receive a PrimerError. The SDK already surfaces failures inside the checkout UI, so you typically use the error for logging, analytics, and support workflows rather than for displaying a message yourself.
PropertyTypeDescription
errorIdStringA stable, machine-readable identifier for the error category.
diagnosticsIdStringA unique ID for this specific occurrence — include it in support requests.
errorDescriptionString?A formatted description combining the error ID, message, and diagnostics ID.
recoverySuggestionString?A suggested recovery action, when one is available.
func report(_ error: PrimerError) {
  analytics.track("checkout_failed", properties: [
    "error_id": error.errorId,
    "diagnostics_id": error.diagnosticsId
  ])

  if let suggestion = error.recoverySuggestion {
    print("Recovery: \(suggestion)")
  }
}
Always log diagnosticsId. Primer support uses it to trace the exact failed request when you raise an issue.

See also

PrimerCheckoutState

The full state enum and the PaymentResult model.

PrimerError

Error categories, IDs, and diagnostics.

PrimerCheckoutPresenter

The UIKit entry point and delegate callbacks.

PrimerCheckout

The prebuilt SwiftUI checkout view.