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

# PrimerCardFormSession

> Observable card form session: field updates, validation, network selection, and submission

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

`PrimerCardFormSession` is the observable session for the card payment form. It publishes the current
[`PrimerCardFormState`](#primercardformstate) and exposes field update methods, co-badge network
selection, and submission controls.

The session is created by the SDK and injected into every [`PrimerCardForm`](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form)
slot closure. Read `state` to render your UI and call the session's methods to mutate the form.

```swift theme={"dark"}
@available(iOS 15.0, *)
@MainActor
public final class PrimerCardFormSession: ObservableObject
```

> [!NOTE]
>
> You never instantiate `PrimerCardFormSession` yourself. Each `PrimerCardForm` slot
> (`cardDetails`, `billingAddress`, `submitButton`) receives the session as a closure parameter.
> Observe it with `@ObservedObject`.

## Accessing the session

The session is passed to each `PrimerCardForm` slot. Capture it and observe `state` to drive your UI:

```swift theme={"dark"}
PrimerCardForm(
    submitButton: { session in
        Button("Pay") { session.submit() }
            .disabled(!session.state.isValid || session.state.isLoading)
    }
)
```

To observe state changes inside a custom subview, mark the captured session as `@ObservedObject`:

```swift theme={"dark"}
struct SubmitButton: View {
    @ObservedObject var session: PrimerCardFormSession

    var body: some View {
        Button("Pay") { session.submit() }
            .disabled(!session.state.isValid || session.state.isLoading)
    }
}
```

## Property

| Property | Type | Description |
| - | - | - |
| `state` | `PrimerCardFormState` | The latest card form state. `@Published private(set)` — read-only, observe it through `@ObservedObject`. Contains field values, validation errors, loading state, network selection, and surcharge information. |

## Methods

### Field update methods

Each setter forwards a raw string to the form and the session republishes `state`. The setters do
not validate. Validation runs in the SDK-rendered field for that type, a `CardFormDefaults` field or
a default slot body, while it is on screen. The SDK never validates a field you render yourself, so
setter calls alone never make `state.isValid` `true`. Updating the card number triggers network
detection and may populate `state.availableNetworks` and `state.binData`.

| Method | Description |
| - | - |
| `updateCardNumber(_ value: String)` | Updates the card number. Triggers network detection. |
| `updateCvv(_ value: String)` | Updates the CVV / security code. |
| `updateExpiryDate(_ value: String)` | Updates the expiry date (MM/YY). |
| `updateCardholderName(_ value: String)` | Updates the cardholder name. |
| `updatePostalCode(_ value: String)` | Updates the billing postal / ZIP code. |
| `updateCountryCode(_ value: String)` | Updates the billing country code (ISO 3166-1 alpha-2). |
| `updateCity(_ value: String)` | Updates the billing city. |
| `updateState(_ value: String)` | Updates the billing state / region. |
| `updateAddressLine1(_ value: String)` | Updates billing address line 1. |
| `updateAddressLine2(_ value: String)` | Updates billing address line 2. |
| `updatePhoneNumber(_ value: String)` | Updates the billing phone number. |
| `updateFirstName(_ value: String)` | Updates the billing first name. |
| `updateLastName(_ value: String)` | Updates the billing last name. |

Every setter drives the SDK-rendered field the customer sees. The value you set becomes what that
field displays. When that field is on screen, the form re-validates the value, so a prefilled field
does not hold back the submit button. While the field has focus, the customer's typing wins and the
setter does not change the visible text. The card
number shows formatted, the same way typing it would. `updateCountryCode` also resolves the country
picker's selection from the ISO code. See
[Pre-fill cardholder name](https://primer.io/docs/checkout/primer-checkout/guides-and-recipes/prefill-cardholder-name.md).

Writes and reads are asymmetric on purpose. `updateCardNumber` and `updateCvv` accept a raw value
and show it in the SDK-rendered field, but the card number and CVV are never handed back to your
code: `state.data` carries a masked card number and an empty CVV.

> [!WARNING]
>
> If your code passes a card number or CVV to these setters, your application handles cardholder
> data, and your PCI DSS obligations can change. Confirm your scope with your acquirer or a
> qualified assessor before you collect card data in your own UI. If you use only the SDK-rendered
> card fields and never call these two setters, this warning does not apply to you.

### Network selection

| Method | Description |
| - | - |
| `selectCardNetwork(_ network: PrimerCardNetwork)` | Selects a network for co-badged cards. Applicable only when `state.availableNetworks` contains more than one network. The picked network is sent as the card's preferred network while it is still the one shown. Without a pick, no preferred network is sent. The surcharge follows the network shown either way. See [PrimerCardNetwork](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-card-network.md). |

### Saving the card

| Method | Description |
| - | - |
| `setVaultOnSuccess(_ enabled: Bool) async throws` | Updates the client session so a successful card payment saves the card (`true`) or does not save it (`false`). Use it for a shopper-facing "save card" control. The client session must have a customer ID. Throws when the client session update fails. |

```swift theme={"dark"}
Toggle("Save card for next time", isOn: $saveCard)
    .onChange(of: saveCard) { enabled in
        Task { try? await session.setVaultOnSuccess(enabled) }
    }
```

### Lifecycle

| Method | Description |
| - | - |
| `submit()` | Tokenizes the card and creates the payment. If you set `onBeforePaymentCreate`, it runs first and can abort the payment. `state.isLoading` is `true` from the call until the payment succeeds or fails. Calls made while `state.isLoading` is `true` are ignored, so a double tap starts only one payment. `submit()` does not check `state.isValid`: gate your button on it. |
| `cancel()` | Leaves the card form. If the form was opened from the payment method list, it returns to that list (no outcome is delivered). Otherwise it dismisses the checkout and `.dismissed` is delivered. Ignored while a payment runs (`state.isLoading` is `true`) and while `onBeforePaymentCreate` waits for your decision, because the payment cannot be stopped. |

When wiring custom `TextField`s, drive each field with the matching `update*` method. You can round-trip
through `session.state.data` for every field except the card number and the CVV: that map is the
merchant-facing state, so the card number reads back **masked** (`424242••••••4242`) and the CVV reads
back **empty**. Binding a custom card-number field to it would feed the mask straight back into the form
on the next keystroke.

> [!WARNING]
>
> Never bind a custom card number or CVV field to `session.state.data`. Hold your own `@State` for those
> two and write one way, into the session.

The SDK validates only the fields it renders. When you replace a card field with your own,
`state.isValid` stays `false` and the default submit button stays disabled. Validate your fields
yourself and provide your own `submitButton`. Pass the expiry date to `updateExpiryDate` as `MM/YY`.

```swift theme={"dark"}
struct CustomCardFields: View {
    @State private var cardNumber = ""
    @State private var cvv = ""

    var body: some View {
        PrimerCardForm(
            cardDetails: { session in
                // Card number and CVV: one-way. Your own state is the source of truth.
                TextField("Card number", text: $cardNumber)
                    .onChange(of: cardNumber) { session.updateCardNumber($0) }
                TextField("CVV", text: $cvv)
                    .onChange(of: cvv) { session.updateCvv($0) }

                // Non-PCI fields can round-trip through the form state.
                TextField("MM/YY", text: Binding(
                    get: { session.state.data[.expiryDate] },
                    set: { session.updateExpiryDate($0) }
                ))
                TextField("Name on card", text: Binding(
                    get: { session.state.data[.cardholderName] },
                    set: { session.updateCardholderName($0) }
                ))
            },
            // The SDK validates only the fields it renders, so `state.isValid` stays `false`
            // with custom fields. Gate your own button on your own validation.
            submitButton: { session in
                Button("Pay") { session.submit() }
                    .disabled(!isComplete(session.state) || session.state.isLoading)
            }
        )
    }

    private func isComplete(_ state: PrimerCardFormState) -> Bool {
        cardNumber.count >= 12 && cvv.count >= 3
            && !state.data[.expiryDate].isEmpty && !state.data[.cardholderName].isEmpty
    }
}
```

## PrimerCardFormState

```swift theme={"dark"}
@available(iOS 15.0, *)
public struct PrimerCardFormState: Equatable
```

The complete state of the card payment form: configuration, current field values, validation errors,
loading and validity flags, co-badge network selection, and surcharge information.

### Properties

| Property | Type | Description |
| - | - | - |
| `configuration` | `CardFormConfiguration` | Which fields the form requires. See [CardFormConfiguration](#cardformconfiguration). |
| `data` | `FormData` | Current field values keyed by `PrimerInputElementType`. See [FormData](#formdata). |
| `fieldErrors` | `[FieldError]` | Validation errors per field. Empty when there are no errors. See [FieldError](#fielderror). |
| `isLoading` | `Bool` | `true` while a payment is being submitted. |
| `isValid` | `Bool` | `true` when the card number, expiry date, CVV and cardholder name pass validation in their SDK-rendered fields. Billing address fields do not affect it. The SDK never validates a field you render yourself, so it keeps `isValid` `false`. |
| `selectedCountry` | `PrimerCountry?` | Selected billing country, or `nil`. See [PrimerCountry](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-country.md). |
| `selectedNetwork` | `PrimerCardNetwork?` | The network the form shows: the one picked with the co-badge selector or `selectCardNetwork(_:)` while it is still available, otherwise the first detected network. `nil` when no network is detected. |
| `availableNetworks` | `[PrimerCardNetwork]` | Card networks detected from the card number. More than one indicates a co-badged card. |
| `surchargeAmountRaw` | `Int?` | Surcharge amount in the smallest currency unit (e.g. cents), or `nil`. |
| `surchargeAmount` | `String?` | Formatted surcharge amount for display, or `nil`. |
| `binData` | `PrimerBinData?` | BIN metadata detected from the card number, or `nil`. `preferred` is the first of the detected networks that your client session's ordered allowed card networks list contains, or `nil` if none is allowed. `alternatives` holds the other detected networks: allowed ones in that order, then any that are not allowed. |

### Convenience properties

| Property | Type | Description |
| - | - | - |
| `displayFields` | `[PrimerInputElementType]` | All fields that should be rendered (card fields plus billing fields when enabled). |

### Helper methods

| Method | Description |
| - | - |
| `hasError(for fieldType: PrimerInputElementType) -> Bool` | Returns `true` when the given field currently has a validation error. |
| `errorMessage(for fieldType: PrimerInputElementType) -> String?` | Returns the human-readable error message for the field, or `nil` when there is none. |

```swift theme={"dark"}
PrimerCardForm(
    cardDetails: { session in
        VStack(alignment: .leading) {
            CardFormDefaults.cardNumber(session)
            if session.state.hasError(for: .cardNumber),
               let message = session.state.errorMessage(for: .cardNumber) {
                Text(message).foregroundColor(.red)
            }
        }
    }
)
```

> [!NOTE]
>
> Validation errors surface in `fieldErrors` after the form evaluates a field. Use `isValid` to
> gate the submit button and `errorMessage(for:)` to display per-field messages. See
> [ValidationResult](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/validation-result.md).

## CardFormConfiguration

```swift theme={"dark"}
@available(iOS 15.0, *)
public struct CardFormConfiguration: Equatable
```

Declares which fields the card form requires.

| Property | Type | Description |
| - | - | - |
| `cardFields` | `[PrimerInputElementType]` | Card-specific fields (card number, expiry, CVV, cardholder name). |
| `billingFields` | `[PrimerInputElementType]` | Billing address fields. Empty when billing address collection is disabled. |
| `requiresBillingAddress` | `Bool` | `true` when billing address collection is enabled. |
| `allFields` | `[PrimerInputElementType]` | Computed: `cardFields + billingFields`. |

```swift theme={"dark"}
public init(
    cardFields: [PrimerInputElementType],
    billingFields: [PrimerInputElementType] = [],
    requiresBillingAddress: Bool = false
)
```

The static `CardFormConfiguration.default` requires `[.cardNumber, .expiryDate, .cvv, .cardholderName]`
with no billing fields.

## FormData

```swift theme={"dark"}
@available(iOS 15.0, *)
public struct FormData: Equatable
```

Type-safe container for field values keyed by `PrimerInputElementType`. Subscript access returns an
empty string for unset fields.

| Member | Type | Description |
| - | - | - |
| `subscript(fieldType: PrimerInputElementType)` | `String` | Reads or writes the value for a field. Returns `""` when unset. |
| `dictionary` | `[PrimerInputElementType: String]` | All current field values as a dictionary. |

```swift theme={"dark"}
let cardNumber = session.state.data[.cardNumber]
let allValues = session.state.data.dictionary
```

## FieldError

```swift theme={"dark"}
@available(iOS 15.0, *)
public struct FieldError: Equatable, Identifiable
```

A validation error for a specific form field.

| Property | Type | Description |
| - | - | - |
| `id` | `PrimerInputElementType` | Stable identifier (the field type) for SwiftUI diffing. |
| `fieldType` | `PrimerInputElementType` | The field that has the error. |
| `message` | `String` | Human-readable error message to display. |
| `errorCode` | `String?` | Machine-readable error code for programmatic handling, or `nil`. |

```swift theme={"dark"}
ForEach(session.state.fieldErrors) { error in
    Text(error.message)
}
```

## PrimerInputElementType

The field identifier used throughout `PrimerCardFormState` (in `configuration`, `data`, and
`fieldErrors`).

| Case | Field |
| - | - |
| `.cardNumber` | Card number |
| `.expiryDate` | Expiry date (MM/YY) |
| `.cvv` | CVV / security code |
| `.cardholderName` | Cardholder name |
| `.postalCode` | Billing postal code |
| `.countryCode` | Billing country |
| `.city` | Billing city |
| `.state` | Billing state / region |
| `.addressLine1` | Billing address line 1 |
| `.addressLine2` | Billing address line 2 |
| `.phoneNumber` | Billing phone number |
| `.firstName` | Billing first name |
| `.lastName` | Billing last name |
| `.email` | Email address |
| `.otp` | One-time passcode |
| `.retailer` | Retailer |
| `.all` | All fields |
| `.unknown` | Unknown / unspecified |

## See also

> [card] [PrimerCardForm](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form.md)
>
> The card form component and its customizable slots.

> [card] [Card field components](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-field-components.md)
>
> Prebuilt field views from CardFormDefaults.

> [card] [ValidationResult](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/validation-result.md)
>
> Field validation model and error handling.

> [card] [PrimerCountry](https://primer.io/docs/sdk/ios-checkout/v3.0.0-beta/api/common/primer-country.md)
>
> Country model for billing address selection.


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