'use client'; import { useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser'; import { KeyRoundIcon } from 'lucide-react'; import { signIn } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { FaIdCardClip } from 'react-icons/fa6'; import { FcGoogle } from 'react-icons/fc'; import { match } from 'ts-pattern'; import { z } from 'zod'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; import { trpc } from '@documenso/trpc/react'; import { ZCurrentPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ERROR_MESSAGES: Partial> = { [ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect', [ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect', [ErrorCode.USER_MISSING_PASSWORD]: 'This account appears to be using a social login method, please sign in using that method', [ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect', [ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect', [ErrorCode.UNVERIFIED_EMAIL]: 'This account has not been verified. Please verify your account before signing in.', }; const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS; const LOGIN_REDIRECT_PATH = '/documents'; export const ZSignInFormSchema = z.object({ email: z.string().email().min(1), password: ZCurrentPasswordSchema, totpCode: z.string().trim().optional(), backupCode: z.string().trim().optional(), }); export type TSignInFormSchema = z.infer; export type SignInFormProps = { className?: string; initialEmail?: string; isGoogleSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean; }; export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled, isOIDCSSOEnabled, }: SignInFormProps) => { const { toast } = useToast(); const { getFlag } = useFeatureFlags(); const router = useRouter(); const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] = useState(false); const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< 'totp' | 'backup' >('totp'); const [isPasskeyLoading, setIsPasskeyLoading] = useState(false); const isPasskeyEnabled = getFlag('app_passkey'); const { mutateAsync: createPasskeySigninOptions } = trpc.auth.createPasskeySigninOptions.useMutation(); const form = useForm({ values: { email: initialEmail ?? '', password: '', totpCode: '', backupCode: '', }, resolver: zodResolver(ZSignInFormSchema), }); const isSubmitting = form.formState.isSubmitting; const onCloseTwoFactorAuthenticationDialog = () => { form.setValue('totpCode', ''); form.setValue('backupCode', ''); setIsTwoFactorAuthenticationDialogOpen(false); }; const onToggleTwoFactorAuthenticationMethodClick = () => { const method = twoFactorAuthenticationMethod === 'totp' ? 'backup' : 'totp'; if (method === 'totp') { form.setValue('backupCode', ''); } if (method === 'backup') { form.setValue('totpCode', ''); } setTwoFactorAuthenticationMethod(method); }; const onSignInWithPasskey = async () => { if (!browserSupportsWebAuthn()) { toast({ title: 'Not supported', description: 'Passkeys are not supported on this browser', duration: 10000, variant: 'destructive', }); return; } try { setIsPasskeyLoading(true); const options = await createPasskeySigninOptions(); const credential = await startAuthentication(options); const result = await signIn('webauthn', { credential: JSON.stringify(credential), callbackUrl: LOGIN_REDIRECT_PATH, redirect: false, }); if (!result?.url || result.error) { throw new AppError(result?.error ?? ''); } window.location.href = result.url; } catch (err) { setIsPasskeyLoading(false); if (err.name === 'NotAllowedError') { return; } const error = AppError.parseError(err); const errorMessage = match(error.code) .with( AppErrorCode.NOT_SETUP, () => 'This passkey is not configured for this application. Please login and add one in the user settings.', ) .with(AppErrorCode.EXPIRED_CODE, () => 'This session has expired. Please try again.') .otherwise(() => 'Please try again later or login using your normal details'); toast({ title: 'Something went wrong', description: errorMessage, duration: 10000, variant: 'destructive', }); } }; const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => { try { const credentials: Record = { email, password, }; if (totpCode) { credentials.totpCode = totpCode; } if (backupCode) { credentials.backupCode = backupCode; } const result = await signIn('credentials', { ...credentials, callbackUrl: LOGIN_REDIRECT_PATH, redirect: false, }); if (result?.error && isErrorCode(result.error)) { if (result.error === TwoFactorEnabledErrorCode) { setIsTwoFactorAuthenticationDialogOpen(true); return; } const errorMessage = ERROR_MESSAGES[result.error]; if (result.error === ErrorCode.UNVERIFIED_EMAIL) { router.push(`/unverified-account`); toast({ title: 'Unable to sign in', description: errorMessage ?? 'An unknown error occurred', }); return; } toast({ variant: 'destructive', title: 'Unable to sign in', description: errorMessage ?? 'An unknown error occurred', }); return; } if (!result?.url) { throw new Error('An unknown error occurred'); } window.location.href = result.url; } catch (err) { toast({ title: 'An unknown error occurred', description: 'We encountered an unknown error while attempting to sign you In. Please try again later.', }); } }; const onSignInWithGoogleClick = async () => { try { await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH }); } catch (err) { toast({ title: 'An unknown error occurred', description: 'We encountered an unknown error while attempting to sign you In. Please try again later.', variant: 'destructive', }); } }; const onSignInWithOIDCClick = async () => { try { await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH }); } catch (err) { toast({ title: 'An unknown error occurred', description: 'We encountered an unknown error while attempting to sign you In. Please try again later.', variant: 'destructive', }); } }; return (
( Email )} /> ( Password

Forgot your password?

)} /> {(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
Or continue with
)} {isGoogleSSOEnabled && ( )} {isOIDCSSOEnabled && ( )} {isPasskeyEnabled && ( )}
Two-Factor Authentication
{twoFactorAuthenticationMethod === 'totp' && ( ( Token {Array(6) .fill(null) .map((_, i) => ( ))} )} /> )} {twoFactorAuthenticationMethod === 'backup' && ( ( Backup Code )} /> )}
); };