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

# Scope Architecture Explorer — iOS

> Explore the iOS Checkout scope 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 SCOPE_INFO = {
  PrimerCheckoutScope: {
    color: DS.color.brand,
    label: 'root',
    desc: 'Top-level scope. Wraps the entire checkout.',
    properties: [{
      name: 'container',
      type: 'ContainerComponent?',
      desc: 'Wraps all checkout content'
    }, {
      name: 'splashScreen',
      type: 'Component?',
      desc: 'Shown during initialization'
    }, {
      name: 'loadingScreen',
      type: 'Component?',
      desc: 'Shown during payment processing'
    }, {
      name: 'errorScreen',
      type: 'ErrorComponent?',
      desc: 'Shown on failure. Receives error message.'
    }],
    actions: ['onDismiss()'],
    state: 'PrimerCheckoutState',
    code: 'PrimerCheckout(\n  clientToken: clientToken,\n  scope: { checkoutScope in\n    checkoutScope.splashScreen = { AnyView(MySplash()) }\n    checkoutScope.loadingScreen = { AnyView(MyLoading()) }\n    checkoutScope.errorScreen = { msg in AnyView(MyError(message: msg)) }\n  }\n)'
  },
  PrimerPaymentMethodSelectionScope: {
    color: DS.color.purple,
    label: 'selection',
    desc: 'Payment method list, vaulted methods, and selection.',
    properties: [{
      name: 'screen',
      type: 'PaymentMethodSelectionScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'paymentMethodItem',
      type: 'PaymentMethodItemComponent?',
      desc: 'Custom payment method row'
    }, {
      name: 'categoryHeader',
      type: 'CategoryHeaderComponent?',
      desc: 'Section header'
    }, {
      name: 'emptyStateView',
      type: 'Component?',
      desc: 'Shown when no methods available'
    }],
    actions: ['onPaymentMethodSelected(paymentMethod:)', 'cancel()'],
    state: 'PrimerPaymentMethodSelectionState',
    code: 'checkoutScope.paymentMethodSelection.paymentMethodItem = { method in\n  AnyView(\n    HStack {\n      if let icon = method.icon {\n        Image(uiImage: icon)\n          .resizable()\n          .frame(width: 32, height: 32)\n      }\n      Text(method.name)\n      Spacer()\n    }\n    .padding(.vertical, 12)\n  )\n}'
  },
  PrimerCardFormScope: {
    color: DS.color.success,
    label: 'card',
    desc: 'Card form fields, validation, and submission.',
    properties: [{
      name: 'screen',
      type: 'CardFormScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'cardInputSection',
      type: 'Component?',
      desc: 'Card number, expiry, CVV group'
    }, {
      name: 'billingAddressSection',
      type: 'Component?',
      desc: 'Billing address fields group'
    }, {
      name: 'submitButton',
      type: 'Component?',
      desc: 'Submit button area'
    }, {
      name: 'errorScreen',
      type: 'ErrorComponent?',
      desc: 'Error display within card form'
    }, {
      name: 'cobadgedCardsView',
      type: 'Closure?',
      desc: 'Network selector for co-badged cards'
    }],
    fieldConfigs: ['cardNumberConfig', 'expiryDateConfig', 'cvvConfig', 'cardholderNameConfig', 'postalCodeConfig', 'countryConfig', 'cityConfig', 'stateConfig', 'addressLine1Config', 'addressLine2Config', 'phoneNumberConfig', 'firstNameConfig', 'lastNameConfig', 'emailConfig', 'retailOutletConfig', 'otpCodeConfig'],
    actions: ['submit()', 'cancel()', 'onBack()'],
    state: 'PrimerCardFormState',
    concrete: 'DefaultCardFormScope',
    code: 'if let cardScope = checkoutScope.getPaymentMethodScope(DefaultCardFormScope.self) {\n  cardScope.screen = { scope in\n    AnyView(\n      VStack(spacing: 16) {\n        scope.PrimerCardNumberField(label: "Card number", styling: nil)\n        HStack(spacing: 12) {\n          scope.PrimerExpiryDateField(label: "Expiry", styling: nil)\n          scope.PrimerCvvField(label: "CVV", styling: nil)\n        }\n      }\n      .padding()\n    )\n  }\n}'
  },
  PrimerApplePayScope: {
    color: '#333',
    label: 'apple pay',
    desc: 'Apple Pay button and payment. Check availability via state.',
    properties: [{
      name: 'screen',
      type: '((any PrimerApplePayScope) -> any View)?',
      desc: 'Full screen replacement'
    }, {
      name: 'applePayButton',
      type: '((@escaping () -> Void) -> any View)?',
      desc: 'Custom Apple Pay button'
    }],
    actions: ['submit()', 'cancel()', 'onBack()'],
    state: 'PrimerApplePayState',
    concrete: 'DefaultApplePayScope',
    code: 'if let applePayScope = checkoutScope.getPaymentMethodScope(DefaultApplePayScope.self) {\n  applePayScope.applePayButton = { action in\n    AnyView(\n      Button(action: action) {\n        HStack {\n          Image(systemName: "apple.logo")\n          Text("Pay")\n        }\n        .frame(maxWidth: .infinity)\n        .frame(height: 50)\n        .background(.black)\n        .foregroundColor(.white)\n        .cornerRadius(10)\n      }\n    )\n  }\n}'
  },
  PrimerPayPalScope: {
    color: '#003087',
    label: 'paypal',
    desc: 'PayPal redirect flow.',
    properties: [{
      name: 'screen',
      type: 'PayPalScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'payButton',
      type: 'PayPalButtonComponent?',
      desc: 'Custom pay button'
    }, {
      name: 'submitButtonText',
      type: 'String?',
      desc: 'Button label'
    }],
    actions: ['start()', 'submit()', 'cancel()', 'onBack()'],
    state: 'PrimerPayPalState',
    concrete: 'DefaultPayPalScope',
    code: 'if let paypalScope = checkoutScope.getPaymentMethodScope(DefaultPayPalScope.self) {\n  paypalScope.submitButtonText = "Pay with PayPal"\n}'
  },
  PrimerKlarnaScope: {
    color: '#FFB3C7',
    label: 'klarna',
    desc: 'Klarna multi-step: category selection, authorization, finalization.',
    properties: [{
      name: 'screen',
      type: 'KlarnaScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'authorizeButton',
      type: 'KlarnaButtonComponent?',
      desc: 'Custom authorize button'
    }, {
      name: 'finalizeButton',
      type: 'KlarnaButtonComponent?',
      desc: 'Custom finalize button'
    }],
    actions: ['selectPaymentCategory(_:)', 'authorizePayment()', 'finalizePayment()', 'cancel()', 'onBack()'],
    state: 'PrimerKlarnaState',
    concrete: 'DefaultKlarnaScope',
    code: 'if let klarnaScope = checkoutScope.getPaymentMethodScope(DefaultKlarnaScope.self) {\n  klarnaScope.authorizeButton = { scope in\n    AnyView(\n      Button("Authorize with Klarna") { scope.authorizePayment() }\n        .buttonStyle(.borderedProminent)\n    )\n  }\n}'
  },
  PrimerAchScope: {
    color: DS.color.warning,
    label: 'ach',
    desc: 'ACH multi-step: user details, bank collection, mandate.',
    properties: [{
      name: 'screen',
      type: 'AchScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'userDetailsScreen',
      type: 'AchScreenComponent?',
      desc: 'User details step'
    }, {
      name: 'mandateScreen',
      type: 'AchScreenComponent?',
      desc: 'Mandate acceptance step'
    }, {
      name: 'submitButton',
      type: 'AchButtonComponent?',
      desc: 'Custom submit button'
    }],
    actions: ['submitUserDetails()', 'acceptMandate()', 'declineMandate()', 'cancel()', 'onBack()'],
    state: 'PrimerAchState',
    concrete: 'DefaultAchScope',
    code: 'if let achScope = checkoutScope.getPaymentMethodScope(DefaultAchScope.self) {\n  achScope.userDetailsScreen = { scope in\n    AnyView(MyCustomUserDetailsForm(scope: scope))\n  }\n}'
  },
  PrimerWebRedirectScope: {
    color: '#0891b2',
    label: 'web redirect',
    desc: 'Web redirect flow. Redirects to external page, then polls for result.',
    properties: [{
      name: 'screen',
      type: 'WebRedirectScreenComponent?',
      desc: 'Full screen replacement'
    }, {
      name: 'payButton',
      type: 'WebRedirectButtonComponent?',
      desc: 'Custom pay button'
    }, {
      name: 'submitButtonText',
      type: 'String?',
      desc: 'Button label'
    }],
    actions: ['start()', 'submit()', 'cancel()', 'onBack()'],
    state: 'PrimerWebRedirectState',
    concrete: 'DefaultWebRedirectScope',
    code: 'if let webRedirectScope = checkoutScope.getPaymentMethodScope(DefaultWebRedirectScope.self) {\n  webRedirectScope.submitButtonText = "Pay with \\(webRedirectScope.paymentMethodType)"\n}'
  },
  PrimerFormRedirectScope: {
    color: '#7c3aed',
    label: 'form redirect',
    desc: 'Form-based redirect flow. Collects OTP or phone number before external completion.',
    properties: [{
      name: 'screen',
      type: 'FormRedirectScreenComponent?',
      desc: 'Full screen replacement (form + pending)'
    }, {
      name: 'formSection',
      type: 'FormRedirectFormSectionComponent?',
      desc: 'Custom form fields area'
    }, {
      name: 'submitButton',
      type: 'FormRedirectButtonComponent?',
      desc: 'Custom submit button'
    }, {
      name: 'submitButtonText',
      type: 'String?',
      desc: 'Button label'
    }],
    actions: ['start()', 'submit()', 'cancel()', 'onBack()', 'updateField(_:value:)'],
    state: 'PrimerFormRedirectState',
    concrete: 'DefaultFormRedirectScope',
    code: 'if let formScope = checkoutScope.getPaymentMethodScope(DefaultFormRedirectScope.self) {\n  // Update OTP code\n  formScope.updateField(.otpCode, value: "123456")\n\n  // Observe submit readiness\n  for await state in formScope.state {\n    if state.isSubmitEnabled {\n      formScope.submit()\n    }\n  }\n}'
  },
  PrimerQRCodeScope: {
    color: '#059669',
    label: 'qr code',
    desc: 'QR code display and automatic polling. No user submit needed.',
    properties: [{
      name: 'screen',
      type: 'QRCodeScreenComponent?',
      desc: 'Full screen replacement'
    }],
    actions: ['start()', 'cancel()', 'onBack()'],
    state: 'PrimerQRCodeState',
    concrete: 'DefaultQRCodeScope',
    code: 'if let qrScope = checkoutScope.getPaymentMethodScope(DefaultQRCodeScope.self) {\n  qrScope.screen = { scope in\n    AnyView(\n      VStack {\n        if let data = scope.currentState.qrCodeImageData,\n           let uiImage = UIImage(data: data) {\n          Image(uiImage: uiImage)\n            .resizable()\n            .frame(width: 200, height: 200)\n        }\n        Text("Scan to pay")\n      }\n    )\n  }\n}'
  },
  PrimerSelectCountryScope: {
    color: DS.color.textMuted,
    label: 'country',
    desc: 'Country picker, accessed via PrimerCardFormScope.selectCountry.',
    properties: [{
      name: 'screen',
      type: '((PrimerSelectCountryScope) -> AnyView)?',
      desc: 'Full screen replacement'
    }, {
      name: 'searchBar',
      type: 'Closure?',
      desc: 'Custom search bar'
    }, {
      name: 'countryItem',
      type: 'CountryItemComponent?',
      desc: 'Custom country row'
    }],
    actions: ['onCountrySelected(countryCode:countryName:)', 'cancel()', 'onSearch(query:)'],
    state: 'PrimerSelectCountryState',
    code: 'let countryScope = cardScope.selectCountry\ncountryScope.countryItem = { country, onSelect in\n  AnyView(\n    Button(action: onSelect) {\n      HStack {\n        Text(country.flag ?? "")\n        Text(country.name)\n        Spacer()\n        Text(country.code)\n          .foregroundColor(.secondary)\n      }\n    }\n  )\n}'
  }
};

