import { Alert, Button, PinInput, Stack, Text } from "@mantine/core"; import { AlertCircle, ArrowLeft, RotateCw, ShieldCheck } from "lucide-react"; import { maskEmail, maskPhone } from "@/utils/identifier"; export const OTP_LENGTH = 6; export interface OtpChannelStepProps { /** * Raw contacts the code was sent to; masked before display. The API sends one * code to every contact on the account, so both are usually set — pass only * what the client actually knows. Omit both when the client cannot know them * (the forgot-password flow deliberately never reveals an account's contacts) * and a generic line is shown instead. */ email?: string; phone?: string; value: string; onChange: (otp: string) => void; onVerify: () => void; onBack: () => void; onResend: () => void; /** Seconds until resend is allowed; 0 enables the button. */ resendIn: number; sending: boolean; verifying: boolean; error: string | null; title?: string; description?: string; submitLabel: string; } /** * The "enter the code we sent you" stage. Shared by signup and the * forgot-password flow — both send through the same `/api/otp/*` service, which * delivers a single code to the account's email AND phone; whichever message * arrives first can be typed here. */ export default function OtpChannelStep({ email, phone, value, onChange, onVerify, onBack, onResend, resendIn, sending, verifying, error, title, description, submitLabel, }: OtpChannelStepProps) { const maskedTargets = [ email ? maskEmail(email) : null, phone ? maskPhone(phone) : null, ].filter(Boolean) as string[]; const busy = sending || verifying; return (

{title ?? "Verify it's you"}

We sent a {OTP_LENGTH}-digit code to{" "} {maskedTargets.length ? ( maskedTargets.map((target, index) => ( {index > 0 ? " and " : null} {target} )) ) : ( the email and phone on your account )} . {description ?? "Enter it to continue."}

{error ? ( }> {error} ) : null} Verification code
); }