@burdenoff/microfe-auth 2026.1102.1 → 2026.1107.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"MfaVerifyPage.js","names":["_Fragment","MfaVerifyPage","tr","useTr","navigate","useNavigate","storeChallengeId","useMfaOperations","code","setCode","useState","error","setError","inputRefs","useRef","sessionChallengeId","MFA_CHALLENGE_ID","mfaChallengeId","MFA_PENDING_TOKEN","MFA_REDIRECT","handleChange","digit","value","newCode","index","fullCode","handleSubmit","handleKeyDown","e","handlePaste","pastedData","i","char","lastIndex","codeToSubmit","result","verifyMFA","ssoRedirect","MFA_PENDING_REFRESH","useAuthStore","rawErr","isTechnical","err","AuthLayout","PagePurpose","AlertIcon","el","Button","isLoading","Fragment","SpinnerIcon"],"sources":["../../../src/auth/pages/MfaVerifyPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { useNavigate } from \"react-router\";\nimport { useMfaOperations } from \"../hooks/useMfaOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport {\n MFA_CHALLENGE_ID,\n MFA_PENDING_TOKEN,\n MFA_PENDING_REFRESH,\n MFA_REDIRECT,\n} from \"../constants/sessionKeys\";\n\n/**\n * MFA verification page with professional design\n * Shown when user needs to verify MFA code during login\n * Handles both regular login MFA and SSO MFA flows\n */\nexport function MfaVerifyPage() {\n const tr = useTr();\n\n const navigate = useNavigate();\n const {\n verifyMFA,\n mfaChallengeId: storeChallengeId,\n isLoading,\n } = useMfaOperations();\n const [code, setCode] = useState([\"\", \"\", \"\", \"\", \"\", \"\"]);\n const [error, setError] = useState<string | null>(null);\n const inputRefs = useRef<(HTMLInputElement | null)[]>([]);\n\n // Get challenge ID from store or sessionStorage (SSO flow)\n const sessionChallengeId =\n typeof window !== \"undefined\"\n ? sessionStorage.getItem(MFA_CHALLENGE_ID)\n : null;\n const mfaChallengeId = storeChallengeId || sessionChallengeId;\n\n // Focus first input on mount and detect SSO flow\n useEffect(() => {\n inputRefs.current[0]?.focus();\n\n // Check if this is an SSO flow (has pending tokens in sessionStorage)\n const hasPendingToken = sessionStorage.getItem(MFA_PENDING_TOKEN);\n const hasRedirect = sessionStorage.getItem(MFA_REDIRECT);\n if (hasPendingToken || hasRedirect || sessionChallengeId) {\n }\n }, [sessionChallengeId]);\n\n const handleChange = (index: number, value: string) => {\n // Only allow digits\n const digit = value.replace(/\\D/g, \"\").slice(-1);\n\n const newCode = [...code];\n newCode[index] = digit;\n setCode(newCode);\n\n // Auto-focus next input\n if (digit && index < 5) {\n inputRefs.current[index + 1]?.focus();\n }\n\n // Auto-submit when all digits are entered\n if (digit && index === 5) {\n const fullCode = newCode.join(\"\");\n if (fullCode.length === 6) {\n handleSubmit(fullCode);\n }\n }\n };\n\n const handleKeyDown = (index: number, e: React.KeyboardEvent) => {\n if (e.key === \"Backspace\" && !code[index] && index > 0) {\n inputRefs.current[index - 1]?.focus();\n }\n };\n\n const handlePaste = (e: React.ClipboardEvent) => {\n e.preventDefault();\n const pastedData = e.clipboardData\n .getData(\"text\")\n .replace(/\\D/g, \"\")\n .slice(0, 6);\n const newCode = [...code];\n pastedData.split(\"\").forEach((char, i) => {\n if (i < 6) newCode[i] = char;\n });\n setCode(newCode);\n\n // Focus last filled input or first empty\n const lastIndex = Math.min(pastedData.length - 1, 5);\n inputRefs.current[lastIndex]?.focus();\n\n // Auto-submit if full code pasted\n if (pastedData.length === 6) {\n handleSubmit(pastedData);\n }\n };\n\n const handleSubmit = async (fullCode?: string) => {\n const codeToSubmit = fullCode || code.join(\"\");\n if (codeToSubmit.length !== 6) return;\n\n setError(null);\n\n if (!mfaChallengeId) {\n setError(\n tr(\n \"auth.mfaVerify.sessionExpired\",\n \"MFA session expired. Please log in again.\",\n ),\n );\n return;\n }\n\n try {\n const result = await verifyMFA({\n challengeId: mfaChallengeId,\n code: codeToSubmit,\n method: \"TOTP\",\n });\n\n if (result.success) {\n // Clean up SSO session data\n const ssoRedirect = sessionStorage.getItem(MFA_REDIRECT);\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n\n // Redirect to SSO redirect URL or default to dashboard\n const redirectUrl = ssoRedirect || \"/\";\n navigate(redirectUrl, { replace: true });\n } else {\n // Handle expired challenge - redirect to login\n if (result.error === \"mfa_challenge_expired\") {\n // Clear all MFA state\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n\n // Redirect to login with message\n navigate(\"/auth/login\", {\n replace: true,\n state: {\n message: tr(\n \"auth.mfaVerify.sessionExpiredMessage\",\n \"Your verification session expired. Please sign in again.\",\n ),\n },\n });\n return;\n }\n\n // Sanitize technical / null-deref-style messages bubbling up from\n // resolver internals — always show a user-friendly fallback.\n const rawErr = result.error ?? \"\";\n const isTechnical =\n /cannot read propert|null|undefined|typeerror/i.test(rawErr);\n setError(\n !rawErr || isTechnical\n ? tr(\n \"auth.mfaVerify.invalidCode\",\n \"Invalid verification code. Please try again.\",\n )\n : rawErr,\n );\n setCode([\"\", \"\", \"\", \"\", \"\", \"\"]);\n inputRefs.current[0]?.focus();\n }\n } catch (err) {\n console.error(\"MFA verification failed:\", err);\n\n // Check if the error is about expired challenge\n const errorMessage = err instanceof Error ? err.message : String(err);\n if (errorMessage.includes(\"mfa_challenge_expired\")) {\n // Clear all MFA state\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n\n // Redirect to login with message\n navigate(\"/auth/login\", {\n replace: true,\n state: {\n message: tr(\n \"auth.mfaVerify.sessionExpiredMessage\",\n \"Your verification session expired. Please sign in again.\",\n ),\n },\n });\n return;\n }\n\n setError(\n tr(\n \"auth.mfaVerify.verificationFailed\",\n \"Verification failed. Please try again.\",\n ),\n );\n setCode([\"\", \"\", \"\", \"\", \"\", \"\"]);\n inputRefs.current[0]?.focus();\n }\n };\n\n return (\n <AuthLayout\n title={tr(\"auth.mfaVerify.title\", \"Two-factor authentication\")}\n subtitle={tr(\n \"auth.mfaVerify.subtitle\",\n \"Enter the 6-digit code from your authenticator app\",\n )}\n >\n {/* Shield Icon */}\n <div className=\"flex justify-center mb-6\">\n <div className=\"size-16 rounded-full bg-[var(--color-bg-accent)] flex items-center justify-center\">\n <svg\n className=\"size-8 text-[var(--color-text-link)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n </div>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.mfaVerify.purpose\",\n \"This extra step confirms it's really you. Open your authenticator app and enter the current 6-digit code to finish signing in.\",\n )}\n </PagePurpose>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n {/* Error Alert */}\n {error && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-[var(--color-status-error-bg-subtle)] border border-[var(--color-status-error-border)]\">\n <AlertIcon className=\"size-5 text-[var(--color-status-error-text)] flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-[var(--color-status-error-text)]\">\n {error}\n </p>\n </div>\n )}\n\n {/* Code Input */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium text-[var(--color-text-primary)] text-center\">\n {tr(\"auth.mfaVerify.verificationCode\", \"Verification Code\")}\n </label>\n <div className=\"flex justify-center gap-2 sm:gap-3\">\n {code.map((digit, index) => (\n <input\n key={index}\n ref={(el) => {\n inputRefs.current[index] = el;\n }}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n maxLength={1}\n aria-label={\n tr(\n \"auth.mfaVerify.digitAriaLabel\",\n \"Verification code digit\",\n ) + ` ${index + 1}`\n }\n value={digit}\n onChange={(e) => handleChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={handlePaste}\n className=\"w-10 h-12 sm:w-12 sm:h-14 text-center text-xl sm:text-2xl font-semibold rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n ))}\n </div>\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading || code.join(\"\").length !== 6}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>{tr(\"auth.mfaVerify.verifying\", \"Verifying…\")}</span>\n </>\n ) : (\n tr(\"auth.mfaVerify.verify\", \"Verify\")\n )}\n </Button>\n\n {/* Recovery Options */}\n <div className=\"text-center space-y-3\">\n <p className=\"text-sm text-[var(--color-text-secondary)]\">\n {tr(\n \"auth.mfaVerify.cantAccess\",\n \"Can't access your authenticator app?\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/mfa-recovery\")}\n className=\"text-sm font-medium text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.mfaVerify.useRecoveryCode\", \"Use a recovery code\")}\n </button>\n </div>\n </form>\n\n {/* Back to Login */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n <button\n type=\"button\"\n onClick={() => {\n // Clear MFA state so LoginPage doesn't immediately redirect back here\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n navigate(\"/auth/login\");\n }}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.mfaVerify.backToSignIn\", \"Back to sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"MfaVerifyPage.js","names":["_Fragment","MfaVerifyPage","tr","useTr","navigate","useNavigate","storeChallengeId","useMfaOperations","code","setCode","useState","error","setError","inputRefs","useRef","sessionChallengeId","MFA_CHALLENGE_ID","mfaChallengeId","handleChange","digit","value","newCode","index","fullCode","handleSubmit","handleKeyDown","e","handlePaste","pastedData","i","char","lastIndex","codeToSubmit","result","verifyMFA","ssoRedirect","MFA_REDIRECT","MFA_PENDING_TOKEN","MFA_PENDING_REFRESH","useAuthStore","rawErr","isTechnical","err","AuthLayout","PagePurpose","AlertIcon","el","Button","isLoading","Fragment","SpinnerIcon"],"sources":["../../../src/auth/pages/MfaVerifyPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { useNavigate } from \"react-router\";\nimport { useMfaOperations } from \"../hooks/useMfaOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { PagePurpose } from \"@burdenoff/fe-libs/ui\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport {\n MFA_CHALLENGE_ID,\n MFA_PENDING_TOKEN,\n MFA_PENDING_REFRESH,\n MFA_REDIRECT,\n} from \"../constants/sessionKeys\";\n\n/**\n * MFA verification page with professional design\n * Shown when user needs to verify MFA code during login\n * Handles both regular login MFA and SSO MFA flows\n */\nexport function MfaVerifyPage() {\n const tr = useTr();\n\n const navigate = useNavigate();\n const {\n verifyMFA,\n mfaChallengeId: storeChallengeId,\n isLoading,\n } = useMfaOperations();\n const [code, setCode] = useState([\"\", \"\", \"\", \"\", \"\", \"\"]);\n const [error, setError] = useState<string | null>(null);\n const inputRefs = useRef<(HTMLInputElement | null)[]>([]);\n\n // Get challenge ID from store or sessionStorage (SSO flow)\n const sessionChallengeId =\n typeof window !== \"undefined\"\n ? sessionStorage.getItem(MFA_CHALLENGE_ID)\n : null;\n const mfaChallengeId = storeChallengeId || sessionChallengeId;\n\n // Focus the first input on mount (and when the SSO challenge changes).\n useEffect(() => {\n inputRefs.current[0]?.focus();\n }, [sessionChallengeId]);\n\n const handleChange = (index: number, value: string) => {\n // Only allow digits\n const digit = value.replace(/\\D/g, \"\").slice(-1);\n\n const newCode = [...code];\n newCode[index] = digit;\n setCode(newCode);\n\n // Auto-focus next input\n if (digit && index < 5) {\n inputRefs.current[index + 1]?.focus();\n }\n\n // Auto-submit when all digits are entered\n if (digit && index === 5) {\n const fullCode = newCode.join(\"\");\n if (fullCode.length === 6) {\n handleSubmit(fullCode);\n }\n }\n };\n\n const handleKeyDown = (index: number, e: React.KeyboardEvent) => {\n if (e.key === \"Backspace\" && !code[index] && index > 0) {\n inputRefs.current[index - 1]?.focus();\n }\n };\n\n const handlePaste = (e: React.ClipboardEvent) => {\n e.preventDefault();\n const pastedData = e.clipboardData\n .getData(\"text\")\n .replace(/\\D/g, \"\")\n .slice(0, 6);\n const newCode = [...code];\n pastedData.split(\"\").forEach((char, i) => {\n if (i < 6) newCode[i] = char;\n });\n setCode(newCode);\n\n // Focus last filled input or first empty\n const lastIndex = Math.min(pastedData.length - 1, 5);\n inputRefs.current[lastIndex]?.focus();\n\n // Auto-submit if full code pasted\n if (pastedData.length === 6) {\n handleSubmit(pastedData);\n }\n };\n\n const handleSubmit = async (fullCode?: string) => {\n const codeToSubmit = fullCode || code.join(\"\");\n if (codeToSubmit.length !== 6) return;\n\n setError(null);\n\n if (!mfaChallengeId) {\n setError(\n tr(\n \"auth.mfaVerify.sessionExpired\",\n \"MFA session expired. Please log in again.\",\n ),\n );\n return;\n }\n\n try {\n const result = await verifyMFA({\n challengeId: mfaChallengeId,\n code: codeToSubmit,\n method: \"TOTP\",\n });\n\n if (result.success) {\n // Clean up SSO session data\n const ssoRedirect = sessionStorage.getItem(MFA_REDIRECT);\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n\n // Redirect to SSO redirect URL or default to dashboard\n const redirectUrl = ssoRedirect || \"/\";\n navigate(redirectUrl, { replace: true });\n } else {\n // Handle expired challenge - redirect to login\n if (result.error === \"mfa_challenge_expired\") {\n // Clear all MFA state\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n\n // Redirect to login with message\n navigate(\"/auth/login\", {\n replace: true,\n state: {\n message: tr(\n \"auth.mfaVerify.sessionExpiredMessage\",\n \"Your verification session expired. Please sign in again.\",\n ),\n },\n });\n return;\n }\n\n // Sanitize technical / null-deref-style messages bubbling up from\n // resolver internals — always show a user-friendly fallback.\n const rawErr = result.error ?? \"\";\n const isTechnical =\n /cannot read propert|null|undefined|typeerror/i.test(rawErr);\n setError(\n !rawErr || isTechnical\n ? tr(\n \"auth.mfaVerify.invalidCode\",\n \"Invalid verification code. Please try again.\",\n )\n : rawErr,\n );\n setCode([\"\", \"\", \"\", \"\", \"\", \"\"]);\n inputRefs.current[0]?.focus();\n }\n } catch (err) {\n console.error(\"MFA verification failed:\", err);\n\n // Check if the error is about expired challenge\n const errorMessage = err instanceof Error ? err.message : String(err);\n if (errorMessage.includes(\"mfa_challenge_expired\")) {\n // Clear all MFA state\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n\n // Redirect to login with message\n navigate(\"/auth/login\", {\n replace: true,\n state: {\n message: tr(\n \"auth.mfaVerify.sessionExpiredMessage\",\n \"Your verification session expired. Please sign in again.\",\n ),\n },\n });\n return;\n }\n\n setError(\n tr(\n \"auth.mfaVerify.verificationFailed\",\n \"Verification failed. Please try again.\",\n ),\n );\n setCode([\"\", \"\", \"\", \"\", \"\", \"\"]);\n inputRefs.current[0]?.focus();\n }\n };\n\n return (\n <AuthLayout\n title={tr(\"auth.mfaVerify.title\", \"Two-factor authentication\")}\n subtitle={tr(\n \"auth.mfaVerify.subtitle\",\n \"Enter the 6-digit code from your authenticator app\",\n )}\n >\n {/* Shield Icon */}\n <div className=\"flex justify-center mb-6\">\n <div className=\"size-16 rounded-full bg-[var(--color-bg-accent)] flex items-center justify-center\">\n <svg\n className=\"size-8 text-[var(--color-text-link)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n </div>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.mfaVerify.purpose\",\n \"This extra step confirms it's really you. Open your authenticator app and enter the current 6-digit code to finish signing in.\",\n )}\n </PagePurpose>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n handleSubmit();\n }}\n className=\"space-y-6\"\n >\n {/* Error Alert */}\n {error && (\n <div className=\"flex items-start gap-3 p-4 rounded-xl bg-[var(--color-status-error-bg-subtle)] border border-[var(--color-status-error-border)]\">\n <AlertIcon className=\"size-5 text-[var(--color-status-error-text)] flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-[var(--color-status-error-text)]\">\n {error}\n </p>\n </div>\n )}\n\n {/* Code Input */}\n <div className=\"space-y-2\">\n <label className=\"block text-sm font-medium text-[var(--color-text-primary)] text-center\">\n {tr(\"auth.mfaVerify.verificationCode\", \"Verification Code\")}\n </label>\n <div className=\"flex justify-center gap-2 sm:gap-3\">\n {code.map((digit, index) => (\n <input\n key={index}\n ref={(el) => {\n inputRefs.current[index] = el;\n }}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n maxLength={1}\n aria-label={\n tr(\n \"auth.mfaVerify.digitAriaLabel\",\n \"Verification code digit\",\n ) + ` ${index + 1}`\n }\n value={digit}\n onChange={(e) => handleChange(index, e.target.value)}\n onKeyDown={(e) => handleKeyDown(index, e)}\n onPaste={handlePaste}\n className=\"w-10 h-12 sm:w-12 sm:h-14 text-center text-xl sm:text-2xl font-semibold rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n ))}\n </div>\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n disabled={isLoading || code.join(\"\").length !== 6}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>{tr(\"auth.mfaVerify.verifying\", \"Verifying…\")}</span>\n </>\n ) : (\n tr(\"auth.mfaVerify.verify\", \"Verify\")\n )}\n </Button>\n\n {/* Recovery Options */}\n <div className=\"text-center space-y-3\">\n <p className=\"text-sm text-[var(--color-text-secondary)]\">\n {tr(\n \"auth.mfaVerify.cantAccess\",\n \"Can't access your authenticator app?\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/mfa-recovery\")}\n className=\"text-sm font-medium text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.mfaVerify.useRecoveryCode\", \"Use a recovery code\")}\n </button>\n </div>\n </form>\n\n {/* Back to Login */}\n <p className=\"mt-8 text-center text-sm text-[var(--color-text-secondary)]\">\n <button\n type=\"button\"\n onClick={() => {\n // Clear MFA state so LoginPage doesn't immediately redirect back here\n sessionStorage.removeItem(MFA_CHALLENGE_ID);\n sessionStorage.removeItem(MFA_PENDING_TOKEN);\n sessionStorage.removeItem(MFA_PENDING_REFRESH);\n sessionStorage.removeItem(MFA_REDIRECT);\n useAuthStore.getState().setMfaRequired(false);\n navigate(\"/auth/login\");\n }}\n className=\"font-semibold text-text-link hover:text-text-linkHover transition-colors\"\n >\n {tr(\"auth.mfaVerify.backToSignIn\", \"Back to sign in\")}\n </button>\n </p>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}