> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting — iOS

> Solutions for common Primer Checkout iOS issues

## Quick diagnosis

| Symptom                                       | Likely Cause                                                                      | Solution                                                                                                      |
| --------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Error screen with retry button                | Invalid or expired client token                                                   | Generate a fresh `clientToken` from your server for each session                                              |
| No payment methods shown                      | Dashboard misconfiguration or unsupported currency/country                        | Verify Dashboard settings and `currencyCode`/`countryCode` in client session                                  |
| Composable views render nothing               | `.primerCheckoutSession` modifier missing above them, or session not yet `.ready` | Apply `.primerCheckoutSession(_:onCompletion:)` around the views; they bind once the session reaches `.ready` |
| Session re-initializes unexpectedly           | `PrimerCheckoutSession` recreated on view updates                                 | Hold the session as a `@StateObject`, not a `let` or `@State` value                                           |
| Theme or settings not applied                 | `PrimerCheckoutTheme`/`PrimerSettings` created inside view `body`                 | Define as constants outside the view body                                                                     |
| Delegate callbacks never fire (UIKit)         | `PrimerCheckoutPresenterDelegate` not set or deallocated                          | Set `delegate` on `.shared` before calling `presentCheckout` and retain the delegate                          |
| Apple Pay not available                       | Simulator, no cards in Wallet, or merchant ID mismatch                            | Test on a real device with cards in Wallet; verify Apple Developer & Dashboard config                         |
| Card form shows invalid despite filled fields | Hidden billing fields are also required                                           | Inspect `fieldErrors` on the `PrimerCardFormSession` handed to your card-form slot                            |
| Payment fails after submission                | Declined card, network issue, or processor error                                  | Handle the `.failure(PrimerError)` terminal state and log `diagnosticsId`                                     |
| Build error on iOS 14                         | Minimum deployment target not met                                                 | Set 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.

```swift theme={"dark"}
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)")
    }
  }
)
```

<Info>
  If you see `invalid-client-token` in the error, your token has expired or was already consumed. Generate a new one from your server.
</Info>

### Payment methods not showing

**Cause:** Multiple possible causes:

* No payment methods configured in the [Primer Dashboard](https://dashboard.primer.io) for the given currency/country
* The SDK only renders payment methods it supports — 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. When using the composable [`PrimerPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods) view, read the available methods from `session.selection?.state.paymentMethods`.

## SwiftUI integration

### Composable views render nothing

**Cause:** The composable views — [`PrimerCardForm`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form), [`PrimerPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods), and [`PrimerVaultedPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/vaulted-payment-methods) — resolve their session from the SwiftUI environment. Without the [`.primerCheckoutSession(_:onCompletion:)`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session#view-modifier) modifier above them, they have no session to bind to. They also stay empty until the session reaches `.ready`.

<Warning>
  A composable view without `.primerCheckoutSession` above it in the hierarchy cannot resolve a session and will not function. This is the most common cause of "nothing renders" issues.
</Warning>

**Solution:** Hold a [`PrimerCheckoutSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) as a `@StateObject` and attach the modifier once around your composable views. The modifier injects the session, the derived `PrimerCardFormSession`, and the `PrimerSelectionSession` into the environment, calls `start()` on appear, and tears the session down on disappear:

```swift theme={"dark"}
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()
      }
    }
    .primerCheckoutSession(session) { state in
      // Receives the terminal PrimerCheckoutState exactly once.
    }
  }
}
```

If you only need the prebuilt screens, use the managed [`PrimerCheckout`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) view instead — it owns its own session and requires no modifier.

### Session re-initializes unexpectedly

**Cause:** `PrimerCheckoutSession` is recreated whenever SwiftUI re-evaluates the enclosing `body`. Storing it in a `let`, a computed property, or `@State` (initialized inline) causes a fresh session — and a fresh client-token initialization — on every view update.

**Solution:** Own the session as a `@StateObject` so SwiftUI keeps a single instance for the lifetime of the view:

```swift theme={"dark"}
// WRONG: a new session (and re-initialization) on every body evaluation
struct CheckoutView: View {
  let session = PrimerCheckoutSession(clientToken: clientToken)
  var body: some View { ... }
}

// CORRECT: one session for the lifetime of the view
struct CheckoutView: View {
  @StateObject private var session: PrimerCheckoutSession

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

  var body: some View { ... }
}
```

The `.primerCheckoutSession` modifier drives the lifecycle for you — you normally never call `start()` or `cancel()` yourself. Call `session.refresh()` only when you need to reload after a client-session update (for example, an amount or currency change).

### 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:

```swift theme={"dark"}
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](/docs/checkout/primer-checkout/configuration/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.

<Warning>
  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.
</Warning>

**Solution:** Set the delegate before presenting and implement all required methods:

```swift theme={"dark"}
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:

```swift theme={"dark"}
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 (the Dashboard-driven `CardFormConfiguration`) controls which billing fields are required.

**Solution:** Inspect `state.fieldErrors` on the [`PrimerCardFormSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session) to identify which specific fields are failing validation. The session is handed to each [`PrimerCardForm`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) slot — observe it with `@ObservedObject`:

```swift theme={"dark"}
PrimerCardForm(cardDetails: { session in
  VStack(alignment: .leading) {
    CardFormDefaults.cardDetails(session)
    ForEach(session.state.fieldErrors) { error in
      Text("\(String(describing: error.fieldType)): \(error.message)")
        .foregroundColor(.red)
        .font(.caption)
    }
  }
})
```

Use `session.state.isValid` to gate the submit button and `session.state.errorMessage(for:)` to display a per-field 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. There is no separate Apple Pay view or scope — once you configure Apple Pay via [`PrimerSettings(paymentMethodOptions:)`](/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings) with a `PrimerApplePayOptions(merchantIdentifier:)`, Apple Pay surfaces automatically as a `CheckoutPaymentMethod` (with `type == "APPLE_PAY"`) in [`PrimerPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods), or as a managed screen in [`PrimerCheckout`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout). If it never appears, confirm the method is present in `session.selection?.state.paymentMethods`. See the [Apple Pay integration guide](/docs/sdk/ios-checkout/v3.0.0-beta/guides/apple-pay-integration).

## 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:

```ruby theme={"dark"}
# Podfile
pod 'PrimerSDK'
```

```bash theme={"dark"}
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 Type            | When It Occurs                                        | How It's Handled                                                    |
| --------------------- | ----------------------------------------------------- | ------------------------------------------------------------------- |
| **Validation errors** | During input (invalid format, missing fields)         | Handled automatically by input components; prevents form submission |
| **Payment failures**  | After form submission (declined card, network issues) | Requires explicit handling with error container or custom code      |

<Warning>
  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.
</Warning>

## Debugging tips

### Log the terminal state

The `onCompletion` closure on [`PrimerCheckout`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) or the `.primerCheckoutSession(_:onCompletion:)` modifier fires exactly once with the terminal [`PrimerCheckoutState`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state):

```swift theme={"dark"}
PrimerCheckout(clientToken: clientToken) { state in
  print("[Primer] Terminal state: \(state)")
}
```

For the composable flow, log it from the modifier instead:

```swift theme={"dark"}
.primerCheckoutSession(session) { state in
  print("[Primer] Terminal state: \(state)")
}
```

### Inspect available payment methods

Read the available methods from the [`PrimerSelectionSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-session) once the session is `.ready`:

```swift theme={"dark"}
if let methods = session.selection?.state.paymentMethods {
  for method in methods {
    print("[Primer] Available: \(method.type) (\(method.name))")
  }
}
```

### Check session readiness

The composable views bind only once the session reaches `.ready`. Observe `session.phase` to confirm initialization completed and the child sessions resolved:

```swift theme={"dark"}
print("[Primer] Phase: \(session.phase)")
print("[Primer] Card form ready: \(session.cardForm != nil)")
print("[Primer] Selection ready: \(session.selection != nil)")
```

### Log card form validation errors

Read `fieldErrors` from the [`PrimerCardFormSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session) handed to a `PrimerCardForm` slot:

```swift theme={"dark"}
PrimerCardForm(submitButton: { session in
  let _ = session.state.fieldErrors.forEach { error in
    print("[Primer] Field error - \(error.fieldType): \(error.message)")
  }
  return CardFormDefaults.submitButton(session)
})
```

### Extract diagnostics for support

```swift theme={"dark"}
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

```swift theme={"dark"}
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

<CardGroup cols={2}>
  <Card title="Events guide" icon="bolt" href="/docs/checkout/primer-checkout/configuration/events">
    Event handling patterns
  </Card>

  <Card title="Build a custom card form" icon="credit-card" href="/docs/checkout/primer-checkout/guides-and-recipes/build-custom-card-form">
    Card form tutorial
  </Card>

  <Card title="Best practices" icon="star" href="/docs/checkout/primer-checkout/configuration/best-practices">
    SwiftUI performance tips
  </Card>
</CardGroup>
