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

> Fix blank composable views (missing `.primerCheckoutSession`), sessions re-initializing without `@StateObject`, silent UIKit delegate callbacks, and Apple Pay not appearing.

## Quick diagnosis

| Symptom | Likely Cause | Solution |
| - | - | - |
| Error screen with retry button (`PrimerCheckout`, UIKit), or empty composable views and a `.failure` in `onCompletion` | Invalid or expired client token | Generate a fresh `clientToken` from your server for each session |
| Some payment methods missing, or an error with `misconfigured-payment-methods` / `missing-configuration` | Dashboard misconfiguration, unsupported currency/country, or methods the SDK does not support | 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(_:theme:onCompletion:)` around the views; they bind once the session reaches `.ready` |
| Composable views go empty after a view update | A new `PrimerCheckoutSession` is built whenever SwiftUI re-evaluates the enclosing `body` (stored in a `let` or computed property) | Hold the session as a `@StateObject` |
| 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 missing from the list, or an "Apple Pay Unavailable" screen | Apple Pay not enabled for this session on the Dashboard; device without Apple Pay or without a supported card in Wallet; or, when paying, a missing or wrong merchant ID (`invalid-merchant-identifier`, `unable-to-present-apple-pay`) | Enable Apple Pay on the Dashboard; test on a real device with cards in Wallet; verify Apple Developer & Dashboard config |
| Card form shows invalid despite filled fields | One of the four card fields is invalid or empty: card number, expiry, CVV or cardholder name (for example a field left out of a custom `cardDetails` slot) | 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, checkoutData)` 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. `PrimerCheckout` and `PrimerCheckoutPresenter` show an error screen with a **Retry** button, not a blank screen.

If you pass `PrimerSettings(uiOptions: PrimerUIOptions(isErrorScreenEnabled: false))`, `PrimerCheckout` shows nothing and only delivers `.failure` to `onCompletion`. `PrimerCheckoutPresenter` then closes the sheet and reports the failure to `primerCheckoutPresenterDidFailWithError`.

In the composable flow (`.primerCheckoutSession`), the SDK never shows an error screen for a failed initialization, regardless of `isErrorScreenEnabled`. `onCompletion` receives one `.failure`, `session.phase` stays `.initializing`, and the composable views stay empty.

**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 let .failure(error, _) = state {
      print("[Primer] Error: \(error.errorId)")
      print("[Primer] Description: \(error.errorDescription ?? "N/A")")
      print("[Primer] Diagnostics: \(error.diagnosticsId)")
    }
  }
)
```

> [!NOTE]
>
> If you see `invalid-client-token` in the error, your token is malformed or has expired. Generate a new one from your server.

### Payment methods not showing

**Cause:** If no payment method is left to show, the checkout does not show an empty list. The session never reaches `.ready`. It reports `.failure` through `onCompletion`, or through `primerCheckoutPresenterDidFailWithError` in UIKit. The `errorId` tells you why:

* `misconfigured-payment-methods`: the configuration loaded for this client token lists no payment methods, because none are enabled in the Dashboard for this session
* `missing-configuration`: the SDK supports none of the configured methods, or the session configuration could not be loaded

With `PrimerCheckout` and `PrimerCheckoutPresenter`, the shopper sees the error screen. It has no **Retry** button because no payment was attempted.

Missing individual methods have 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
* Klarna appears only when `PrimerKlarnaSDK` is linked into your app. With CocoaPods, add `pod 'PrimerKlarnaSDK'` next to `PrimerSDK` in your `Podfile`. Without it, Klarna is filtered out of the list, and the debug log shows `Filtering out unregistered payment method: KLARNA`.
* Client session missing required `currencyCode` or `countryCode` fields

**Solution:** The SDK logs nothing by default. In a Debug build, turn on SDK debug logging before you create the checkout: `PrimerLogging.shared.logger = DefaultLogger(logLevel: .debug)`. Then check the Xcode console for 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`](https://primer.io/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`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form), [`PrimerPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods), and [`PrimerVaultedPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/vaulted-payment-methods) — resolve their session from the SwiftUI environment. Without the [`.primerCheckoutSession(_:theme:onCompletion:)`](https://primer.io/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`.

If initialization fails, for example because of an invalid client token or no supported payment methods, the session never reaches `.ready`. The modifier's `onCompletion` receives a single `.failure`, `session.phase` stays `.initializing`, and the views stay empty with no error screen. To find the reason, match `.failure(error, _)` in `onCompletion` and read `error.errorId`. [Error screen after initialization](#error-screen-after-initialization) shows the pattern.

> [!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.

**Solution:** Hold a [`PrimerCheckoutSession`](https://primer.io/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 outcome PrimerCheckoutState.
    }
  }
}
```

If you only need the prebuilt screens, use the managed [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) view instead. It builds its own checkout, so it needs no `PrimerCheckoutSession` and no modifier.

### Session re-initializes unexpectedly

**Cause:** A `let` or computed property builds a new `PrimerCheckoutSession` whenever SwiftUI re-evaluates the enclosing `body`. The modifier starts a session only when the view appears. The new session never starts, so the composable views stay empty. Nothing uses the original session anymore.

**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 on every body evaluation, and the modifier never starts it
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 modifier also tears the session down whenever the views under it disappear. This includes transient cases such as switching tabs, pushing another screen, or presenting the parent sheet again. When the views reappear, the session initializes again from the client token. Attach `.primerCheckoutSession` to a view that stays on screen for the whole checkout.

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](https://primer.io/docs/checkout/primer-checkout/configuration/best-practices.md) 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.

**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, checkoutData: PrimerCheckoutData?) {
    print("Payment failed: \(error.errorId)")
  }

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

### Checkout not presented

**Cause:** Calling `presentCheckout` while a checkout is already on screen or still animating in. The SDK skips a second call only while the sheet is animating in. After that, a second call replaces the presenter's reference to the checkout on screen.

**Solution:** Check `PrimerCheckoutPresenter.isPresenting` before calling. It is `true` from the start of the presentation until the checkout is dismissed or completes:

```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:** `state.isValid` is `true` only when the card number, expiry date, CVV and cardholder name are all valid and non-empty. Billing address fields do not affect it. A field you left out of a custom `cardDetails` slot stays empty, so the form never becomes valid.

