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

# Component Architecture Explorer — iOS

> Explore the iOS Checkout SwiftUI view, session, and slot hierarchy interactively.

export const CopyButton = ({getText}) => {
  const [copied, setCopied] = useState(false);
  const [hovered, setHovered] = useState(false);
  const handleCopy = () => {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(getText());
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    }
  };
  return <button onClick={handleCopy} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
    display: 'flex',
    alignItems: 'center',
    gap: DS.space.xs,
    padding: `${DS.space.xs}px ${DS.space.sm + 2}px`,
    border: `1px solid ${copied ? DS.color.success : hovered ? DS.color.borderHover : DS.color.border}`,
    borderRadius: DS.radius.sm,
    background: copied ? DS.color.successLight : hovered ? DS.color.bgSubtle : DS.color.bgSurface,
    fontSize: DS.font.xxs,
    fontWeight: 600,
    color: copied ? DS.color.success : DS.color.textSecondary,
    cursor: 'pointer',
    transition: `all ${DS.transition.fast}`,
    outline: 'none'
  }}>{copied ? '✓ Copied' : '⎘ Copy'}</button>;
};

export const Badge = ({children, color = DS.color.brand}) => <span style={{
  fontSize: DS.font.xxs - 1,
  fontWeight: 700,
  padding: `1px ${DS.space.sm - 2}px`,
  borderRadius: DS.radius.sm,
  background: `${color}15`,
  color: color,
  textTransform: 'uppercase',
  letterSpacing: '0.3px',
  whiteSpace: 'nowrap'
}}>{children}</span>;

export const Panel = ({title, children, style}) => <div style={Object.assign({
  background: DS.color.bgSurface,
  border: `1px solid ${DS.color.border}`,
  borderRadius: DS.radius.lg,
  overflow: 'hidden'
}, style)}>
    {title && <div style={{
  padding: `${DS.space.sm}px ${DS.space.lg}px`,
  borderBottom: `1px solid ${DS.color.border}`,
  fontSize: DS.font.xxs,
  fontWeight: 700,
  textTransform: 'uppercase',
  letterSpacing: '1px',
  color: DS.color.textMuted,
  background: DS.color.bgSubtle
}}>
        {title}
      </div>}
    {children}
  </div>;

export const DS = {
  color: {
    brand: 'var(--ds-color-brand, #2f98ff)',
    brandLight: 'var(--ds-color-brand-light, #e8f2ff)',
    brandMuted: 'var(--ds-color-brand-muted, rgba(47, 152, 255, 0.12))',
    focus: 'var(--ds-color-brand, #2f98ff)',
    success: 'var(--ds-color-success, #1a8a5c)',
    successLight: 'var(--ds-color-success-light, #edf8f2)',
    successMuted: 'var(--ds-color-success-muted, rgba(26, 138, 92, 0.12))',
    warning: 'var(--ds-color-warning, #c47a20)',
    warningLight: 'var(--ds-color-warning-light, #fef6eb)',
    warningMuted: 'var(--ds-color-warning-muted, rgba(196, 122, 32, 0.12))',
    error: 'var(--ds-color-error, #c44040)',
    errorLight: 'var(--ds-color-error-light, #fef0f0)',
    errorMuted: 'rgba(196, 64, 64, 0.12)',
    purple: '#7c5cbf',
    purpleLight: '#f3effc',
    purpleMuted: 'rgba(124, 92, 191, 0.12)',
    text: 'var(--ds-color-text, #1c1b18)',
    textSecondary: 'var(--ds-color-text-secondary, #5c5953)',
    textMuted: 'var(--ds-color-text-muted, #9d9a92)',
    textDisabled: '#c5c2ba',
    border: 'var(--ds-color-border, #e4e2dd)',
    borderHover: 'var(--ds-color-border-hover, #d0cec8)',
    bgPage: 'var(--ds-color-bg-page, #f5f4f1)',
    bgSurface: 'var(--ds-color-bg-surface, #ffffff)',
    bgSubtle: 'var(--ds-color-bg-subtle, #fafaf8)',
    bgOverlay: 'rgba(0, 0, 0, 0.04)'
  },
  space: {
    xxs: 2,
    xs: 4,
    sm: 8,
    md: 12,
    lg: 16,
    xl: 20,
    xxl: 24,
    xxxl: 32
  },
  font: {
    xxs: 10,
    xs: 11,
    sm: 12,
    md: 13,
    base: 14,
    lg: 15,
    xl: 16,
    mono: "ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace",
    sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
  },
  radius: {
    sm: 4,
    md: 8,
    lg: 12
  },
  transition: {
    fast: '120ms ease',
    normal: '200ms ease'
  }
};

