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 { RecipientRole } from '@prisma/client'; import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/browser'; import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { AppError } from '@documenso/lib/errors/app-error'; import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; import { DialogFooter } from '@documenso/ui/primitives/dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; import { PasskeyCreateDialog } from '~/components/dialogs/passkey-create-dialog'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; export type DocumentSigningAuthPasskeyProps = { actionTarget?: 'FIELD' | 'DOCUMENT'; actionVerb?: string; open: boolean; onOpenChange: (value: boolean) => void; onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise | void; }; const ZPasskeyAuthFormSchema = z.object({ passkeyId: z.string(), }); type TPasskeyAuthFormSchema = z.infer; export const DocumentSigningAuthPasskey = ({ actionTarget = 'FIELD', actionVerb = 'sign', onReauthFormSubmit, open, onOpenChange, }: DocumentSigningAuthPasskeyProps) => { const { _ } = useLingui(); const { recipient, passkeyData, preferredPasskeyId, setPreferredPasskeyId, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating, refetchPasskeys, } = useRequiredDocumentSigningAuthContext(); const form = useForm({ resolver: zodResolver(ZPasskeyAuthFormSchema), defaultValues: { passkeyId: preferredPasskeyId || '', }, }); const { mutateAsync: createPasskeyAuthenticationOptions } = trpc.auth.passkey.createAuthenticationOptions.useMutation(); const [formErrorCode, setFormErrorCode] = useState(null); const onFormSubmit = async ({ passkeyId }: TPasskeyAuthFormSchema) => { try { setPreferredPasskeyId(passkeyId); setIsCurrentlyAuthenticating(true); const { options, tokenReference } = await createPasskeyAuthenticationOptions({ preferredPasskeyId: passkeyId, }); const authenticationResponse = await startAuthentication(options); await onReauthFormSubmit({ type: DocumentAuth.PASSKEY, authenticationResponse, tokenReference, }); setIsCurrentlyAuthenticating(false); onOpenChange(false); } catch (err) { setIsCurrentlyAuthenticating(false); if (err.name === 'NotAllowedError') { return; } const error = AppError.parseError(err); setFormErrorCode(error.code); // Todo: Alert. } }; useEffect(() => { form.reset({ passkeyId: preferredPasskeyId || '', }); setFormErrorCode(null); }, [open, form, preferredPasskeyId]); if (!browserSupportsWebAuthn()) { return (
{/* Todo: Translate */} Your browser does not support passkeys, which is required to {actionVerb.toLowerCase()}{' '} this {actionTarget.toLowerCase()}.
); } if (passkeyData.isInitialLoading || (passkeyData.isError && passkeyData.passkeys.length === 0)) { return (
); } if (passkeyData.isError) { return (
Something went wrong while loading your passkeys.
); } if (passkeyData.passkeys.length === 0) { return (
{/* Todo: Translate */} {recipient.role === RecipientRole.VIEWER && actionTarget === 'DOCUMENT' ? 'You need to setup a passkey to mark this document as viewed.' : `You need to setup a passkey to ${actionVerb.toLowerCase()} this ${actionTarget.toLowerCase()}.`} refetchPasskeys()} trigger={ } />
); } return (
( Passkey )} /> {formErrorCode && ( Unauthorized We were unable to verify your details. Please try again or contact support )}
); };