Skip to main content

Quick diagnosis

SymptomLikely CauseSolution
Error screen with retry buttonInvalid or expired client tokenGenerate a fresh clientToken from your server for each session
No payment methods shownDashboard misconfiguration or unsupported currency/countryVerify Dashboard settings and currencyCode/countryCode in client session
getPaymentMethodScope() returns nilCheckout not yet in .ready stateObserve state stream and access scopes only after .ready
State observation stops unexpectedlySwiftUI .task cancelled on view disappearKeep PrimerCheckout in a stable view (e.g., .sheet) that isn’t recreated
Theme or settings not appliedPrimerCheckoutTheme/PrimerSettings created inside view bodyDefine as constants outside the view body
Delegate callbacks never fire (UIKit)PrimerCheckoutPresenterDelegate not set or deallocatedSet delegate on .shared before calling presentCheckout and retain the delegate
Apple Pay button not visibleSimulator, no cards in Wallet, or merchant ID mismatchTest on a real device with cards in Wallet; verify Apple Developer & Dashboard config
Web redirect stays in .pollingUser cancelled in external browser or app not installedHandle .failure state; check paymentMethodType for app-specific requirements (e.g., Vipps)
BLIK/MBWay form fields not appearinggetPaymentMethodScope called before .ready stateObserve checkoutScope.state and access scopes only after .ready
QR code image not displayingqrCodeImageData is nil before .displaying statusCheck state.status == .displaying before using qrCodeImageData
Build error on iOS 14Minimum deployment target not metSet deployment target to iOS 15.0+

Configuration and initialization

Error screen after initialization

Cause: Invalid, expired, or malformed client token. The SDK shows an error screen with a retry button — not a blank screen. Solution: Generate a fresh clientToken from your server for each checkout session. Client tokens are single-use and expire.
PrimerCheckout(
  clientToken: freshClientToken, // Always fetch from your server
  onCompletion: { state in
    if case .failure(let error) = state {
      print("[Primer] Error: \(error.errorId)")
      print("[Primer] Description: \(error.errorDescription ?? "N/A")")
      print("[Primer] Diagnostics: \(error.diagnosticsId)")
    }
  }
)
If you see invalid-client-token in the error, your token has expired or was already consumed. Generate a new one from your server.

Payment methods not showing

Cause: Multiple possible causes:
  • No payment methods configured in the Primer Dashboard for the given currency/country
  • The SDK only renders payment methods it supports (card, Apple Pay, PayPal, Klarna, ACH, etc.) — unsupported types are silently filtered out
  • Client session missing required currencyCode or countryCode fields
Solution: Check the Xcode console for log messages like "Filtering out unregistered payment method: TYPE". Verify your Dashboard configuration matches the currency and country in your client session.

SwiftUI integration

Payment method scope returns nil

Cause: getPaymentMethodScope() returns nil before the checkout reaches .ready state because scopes aren’t configured yet.
Calling getPaymentMethodScope() before .ready always returns nil. This is the most common cause of “scope is nil” issues.
Solution: Observe the state stream and access scopes only after .ready:
PrimerCheckout(
  clientToken: clientToken,
  scope: { checkoutScope in
    Task {
      for await state in checkoutScope.state {
        if case .ready = state {
          // Safe to access scopes now
          if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {
            // Configure card form
          }
        }
      }
    }
  }
)

State observation stops unexpectedly

Cause: SwiftUI’s .task modifier cancels the Task when the view disappears. If PrimerCheckout is inside a NavigationLink destination, each push/pop recreates it and cancels the scope Task. Solution: Keep PrimerCheckout in a stable container that isn’t destroyed by navigation:
// WRONG: Recreated on every navigation push
NavigationStack {
  NavigationLink("Checkout") {
    PrimerCheckout(clientToken: clientToken)
  }
}

// CORRECT: Stable container using .sheet
struct CheckoutContainer: View {
  let clientToken: String
  @State private var showCheckout = false

  var body: some View {
    Button("Checkout") { showCheckout = true }
      .sheet(isPresented: $showCheckout) {
        PrimerCheckout(clientToken: clientToken)
      }
  }
}

Theme or settings not applied

Cause: Creating PrimerCheckoutTheme() or PrimerSettings() inside the SwiftUI body property. SwiftUI re-evaluates body frequently, creating new objects each time. Solution: Define configuration as constants outside the view body:
private let primerTheme = PrimerCheckoutTheme(
  colors: ColorOverrides(primerColorBrand: .blue)
)
private let primerSettings = PrimerSettings(paymentHandling: .auto)

struct CheckoutView: View {
  let clientToken: String

  var body: some View {
    PrimerCheckout(
      clientToken: clientToken,
      primerSettings: primerSettings,
      primerTheme: primerTheme
    )
  }
}
See Best Practices for more SwiftUI performance tips.

UIKit integration

Delegate callbacks never fire

Cause: PrimerCheckoutPresenterDelegate not set on PrimerCheckoutPresenter.shared before calling presentCheckout, or the delegate object was deallocated.
The delegate is a weak reference. Ensure the delegate object (typically your view controller) is retained for the lifetime of the checkout presentation. If the delegate is deallocated, callbacks silently stop.
Solution: Set the delegate before presenting and implement all required methods:
class CheckoutViewController: UIViewController, PrimerCheckoutPresenterDelegate {

