import { useRef, useState } from "react"; import { Form, useActionData, useLoaderData, useNavigation } from "react-router"; import { useTranslation } from "react-i18next"; import { useRootData } from "~/utils/data/useRootData"; import { AuthShell } from "~/components/auth/AuthShell"; import { Button } from "~/ui/components/Button"; import { Checkbox } from "~/ui/components/Checkbox"; import { InputOtp } from "~/ui/components/InputOtp"; import { LinkButton } from "~/ui/components/LinkButton"; import { TextInput } from "~/ui/components/TextInput"; import type { ActionData, LoaderData } from "~/routeModules/auth/login-mfa.impl"; // D-D0a — 2FA-Code-Eingabe nach Davids Subframe-Design ("_2FA _ Code Entry"): Logo + Card // (Bestaetigung in zwei Schritten) + 6-stellige InputOtp + "Geraet vertrauen"-Checkbox + Bestaetigen + // Recovery-Code-Toggle. An die bestehende login-mfa-Action verdrahtet (code/recoveryCode/rememberDevice/csrf). // Die 6 OTP-Boxen werden zu einem hidden "code"-Feld kombiniert (Auto-Advance, Paste, Backspace). const OTP_LENGTH = 6; export default function LoginMfaRoute() { const { t } = useTranslation(); const { csrf } = useRootData(); const data = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const isSubmitting = navigation.state !== "idle"; const [digits, setDigits] = useState(Array(OTP_LENGTH).fill("")); const [trustDevice, setTrustDevice] = useState(false); const [showRecovery, setShowRecovery] = useState(false); const inputs = useRef<(HTMLInputElement | null)[]>([]); const code = digits.join(""); function handleChange(index: number, raw: string) { const clean = raw.replace(/\D/g, ""); setDigits((prev) => { const next = [...prev]; if (!clean) { next[index] = ""; return next; } // Einzelzeichen ODER Paste (z.B. "123456") ab dieser Box vorwaerts fuellen. for (let k = 0; k < clean.length && index + k < OTP_LENGTH; k++) { next[index + k] = clean[k]; } return next; }); if (clean) { inputs.current[Math.min(index + clean.length, OTP_LENGTH - 1)]?.focus(); } } function handleKeyDown(index: number, event: React.KeyboardEvent) { if (event.key === "Backspace" && !digits[index] && index > 0) { inputs.current[index - 1]?.focus(); setDigits((prev) => { const next = [...prev]; next[index - 1] = ""; return next; }); } } const errorState = Boolean(actionData?.error && navigation.state === "idle"); return (
{csrf ? : null} {!showRecovery ? (
{Array.from({ length: OTP_LENGTH }).map((_, i) => ( { inputs.current[i] = el; }} value={digits[i]} onChange={(event) => handleChange(i, event.target.value)} onKeyDown={(event) => handleKeyDown(i, event)} inputMode="numeric" autoComplete={i === 0 ? "one-time-code" : "off"} maxLength={OTP_LENGTH} aria-label={`${t("account.loginMfa.fields.authenticatorCode")} ${i + 1}`} data-ui-id={i === 0 ? "ui.auth.loginMfa.code" : undefined} /> ))}
) : ( )} {errorState ? (

{actionData?.error}

) : null}
setShowRecovery((v) => !v)} data-ui-id="ui.auth.loginMfa.useRecoveryCode" > {showRecovery ? t("account.loginMfa.useAuthenticator") : t("account.loginMfa.useRecoveryCode")}
); }