export const COMPONENT_INFO = {
  PrimerCheckoutSession: {
    color: DS.color.brand,
    label: 'session owner',
    desc: 'Top-level session owner for composable, inline checkout. Hold it as a @StateObject and wire it into your view hierarchy with the .primerCheckoutSession(_:onCompletion:) modifier. The fully managed PrimerCheckout view is powered by the same session.',
    properties: [{
      name: 'clientToken',
      type: 'String',
      desc: 'Client token from your POST /client-session call'
    }, {
      name: 'settings',
      type: 'PrimerSettings',
      desc: 'SDK configuration (default: PrimerSettings())'
    }, {
      name: 'theme',
      type: 'PrimerCheckoutTheme',
      desc: 'Design tokens applied to all SDK views (default: PrimerCheckoutTheme())'
    }, {
      name: 'cardForm',
      type: 'PrimerCardFormSession?',
      desc: 'Card-form sub-session, non-nil once phase == .ready'
    }, {
      name: 'selection',
      type: 'PrimerSelectionSession?',
      desc: 'Payment-method-selection sub-session, non-nil once phase == .ready'
    }, {
      name: 'onCompletion',
      type: '((PrimerCheckoutState) -> Void)?',
      desc: 'Terminal outcome delivered once via the .primerCheckoutSession modifier'
    }],
    states: ['.initializing', '.ready'],
    code: '@StateObject private var session: PrimerCheckoutSession\n\ninit(clientToken: String) {\n  _session = StateObject(\n    wrappedValue: PrimerCheckoutSession(clientToken: clientToken)\n  )\n}\n\nvar body: some View {\n  ScrollView {\n    VStack(spacing: 24) {\n      PrimerPaymentMethods()\n      PrimerCardForm()\n    }\n    .padding()\n  }\n  .primerCheckoutSession(session) { state in\n    switch state {\n    case .success(let result):\n      print("Payment \\(result.paymentId) succeeded")\n    case .failure(let error):\n      print("Checkout failed: \\(error.localizedDescription)")\n    case .dismissed:\n      print("Checkout dismissed")\n    default:\n      break\n    }\n  }\n}'
  },
  PrimerCardForm: {
    color: DS.color.success,
    label: 'card',
    desc: 'SwiftUI card payment form composed from three @ViewBuilder section slots: cardDetails, billingAddress, and submitButton. Each slot receives the PrimerCardFormSession and defaults to a CardFormDefaults builder. Resolves its session from the environment.',
    properties: [{
      name: 'cardDetails',
      type: '(PrimerCardFormSession) -> CardDetails',
      desc: 'Card input fields slot (default: CardFormDefaults.cardDetails)'
    }, {
      name: 'billingAddress',
      type: '(PrimerCardFormSession) -> Billing',
      desc: 'Billing address fields slot, rendered only when required (default: CardFormDefaults.billingAddress)'
    }, {
      name: 'submitButton',
      type: '(PrimerCardFormSession) -> Submit',
      desc: 'Submit/pay button slot (default: CardFormDefaults.submitButton)'
    }],
    code: 'PrimerCardForm(\n  submitButton: { session in\n    Button("Pay now") { session.submit() }\n      .buttonStyle(.borderedProminent)\n      .disabled(!session.state.isValid || session.state.isLoading)\n  }\n)'
  },
  cardDetails: {
    color: DS.color.success,
    label: 'defaults',
    desc: 'Default card-details section (CardFormDefaults.cardDetails) — card number, expiry+CVV row, cardholder name, and the co-badged network selector when multiple networks are detected. Config-aware: only the fields the session requires are rendered.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session passed into the slot'
    }],
    code: 'PrimerCardForm(cardDetails: { session in\n  VStack(alignment: .leading, spacing: 16) {\n    Text("Card details").font(.headline)\n    CardFormDefaults.cardDetails(session)\n    Text("Your card is encrypted and never stored on this device.")\n      .font(.footnote)\n      .foregroundColor(.secondary)\n  }\n  .padding()\n})'
  },
  cardNumber: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CardFormDefaults.cardNumber building block. Card number input with network-aware grouping, Luhn validation, and detected-network icon. Self-hiding when .cardNumber is not in the configuration.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'CardFormDefaults.cardNumber(session)'
  },
  expiryDate: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CardFormDefaults.expiryDate building block. Expiry field; auto-inserts the / separator (MM/YY) and validates for a valid, future month.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'HStack(spacing: 16) {\n  CardFormDefaults.expiryDate(session)\n  CardFormDefaults.cvv(session)\n}'
  },
  cvv: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CardFormDefaults.cvv building block. CVV field; masked input, max length adapts to the detected network (4 for Amex, 3 otherwise).',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'CardFormDefaults.cvv(session)'
  },
  cardholderName: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CardFormDefaults.cardholderName building block. Cardholder name field with word capitalization. Self-hiding when not configured.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'CardFormDefaults.cardholderName(session)'
  },
  cardNetwork: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CardFormDefaults.cardNetwork co-badged network selector. Renders only when more than one network is detected (state.availableNetworks.count > 1). Tapping commits the choice via session.selectCardNetwork(_:). Add it yourself only when you recompose individual card fields — the full cardDetails section already renders it.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: '// Shown automatically for co-badged cards.\n// Read network info from the published session state:\nlet available = session.state.availableNetworks\nlet selected = session.state.selectedNetwork\n\n// Commit a choice when the user taps a network:\nif let first = available.first {\n  session.selectCardNetwork(first)\n}'
  },
  submitButton: {
    color: DS.color.brand,
    label: 'action',
    desc: 'CardFormDefaults.submitButton — the default pay button, enabled when the form is valid and not loading. Calls session.submit() to trigger tokenization and payment.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'PrimerCardForm(submitButton: { session in\n  CardFormDefaults.submitButton(session)\n})'
  },
  billingAddress: {
    color: DS.color.textMuted,
    label: 'defaults',
    desc: 'Default billing-address section (CardFormDefaults.billingAddress). Renders only when the session configuration requires billing fields, so it is always safe to include.',
    properties: [{
      name: 'session',
      type: 'PrimerCardFormSession',
      desc: 'The active card form session'
    }],
    code: 'PrimerCardForm(billingAddress: { session in\n  CardFormDefaults.billingAddress(session)\n})'
  },
  PrimerPaymentMethods: {
    color: DS.color.purple,
    label: 'methods',
    desc: 'SwiftUI list of available payment methods composed from three @ViewBuilder slots: header, per-method row, and emptyState. Resolves its PrimerSelectionSession from the environment. Selecting a method through onSelect starts that method\'s flow (card form, redirect, native wallet, etc.).',
    properties: [{
      name: 'header',
      type: '(PrimerSelectionSession) -> Header',
      desc: 'Section header slot (default: PaymentMethodsDefaults.header)'
    }, {
      name: 'method',
      type: '(CheckoutPaymentMethod, () -> Void) -> Method',
      desc: 'Per-method row slot. Receives the method and an onSelect closure (default: PaymentMethodsDefaults.method)'
    }, {
      name: 'emptyState',
      type: '(PrimerSelectionSession) -> Empty',
      desc: 'Shown when no methods are available (default: PaymentMethodsDefaults.emptyState)'
    }],
    code: 'PrimerPaymentMethods(\n  method: { paymentMethod, onSelect in\n    Button(action: onSelect) {\n      HStack {\n        if let icon = paymentMethod.icon {\n          Image(uiImage: icon)\n            .resizable()\n            .scaledToFit()\n            .frame(width: 28, height: 28)\n        }\n        Text(paymentMethod.buttonText ?? paymentMethod.name)\n        Spacer()\n        if let surcharge = paymentMethod.formattedSurcharge {\n          Text(surcharge).foregroundColor(.secondary)\n        }\n      }\n      .padding()\n    }\n    .buttonStyle(.plain)\n  }\n)'
  },
  PrimerVaultedPaymentMethods: {
    color: DS.color.purple,
    label: 'vaulted',
    desc: 'SwiftUI list of saved (vaulted) payment methods composed from header, per-method item, and submitButton slots. Tapping a row selects that method; the submit button pays with the selected one. Selection, deletion, and CVV recapture are driven through PrimerSelectionSession.',
    properties: [{
      name: 'header',
      type: '(PrimerSelectionSession) -> AnyView',
      desc: 'Header slot (default: VaultedPaymentMethodsDefaults.header)'
    }, {
      name: 'item',
      type: '(VaultedMethod, Bool, () -> Void) -> AnyView',
      desc: 'Row slot. Receives the method, whether it is selected, and an onSelect callback (default: VaultedPaymentMethodsDefaults.item)'
    }, {
      name: 'submitButton',
      type: '(Bool, Bool, () -> Void) -> AnyView',
      desc: 'Submit slot. Receives isLoading, isEnabled, and onSubmit (default: VaultedPaymentMethodsDefaults.submitButton)'
    }],
    code: 'PrimerVaultedPaymentMethods(\n  item: { method, isSelected, onSelect in\n    AnyView(\n      Button(action: onSelect) {\n        HStack {\n          Text(method.paymentMethodType)\n          Spacer()\n          if isSelected {\n            Image(systemName: "checkmark.circle.fill")\n          }\n        }\n        .padding()\n      }\n      .buttonStyle(.plain)\n    )\n  }\n)'
  }
};