export const ScopeNode = ({name, selected, onClick, depth = 0, children}) => {
  const [hovered, setHovered] = useState(false);
  const info = SCOPE_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,
    overflow: 'hidden',
    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>
        {info.concrete && <Badge color={DS.color.textMuted}>{info.concrete}</Badge>}
      </div>
      {children && <div style={{
    marginTop: DS.space.sm
  }}>{children}</div>}
    </div>;
};

export const PropLine = ({name, desc, color}) => <div style={{
  display: 'flex',
  alignItems: 'center',
  gap: DS.space.sm,
  margin: DS.space.sm + 'px 0 ' + (DS.space.xs + 2) + 'px',
  minWidth: 0
}}>
    <div style={{
  height: 0,
  width: DS.space.md,
  borderTop: '1px dashed ' + color + '50',
  flexShrink: 0
}} />
    <Badge color={color}>{name}</Badge>
    <span style={{
  fontSize: DS.font.xxs,
  color: DS.color.textMuted,
  fontFamily: DS.font.mono,
  whiteSpace: 'nowrap',
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  minWidth: 0
}}>{desc}</span>
    <div style={{
  height: 0,
  flex: '1 1 0',
  minWidth: DS.space.sm,
  borderTop: '1px dashed ' + color + '50'
}} />
  </div>;

