"use client"; //=========================================== // THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY UNLESS YOU ALSO EDIT THE CORRESPONDING FILE IN packages/template //=========================================== import { KnownErrors } from "@hexclave/shared"; import { Button, InputOTP, InputOTPGroup, InputOTPSlot, Spinner, Typography, cn, } from "@hexclave/ui"; import { CheckIcon } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useStackApp } from ".."; import { FormWarningText } from "../components/elements/form-warning"; import { MaybeFullPage } from "../components/elements/maybe-full-page"; import { useTranslation } from "../lib/translations"; function MfaForm({ onSuccess, onCancel }: { onSuccess?: () => void, onCancel?: () => void, }) { const hexclaveApp = useStackApp(); const { t } = useTranslation(); const [otp, setOtp] = useState(""); const formRef = useRef(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [verified, setVerified] = useState(false); const [attemptCode, setAttemptCode] = useState(null); useEffect(() => { if (!attemptCode && typeof window !== "undefined") { // Hexclave rebrand: prefer the new MFA attempt code key, fall back to the legacy key. const code = window.sessionStorage.getItem("hexclave_mfa_attempt_code") ?? window.sessionStorage.getItem("stack_mfa_attempt_code"); if (code) { setAttemptCode(code); } } }, [ attemptCode]); // Handle OTP verification when code is complete useEffect(() => { if (otp.length === 6 && !submitting) { // Blur any focused inputs if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } if (formRef.current) { const inputs = formRef.current.querySelectorAll('input'); for (const input of inputs) { input.blur(); } } setSubmitting(true); setError(null); if (attemptCode) { hexclaveApp .signInWithMfa(otp, attemptCode, { noRedirect: true }) .then(async (result) => { if (result.status === "ok") { setVerified(true); // Cleanup session storage if (typeof window !== "undefined") { // Hexclave rebrand: remove both the new and legacy MFA attempt code keys. window.sessionStorage.removeItem("hexclave_mfa_attempt_code"); window.sessionStorage.removeItem("stack_mfa_attempt_code"); } if (onSuccess) { onSuccess(); } else { await hexclaveApp.redirectToAfterSignIn(); } } else { throw result.error; } }) .catch((e) => { if (e instanceof KnownErrors.InvalidTotpCode) { setError(t("Invalid TOTP code")); } else { setError(t("Verification failed")); } }) .finally(() => { setSubmitting(false); if (!verified) { setOtp(""); } }); } else { setSubmitting(false); setError(t("Missing verification information")); } } // Clear error when user is typing if (otp.length !== 0 && otp.length !== 6) { setError(null); } }, [otp, submitting, onSuccess, attemptCode, hexclaveApp, t, verified]); const inputStyleClass = useMemo(() => { if (verified) { return "opacity-85 transition-all duration-300"; } if (error) { return "ring-red-500 border-red-500"; } return "focus:ring-primary/50"; }, [error, verified]); return (
setOtp(value.toUpperCase())} disabled={submitting || verified} > {[0, 1, 2, 3, 4, 5].map((index) => ( ))} {/* Verification Status */}
{verified ? (
{t("Verified! Redirecting...")}
) : submitting ? (
{t("Verifying...")}
) : null} {/* Error reporting */} {error !== null && !submitting && !verified ? : null}
{/* Cancel Button */} {onCancel && !verified && ( )}
); } export function MFA(props: { fullPage?: boolean, onSuccess?: () => void, onCancel?: () => void, }) { const { t } = useTranslation(); const headerText = t("Multi-Factor Authentication"); const instructionText = t("Enter the six-digit code from your authenticator app"); return (
{headerText} {instructionText}
); }