export const CompNode = ({name, selected, onClick, depth = 0, children}) => {
  const [hovered, setHovered] = useState(false);
  const info = COMPONENT_INFO[name];
  if (!info) return null;
  const isActive = selected === name;
  return <div onClick={e => {
    e.stopPropagation();
    onClick(name);
  }} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{
    background: isActive ? info.color + '08' : hovered ? DS.color.bgSubtle : DS.color.bgPage,
    border: '2px solid ' + (isActive ? info.color : hovered ? DS.color.borderHover : DS.color.border),
    borderLeftWidth: 3,
    borderLeftColor: isActive ? info.color : hovered ? DS.color.borderHover : DS.color.border,
    borderRadius: DS.radius.md,
    padding: depth > 1 ? DS.space.sm + 'px ' + (DS.space.sm + 2) + 'px' : DS.space.sm + 2 + 'px ' + DS.space.md + 'px',
    margin: DS.space.xs + 'px 0',
    cursor: 'pointer',
    transition: 'border-color ' + DS.transition.fast + ', background ' + DS.transition.fast,
    minWidth: 0
  }}>
      <div style={{
    fontFamily: DS.font.mono,
    fontWeight: 600,
    fontSize: depth > 1 ? DS.font.xs : DS.font.sm,
    color: isActive ? info.color : DS.color.textSecondary,
    display: 'flex',
    alignItems: 'center',
    gap: DS.space.sm,
    flexWrap: 'wrap'
  }}>
        <span>{name}</span>
        <Badge color={info.color}>{info.label}</Badge>
      </div>
      {children && <div style={{
    marginTop: DS.space.sm
  }}>{children}</div>}
    </div>;
};