export const FieldChip = ({name, color}) => <span style={{
  display: 'inline-block',
  padding: DS.space.xxs + 1 + 'px ' + DS.space.sm + 'px',
  borderRadius: DS.radius.sm,
  fontFamily: DS.font.mono,
  fontSize: DS.font.xxs,
  background: color + '15',
  color: color,
  border: '1px solid ' + color + '30',
  margin: 2
}}>{name}</span>;

export const ScopeArchitectureExplorer = () => {
  const [selected, setSelected] = useState(null);
  const data = selected ? SCOPE_INFO[selected] : null;
  const codeText = data ? data.code : '';
  return <div style={{
    display: 'grid',
    gridTemplateColumns: '1fr 280px',
    gap: DS.space.xl
  }}>
      <Panel title="Scope Hierarchy" style={{
    minWidth: 0,
    overflow: 'hidden'
  }}>
        <div style={{
    padding: DS.space.lg
  }}>
          <ScopeNode name="PrimerCheckoutScope" selected={selected} onClick={setSelected}>
            <PropLine name="container" desc="ContainerComponent?" color={DS.color.brand} />
            <PropLine name="splashScreen" desc="Component?" color={DS.color.brand} />
            <PropLine name="loadingScreen" desc="Component?" color={DS.color.brand} />
            <PropLine name="errorScreen" desc="ErrorComponent?" color={DS.color.brand} />

            <ScopeNode name="PrimerPaymentMethodSelectionScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={DS.color.purple} />
              <PropLine name="paymentMethodItem" desc="custom method row" color={DS.color.purple} />
              <PropLine name="categoryHeader" desc="section header" color={DS.color.purple} />
              <PropLine name="emptyStateView" desc="empty state" color={DS.color.purple} />
            </ScopeNode>

            <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
  }}>
              via getPaymentMethodScope(_:)
            </div>

            <ScopeNode name="PrimerCardFormScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={DS.color.success} />
              <PropLine name="cardInputSection" desc="card fields group" color={DS.color.success} />
              <PropLine name="billingAddressSection" desc="address fields" color={DS.color.success} />
              <PropLine name="submitButton" desc="submit area" color={DS.color.success} />
              <PropLine name="errorScreen" desc="error display" color={DS.color.success} />
              <PropLine name="cobadgedCardsView" desc="network selector" color={DS.color.success} />

              <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: 3,
    paddingTop: 2
  }}>
                {SCOPE_INFO.PrimerCardFormScope.fieldConfigs.map(f => <FieldChip key={f} name={f} color={DS.color.warning} />)}
              </div>

              <ScopeNode name="PrimerSelectCountryScope" selected={selected} onClick={setSelected} depth={2}>
                <PropLine name="screen" desc="full screen replacement" color={DS.color.textMuted} />
                <PropLine name="searchBar" desc="custom search" color={DS.color.textMuted} />
                <PropLine name="countryItem" desc="custom country row" color={DS.color.textMuted} />
              </ScopeNode>
            </ScopeNode>

            <ScopeNode name="PrimerApplePayScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#333'} />
              <PropLine name="applePayButton" desc="custom button" color={'#333'} />
            </ScopeNode>

            <ScopeNode name="PrimerPayPalScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#003087'} />
              <PropLine name="payButton" desc="custom button" color={'#003087'} />
            </ScopeNode>

            <ScopeNode name="PrimerKlarnaScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#FFB3C7'} />
              <PropLine name="authorizeButton" desc="authorize button" color={'#FFB3C7'} />
              <PropLine name="finalizeButton" desc="finalize button" color={'#FFB3C7'} />
            </ScopeNode>

            <ScopeNode name="PrimerAchScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={DS.color.warning} />
              <PropLine name="userDetailsScreen" desc="user details step" color={DS.color.warning} />
              <PropLine name="mandateScreen" desc="mandate step" color={DS.color.warning} />
              <PropLine name="submitButton" desc="custom button" color={DS.color.warning} />
            </ScopeNode>

            <ScopeNode name="PrimerWebRedirectScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#0891b2'} />
              <PropLine name="payButton" desc="custom button" color={'#0891b2'} />
            </ScopeNode>

            <ScopeNode name="PrimerFormRedirectScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#7c3aed'} />
              <PropLine name="formSection" desc="form fields area" color={'#7c3aed'} />
              <PropLine name="submitButton" desc="custom button" color={'#7c3aed'} />
            </ScopeNode>

            <ScopeNode name="PrimerQRCodeScope" selected={selected} onClick={setSelected} depth={1}>
              <PropLine name="screen" desc="full screen replacement" color={'#059669'} />
            </ScopeNode>
          </ScopeNode>

          <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
  }} />
              Scope
            </span>
            <span style={{
    display: 'inline-flex',
    alignItems: 'center',
    gap: DS.space.sm
  }}>
              <span style={{
    width: 24,
    height: 0,
    borderTop: '1px dashed ' + DS.color.purple + '50'
  }} />
              <Badge color={DS.color.purple}>prop</Badge>
              Customization point
            </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.textMuted}>DefaultCardFormScope</Badge>
              Concrete class
            </span>
          </div>
        </div>
      </Panel>

      <Panel title="Scope 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
  }}>
                Customization Points ({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.fieldConfigs && <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
  }}>
                    Field Configs ({data.fieldConfigs.length})
                  </div>
                  <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: 3
  }}>
                    {data.fieldConfigs.map(f => <span key={f} 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'
  }}>{f}</span>)}
                  </div>
                </div>}

              <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
  }}>
                  Actions
                </div>
                <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: 3
  }}>
                  {data.actions.map(a => <span key={a} style={{
    display: 'inline-block',
    padding: '2px ' + DS.space.sm + 'px',
    borderRadius: DS.radius.sm,
    fontFamily: DS.font.mono,
    fontSize: DS.font.xxs,
    background: DS.color.brandMuted,
    color: DS.color.brand,
    border: '1px solid ' + DS.color.brand + '30'
  }}>{a}</span>)}
                </div>
              </div>

              <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
  }}>
                  State type
                </div>
                <Badge color={data.color}>{data.state}</Badge>
              </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 scope on the left to view its customization points, actions, and example code.
            </div>}
        </div>
      </Panel>
    </div>;
};

