v0.1 public beta early access on GitHub
Home / Screens / auth-verify-identity
Screen template

Verify Identity Screen

KYC stepper — document-type radio choice, requirement facts, step progress, and continue CTA

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection · medium
$ npx lumioui add auth-verify-identity
import { VerifyIdentityScreen } from "@/screens/auth/verify-identity";
Registry dependencies5
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
auth/verify-identity.tsxsource · yours to edit
import * as React from "react";
import { View, Pressable, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState } from "react";
import { Button, Icon, RadioGroup, RadioGroupItem, Steps, Text, type IconName } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Overline, World } from "../shared";

export interface IdentityDocument {
  value: string;
  label: string;
  /** What the camera needs to see — e.g. "Photo page". */
  detail: string;
  icon: IconName;
}

export interface VerifyIdentityScreenProps {
  /** Wizard steps shown at the top — current index marks where the user is. */
  steps?: string[];
  currentStep?: number;
  documents?: IdentityDocument[];
  onContinue?: (document: string) => void;
  onWhyVerify?: () => void;
  onBack?: () => void;
}

const DEFAULT_DOCUMENTS: IdentityDocument[] = [
  { value: "passport", label: "Passport", detail: "Photo page", icon: "globe" },
  { value: "drivers-license", label: "Driver's license", detail: "Front and back", icon: "credit-card" },
  { value: "national-id", label: "National ID card", detail: "Front and back", icon: "user" },
];

const REQUIREMENTS: { icon: IconName; text: string }[] = [
  { icon: "clock", text: "Takes about 2 minutes" },
  { icon: "camera", text: "A clear photo in good light" },
  { icon: "shield", text: "Encrypted and never shared" },
];

/** Verify identity — document-picker grammar: each option carries a little
 *  document mock (booklet, license card, ID) so the choice reads as the
 *  object it produces, not three identical radio rows. */
export function VerifyIdentityScreen({
  steps = ["Document", "Selfie", "Review"],
  currentStep = 0,
  documents = DEFAULT_DOCUMENTS,
  onContinue,
  onWhyVerify,
  onBack,
}: VerifyIdentityScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [document, setDocument] = useState(documents[0]?.value ?? "");
  const [loading, setLoading] = useState(false);

  const submit = () => {
    setLoading(true);
    haptics.medium();
    onContinue?.(document);
    setTimeout(() => setLoading(false), 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Top bar — back affordance clears the status bar */}
      <View className="px-2" style={{ paddingTop: insets.top + 4 }}>
        <Pressable
          onPress={() => {
            haptics.light();
            onBack?.();
          }}
          accessibilityRole="button"
          accessibilityLabel="Go back"
          hitSlop={8}
          className="h-11 flex-row items-center gap-1 self-start px-2"
        >
          <Icon name="chevron-left" size="sm" color="#65A30D" />
          <Text className="text-base text-[#65A30D]">Back</Text>
        </Pressable>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerClassName="flex-grow px-6"
        contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 24 }}
      >
        <Mount delay={0}>
          <Overline>Step {currentStep + 1} of {steps.length}</Overline>
          <Text variant="h1" className="mt-2 tracking-tight">
            Verify your{"\n"}<Text className="italic text-[#65A30D]">identity</Text>
          </Text>
          <Text variant="muted" className="mt-3 max-w-xs text-base leading-relaxed">
            Choose a document to photograph — the next step walks you through it.
          </Text>
        </Mount>

        <Mount delay={100}>
          <View className="mt-7">
            <Steps steps={steps} current={currentStep} interactive={false} />
          </View>
        </Mount>

        {/* Document choice — each option shows the physical object */}
        <Mount delay={180}>
          <View className="mt-8 gap-3">
            <Overline>Choose your document</Overline>
            <RadioGroup
              value={document}
              onValueChange={(v) => {
                haptics.selection();
                setDocument(v);
              }}
              accessibilityLabel="Document type"
              className="gap-2.5"
            >
              {documents.map((doc) => (
                <RadioGroupItem
                  key={doc.value}
                  value={doc.value}
                  accessibilityLabel={doc.label}
                  className="items-center overflow-hidden rounded-2xl"
                >
                  <View className="absolute inset-0 rounded-2xl bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
                  <DocMock kind={doc.value} />
                  <View className="ml-1 flex-1">
                    <Text className="text-base font-medium text-foreground">{doc.label}</Text>
                    <Text className="text-sm text-muted-foreground" numberOfLines={1}>
                      {doc.detail}
                    </Text>
                  </View>
                </RadioGroupItem>
              ))}
            </RadioGroup>
          </View>
        </Mount>

        {/* Requirement facts — grouped on one tonal card */}
        <Mount delay={260}>
          <View className="mt-8 gap-3.5 overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
            {REQUIREMENTS.map((req) => (
              <View key={req.text} className="flex-row items-center gap-3">
                <Icon name={req.icon} size="sm" color="mutedForeground" />
                <Text variant="small" className="text-muted-foreground">
                  {req.text}
                </Text>
              </View>
            ))}
          </View>
        </Mount>

        <Mount delay={340} className="mt-auto">
          <View className="gap-1 pt-9">
            <Button
              size="lg"
              loading={loading}
              onPress={submit}
              haptic={false}
              className="bg-[#65A30D] w-full"
              accessibilityLabel="Continue to document photo"
            >
              Continue
            </Button>
            {onWhyVerify ? (
              <Pressable
                onPress={() => {
                  haptics.light();
                  onWhyVerify();
                }}
                accessibilityRole="link"
                accessibilityLabel="Why we verify your identity"
                hitSlop={8}
                className="min-h-11 items-center justify-center px-2"
              >
                <Text variant="muted" className="text-center text-sm">
                  Why do we ask for this?
                </Text>
              </Pressable>
            ) : null}
          </View>
        </Mount>
      </ScrollView>
    </View>
    </World>
  );
}
VerifyIdentityScreen.displayName = "VerifyIdentityScreen";