export const ComponentArchitectureExplorer = () => {
  const [selected, setSelected] = useState(null);
  const data = selected ? COMPONENT_INFO[selected] : null;
  const codeText = data ? data.code : '';
  return <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 320px',
    gap: DS.space.xl
  }}>
      <Panel title="View & Session Hierarchy" style={{
    minWidth: 0,
    overflow: 'hidden'
  }}>
        <div style={{
    padding: DS.space.lg
  }}>
          <CompNode name="PrimerCheckoutSession" selected={selected} onClick={setSelected}>

            <div style={{
    fontSize: DS.font.xxs,
    fontWeight: 600,
    textTransform: 'uppercase',
    letterSpacing: '0.8px',
    color: DS.color.textMuted,
    margin: DS.space.sm + 'px 0 ' + DS.space.xs + 'px',
    paddingLeft: DS.space.xs
  }}>
              .ready → composable views via .primerCheckoutSession
            </div>

            <CompNode name="PrimerCardForm" selected={selected} onClick={setSelected} depth={1}>
              <CompNode name="cardDetails" selected={selected} onClick={setSelected} depth={2}>
                <CompNode name="cardNumber" selected={selected} onClick={setSelected} depth={3} />
                <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: DS.space.xs
  }}>
                  <CompNode name="expiryDate" selected={selected} onClick={setSelected} depth={3} />
                  <CompNode name="cvv" selected={selected} onClick={setSelected} depth={3} />
                </div>
                <CompNode name="cardholderName" selected={selected} onClick={setSelected} depth={3} />
                <CompNode name="cardNetwork" selected={selected} onClick={setSelected} depth={3} />
              </CompNode>
              <CompNode name="billingAddress" selected={selected} onClick={setSelected} depth={2} />
              <CompNode name="submitButton" selected={selected} onClick={setSelected} depth={2} />
            </CompNode>

            <CompNode name="PrimerPaymentMethods" selected={selected} onClick={setSelected} depth={1} />
            <CompNode name="PrimerVaultedPaymentMethods" selected={selected} onClick={setSelected} depth={1} />
          </CompNode>

          <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: DS.space.lg,
    paddingTop: DS.space.lg,
    marginTop: DS.space.lg,
    borderTop: '1px solid ' + DS.color.border,
    fontSize: DS.font.xs,
    color: DS.color.textMuted
  }}>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <span style={{
    width: 16,
    height: 12,
    borderRadius: 3,
    border: '2px solid ' + DS.color.border,
    background: DS.color.bgSubtle
  }} />
              View / session
            </span>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <Badge color={DS.color.success}>card</Badge>
              Composable view
            </span>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <Badge color={DS.color.warning}>field</Badge>
              CardFormDefaults building block
            </span>
          </div>
        </div>
      </Panel>

      <Panel title="Component Details">
        <div style={{
    padding: DS.space.lg
  }}>
          {data ? <div>
              <div style={{
    fontFamily: DS.font.mono,
    fontSize: DS.font.base,
    fontWeight: 600,
    color: data.color,
    marginBottom: DS.space.xs
  }}>
                {selected}
              </div>
              <div style={{
    fontSize: DS.font.sm,
    color: DS.color.textSecondary,
    marginBottom: DS.space.md,
    lineHeight: 1.5
  }}>
                {data.desc}
              </div>

              <div style={{
    fontSize: DS.font.xxs,
    fontWeight: 700,
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    color: DS.color.textMuted,
    marginBottom: DS.space.sm
  }}>
                Parameters ({data.properties.length})
              </div>
              {data.properties.map(p => <div key={p.name} style={{
    padding: DS.space.sm + 2,
    background: DS.color.bgSubtle,
    borderRadius: DS.radius.sm,
    marginBottom: DS.space.sm,
    borderLeft: '3px solid ' + data.color
  }}>
                  <div style={{
    fontFamily: DS.font.mono,
    fontSize: DS.font.xs,
    fontWeight: 600,
    color: data.color
  }}>
                    {p.name}
                  </div>
                  <div style={{
    fontSize: DS.font.xxs,
    color: DS.color.textMuted,
    marginTop: 2,
    fontFamily: DS.font.mono
  }}>
                    {p.type}
                  </div>
                  <div style={{
    fontSize: DS.font.xs,
    color: DS.color.textSecondary,
    marginTop: DS.space.xs
  }}>
                    {p.desc}
                  </div>
                </div>)}

              {data.states && <div style={{
    marginTop: DS.space.md
  }}>
                  <div style={{
    fontSize: DS.font.xxs,
    fontWeight: 700,
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    color: DS.color.textMuted,
    marginBottom: DS.space.sm
  }}>
                    Lifecycle phases
                  </div>
                  <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: 3
  }}>
                    {data.states.map(s => <span key={s} style={{
    display: 'inline-block',
    padding: '2px ' + DS.space.sm + 'px',
    borderRadius: DS.radius.sm,
    fontFamily: DS.font.mono,
    fontSize: DS.font.xxs,
    background: data.color + '15',
    color: data.color,
    border: '1px solid ' + data.color + '30'
  }}>{s}</span>)}
                  </div>
                </div>}

              <div style={{
    marginTop: DS.space.md
  }}>
                <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: DS.space.sm
  }}>
                  <div style={{
    fontSize: DS.font.xxs,
    fontWeight: 700,
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    color: DS.color.textMuted
  }}>
                    Example
                  </div>
                  <CopyButton getText={() => codeText} />
                </div>
                <pre style={{
    padding: DS.space.md,
    fontFamily: DS.font.mono,
    fontSize: DS.font.xxs,
    lineHeight: 1.6,
    color: DS.color.textSecondary,
    background: DS.color.bgSubtle,
    borderRadius: DS.radius.sm,
    border: '1px solid ' + DS.color.border,
    overflowX: 'auto',
    whiteSpace: 'pre',
    margin: 0
  }}>{codeText}</pre>
              </div>

              <button onClick={() => setSelected(null)} style={{
    marginTop: DS.space.lg,
    padding: DS.space.sm + 'px ' + DS.space.md + 'px',
    border: '1px solid ' + DS.color.border,
    borderRadius: DS.radius.sm,
    background: DS.color.bgSurface,
    color: DS.color.textMuted,
    fontSize: DS.font.sm,
    cursor: 'pointer',
    width: '100%'
  }}>Clear Selection</button>
            </div> : <div style={{
    color: DS.color.textMuted,
    fontSize: DS.font.sm,
    fontStyle: 'italic'
  }}>
              Click a view, slot, or field on the left to see its parameters, lifecycle phases, and copyable Swift code.
            </div>}
        </div>
      </Panel>
    </div>;
};