Explore the iOS Checkout scope hierarchy interactively. Click on any scope to see its customization points, available actions, state type, and copyable Swift code examples. Every property and method name reflects the latest SDK API.

<ScopeArchitectureExplorer />

***

## Type Aliases

Scopes use type aliases for customization closures:

| Alias                                   | Signature                                           | Used by                                             |
| --------------------------------------- | --------------------------------------------------- | --------------------------------------------------- |
| `Component`                             | `() -> any View`                                    | `splashScreen`, `loadingScreen`, `cardInputSection` |
| `ContainerComponent`                    | `(@escaping () -> any View) -> any View`            | `container`                                         |
| `ErrorComponent`                        | `(String) -> any View`                              | `errorScreen`                                       |
| `PaymentMethodItemComponent`            | `(CheckoutPaymentMethod) -> any View`               | `paymentMethodItem`                                 |
| `CountryItemComponent`                  | `(PrimerCountry, @escaping () -> Void) -> any View` | `countryItem`                                       |
| `CategoryHeaderComponent`               | `(String) -> any View`                              | `categoryHeader`                                    |
| `CardFormScreenComponent`               | `(any PrimerCardFormScope) -> any View`             | Card form `screen`                                  |
| `PaymentMethodSelectionScreenComponent` | `(PrimerPaymentMethodSelectionScope) -> any View`   | Selection `screen`                                  |
| `WebRedirectScreenComponent`            | `(any PrimerWebRedirectScope) -> any View`          | Web redirect `screen`                               |
| `WebRedirectButtonComponent`            | `(any PrimerWebRedirectScope) -> any View`          | Web redirect `payButton`                            |
| `FormRedirectScreenComponent`           | `(any PrimerFormRedirectScope) -> any View`         | Form redirect `screen`                              |
| `FormRedirectButtonComponent`           | `(any PrimerFormRedirectScope) -> any View`         | Form redirect `submitButton`                        |
| `FormRedirectFormSectionComponent`      | `(any PrimerFormRedirectScope) -> any View`         | Form redirect `formSection`                         |
| `QRCodeScreenComponent`                 | `(any PrimerQRCodeScope) -> any View`               | QR code `screen`                                    |

***

## Base Protocol

All payment method scopes conform to `PrimerPaymentMethodScope`:

```swift theme={"dark"}
@MainActor
protocol PrimerPaymentMethodScope: AnyObject {
  associatedtype State: Equatable

  var state: AsyncStream<State> { get }
  func start()
  func submit()
  func cancel()
  func onBack()
  func onDismiss()
}
```

***

## Related

* [Scopes Overview](/sdk/ios-checkout/v3.0.0-beta/configuration/scopes-overview) — Full scope reference
* [Design Tokens Explorer](/checkout/primer-checkout/build-your-ui/design-tokens-explorer-ios) — Explore theme tokens
* [Card Form Layout Builder](/checkout/primer-checkout/build-your-ui/card-form-layout-builder) — Build card form layouts
