'use client'; import { useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { useMutation } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import useFetchAuthFactors from '@kit/supabase/hooks/use-fetch-mfa-factors'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useSupabase } from '@kit/supabase/hooks/use-supabase'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Button } from '@kit/ui/button'; import { Form, FormControl, FormDescription, FormField, FormItem, FormMessage, } from '@kit/ui/form'; import { If } from '@kit/ui/if'; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, } from '@kit/ui/input-otp'; import { Spinner } from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; export function MultiFactorChallengeContainer({ paths, }: React.PropsWithChildren<{ paths: { redirectPath: string; }; }>) { const router = useRouter(); const verifyMFAChallenge = useVerifyMFAChallenge(); const onSuccess = useCallback(() => { router.replace(paths.redirectPath); }, [router, paths.redirectPath]); const verificationCodeForm = useForm({ resolver: zodResolver( z.object({ factorId: z.string().min(1), verificationCode: z.string().min(6).max(6), }), ), defaultValues: { factorId: '', verificationCode: '', }, }); const factorId = verificationCodeForm.watch('factorId'); if (!factorId) { return ( { verificationCodeForm.setValue('factorId', factorId); }} onSuccess={onSuccess} /> ); } return (
{ await verifyMFAChallenge.mutateAsync({ factorId, verificationCode: data.verificationCode, }); onSuccess(); })} >
{ return ( ); }} />
); } function useVerifyMFAChallenge() { const client = useSupabase(); const mutationKey = ['mfa-verify-challenge']; const mutationFn = async (params: { factorId: string; verificationCode: string; }) => { const { factorId, verificationCode: code } = params; const response = await client.auth.mfa.challengeAndVerify({ factorId, code, }); if (response.error) { throw response.error; } return response.data; }; return useMutation({ mutationKey, mutationFn }); } function FactorsListContainer({ onSuccess, onSelect, }: React.PropsWithChildren<{ onSuccess: () => void; onSelect: (factor: string) => void; }>) { const signOut = useSignOut(); const { data: factors, isLoading, error } = useFetchAuthFactors(); const isSuccess = factors && !isLoading && !error; useEffect(() => { // If there are no factors, continue if (isSuccess && !factors.totp.length) { onSuccess(); } }, [factors?.totp.length, isSuccess, onSuccess]); useEffect(() => { // If there is an error, sign out if (error) { void signOut.mutateAsync(); } }, [error, signOut]); useEffect(() => { // If there is only one factor, select it automatically if (isSuccess && factors.totp.length === 1) { const factorId = factors.totp[0]?.id; if (factorId) { onSelect(factorId); } } }); if (isLoading) { return (
); } if (error) { return (
); } const verifiedFactors = factors?.totp ?? []; return (
{verifiedFactors.map((factor) => (
))}
); }