**Solution:** Inspect `state.fieldErrors` on the [`PrimerCardFormSession`](https://primer.io/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`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) slot, and `PrimerCardForm` already observes it, so the slot re-renders on every state change:

```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)
    }
  }
})
```

`fieldErrors` lists only fields that the shopper filled in and that failed validation. A field that was never filled in or never shown has no entry. If `fieldErrors` is empty but `isValid` is `false`, check `session.state.data` for the four card fields.

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:

* Apple Pay is not enabled in the Primer Dashboard for this client session, so it is not in the payment method list
* The device cannot make Apple Pay payments, or Wallet has no card for the supported networks. Apple Pay still appears in the list. Selecting it opens an "Apple Pay Unavailable" screen. The text under the title is the error description, for example `[apple-pay-device-not-supported] Device does not support Apple Pay (diagnosticsId: ...)` or `[apple-pay-no-cards-in-wallet] Apple Pay has no cards in wallet (diagnosticsId: ...)`. When the shopper came from the payment method list, the screen has a **Choose Another Payment Method** button.
* No merchant identifier: the payment fails with `invalid-merchant-identifier` because `PrimerSettings` has no `PrimerApplePayOptions`.
* Wrong merchant identifier: the identifier does not match the Apple Developer portal and the Primer Dashboard. Apple rejects the request. If the Apple Pay sheet cannot open, the payment fails with `unable-to-present-apple-pay`.

**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. Apple Pay appears whenever the Dashboard enables it for the session. It shows as a `CheckoutPaymentMethod` (with `type == "APPLE_PAY"`) in [`PrimerPaymentMethods`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/primer-payment-methods), or as a managed screen in [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout).

To pay, pass your merchant identifier through [`PrimerSettings(paymentMethodOptions:)`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-settings) with `PrimerApplePayOptions(merchantIdentifier: "merchant.com.myapp", merchantName: nil)`. `merchantName` has no default value, so you must pass it. If Apple Pay never appears, confirm the method is present in `session.selection?.state.paymentMethods` and check the Dashboard. See the [Apple Pay integration guide](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/guides/apple-pay-integration.md).

## 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 `https://github.com/primer-io/primer-sdk-ios`
3. Set the dependency rule to **Exact Version** `3.0.0-beta.8`, then add the `PrimerSDK` product to your target

### CocoaPods installation fails

**Cause:** `pod install` fails or doesn't include the Primer SDK.

**Solution:** Verify that your Podfile pins the beta version, then reinstall. Without a version, CocoaPods resolves the latest stable 2.x release, which does not include Primer Checkout.

```ruby theme={"dark"}
# Podfile
pod 'PrimerSDK', '3.0.0-beta.8'
```

```bash theme={"dark"}
pod install --repo-update
```

### Minimum deployment target

**Cause:** Your app's deployment target (or the Podfile's `platform :ios`) is below iOS 15.0. The build or install fails before the app runs. Depending on how you integrate, you see one of these errors:

* Swift Package Manager: `The package product 'PrimerSDK' requires minimum platform version 15.0 for the iOS platform, but this target supports 14.0`
* Xcode build: `compiling for iOS 14.0, but module 'PrimerSDK' has a minimum deployment target of iOS 15.0`
* `pod install`: a CocoaPods message that the matching PrimerSDK spec requires a higher minimum deployment target

**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) | Shown on the SDK error screen (unless `uiOptions.isErrorScreenEnabled` is `false`) and delivered as `.failure(error, checkoutData)` to `onCompletion`, or to `primerCheckoutPresenterDidFailWithError` in UIKit |

> [!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.

## Debugging tips

### Log the outcome state

The `onCompletion` closure on [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) or the `.primerCheckoutSession(_:theme:onCompletion:)` modifier receives the outcome [`PrimerCheckoutState`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state). Expect it more than once when a payment attempt fails and the customer retries. `.success` and `.dismissed` end the session. In the composable flow, a failed initialization also ends it: the modifier delivers that one `.failure` and nothing after it.

```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`](https://primer.io/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)")
```

`phase` has no failed value. If it stays `.initializing`, the session is either still loading or initialization failed, so check the `onCompletion` closure for a `.failure`.

### Log card form validation errors

Read `fieldErrors` from the [`PrimerCardFormSession`](https://primer.io/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 let .failure(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 let .failure(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

> [card] [Events guide](https://primer.io/docs/checkout/primer-checkout/configuration/events.md)
>
> Event handling patterns

> [card] [Build a custom card form](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/build-custom-card-form.md)
>
> Card form tutorial

> [card] [Best practices](https://primer.io/docs/checkout/primer-checkout/configuration/best-practices.md)
>
> SwiftUI performance tips


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.