  func showCheckout() {
    // Set delegate BEFORE presenting
    PrimerCheckoutPresenter.shared.delegate = self

    PrimerCheckoutPresenter.presentCheckout(
      clientToken: clientToken,
      from: self
    )
  }

  // Required delegate methods
  func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult) {
    // Handle successful payment
  }

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

  func primerCheckoutPresenterDidDismiss() {
    // Handle checkout dismissed
  }
}

Checkout not presented

Cause: Calling presentCheckout while already presenting — the call is silently ignored with no visible feedback. Solution: Check PrimerCheckoutPresenter.isPresenting before calling:
guard !PrimerCheckoutPresenter.isPresenting else {
  print("Checkout already visible")
  return
}
PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)

Card form

Form shows invalid despite all visible fields being filled

Cause: The form validation considers all configured fields, including billing address fields that may not be visible on screen. Backend configuration controls which billing fields are required. Solution: Observe fieldErrors to identify which specific fields are failing validation:
if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {
  Task {
    for await state in cardScope.state {
      for error in state.fieldErrors {
        print("[Primer] Field error - \(error.fieldType): \(error.message)")
      }
    }
  }
}

Apple Pay

Apple Pay not available

Cause: Multiple possible causes:
  • Running on the iOS Simulator (Apple Pay requires a real device)
  • No payment cards added to the user’s Wallet
  • Apple Pay not configured in the Primer Dashboard
  • Merchant identifier mismatch between Apple Developer portal and Primer Dashboard
Solution: Test on a physical device with a card added to Wallet. Verify Apple Pay configuration in both the Apple Developer portal and Primer Dashboard. Check if DefaultApplePayScope is returned from getPaymentMethodScope() — if nil, Apple Pay is not available for the current session.

Installation

SPM package not found

Cause: Build fails with “No such module ‘PrimerSDK’”. Solution: Re-add the SPM package:
  1. In Xcode, go to File > Add Package Dependencies
  2. Enter the Primer SDK repository URL
  3. Select the correct version and add to your target

CocoaPods installation fails

Cause: pod install fails or doesn’t include the Primer SDK. Solution: Verify your Podfile and reinstall:
# Podfile
pod 'PrimerSDK'
pod install --repo-update

Minimum deployment target

Cause: Crash or build error on iOS 14 or earlier. Solution: The Primer Checkout iOS SDK requires iOS 15.0 or later. Update your deployment target:
// In Xcode: General > Deployment Info > iOS 15.0

Validation vs payment errors

Understanding the difference helps with proper error handling:
Error TypeWhen It OccursHow It’s Handled
Validation errorsDuring input (invalid format, missing fields)Handled automatically by input components; prevents form submission
Payment failuresAfter form submission (declined card, network issues)Requires explicit handling with error container or custom code
Don’t confuse these two error types. Validation errors prevent form submission and are shown inline. Payment failures occur after the form is submitted and require explicit handling.

Debugging tips

Log all state changes

PrimerCheckout(
  clientToken: clientToken,
  scope: { checkoutScope in
    Task {
      for await state in checkoutScope.state {
        print("[Primer] Checkout state: \(state)")
      }
    }
  },
  onCompletion: { state in
    print("[Primer] Terminal state: \(state)")
  }
)

Inspect available payment methods

Task {
  for await state in checkoutScope.paymentMethodSelection.state {
    for method in state.paymentMethods {
      print("[Primer] Available: \(method.type) (\(method.name))")
    }
  }
}

Check scope availability

After the checkout reaches .ready state, check which payment method scopes are available:
let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self)
let applePayScope = checkoutScope.getPaymentMethodScope(DefaultApplePayScope.self)
let paypalScope = checkoutScope.getPaymentMethodScope(DefaultPayPalScope.self)

print("[Primer] Card form available: \(cardScope != nil)")
print("[Primer] Apple Pay available: \(applePayScope != nil)")
print("[Primer] PayPal available: \(paypalScope != nil)")

Log card form validation errors

if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {
  Task {
    for await state in cardScope.state {
      for error in state.fieldErrors {
        print("[Primer] Field error - \(error.fieldType): \(error.message)")
      }
    }
  }
}

Extract diagnostics for support

if case .failure(let error) = state {
  print("[Primer] Error ID: \(error.errorId)")
  print("[Primer] Description: \(error.errorDescription ?? "N/A")")
  print("[Primer] Diagnostics ID: \(error.diagnosticsId)")
  print("[Primer] Recovery: \(error.recoverySuggestion ?? "N/A")")
}

Getting help

When contacting Primer support, include:
  1. The diagnosticsId from any error callbacks
  2. Your iOS version, Xcode version, and SDK version
  3. Steps to reproduce the issue
if case .failure(let error) = state {
  print("Include in support request:")
  print("  Error ID: \(error.errorId)")
  print("  Description: \(error.errorDescription ?? "N/A")")
  print("  Diagnostics ID: \(error.diagnosticsId)")
  print("  Recovery: \(error.recoverySuggestion ?? "N/A")")
}

See also

Events guide

Event handling patterns

Build a custom card form

Card form tutorial

Best practices

SwiftUI performance tips