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

# Handle the payment result

> Receive and handle checkout outcomes in SwiftUI and UIKit

> [!WARNING]
>
> Primer Checkout iOS SDK is currently in **beta** (v3.0.0-beta.8).
> The API is subject to change before the stable release.

Every checkout ends in one of three outcomes: the payment **succeeds**, it **fails**, or the user **dismisses** the flow. This guide shows how to receive and react to that outcome in both SwiftUI and UIKit.

The outcome is always delivered as a [`PrimerCheckoutState`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state) value:

| Case | Associated value | Meaning |
| - | - | - |
| `.success` | [`PaymentResult`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state) | The payment completed. |
| `.failure` | [`PrimerError`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error), `checkoutData: PrimerCheckoutData?` | The payment or checkout failed. `checkoutData` carries the payment ID and order ID when a payment was created before the failure (for example, a decline). |
| `.dismissed` | — | The user closed the checkout without completing a payment. |

> [!NOTE]
>
> The same `PrimerCheckoutState` also has lifecycle cases (`.initializing`, `.ready`). `onCompletion` never receives them. With [`PrimerCheckoutSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session), observe its `phase` for the lifecycle instead. Always include a `default` case when switching, so future enum additions stay source-compatible.

## SwiftUI

### Using PrimerCheckout

The prebuilt [`PrimerCheckout`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) view accepts an `onCompletion` closure. It receives `.failure` once per failed attempt, including a failed initialization. While the SDK error screen is enabled (`uiOptions.isErrorScreenEnabled`, the default), the checkout stays open so the customer can retry. It then receives `.success` or `.dismissed` exactly once, and nothing after that.

```swift theme={"dark"}
import SwiftUI
import PrimerSDK

struct CheckoutView: View {
  let clientToken: String
  @State private var paymentResult: PaymentResult?

  var body: some View {
    if let paymentResult {
      ConfirmationView(result: paymentResult)
    } else {
      PrimerCheckout(clientToken: clientToken) { state in
        switch state {
        case let .success(result):
          paymentResult = result
        case let .failure(error, _):
          print("Checkout failed: \(error.errorId) — \(error.diagnosticsId)")
        case .dismissed:
          break
        default:
          break
        }
      }
    }
  }
}
```

### Using the .primerCheckoutSession modifier

When you embed the composable views (for example [`PrimerCardForm`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form)) inline in your own layout, hold a [`PrimerCheckoutSession`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) as a `@StateObject` and receive the outcome through the `onCompletion` parameter of the `.primerCheckoutSession(_:theme:onCompletion:)` modifier.

```swift theme={"dark"}
import SwiftUI
import PrimerSDK

struct InlineCheckoutView: View {
  @StateObject private var session: PrimerCheckoutSession
  @State private var paymentResult: PaymentResult?

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

  var body: some View {
    Group {
      if let paymentResult {
        ConfirmationView(result: paymentResult)
      } else {
        ScrollView {
          PrimerCardForm()
        }
      }
    }
    .primerCheckoutSession(session) { state in
      switch state {
      case let .success(result):
        paymentResult = result
      case let .failure(error, _):
        print("Checkout failed: \(error.errorId) — \(error.diagnosticsId)")
      case .dismissed:
        break
      default:
        break
      }
    }
  }
}
```

> [!NOTE]
>
> `.success` and `.dismissed` are delivered at most once — the session latches the first state that ends it, so a later `.dismissed` produced by a view-lifecycle teardown is ignored.
>
> `.failure` is different. Once checkout is ready, a failed payment attempt does **not** end the session: the SDK shows its error screen, the customer can retry, and `onCompletion` is called again with the next attempt's outcome. Only a failure *before* the checkout becomes ready is terminal. Keep your checkout UI alive on `.failure` — otherwise a customer who retries after a decline and then succeeds never reaches your success handling.

### Navigating on success

Use the `PaymentResult` to drive navigation. `PaymentResult` is `Equatable` but not `Hashable`, so it cannot be appended to a `NavigationPath` or used with `navigationDestination(for:)` directly. Instead, store the result in `@State` and present the confirmation screen with `navigationDestination(isPresented:)`. `NavigationStack` and `navigationDestination(isPresented:)` require iOS 16. On iOS 15, use `NavigationView` with a `NavigationLink(isActive:)` instead.

```swift theme={"dark"}
import SwiftUI
import PrimerSDK

@available(iOS 16.0, *)
struct CheckoutFlowView: View {
  let clientToken: String
  @State private var paymentResult: PaymentResult?

  private var showsConfirmation: Binding<Bool> {
    Binding(
      get: { paymentResult != nil },
      set: { if !$0 { paymentResult = nil } }
    )
  }

  var body: some View {
    NavigationStack {
      PrimerCheckout(clientToken: clientToken) { state in
        if case let .success(result) = state {
          paymentResult = result
        }
      }
      .navigationDestination(isPresented: showsConfirmation) {
        if let paymentResult {
          ConfirmationView(result: paymentResult)
        }
      }
    }
  }
}
```

## UIKit

In UIKit, present the checkout with [`PrimerCheckoutPresenter`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter) and adopt `PrimerCheckoutPresenterDelegate` to receive the outcome. The presenter closes its own sheet, so you don't dismiss it yourself. It finishes dismissing the sheet before it calls `primerCheckoutPresenterDidCompleteWithSuccess(_:)`. `primerCheckoutPresenterDidDismiss()` is called when the customer closes the checkout without completing a payment. `primerCheckoutPresenterDidFailWithError(_:checkoutData:)` is called once per failed attempt, including a failed initialization. The sheet stays open on the SDK error screen so the customer can retry. It closes on failure only when `uiOptions.isErrorScreenEnabled` is `false`.

```swift theme={"dark"}
import UIKit
import PrimerSDK

final class CheckoutViewController: UIViewController {
  let clientToken: String

  init(clientToken: String) {
    self.clientToken = clientToken
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

  func startCheckout() {
    PrimerCheckoutPresenter.shared.delegate = self
    PrimerCheckoutPresenter.presentCheckout(clientToken: clientToken, from: self)
  }
}

extension CheckoutViewController: PrimerCheckoutPresenterDelegate {
  func primerCheckoutPresenterDidCompleteWithSuccess(_ result: PaymentResult) {
    let confirmation = ConfirmationViewController(result: result)
    navigationController?.pushViewController(confirmation, animated: true)
  }

  func primerCheckoutPresenterDidFailWithError(_ error: PrimerError, checkoutData: PrimerCheckoutData?) {
    print("Checkout failed: \(error.errorId) — \(error.diagnosticsId)")
  }

  func primerCheckoutPresenterDidDismiss() {
    // User closed the checkout without completing a payment.
  }
}
```

> [!TIP]
>
> Set the `delegate` before calling `presentCheckout`. If no delegate is set when checkout completes, the outcome is logged and otherwise dropped.

The delegate maps one-to-one onto the SwiftUI `PrimerCheckoutState` cases:

| Delegate method | Equivalent state |
| - | - |
| `primerCheckoutPresenterDidCompleteWithSuccess(_:)` | `.success(PaymentResult)` |
| `primerCheckoutPresenterDidFailWithError(_:checkoutData:)` | `.failure(PrimerError, checkoutData:)` |
| `primerCheckoutPresenterDidDismiss()` | `.dismissed` |

## Reading the PaymentResult

On success you receive a `PaymentResult`. Read its properties to build your confirmation UI, send analytics, or reconcile against your backend.

| Property | Type | Description |
| - | - | - |
| `paymentId` | `String` | The Primer payment ID. |
| `status` | `PaymentStatus` | The payment status: `.pending`, `.success`, or `.failed`. |
| `token` | `String?` | An identifier for the payment. For card and saved payment methods, this is the payment ID. For other methods, it is the payment method token when the method returns one, otherwise `nil`. |
| `redirectUrl` | `String?` | Reserved. Always `nil`. |
| `errorMessage` | `String?` | Reserved. Always `nil`. |
| `amount` | `Int?` | The amount in minor units (for example, cents for USD), when the payment response includes it. `nil` for card, saved and Stripe ACH payment methods. |
| `currencyCode` | `String?` | The ISO 4217 currency code (for example, `"USD"`), when available. `nil` for card, saved and Stripe ACH payment methods. |
| `paymentMethodType` | `String?` | The payment method type used (for example, `"PAYMENT_CARD"`). |

`amount` and `currencyCode` can be `nil`, and they are always `nil` for card, saved and Stripe ACH payment methods. If you need them, read them from your own order or from your backend, using `paymentId`. The `Charged` line in the sample below only shows when both values are present.

```swift theme={"dark"}
import SwiftUI
import PrimerSDK

struct ConfirmationView: View {
  let result: PaymentResult

  var body: some View {
    VStack(spacing: 12) {
      Text("Payment confirmed")
        .font(.title2.bold())
      Text("Payment ID: \(result.paymentId)")
      if let amount = result.amount, let currency = result.currencyCode {
        Text("Charged: \(amount) \(currency)")
      }
    }
  }
}
```

> [!NOTE]
>
> `status` is always `.success` for card, saved and Stripe ACH payment methods in a `.success(PaymentResult)` outcome. Other payment methods report the status from the Payments API response. It can be `.pending` until the processor confirms the payment, as with some Klarna, PayPal, web redirect or QR code payments. Check `status` before you fulfill an order. Primer Checkout creates the payment for you, so `PrimerSettings(paymentHandling: .manual)` is not supported.

## Handling the PrimerError

On failure you receive a [`PrimerError`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error). The SDK already surfaces failures inside the checkout UI, so you typically use the error for logging, analytics, and support workflows rather than for displaying a message yourself.

| Property | Type | Description |
| - | - | - |
| `errorId` | `String` | A stable, machine-readable identifier for the error category. |
| `diagnosticsId` | `String` | A unique ID for this specific occurrence — include it in support requests. |
| `errorDescription` | `String?` | A formatted description combining the error ID, message, and diagnostics ID. |
| `recoverySuggestion` | `String?` | A suggested recovery action, when one is available. |

Together with the error, you receive `checkoutData: PrimerCheckoutData?`. In SwiftUI, bind it with `case let .failure(error, checkoutData)`. In UIKit, it is the `checkoutData` parameter of `primerCheckoutPresenterDidFailWithError(_:checkoutData:)`. If a payment was created before the failure, for example on a decline, use `checkoutData?.payment?.id` and `checkoutData?.payment?.orderId` to find it on your backend. `checkoutData` is `nil` if the failure happened before a payment existed, for example during initialization or tokenization, or after a connectivity error.

```swift theme={"dark"}
func report(_ error: PrimerError) {
  analytics.track("checkout_failed", properties: [
    "error_id": error.errorId,
    "diagnostics_id": error.diagnosticsId
  ])

  if let suggestion = error.recoverySuggestion {
    print("Recovery: \(suggestion)")
  }
}
```

> [!WARNING]
>
> Always log `diagnosticsId`. Primer support uses it to trace the exact failed request when you raise an issue.

## See also

> [card] [PrimerCheckoutState](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state.md)
>
> The full state enum and the PaymentResult model.

> [card] [PrimerError](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-error.md)
>
> Error categories, IDs, and diagnostics.

> [card] [PrimerCheckoutPresenter](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter.md)
>
> The UIKit entry point and delegate callbacks.

> [card] [PrimerCheckout](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout.md)
>
> The prebuilt SwiftUI checkout view.


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