Explore the iOS Checkout SwiftUI architecture interactively. The redesigned SDK is a SwiftUI slot API: a `PrimerCheckoutSession` owner wired into your view hierarchy with the `.primerCheckoutSession(_:onCompletion:)` modifier, plus composable views (`PrimerCardForm`, `PrimerPaymentMethods`, `PrimerVaultedPaymentMethods`) whose `@ViewBuilder` slots default to the `*Defaults` building blocks. Click any view, slot, or field to see its parameters, lifecycle phases, and copyable Swift code examples.

<ComponentArchitectureExplorer />

***

## Two ways to integrate

| Goal                                 | Use                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fastest integration, default screens | [`PrimerCheckout`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout) — the fully managed SwiftUI modal. No slots; it renders Primer's default screens end to end.                                                                                                                                                                                                                                                                              |
| Custom SwiftUI layout and slots      | [`PrimerCheckoutSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) + `.primerCheckoutSession(_:onCompletion:)`, composing [`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) / [`PrimerVaultedPaymentMethods`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/vaulted-payment-methods). |
| UIKit apps                           | [`PrimerCheckoutPresenter`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-presenter) + `PrimerCheckoutPresenterDelegate`.                                                                                                                                                                                                                                                                                                                   |

`PrimerCheckoutSession` is the iOS analog of Android's `PrimerCheckoutHost`. Where Android wraps your layout in a composable host, iOS injects the session into the SwiftUI environment with a view modifier — child views read their per-feature session (`PrimerCardFormSession`, `PrimerSelectionSession`) from `EnvironmentValues`.