/** Miniature document facsimile shown inside each picker row — a booklet
 *  for passports, a landscape card with photo block for licenses/IDs. */
function DocMock({ kind }: { kind: string }) {
  if (kind === "passport") {
    return (
      <View className="h-12 w-9 items-center justify-center rounded-md bg-[#2B3566] py-1.5">
        <View className="h-2 w-2 items-center justify-center rounded-full border border-white/70" />
        <View className="mt-1.5 h-px w-4 bg-white/60" />
        <View className="mt-1 h-px w-3 bg-white/40" />
      </View>
    );
  }
  return (
    <View className="h-12 w-16 flex-row items-center gap-1.5 rounded-md border border-border bg-muted/60 p-1.5">
      <View className="h-7 w-6 items-center justify-center rounded-sm bg-[#65A30D]/15">
        <Icon name="user" size="sm" color="#65A30D" />
      </View>
      <View className="flex-1 gap-1">
        <View className={cn("h-1 rounded-full bg-foreground/25", kind === "national-id" ? "w-8" : "w-7")} />
        <View className="h-1 w-5 rounded-full bg-foreground/20" />
        <View className="h-1 w-6 rounded-full bg-foreground/15" />
      </View>
    </View>
  );
}
DocMock.displayName = "DocMock";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Verify Identity Screen" screen in React Native using LumioUI.

Goal: KYC stepper — document-type radio choice, requirement facts, step progress, and continue CTA Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add auth-verify-identity
```

Or pull just the components: `npx lumioui add button icon radio-group steps text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3

### `radio-group` — Single-select options with animated indicator.
- `<RadioGroup>` ×1
- `<RadioGroupItem>` ×1

### `steps` — Horizontal step indicator with numbered or dot markers, check-on-complete pop, and connectors that fill as progress advances
- `<Steps>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×8

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/auth/verify-identity.tsx` exporting VerifyIdentityScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { View, Pressable, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState } from "react";
import { Button, Icon, RadioGroup, RadioGroupItem, Steps, Text, type IconName } from "@lumioui/ui";
import { cn, useHaptic } from "@lumioui/core";
import { Mount, Overline, World } from "../shared";

export interface IdentityDocument {
  value: string;
  label: string;
  /** What the camera needs to see — e.g. "Photo page". */
  detail: string;
  icon: IconName;
}

export interface VerifyIdentityScreenProps {
  /** Wizard steps shown at the top — current index marks where the user is. */
  steps?: string[];
  currentStep?: number;
  documents?: IdentityDocument[];
  onContinue?: (document: string) => void;
  onWhyVerify?: () => void;
  onBack?: () => void;
}

const DEFAULT_DOCUMENTS: IdentityDocument[] = [
  { value: "passport", label: "Passport", detail: "Photo page", icon: "globe" },
  { value: "drivers-license", label: "Driver's license", detail: "Front and back", icon: "credit-card" },
  { value: "national-id", label: "National ID card", detail: "Front and back", icon: "user" },
];

const REQUIREMENTS: { icon: IconName; text: string }[] = [
  { icon: "clock", text: "Takes about 2 minutes" },
  { icon: "camera", text: "A clear photo in good light" },
  { icon: "shield", text: "Encrypted and never shared" },
];

/** Verify identity — document-picker grammar: each option carries a little
 *  document mock (booklet, license card, ID) so the choice reads as the
 *  object it produces, not three identical radio rows. */
