import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { ArrowLeftIcon, KeyIcon, MailIcon } from 'lucide-react'; import { DateTime } from 'luxon'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Alert } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Form, FormField, FormItem } from '@documenso/ui/primitives/form/form'; import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; type FormStep = 'method-selection' | 'code-input'; type TwoFactorMethod = 'email' | 'authenticator'; const ZAccessAuth2FAFormSchema = z.object({ token: z.string().length(6, { message: 'Token must be 6 characters long' }), }); type TAccessAuth2FAFormSchema = z.infer; export type AccessAuth2FAFormProps = { onSubmit: (accessAuthOptions: TRecipientAccessAuth) => void; token: string; error?: string | null; }; export const AccessAuth2FAForm = ({ onSubmit, token, error }: AccessAuth2FAFormProps) => { const [step, setStep] = useState('method-selection'); const [selectedMethod, setSelectedMethod] = useState(null); const [expiresAt, setExpiresAt] = useState(null); const [millisecondsRemaining, setMillisecondsRemaining] = useState(null); const { _ } = useLingui(); const { toast } = useToast(); const { user } = useRequiredDocumentSigningAuthContext(); const { mutateAsync: request2FAEmail, isPending: isRequesting2FAEmail } = trpc.document.accessAuth.request2FAEmail.useMutation(); const form = useForm({ resolver: zodResolver(ZAccessAuth2FAFormSchema), defaultValues: { token: '', }, }); const hasAuthenticatorEnabled = user?.twoFactorEnabled === true; const onMethodSelect = async (method: TwoFactorMethod) => { setSelectedMethod(method); if (method === 'email') { try { const result = await request2FAEmail({ token: token, }); setExpiresAt(result.expiresAt); setMillisecondsRemaining(result.expiresAt.valueOf() - Date.now()); setStep('code-input'); } catch (error) { toast({ title: _(msg`An error occurred`), description: _( msg`We encountered an unknown error while attempting to request the two-factor authentication code. Please try again later.`, ), variant: 'destructive', }); return; } } setStep('code-input'); }; const onFormSubmit = (data: TAccessAuth2FAFormSchema) => { if (!selectedMethod) { return; } // Prepare the auth options for the completion attempt const accessAuthOptions: TRecipientAccessAuth = { type: 'TWO_FACTOR_AUTH', token: data.token, // Just the user's code - backend will validate using method type method: selectedMethod, }; onSubmit(accessAuthOptions); }; const onGoBack = () => { setStep('method-selection'); setSelectedMethod(null); setExpiresAt(null); setMillisecondsRemaining(null); }; const onResendEmail = async () => { if (selectedMethod !== 'email') { return; } try { const result = await request2FAEmail({ token: token, }); setExpiresAt(result.expiresAt); setMillisecondsRemaining(result.expiresAt.valueOf() - Date.now()); } catch (error) { toast({ title: _(msg`An error occurred`), description: _( msg`We encountered an unknown error while attempting to request the two-factor authentication code. Please try again later.`, ), variant: 'destructive', }); } }; useEffect(() => { const interval = setInterval(() => { if (expiresAt) { setMillisecondsRemaining(expiresAt.valueOf() - Date.now()); } }, 1000); return () => clearInterval(interval); }, [expiresAt]); return (
{step === 'method-selection' && (

Choose verification method

Please select how you'd like to receive your verification code.

{error && ( {error} )}
{hasAuthenticatorEnabled && ( )}
)} {step === 'code-input' && (

Enter verification code

{selectedMethod === 'email' ? ( We've sent a 6-digit verification code to your email. Please enter it below to complete the document. ) : ( Please open your authenticator app and enter the 6-digit code for this document. )}
( {expiresAt && millisecondsRemaining !== null && (
Expires in{' '} {DateTime.fromMillis(Math.max(millisecondsRemaining, 0)).toFormat( 'mm:ss', )}
)}
)} />
{selectedMethod === 'email' && ( )}
)}
); };