Skip to main content
Explore the iOS Checkout SwiftUI architecture interactively. The redesigned SDK is a SwiftUI slot API: a PrimerCheckoutSession owner wired into your view hierarchy with the .primerCheckoutSession(_:onCompletion:) modifier, plus composable views (PrimerCardForm, PrimerPaymentMethods, PrimerVaultedPaymentMethods) whose @ViewBuilder slots default to the *Defaults building blocks. Click any view, slot, or field to see its parameters, lifecycle phases, and copyable Swift code examples.

Two ways to integrate

GoalUse
Fastest integration, default screensPrimerCheckout — the fully managed SwiftUI modal. No slots; it renders Primer’s default screens end to end.
Custom SwiftUI layout and slotsPrimerCheckoutSession + .primerCheckoutSession(_:onCompletion:), composing PrimerCardForm / PrimerPaymentMethods / PrimerVaultedPaymentMethods.
UIKit appsPrimerCheckoutPresenter + PrimerCheckoutPresenterDelegate.
PrimerCheckoutSession is the iOS analog of Android’s PrimerCheckoutHost. Where Android wraps your layout in a composable host, iOS injects the session into the SwiftUI environment with a view modifier — child views read their per-feature session (PrimerCardFormSession, PrimerSelectionSession) from EnvironmentValues.

Slots and defaults

Every composable view exposes @ViewBuilder slots that default to a matching *Defaults namespace builder. Override one slot and the rest keep their default rendering — you never re-implement an entire view.
ViewSlotsDefaults namespace
PrimerCardFormcardDetails, billingAddress, submitButtonCardFormDefaults
PrimerPaymentMethodsheader, method, emptyStatePaymentMethodsDefaults
PrimerVaultedPaymentMethodsheader, item, submitButtonVaultedPaymentMethodsDefaults
CardFormDefaults also exposes per-field building blocks for recomposing the card section one field at a time: cardNumber, expiryDate, cvv, cardholderName, cardNetwork, plus the billing fields firstName, lastName, email, phoneNumber, addressLine1, addressLine2, city, state, postalCode, countryCode. Each block is self-hiding — it renders nothing unless its field is part of the active CardFormConfiguration. Field presence is Dashboard-driven; visual styling comes from the PrimerCheckoutTheme design tokens.

Sessions and state

Composable views are driven by observable sessions injected into the environment by .primerCheckoutSession(_:onCompletion:). Observe them with @ObservedObject and read their published state.
SessionDrivesPublished state
PrimerCheckoutSessionThe whole flow (owner)phase (.initializing / .ready)
PrimerCardFormSessionPrimerCardForm slotsPrimerCardFormState
PrimerSelectionSessionPrimerPaymentMethods, PrimerVaultedPaymentMethodsPrimerPaymentMethodSelectionState
The terminal outcome of the flow is delivered exactly once through the modifier’s onCompletion closure as a PrimerCheckoutState:
StateAssociated valuesDescription
.initializingLoading configuration and payment methods (lifecycle; never delivered to onCompletion).
.readytotalAmount: Int, currencyCode: StringPayment methods loaded (lifecycle; never delivered to onCompletion).
.successPaymentResultPayment completed. Carries paymentId, status, and other details.
.failurePrimerErrorPayment or checkout failed.
.dismissedCheckout was dismissed without completing a payment.

PrimerCardFormState

PropertyTypeDescription
configurationCardFormConfigurationWhich fields the form requires (Dashboard-driven).
dataFormDataCurrent field values keyed by PrimerInputElementType.
fieldErrors[FieldError]Validation errors per field. Empty when valid.
isLoadingBooltrue while a payment is being submitted.
isValidBooltrue when all required fields pass validation.
selectedCountryPrimerCountry?Selected billing country, or nil.
selectedNetworkPrimerCardNetwork?Selected network for co-badged cards, or nil.
availableNetworks[PrimerCardNetwork]Detected networks; more than one indicates a co-badged card.

Composing custom UI

Hold the session as a @StateObject, place the composable views in your layout, and attach the modifier. The child views resolve their per-feature session from the environment — you do not pass a session into them.
import SwiftUI
import PrimerSDK

@available(iOS 15.0, *)
struct CheckoutView: View {
  @StateObject private var session: PrimerCheckoutSession

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

  var body: some View {
    ScrollView {
      VStack(spacing: 24) {
        PrimerPaymentMethods()
        PrimerCardForm(
          cardDetails: { formSession in
            VStack(spacing: 16) {
              CardFormDefaults.cardNumber(formSession)
              HStack(spacing: 16) {
                CardFormDefaults.expiryDate(formSession)
                CardFormDefaults.cvv(formSession)
              }
              CardFormDefaults.cardholderName(formSession)
              // Co-badged selector: shown only when multiple networks are detected.
              CardFormDefaults.cardNetwork(formSession)
            }
          },
          submitButton: { formSession in
            Button("Pay now") { formSession.submit() }
              .buttonStyle(.borderedProminent)
              .disabled(!formSession.state.isValid || formSession.state.isLoading)
          }
        )
      }
      .padding()
    }
    .primerCheckoutSession(session) { state in
      handle(state)
    }
  }

  private func handle(_ state: PrimerCheckoutState) {
    switch state {
    case let .success(result):
      print("Payment \(result.paymentId) succeeded")
    case let .failure(error):
      print("Checkout failed: \(error.localizedDescription)")
    case .dismissed:
      print("Checkout dismissed")
    default:
      // .initializing and .ready are lifecycle states and are
      // never delivered to onCompletion.
      break
    }
  }
}
Without the .primerCheckoutSession(_:onCompletion:) modifier above them in the hierarchy, composable views such as PrimerCardForm and PrimerPaymentMethods cannot resolve a session and will not function.