export function VerifyIdentityScreen({
  steps = ["Document", "Selfie", "Review"],
  currentStep = 0,
  documents = DEFAULT_DOCUMENTS,
  onContinue,
  onWhyVerify,
  onBack,
}: VerifyIdentityScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const [document, setDocument] = useState(documents[0]?.value ?? "");
  const [loading, setLoading] = useState(false);

  const submit = () => {
    setLoading(true);
    haptics.medium();
    onContinue?.(document);
    setTimeout(() => setLoading(false), 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Top bar — back affordance clears the status bar */}
      <View className="px-2" style={{ paddingTop: insets.top + 4 }}>
        <Pressable
          onPress={() => {
            haptics.light();
            onBack?.();
          }}
          accessibilityRole="button"
          accessibilityLabel="Go back"
          hitSlop={8}
          className="h-11 flex-row items-center gap-1 self-start px-2"
        >
          <Icon name="chevron-left" size="sm" color="#65A30D" />
          <Text className="text-base text-[#65A30D]">Back</Text>
        </Pressable>
      </View>

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerClassName="flex-grow px-6"
        contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 24 }}
      >
        <Mount delay={0}>
          <Overline>Step {currentStep + 1} of {steps.length}</Overline>
          <Text variant="h1" className="mt-2 tracking-tight">
            Verify your{"\n"}<Text className="italic text-[#65A30D]">identity</Text>
          </Text>
          <Text variant="muted" className="mt-3 max-w-xs text-base leading-relaxed">
            Choose a document to photograph — the next step walks you through it.
          </Text>
        </Mount>

        <Mount delay={100}>
          <View className="mt-7">
            <Steps steps={steps} current={currentStep} interactive={false} />
          </View>
        </Mount>

        {/* Document choice — each option shows the physical object */}
        <Mount delay={180}>
          <View className="mt-8 gap-3">
            <Overline>Choose your document</Overline>
            <RadioGroup
              value={document}
              onValueChange={(v) => {
                haptics.selection();
                setDocument(v);
              }}
              accessibilityLabel="Document type"
              className="gap-2.5"
            >
              {documents.map((doc) => (
                <RadioGroupItem
                  key={doc.value}
                  value={doc.value}
                  accessibilityLabel={doc.label}
                  className="items-center overflow-hidden rounded-2xl"
                >
                  <View className="absolute inset-0 rounded-2xl bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
                  <DocMock kind={doc.value} />
                  <View className="ml-1 flex-1">
                    <Text className="text-base font-medium text-foreground">{doc.label}</Text>
                    <Text className="text-sm text-muted-foreground" numberOfLines={1}>
                      {doc.detail}
                    </Text>
                  </View>
                </RadioGroupItem>
              ))}
            </RadioGroup>
          </View>
        </Mount>

        {/* Requirement facts — grouped on one tonal card */}
        <Mount delay={260}>
          <View className="mt-8 gap-3.5 overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
            {REQUIREMENTS.map((req) => (
              <View key={req.text} className="flex-row items-center gap-3">
                <Icon name={req.icon} size="sm" color="mutedForeground" />
                <Text variant="small" className="text-muted-foreground">
                  {req.text}
                </Text>
              </View>
            ))}
          </View>
        </Mount>

        <Mount delay={340} className="mt-auto">
          <View className="gap-1 pt-9">
            <Button
              size="lg"
              loading={loading}
              onPress={submit}
              haptic={false}
              className="bg-[#65A30D] w-full"
              accessibilityLabel="Continue to document photo"
            >
              Continue
            </Button>
            {onWhyVerify ? (
              <Pressable
                onPress={() => {
                  haptics.light();
                  onWhyVerify();
                }}
                accessibilityRole="link"
                accessibilityLabel="Why we verify your identity"
                hitSlop={8}
                className="min-h-11 items-center justify-center px-2"
              >
                <Text variant="muted" className="text-center text-sm">
                  Why do we ask for this?
                </Text>
              </Pressable>
            ) : null}
          </View>
        </Mount>
      </ScrollView>
    </View>
    </World>
  );
}
VerifyIdentityScreen.displayName = "VerifyIdentityScreen";

/** Miniature document facsimile shown inside each picker row — a booklet
 *  for passports, a landscape card with photo block for licenses/IDs. */
function DocMock({ kind }: { kind: string }) {
  if (kind === "passport") {
    return (
      <View className="h-12 w-9 items-center justify-center rounded-md bg-[#2B3566] py-1.5">
        <View className="h-2 w-2 items-center justify-center rounded-full border border-white/70" />
        <View className="mt-1.5 h-px w-4 bg-white/60" />
        <View className="mt-1 h-px w-3 bg-white/40" />
      </View>
    );
  }
  return (
    <View className="h-12 w-16 flex-row items-center gap-1.5 rounded-md border border-border bg-muted/60 p-1.5">
      <View className="h-7 w-6 items-center justify-center rounded-sm bg-[#65A30D]/15">
        <Icon name="user" size="sm" color="#65A30D" />
      </View>
      <View className="flex-1 gap-1">
        <View className={cn("h-1 rounded-full bg-foreground/25", kind === "national-id" ? "w-8" : "w-7")} />
        <View className="h-1 w-5 rounded-full bg-foreground/20" />
        <View className="h-1 w-6 rounded-full bg-foreground/15" />
      </View>
    </View>
  );
}
DocMock.displayName = "DocMock";
```

Screen: https://lumioui.dev/screens/auth-verify-identity
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