Skip to main content
Primer Checkout communicates state through two mechanisms: AsyncStream for continuous observation and onCompletion for the final payment result.

Checkout state lifecycle

PrimerCheckoutState

The top-level checkout state, observed via PrimerCheckoutScope.state:
StateDescription
.initializingSDK is loading configuration and payment methods
.ready(totalAmount:currencyCode:)Checkout is ready for user interaction
.success(PaymentResult)Payment completed successfully
.failure(PrimerError)Payment failed
.dismissedCheckout was dismissed

Observing checkout state

PrimerCheckout(
  clientToken: clientToken,
  scope: { checkoutScope in
    Task {
      for await state in checkoutScope.state {
        switch state {
        case .initializing:
          print("Loading checkout...")
        case .ready(let totalAmount, let currencyCode):
          print("Ready: \(totalAmount) \(currencyCode)")
        case .success(let result):
          print("Success: \(result.payment?.id ?? "")")
        case .failure(let error):
          print("Failed: \(error.errorId)")
        case .dismissed:
          print("Dismissed")
        }
      }
    }
  }
)

onCompletion callback

The onCompletion callback fires when the checkout reaches a terminal state (success, failure, or dismissed):
PrimerCheckout(
  clientToken: clientToken,
  onCompletion: { state in
    switch state {
    case .success(let result):
      navigateToConfirmation(result: result)
    case .failure(let error):
      logError(error)
    case .dismissed:
      navigateBack()
    default:
      break
    }
  }
)
onCompletion receives the same PrimerCheckoutState enum as the AsyncStream. Use onCompletion when you only need to react to the final result, and AsyncStream when you need to track intermediate states.

Payment method scope states

Each payment method scope has its own state type:

PrimerCardFormState

Task {
  let cardFormScope: PrimerCardFormScope = checkoutScope.getPaymentMethodScope(PrimerCardFormScope.self)!
  for await state in cardFormScope.state {
    print("Valid: \(state.isValid)")
    print("Loading: \(state.isLoading)")
    print("Fields: \(state.displayFields)")
    if let network = state.selectedNetwork {
      print("Network: \(network)")
    }
  }
}
PropertyTypeDescription
isValidBoolWhether all required fields pass validation
isLoadingBoolWhether the form is submitting
fieldErrors[FieldError]Current validation errors
selectedNetworkPrimerCardNetwork?Detected card network
availableNetworks[PrimerCardNetwork]Available card networks for co-badged cards
displayFields[PrimerInputElementType]Fields to display based on configuration
selectedCountryPrimerCountry?Selected country for billing address

PrimerPaymentMethodSelectionState

Task {
  for await state in checkoutScope.paymentMethodSelection.state {
    print("Methods: \(state.paymentMethods.count)")
    print("Selected: \(state.selectedPaymentMethod?.name ?? "none")")
    print("Loading: \(state.isLoading)")
  }
}

PrimerApplePayState

Task {
  if let applePayScope: PrimerApplePayScope = checkoutScope.getPaymentMethodScope(for: .applePay) {
    for await state in applePayScope.state {
      print("Available: \(state.isAvailable)")
      print("Loading: \(state.isLoading)")
    }
  }
}

PrimerKlarnaState

Klarna follows a multi-step flow:
Task {
  if let klarnaScope: PrimerKlarnaScope = checkoutScope.getPaymentMethodScope(PrimerKlarnaScope.self) {
    for await state in klarnaScope.state {
      switch state.step {
      case .loading:
        print("Loading Klarna...")
      case .categorySelection:
        print("Categories: \(state.categories)")
      case .viewReady:
        print("Klarna view ready")
      case .authorizationStarted:
        print("Authorizing...")
      case .awaitingFinalization:
        print("Awaiting finalization")
      }
    }
  }
}

PrimerAchState

ACH follows a multi-step flow:
Task {
  if let achScope: PrimerAchScope = checkoutScope.getPaymentMethodScope(PrimerAchScope.self) {
    for await state in achScope.state {
      switch state.step {
      case .loading:
        print("Loading...")
      case .userDetailsCollection:
        print("Collecting user details")
      case .bankAccountCollection:
        print("Collecting bank account")
      case .mandateAcceptance:
        print("Mandate: \(state.mandateText ?? "")")
      case .processing:
        print("Processing...")
      }
    }
  }
}

PrimerWebRedirectState

Web redirect follows a linear flow through redirect and polling:
Task {
  if let webRedirectScope: PrimerWebRedirectScope = checkoutScope.getPaymentMethodScope(PrimerWebRedirectScope.self) {
    for await state in webRedirectScope.state {
      switch state.status {
      case .idle:
        print("Ready to pay")
      case .loading:
        print("Preparing payment...")
      case .redirecting:
        print("Opening external page...")
      case .polling:
        print("Waiting for confirmation...")
      case .success:
        print("Payment completed")
      case .failure(let message):
        print("Failed: \(message)")
      }
    }
  }
}

PrimerFormRedirectState

Form redirect collects user input before completing in an external app:
Task {
  if let formScope: PrimerFormRedirectScope = checkoutScope.getPaymentMethodScope(PrimerFormRedirectScope.self) {
    for await state in formScope.state {
      switch state.status {
      case .ready:
        print("Fields: \(state.fields.count)")
        print("Can submit: \(state.isSubmitEnabled)")
      case .submitting:
        print("Submitting...")
      case .awaitingExternalCompletion:
        print("Pending: \(state.pendingMessage ?? "")")
      case .success:
        print("Payment completed")
      case .failure(let message):
        print("Failed: \(message)")
      }
    }
  }
}

PrimerQRCodeState

QR code auto-initiates payment and polls after displaying the code:
Task {
  if let qrScope: PrimerQRCodeScope = checkoutScope.getPaymentMethodScope(PrimerQRCodeScope.self) {
    for await state in qrScope.state {
      switch state.status {
      case .loading:
        print("Generating QR code...")
      case .displaying:
        if let imageData = state.qrCodeImageData {
          print("QR code ready (\(imageData.count) bytes)")
        }
      case .success:
        print("Payment completed")
      case .failure(let message):
        print("Failed: \(message)")
      }
    }
  }
}

See also

Scopes overview

All available scopes and their relationships

Handle payment result

Navigate after payment completion

Error handling

Handle validation and payment errors