Six-digit code entry with auto-advance cells, backspace navigation, resend countdown, and auto-submit
import * as React from "react";
import { View, TextInput, ScrollView, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState, useRef, useEffect } from "react";
import { interpolateColor, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from "react-native-reanimated";
import { Button, Icon, Text, KeyboardAvoiding } from "@lumioui/ui";
import { AnimatedTextInput, cn, spring, timing, useHaptic, useReducedMotion, useTheme, } from "@lumioui/core";
import { Mount, Press, BLOCK_STAGGER, World } from "../shared";
export interface OTPVerificationScreenProps {
length?: number;
email?: string;
/** Server-side rejection message (e.g. "That code expired"). */
error?: string;
onVerify?: (code: string) => void;
onResend?: () => void;
onBack?: () => void;
/** Seconds before resend is allowed. */
resendCooldown?: number;
/** Prefilled code for demos/previews — production hosts omit it. */
defaultCode?: string;
}
/** OTP verification — oversized cells with a gliding focus ring and digit pop,
* paste-to-fill, countdown resend, auto-submit. */
export function OTPVerificationScreen({
length = 6,
email,
error,
onVerify,
onResend,
onBack,
resendCooldown = 30,
defaultCode,
}: OTPVerificationScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [digits, setDigits] = useState<string[]>(() => {
const arr = Array<string>(length).fill("");
if (defaultCode) {
for (let i = 0; i < Math.min(length, defaultCode.length); i++) arr[i] = defaultCode.charAt(i);
}
return arr;
});
const [focused, setFocused] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(resendCooldown);
const refs = useRef<(TextInput | null)[]>([]);
useEffect(() => {
if (cooldown <= 0) return;
const t = setInterval(() => setCooldown((c) => c - 1), 1000);
return () => clearInterval(t);
}, [cooldown]);
// Surface a haptic rejection when the server-side error arrives.
useEffect(() => {
if (error) haptics.error();
}, [error, haptics]);
const complete = (next: string[]) => {
if (next.every((d) => d) && !loading) {
setLoading(true);
onVerify?.(next.join(""));
setTimeout(() => setLoading(false), 800);
}
};
const setDigit = (i: number, v: string) => {
const chars = v.replace(/\D/g, "");
if (!chars) {
// Deletion — clear this cell; backspace-to-previous is handled in onKey.
const next = [...digits];
next[i] = "";
setDigits(next);
return;
}
const next = [...digits];
if (chars.length > 1) {
// Paste / autofill — distribute the digits across cells from here on.
for (let k = 0; k < chars.length && i + k < length; k++) {
next[i + k] = chars[k] ?? "";
}
const target = Math.min(i + chars.length, length - 1);
refs.current[target]?.focus();
} else {
next[i] = chars.slice(-1);
if (i < length - 1) refs.current[i + 1]?.focus();
}
setDigits(next);
haptics.selection();
complete(next);
};
const onKey = (i: number, key: string) => {
if (key === "Backspace" && !digits[i] && i > 0) {
refs.current[i - 1]?.focus();
const next = [...digits];
next[i - 1] = "";
setDigits(next);
}
};
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 }}>
<Press
onPress={onBack}
haptic="light"
pressedScale={0.97}
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>
</Press>
</View>
<KeyboardAvoiding>
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerClassName="flex-grow justify-center px-6"
contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
>
<View className="gap-9">
{/* Header — tinted icon tile + display title */}
<Mount delay={0}>
<View className="gap-3">
{/* Message-in-a-ring — a delivered-mail motif, not a
generic security tile. */}
<View className="h-16 w-16 items-center justify-center rounded-full border-2 border-[#65A30D]/30 bg-[#65A30D]/10">
<View className="h-11 w-11 items-center justify-center rounded-full bg-[#65A30D]">
<Icon name="mail" size="sm" color="#FFFFFF" />
</View>
</View>
<Text variant="h1" className="mt-3 tracking-tight">
Enter verification code
</Text>
<Text variant="muted" className="max-w-xs text-base leading-relaxed">
{email ? (
<>
We sent a {length}-digit code to{" "}
<Text className="font-semibold text-foreground">{email}</Text>
{". "}
<Text
className="text-[#65A30D] font-medium"
accessibilityRole="link"
onPress={() => {
haptics.light();
onBack?.();
}}
>
Edit
</Text>
</>
) : (
`Enter the ${length}-digit code we sent you.`
)}
</Text>
</View>
</Mount>
{/* iOS autofill hint — the "From Messages" suggestion bar the
keyboard shows. Tapping it fills the code (the same code the
phone-verification preview showed). */}
<Mount delay={BLOCK_STAGGER / 2}>
<Pressable
onPress={() => {
haptics.selection();
"481923".split("").slice(0, length).forEach((d, i) => setDigit(i, d));
}}
accessibilityRole="button"
accessibilityLabel="Autofill code 481923 from messages"
hitSlop={8}
className="mb-4 flex-row items-center gap-2 self-center overflow-hidden rounded-full bg-white px-4 py-2 dark:bg-[#1A1A20]"
>
<Icon name="message-circle" size="sm" color="mutedForeground" />
<Text variant="small" className="text-muted-foreground">
From Messages:{" "}
<Text className="font-semibold tracking-widest text-foreground">481 923</Text>
</Text>
</Pressable>
</Mount>
{/* Cells — flex-1 so the row fills any width evenly */}
<Mount delay={BLOCK_STAGGER}>
<View>
<View
className="flex-row gap-2"
accessibilityRole="adjustable"
accessibilityLabel={`Verification code, ${digits.filter(Boolean).length} of ${length} digits entered`}
>
{digits.map((d, i) => (
<OtpCell
key={i}
index={i}
value={d}
focused={i === focused}
hasError={!!error}
length={length}
inputRef={(r) => {
refs.current[i] = r;
}}
onChange={setDigit}
onKey={onKey}
onFocusCell={setFocused}
/>
))}
</View>
{error ? (
<Text
variant="small"
className="mt-3 text-destructive"
accessibilityLiveRegion="assertive"
accessibilityRole="alert"
>
{error}
</Text>
) : null}
</View>
</Mount>
<Mount delay={BLOCK_STAGGER * 2}>
<Button
size="lg"
loading={loading}
disabled={!digits.every(Boolean)}
onPress={() => onVerify?.(digits.join(""))}
haptic="medium"
className="bg-[#65A30D] w-full"
>
Verify
</Button>
</Mount>
{/* Resend — countdown then a real link */}
<Mount delay={BLOCK_STAGGER * 3}>
<View className="items-center">
{cooldown > 0 ? (
<Text variant="muted" style={{ fontVariant: ["tabular-nums"] }}>
Resend code in {cooldown}s
</Text>
) : (
<Press
onPress={() => {
setCooldown(resendCooldown);
onResend?.();
}}
haptic="light"
pressedScale={0.97}
accessibilityRole="button"
accessibilityLabel="Resend verification code"
hitSlop={8}
className="min-h-11 flex-row items-center justify-center gap-2 px-2"
>
<Icon name="refresh" size="sm" color="#65A30D" />
<Text className="text-[#65A30D] font-semibold" style={{ fontVariant: ["tabular-nums"] }}>
Resend code
</Text>
</Press>
)}
</View>
</Mount>
</View>
</ScrollView>
</KeyboardAvoiding>
</View>
</World>
);
}
interface OtpCellProps {
index: number;
value: string;
focused: boolean;
hasError: boolean;
length: number;
inputRef: (r: TextInput | null) => void;
onChange: (index: number, value: string) => void;
onKey: (index: number, key: string) => void;
onFocusCell: (index: number | null) => void;
}
/** One OTP digit — the border color glides input → ring on focus (same
* interpolateColor idiom as Input), the cell lifts slightly while focused,
* and a digit arrival pops the scale before settling. */
function OtpCell({ index, value, focused, hasError, length, inputRef, onChange, onKey, onFocusCell }: OtpCellProps) {
const { colors } = useTheme();
const reduce = useReducedMotion();
/** Focus progression — drives the border glide and the lift. */
const focusT = useSharedValue(focused ? 1 : 0);
/** Scale driver — filled digits overshoot once, then settle to focus scale. */
const scale = useSharedValue(1);
const wasFilled = useRef(!!value);
useEffect(() => {
const filled = !!value;
if (filled && !wasFilled.current && !reduce) scale.value = 1.1;
wasFilled.current = filled;
const target = focused ? 1.05 : 1;
scale.value = reduce ? target : withSpring(target, spring.press);
}, [value, focused, reduce, scale]);
useEffect(() => {
const target = focused ? 1 : 0;
focusT.value = reduce ? target : withTiming(target, timing.fast);
}, [focused, reduce, focusT]);
const cellStyle = useAnimatedStyle(() => ({
borderColor: hasError
? colors.destructive
: interpolateColor(
focusT.value,
[0, 1],
// Tonal at rest — the border matches the cell surface, so only
// the focus ring glides in; no resting hairline.
[value ? colors.muted : colors.card, colors.ring]
),
transform: [{ scale: scale.value }],
}));
return (
<View className="min-w-0 flex-1">
<View className="absolute inset-0 rounded-2xl bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
<AnimatedTextInput
ref={inputRef}
value={value}
onChangeText={(v) => onChange(index, v)}
onKeyPress={({ nativeEvent }) => onKey(index, nativeEvent.key)}
onFocus={() => onFocusCell(index)}
onBlur={() => onFocusCell(null)}
keyboardType="number-pad"
textContentType="oneTimeCode"
autoComplete="sms-otp"
returnKeyType="done"
// autoFocus removed to prevent auto-scrolling embedded previews
selectTextOnFocus
// Tabular figures keep each cell from jittering as digits change.
style={[{ fontVariant: ["tabular-nums"] }, cellStyle]}
className={cn(
// min-w-0: web inputs carry an intrinsic size=20 min-width that
// flex-1 can't shrink below — six cells would overflow the row.
"h-16 w-full rounded-2xl border-[1.5px] border-transparent bg-transparent text-center text-2xl font-bold text-foreground",
hasError && "bg-destructive/10"
)}
accessibilityLabel={`Digit ${index + 1} of ${length}`}
/>
</View>
);
}
OtpCell.displayName = "OtpCell";
Recreate the "OTP Verification Screen" screen in React Native using LumioUI.
Goal: Six-digit code entry with auto-advance cells, backspace navigation, resend countdown, and auto-submit 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-otp-verification
```
Or pull just the components: `npx lumioui add button icon keyboard-avoiding 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>` ×4
### `keyboard-avoiding` — View that lifts above the keyboard on both platforms.
- `<KeyboardAvoiding>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×10
## 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/otp-verification.tsx` exporting OTPVerificationScreen. 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, TextInput, ScrollView, Pressable } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState, useRef, useEffect } from "react";
import { interpolateColor, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from "react-native-reanimated";
import { Button, Icon, Text, KeyboardAvoiding } from "@lumioui/ui";
import { AnimatedTextInput, cn, spring, timing, useHaptic, useReducedMotion, useTheme, } from "@lumioui/core";
import { Mount, Press, BLOCK_STAGGER, World } from "../shared";
export interface OTPVerificationScreenProps {
length?: number;
email?: string;
/** Server-side rejection message (e.g. "That code expired"). */
error?: string;
onVerify?: (code: string) => void;
onResend?: () => void;
onBack?: () => void;
/** Seconds before resend is allowed. */
resendCooldown?: number;
/** Prefilled code for demos/previews — production hosts omit it. */
defaultCode?: string;
}
/** OTP verification — oversized cells with a gliding focus ring and digit pop,
* paste-to-fill, countdown resend, auto-submit. */
export function OTPVerificationScreen({
length = 6,
email,
error,
onVerify,
onResend,
onBack,
resendCooldown = 30,
defaultCode,
}: OTPVerificationScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [digits, setDigits] = useState<string[]>(() => {
const arr = Array<string>(length).fill("");
if (defaultCode) {
for (let i = 0; i < Math.min(length, defaultCode.length); i++) arr[i] = defaultCode.charAt(i);
}
return arr;
});
const [focused, setFocused] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [cooldown, setCooldown] = useState(resendCooldown);
const refs = useRef<(TextInput | null)[]>([]);
useEffect(() => {
if (cooldown <= 0) return;
const t = setInterval(() => setCooldown((c) => c - 1), 1000);
return () => clearInterval(t);
}, [cooldown]);
// Surface a haptic rejection when the server-side error arrives.
useEffect(() => {
if (error) haptics.error();
}, [error, haptics]);
const complete = (next: string[]) => {
if (next.every((d) => d) && !loading) {
setLoading(true);
onVerify?.(next.join(""));
setTimeout(() => setLoading(false), 800);
}
};
const setDigit = (i: number, v: string) => {
const chars = v.replace(/\D/g, "");
if (!chars) {
// Deletion — clear this cell; backspace-to-previous is handled in onKey.
const next = [...digits];
next[i] = "";
setDigits(next);
return;
}
const next = [...digits];
if (chars.length > 1) {
// Paste / autofill — distribute the digits across cells from here on.
for (let k = 0; k < chars.length && i + k < length; k++) {
next[i + k] = chars[k] ?? "";
}
const target = Math.min(i + chars.length, length - 1);
refs.current[target]?.focus();
} else {
next[i] = chars.slice(-1);
if (i < length - 1) refs.current[i + 1]?.focus();
}
setDigits(next);
haptics.selection();
complete(next);
};
const onKey = (i: number, key: string) => {
if (key === "Backspace" && !digits[i] && i > 0) {
refs.current[i - 1]?.focus();
const next = [...digits];
next[i - 1] = "";
setDigits(next);
}
};
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 }}>
<Press
onPress={onBack}
haptic="light"
pressedScale={0.97}
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>
</Press>
</View>
<KeyboardAvoiding>
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerClassName="flex-grow justify-center px-6"
contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
>
<View className="gap-9">
{/* Header — tinted icon tile + display title */}
<Mount delay={0}>
<View className="gap-3">
{/* Message-in-a-ring — a delivered-mail motif, not a
generic security tile. */}
<View className="h-16 w-16 items-center justify-center rounded-full border-2 border-[#65A30D]/30 bg-[#65A30D]/10">
<View className="h-11 w-11 items-center justify-center rounded-full bg-[#65A30D]">
<Icon name="mail" size="sm" color="#FFFFFF" />
</View>
</View>
<Text variant="h1" className="mt-3 tracking-tight">
Enter verification code
</Text>
<Text variant="muted" className="max-w-xs text-base leading-relaxed">
{email ? (
<>
We sent a {length}-digit code to{" "}
<Text className="font-semibold text-foreground">{email}</Text>
{". "}
<Text
className="text-[#65A30D] font-medium"
accessibilityRole="link"
onPress={() => {
haptics.light();
onBack?.();
}}
>
Edit
</Text>
</>
) : (
`Enter the ${length}-digit code we sent you.`
)}
</Text>
</View>
</Mount>
{/* iOS autofill hint — the "From Messages" suggestion bar the
keyboard shows. Tapping it fills the code (the same code the
phone-verification preview showed). */}
<Mount delay={BLOCK_STAGGER / 2}>
<Pressable
onPress={() => {
haptics.selection();
"481923".split("").slice(0, length).forEach((d, i) => setDigit(i, d));
}}
accessibilityRole="button"
accessibilityLabel="Autofill code 481923 from messages"
hitSlop={8}
className="mb-4 flex-row items-center gap-2 self-center overflow-hidden rounded-full bg-white px-4 py-2 dark:bg-[#1A1A20]"
>
<Icon name="message-circle" size="sm" color="mutedForeground" />
<Text variant="small" className="text-muted-foreground">
From Messages:{" "}
<Text className="font-semibold tracking-widest text-foreground">481 923</Text>
</Text>
</Pressable>
</Mount>
{/* Cells — flex-1 so the row fills any width evenly */}
<Mount delay={BLOCK_STAGGER}>
<View>
<View
className="flex-row gap-2"
accessibilityRole="adjustable"
accessibilityLabel={`Verification code, ${digits.filter(Boolean).length} of ${length} digits entered`}
>
{digits.map((d, i) => (
<OtpCell
key={i}
index={i}
value={d}
focused={i === focused}
hasError={!!error}
length={length}
inputRef={(r) => {
refs.current[i] = r;
}}
onChange={setDigit}
onKey={onKey}
onFocusCell={setFocused}
/>
))}
</View>
{error ? (
<Text
variant="small"
className="mt-3 text-destructive"
accessibilityLiveRegion="assertive"
accessibilityRole="alert"
>
{error}
</Text>
) : null}
</View>
</Mount>
<Mount delay={BLOCK_STAGGER * 2}>
<Button
size="lg"
loading={loading}
disabled={!digits.every(Boolean)}
onPress={() => onVerify?.(digits.join(""))}
haptic="medium"
className="bg-[#65A30D] w-full"
>
Verify
</Button>
</Mount>
{/* Resend — countdown then a real link */}
<Mount delay={BLOCK_STAGGER * 3}>
<View className="items-center">
{cooldown > 0 ? (
<Text variant="muted" style={{ fontVariant: ["tabular-nums"] }}>
Resend code in {cooldown}s
</Text>
) : (
<Press
onPress={() => {
setCooldown(resendCooldown);
onResend?.();
}}
haptic="light"
pressedScale={0.97}
accessibilityRole="button"
accessibilityLabel="Resend verification code"
hitSlop={8}
className="min-h-11 flex-row items-center justify-center gap-2 px-2"
>
<Icon name="refresh" size="sm" color="#65A30D" />
<Text className="text-[#65A30D] font-semibold" style={{ fontVariant: ["tabular-nums"] }}>
Resend code
</Text>
</Press>
)}
</View>
</Mount>
</View>
</ScrollView>
</KeyboardAvoiding>
</View>
</World>
);
}
interface OtpCellProps {
index: number;
value: string;
focused: boolean;
hasError: boolean;
length: number;
inputRef: (r: TextInput | null) => void;
onChange: (index: number, value: string) => void;
onKey: (index: number, key: string) => void;
onFocusCell: (index: number | null) => void;
}
/** One OTP digit — the border color glides input → ring on focus (same
* interpolateColor idiom as Input), the cell lifts slightly while focused,
* and a digit arrival pops the scale before settling. */
function OtpCell({ index, value, focused, hasError, length, inputRef, onChange, onKey, onFocusCell }: OtpCellProps) {
const { colors } = useTheme();
const reduce = useReducedMotion();
/** Focus progression — drives the border glide and the lift. */
const focusT = useSharedValue(focused ? 1 : 0);
/** Scale driver — filled digits overshoot once, then settle to focus scale. */
const scale = useSharedValue(1);
const wasFilled = useRef(!!value);
useEffect(() => {
const filled = !!value;
if (filled && !wasFilled.current && !reduce) scale.value = 1.1;
wasFilled.current = filled;
const target = focused ? 1.05 : 1;
scale.value = reduce ? target : withSpring(target, spring.press);
}, [value, focused, reduce, scale]);
useEffect(() => {
const target = focused ? 1 : 0;
focusT.value = reduce ? target : withTiming(target, timing.fast);
}, [focused, reduce, focusT]);
const cellStyle = useAnimatedStyle(() => ({
borderColor: hasError
? colors.destructive
: interpolateColor(
focusT.value,
[0, 1],
// Tonal at rest — the border matches the cell surface, so only
// the focus ring glides in; no resting hairline.
[value ? colors.muted : colors.card, colors.ring]
),
transform: [{ scale: scale.value }],
}));
return (
<View className="min-w-0 flex-1">
<View className="absolute inset-0 rounded-2xl bg-white dark:bg-[#1A1A20]" pointerEvents="none" />
<AnimatedTextInput
ref={inputRef}
value={value}
onChangeText={(v) => onChange(index, v)}
onKeyPress={({ nativeEvent }) => onKey(index, nativeEvent.key)}
onFocus={() => onFocusCell(index)}
onBlur={() => onFocusCell(null)}
keyboardType="number-pad"
textContentType="oneTimeCode"
autoComplete="sms-otp"
returnKeyType="done"
// autoFocus removed to prevent auto-scrolling embedded previews
selectTextOnFocus
// Tabular figures keep each cell from jittering as digits change.
style={[{ fontVariant: ["tabular-nums"] }, cellStyle]}
className={cn(
// min-w-0: web inputs carry an intrinsic size=20 min-width that
// flex-1 can't shrink below — six cells would overflow the row.
"h-16 w-full rounded-2xl border-[1.5px] border-transparent bg-transparent text-center text-2xl font-bold text-foreground",
hasError && "bg-destructive/10"
)}
accessibilityLabel={`Digit ${index + 1} of ${length}`}
/>
</View>
);
}
OtpCell.displayName = "OtpCell";
```
Screen: https://lumioui.dev/screens/auth-otp-verification