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

> Explore the Android Checkout Compose component hierarchy and customization points 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 = {
  PrimerCheckoutHost: {
    color: DS.color.brand,
    label: 'root',
    desc: 'Top-level composable. Provides the checkout controller and hosts the checkout state machine. Use with rememberPrimerCheckoutController().',
    properties: [{
      name: 'checkout',
      type: 'PrimerCheckoutController',
      desc: 'Controller from rememberPrimerCheckoutController()'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier (default: Modifier)'
    }, {
      name: 'onEvent',
      type: '(PrimerCheckoutEvent) -> Unit',
      desc: 'Callback for payment outcomes (Success, Failure)'
    }, {
      name: 'theme',
      type: 'PrimerTheme',
      desc: 'Design token theme (default: PrimerTheme())'
    }, {
      name: 'content',
      type: '@Composable () -> Unit',
      desc: 'Custom Compose layout for the checkout UI'
    }],
    states: ['Loading', 'Ready'],
    code: 'val checkout = rememberPrimerCheckoutController(\n    clientToken = clientToken,\n    settings = PrimerSettings()\n)\n\nPrimerCheckoutHost(\n    checkout = checkout,\n    onEvent = { event ->\n        when (event) {\n            is PrimerCheckoutEvent.Success -> { /* navigate */ }\n            is PrimerCheckoutEvent.Failure -> { /* log error */ }\n        }\n    },\n) {\n    val state by checkout.state.collectAsStateWithLifecycle()\n    when (state) {\n        is PrimerCheckoutState.Loading -> LoadingScreen()\n        is PrimerCheckoutState.Ready -> PaymentMethods(state)\n    }\n}'
  },
  PrimerCardForm: {
    color: DS.color.success,
    label: 'card',
    desc: 'Card payment form with configurable field layout, validation, and submission.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller from rememberCardFormController()'
    }, {
      name: 'cardDetails',
      type: '@Composable () -> Unit',
      desc: 'Custom card details content slot'
    }, {
      name: 'billingAddress',
      type: '@Composable () -> Unit',
      desc: 'Custom billing address content slot'
    }, {
      name: 'submitButton',
      type: '@Composable () -> Unit',
      desc: 'Custom submit button slot'
    }],
    code: 'val controller = rememberCardFormController(checkout)\n\nPrimerCardForm(\n    controller = controller,\n    cardDetails = { CardFormDefaults.CardDetailsContent(controller) },\n    submitButton = { CardFormDefaults.SubmitButton(controller) },\n)'
  },
  CardDetailsContent: {
    color: DS.color.success,
    label: 'defaults',
    desc: 'Default card details layout with card number, expiry+CVV row, and optional cardholder.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'cardholderName',
      type: '@Composable () -> Unit',
      desc: 'Custom cardholder slot (pass {} to hide)'
    }],
    code: 'CardFormDefaults.CardDetailsContent(\n    controller = controller,\n    cardholderName = {\n        CardFormDefaults.CardholderField(controller)\n    },\n)'
  },
  CardNumberField: {
    color: DS.color.warning,
    label: 'field',
    desc: 'Card number input with automatic formatting and card network detection.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier for layout customization'
    }],
    code: 'CardFormDefaults.CardNumberField(\n    controller,\n    modifier = Modifier.fillMaxWidth()\n)'
  },
  ExpiryField: {
    color: DS.color.warning,
    label: 'field',
    desc: 'Expiry date input with MM/YY formatting.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier'
    }],
    code: 'CardFormDefaults.ExpiryField(\n    controller,\n    modifier = Modifier.weight(1f)\n)'
  },
  CvvField: {
    color: DS.color.warning,
    label: 'field',
    desc: 'CVV/CVC input with card-type-aware length validation.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier'
    }],
    code: 'CardFormDefaults.CvvField(\n    controller,\n    modifier = Modifier.weight(1f)\n)'
  },
  CardholderField: {
    color: DS.color.warning,
    label: 'field',
    desc: 'Cardholder name text input.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier'
    }],
    code: 'CardFormDefaults.CardholderField(\n    controller,\n    modifier = Modifier.fillMaxWidth()\n)'
  },
  CardNetworkField: {
    color: DS.color.warning,
    label: 'field',
    desc: 'Network selector for co-badged cards. Only shown when multiple networks are available.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }],
    code: '// Shown automatically for co-badged cards\n// Access network info via controller state:\nval networkSelection = controller.state.networkSelection\nval available = networkSelection.availableNetworks\nval selected = networkSelection.selectedNetwork'
  },
  SubmitButton: {
    color: DS.color.brand,
    label: 'action',
    desc: 'Submit button that triggers card form validation and payment.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }, {
      name: 'modifier',
      type: 'Modifier',
      desc: 'Compose modifier'
    }],
    code: 'CardFormDefaults.SubmitButton(\n    controller,\n    modifier = Modifier.fillMaxWidth()\n)'
  },
  BillingAddressContent: {
    color: DS.color.textMuted,
    label: 'defaults',
    desc: 'Default billing address form layout.',
    properties: [{
      name: 'controller',
      type: 'PrimerCardFormController',
      desc: 'Card form controller'
    }],
    code: 'PrimerCardForm(\n    controller = controller,\n    billingAddress = {\n        CardFormDefaults.BillingAddressContent(controller)\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
  }}>
      <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="Component Hierarchy" style={{
    minWidth: 0,
    overflow: 'hidden'
  }}>
        <div style={{
    padding: DS.space.lg
  }}>
          <CompNode name="PrimerCheckoutHost" 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 state → Payment Methods
            </div>

            <CompNode name="PrimerCardForm" selected={selected} onClick={setSelected} depth={1}>
              <CompNode name="CardDetailsContent" selected={selected} onClick={setSelected} depth={2}>
                <CompNode name="CardNumberField" selected={selected} onClick={setSelected} depth={3} />
                <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: DS.space.xs
  }}>
                  <CompNode name="ExpiryField" selected={selected} onClick={setSelected} depth={3} />
                  <CompNode name="CvvField" selected={selected} onClick={setSelected} depth={3} />
                </div>
                <CompNode name="CardholderField" selected={selected} onClick={setSelected} depth={3} />
                <CompNode name="CardNetworkField" selected={selected} onClick={setSelected} depth={3} />
              </CompNode>
              <CompNode name="BillingAddressContent" selected={selected} onClick={setSelected} depth={2} />
              <CompNode name="SubmitButton" selected={selected} onClick={setSelected} depth={2} />
            </CompNode>

            <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: DS.space.xs,
    marginTop: DS.space.sm
  }}>
              {['Google Pay', 'PayPal', 'Klarna', 'Redirect payments'].map(name => <div key={name} style={{
    backgroundColor: DS.color.bgSubtle,
    border: '2px solid ' + DS.color.border,
    borderRadius: DS.radius.md,
    padding: DS.space.xs + 2 + 'px ' + (DS.space.sm + 2) + 'px',
    flex: 1,
    minWidth: 0
  }}>
                  <div style={{
    fontFamily: DS.font.mono,
    fontSize: DS.font.xxs,
    fontWeight: 600,
    color: DS.color.textSecondary
  }}>
                    {name}
                  </div>
                </div>)}
            </div>
          </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
  }} />
              Composable
            </span>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <Badge color={DS.color.success}>card</Badge>
              Type label
            </span>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <Badge color={DS.color.warning}>field</Badge>
              Input field
            </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
  }}>
                    States
                  </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 component on the left to view its parameters, states, and example code.
            </div>}
        </div>
      </Panel>
    </div>;
};