***

## Slots and defaults

Every composable view exposes `@ViewBuilder` slots that default to a matching `*Defaults` namespace builder. Override one slot and the rest keep their default rendering — you never re-implement an entire view.

| View                          | Slots                                           | Defaults namespace                                                                                     |
| ----------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `PrimerCardForm`              | `cardDetails`, `billingAddress`, `submitButton` | [`CardFormDefaults`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-defaults)                   |
| `PrimerPaymentMethods`        | `header`, `method`, `emptyState`                | [`PaymentMethodsDefaults`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-defaults) |
| `PrimerVaultedPaymentMethods` | `header`, `item`, `submitButton`                | `VaultedPaymentMethodsDefaults`                                                                        |

`CardFormDefaults` also exposes per-field building blocks for recomposing the card section one field at a time: `cardNumber`, `expiryDate`, `cvv`, `cardholderName`, `cardNetwork`, plus the billing fields `firstName`, `lastName`, `email`, `phoneNumber`, `addressLine1`, `addressLine2`, `city`, `state`, `postalCode`, `countryCode`. Each block is self-hiding — it renders nothing unless its field is part of the active [`CardFormConfiguration`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session). Field presence is Dashboard-driven; visual styling comes from the [`PrimerCheckoutTheme`](/docs/sdk/ios-checkout/v3.0.0-beta/api/theming/primer-theme) design tokens.

***

## Sessions and state

Composable views are driven by observable sessions injected into the environment by `.primerCheckoutSession(_:onCompletion:)`. Observe them with `@ObservedObject` and read their published `state`.

| Session                                                                                               | Drives                                                | Published state                      |
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------ |
| [`PrimerCheckoutSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session)                  | The whole flow (owner)                                | `phase` (`.initializing` / `.ready`) |
| [`PrimerCardFormSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/card-form-session)              | `PrimerCardForm` slots                                | `PrimerCardFormState`                |
| [`PrimerSelectionSession`](/docs/sdk/ios-checkout/v3.0.0-beta/api/payment-methods/payment-methods-session) | `PrimerPaymentMethods`, `PrimerVaultedPaymentMethods` | `PrimerPaymentMethodSelectionState`  |

