"use client"; import { useTranslations } from "next-intl"; import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { v4 } from "uuid"; import { errorToast } from "../../../../components"; import { useI18nRouter, usePageUrlGenerator } from "../../../../hooks"; import { Button, CardContent, CardDescription, CardHeader, CardTitle, Input, Tabs, TabsContent, TabsList, TabsTrigger, } from "../../../../shadcnui"; import { UserInterface } from "../../../user"; import { useCurrentUserContext } from "../../../user/contexts"; import { useAuthContext } from "../../contexts"; import { AuthInterface } from "../../data/auth.interface"; import { TwoFactorService } from "../../data/two-factor.service"; import { PasskeyButton } from "../two-factor/PasskeyButton"; import { TotpInput } from "../two-factor/TotpInput"; export function TwoFactorChallenge() { const t = useTranslations(); const { setUser } = useCurrentUserContext(); const { pendingTwoFactor, setPendingTwoFactor } = useAuthContext(); const generateUrl = usePageUrlGenerator(); const i18nRouter = useI18nRouter(); const nativeRouter = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl"); const [isVerifying, setIsVerifying] = useState(false); const [totpError, setTotpError] = useState(); const [backupCode, setBackupCode] = useState(""); const [backupError, setBackupError] = useState(); if (!pendingTwoFactor) { return null; } const handleSuccess = (auth: AuthInterface) => { setPendingTwoFactor(undefined); setUser(auth.user); if (callbackUrl) { nativeRouter.replace(callbackUrl); } else { i18nRouter.replace(generateUrl({ page: "/" })); } }; const handleTotpComplete = async (code: string) => { setIsVerifying(true); setTotpError(undefined); try { const auth = await TwoFactorService.verifyTotp({ id: v4(), pendingToken: pendingTwoFactor.pendingToken, code, }); handleSuccess(auth); } catch (error) { setTotpError(t("auth.two_factor.invalid_code")); errorToast({ title: t("common.errors.error"), error, }); } finally { setIsVerifying(false); } }; const handlePasskeyError = (error: Error) => { errorToast({ title: t("common.errors.error"), error, }); }; const handleBackupSubmit = async () => { if (backupCode.length < 8) { setBackupError(t("auth.two_factor.invalid_backup_code")); return; } setIsVerifying(true); setBackupError(undefined); try { const auth = await TwoFactorService.verifyBackupCode({ id: v4(), pendingToken: pendingTwoFactor.pendingToken, code: backupCode, }); handleSuccess(auth); } catch (error) { setBackupError(t("auth.two_factor.invalid_backup_code")); errorToast({ title: t("common.errors.error"), error, }); } finally { setIsVerifying(false); } }; const availableMethods = pendingTwoFactor.availableMethods; const hasTotp = availableMethods.includes("totp"); const hasPasskey = availableMethods.includes("passkey"); const hasBackup = availableMethods.includes("backup"); return ( <> {t("auth.two_factor.verification_required")} {t("auth.two_factor.enter_verification_code")} {hasTotp && ( {t("auth.two_factor.authenticator")} )} {hasPasskey && ( {t("auth.two_factor.passkey")} )} {hasBackup && ( {t("auth.two_factor.backup_code")} )} {hasTotp && (

{t("auth.two_factor.enter_authenticator_code")}

)} {hasPasskey && (

{t("auth.two_factor.use_passkey_description")}

)} {hasBackup && (

{t("auth.two_factor.enter_backup_code")}

setBackupCode(e.target.value.toUpperCase())} placeholder="XXXXXXXX" maxLength={8} className={`w-48 text-center tabular-nums uppercase ${backupError ? "border-destructive" : ""}`} disabled={isVerifying} data-testid="backup-code-input" /> {backupError &&

{backupError}

}
)}
); }