Explore the Android Checkout Compose component hierarchy interactively. Click on any composable to see its parameters, available states, and copyable Kotlin code examples.

<ComponentArchitectureExplorer />

***

## Key State Types

### PrimerCheckoutState

| State     | Description                                          |
| --------- | ---------------------------------------------------- |
| `Loading` | Checkout is initializing                             |
| `Ready`   | Payment methods available (contains `clientSession`) |

### PrimerCardFormController.State

| Property           | Type                         | Description                                                                          |
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------ |
| `cardFields`       | `CardFields`                 | Card field values and validation                                                     |
| `billingFields`    | `BillingFields`              | Billing address field values                                                         |
| `fieldErrors`      | `List<SyncValidationError>?` | Validation errors (each with `inputElementType`, `errorId`, `fieldId`, `errorResId`) |
| `data`             | `CardFormData`               | Aggregated form data                                                                 |
| `isLoading`        | `Boolean`                    | Whether form is processing                                                           |
| `isFormValid`      | `Boolean`                    | Whether all required fields are valid                                                |
| `networkSelection` | `NetworkSelection`           | Co-badged card network info                                                          |

### NetworkSelection

| Property              | Type                | Description                      |
| --------------------- | ------------------- | -------------------------------- |
| `selectedNetwork`     | `CardNetwork?`      | Currently selected network       |
| `availableNetworks`   | `List<CardNetwork>` | Available networks for this card |
| `isNetworkSelectable` | `Boolean`           | Whether user can choose network  |

***

## Related

* [Card Form Layout Builder](/checkout/primer-checkout/build-your-ui/card-form-layout-builder) — Interactive card form builder
* [Design Tokens Explorer](/checkout/primer-checkout/build-your-ui/design-tokens-explorer-android) — Android theme token explorer