The terminal outcome of the flow is delivered exactly once through the modifier's `onCompletion` closure as a [`PrimerCheckoutState`](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-state):

| State           | Associated values                          | Description                                                                               |
| --------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `.initializing` | —                                          | Loading configuration and payment methods (lifecycle; never delivered to `onCompletion`). |
| `.ready`        | `totalAmount: Int`, `currencyCode: String` | Payment methods loaded (lifecycle; never delivered to `onCompletion`).                    |
| `.success`      | `PaymentResult`                            | Payment completed. Carries `paymentId`, `status`, and other details.                      |
| `.failure`      | `PrimerError`                              | Payment or checkout failed.                                                               |
| `.dismissed`    | —                                          | Checkout was dismissed without completing a payment.                                      |

### PrimerCardFormState

| Property            | Type                    | Description                                                  |
| ------------------- | ----------------------- | ------------------------------------------------------------ |
| `configuration`     | `CardFormConfiguration` | Which fields the form requires (Dashboard-driven).           |
| `data`              | `FormData`              | Current field values keyed by `PrimerInputElementType`.      |
| `fieldErrors`       | `[FieldError]`          | Validation errors per field. Empty when valid.               |
| `isLoading`         | `Bool`                  | `true` while a payment is being submitted.                   |
| `isValid`           | `Bool`                  | `true` when all required fields pass validation.             |
| `selectedCountry`   | `PrimerCountry?`        | Selected billing country, or `nil`.                          |
| `selectedNetwork`   | `PrimerCardNetwork?`    | Selected network for co-badged cards, or `nil`.              |
| `availableNetworks` | `[PrimerCardNetwork]`   | Detected networks; more than one indicates a co-badged card. |

***

## Composing custom UI

Hold the session as a `@StateObject`, place the composable views in your layout, and attach the modifier. The child views resolve their per-feature session from the environment — you do not pass a session into them.

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

@available(iOS 15.0, *)
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(
          cardDetails: { formSession in
            VStack(spacing: 16) {
              CardFormDefaults.cardNumber(formSession)
              HStack(spacing: 16) {
                CardFormDefaults.expiryDate(formSession)
                CardFormDefaults.cvv(formSession)
              }
              CardFormDefaults.cardholderName(formSession)
              // Co-badged selector: shown only when multiple networks are detected.
              CardFormDefaults.cardNetwork(formSession)
            }
          },
          submitButton: { formSession in
            Button("Pay now") { formSession.submit() }
              .buttonStyle(.borderedProminent)
              .disabled(!formSession.state.isValid || formSession.state.isLoading)
          }
        )
      }
      .padding()
    }
    .primerCheckoutSession(session) { state in
      handle(state)
    }
  }

  private func handle(_ state: PrimerCheckoutState) {
    switch state {
    case let .success(result):
      print("Payment \(result.paymentId) succeeded")
    case let .failure(error):
      print("Checkout failed: \(error.localizedDescription)")
    case .dismissed:
      print("Checkout dismissed")
    default:
      // .initializing and .ready are lifecycle states and are
      // never delivered to onCompletion.
      break
    }
  }
}
```

<Warning>
  Without the `.primerCheckoutSession(_:onCompletion:)` modifier above them in the hierarchy, composable views such as `PrimerCardForm` and `PrimerPaymentMethods` cannot resolve a session and will not function.
</Warning>

***

## Related

* [API overview](/docs/sdk/ios-checkout/v3.0.0-beta/api/overview) — Full iOS Checkout API reference
* [PrimerCheckoutSession](/docs/sdk/ios-checkout/v3.0.0-beta/api/primer-checkout-session) — The session owner and the `.primerCheckoutSession` modifier
* [PrimerCardForm](/docs/sdk/ios-checkout/v3.0.0-beta/api/card-form/primer-card-form) — Slot-based card form
* [Design Tokens Explorer](/docs/checkout/primer-checkout/build-your-ui/design-tokens-explorer-ios) — Explore theme tokens
* [Card Form Layout Builder](/docs/checkout/primer-checkout/build-your-ui/card-form-layout-builder) — Build card form layouts
