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 type * as DialogPrimitive from '@radix-ui/react-dialog'; import { startRegistration } from '@simplewebauthn/browser'; import { KeyRoundIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { UAParser } from 'ua-parser-js'; import { z } from 'zod'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import { AppError } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } 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 { useToast } from '@documenso/ui/primitives/use-toast'; export type PasskeyCreateDialogProps = { trigger?: React.ReactNode; onSuccess?: () => void; } & Omit; const ZCreatePasskeyFormSchema = z.object({ passkeyName: z.string().min(3), }); type TCreatePasskeyFormSchema = z.infer; const parser = new UAParser(); export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCreateDialogProps) => { const [open, setOpen] = useState(false); const [formError, setFormError] = useState(null); const { _ } = useLingui(); const { toast } = useToast(); const form = useForm({ resolver: zodResolver(ZCreatePasskeyFormSchema), defaultValues: { passkeyName: '', }, }); const { mutateAsync: createPasskeyRegistrationOptions, isPending } = trpc.auth.passkey.createRegistrationOptions.useMutation(); const { mutateAsync: createPasskey } = trpc.auth.passkey.create.useMutation(); const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => { setFormError(null); try { const passkeyRegistrationOptions = await createPasskeyRegistrationOptions(); const registrationResult = await startRegistration(passkeyRegistrationOptions); await createPasskey({ passkeyName, verificationResponse: registrationResult, }); toast({ description: _(msg`Successfully created passkey`), duration: 5000, }); onSuccess?.(); setOpen(false); } catch (err) { if (err.name === 'NotAllowedError') { return; } const error = AppError.parseError(err); setFormError(err.code || error.code); } }; const extractDefaultPasskeyName = () => { if (!window || !window.navigator) { return; } parser.setUA(window.navigator.userAgent); const result = parser.getResult(); const operatingSystem = result.os.name; const browser = result.browser.name; let passkeyName = ''; if (operatingSystem && browser) { passkeyName = `${browser} (${operatingSystem})`; } return passkeyName; }; useEffect(() => { if (!open) { const defaultPasskeyName = extractDefaultPasskeyName(); form.reset({ passkeyName: defaultPasskeyName, }); setFormError(null); } }, [open, form]); return ( !form.formState.isSubmitting && setOpen(value)} > e.stopPropagation()} asChild={true}> {trigger ?? ( )} Add passkey Passkeys allow you to sign in and authenticate using biometrics, password managers, etc.
( Passkey name )} /> When you click continue, you will be prompted to add the first available authenticator on your system. If you do not want to use the authenticator prompted, you can close it, which will then display the next available authenticator. {formError && ( {match(formError) .with('ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', () => ( This passkey has already been registered. )) .with('TOO_MANY_PASSKEYS', () => ( You cannot have more than {MAXIMUM_PASSKEYS} passkeys. )) .with('InvalidStateError', () => ( <> Passkey creation cancelled due to one of the following reasons:
  • Cancelled by user
  • Passkey already exists for the provided authenticator
  • Exceeded timeout
)) .otherwise(() => ( Something went wrong. Please try again or contact support. ))}
)}
); };