import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { Loader } from 'lucide-react'; import { useRevalidator } from 'react-router'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { useRequiredDocumentSigningContext } from './document-signing-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text'; export type DocumentSigningSignatureFieldProps = { field: FieldWithSignature; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; typedSignatureEnabled?: boolean; uploadSignatureEnabled?: boolean; drawSignatureEnabled?: boolean; }; export const DocumentSigningSignatureField = ({ field, onSignField, onUnsignField, typedSignatureEnabled, uploadSignatureEnabled, drawSignatureEnabled, }: DocumentSigningSignatureFieldProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { revalidate } = useRevalidator(); const { recipient } = useDocumentSigningRecipientContext(); const signatureRef = useRef(null); const containerRef = useRef(null); const [fontSize, setFontSize] = useState(2); const { signature: providedSignature, setSignature: setProvidedSignature } = useRequiredDocumentSigningContext(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); const { mutateAsync: removeSignedFieldWithToken, isPending: isRemoveSignedFieldWithTokenLoading, } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); const { signature } = field; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; const [showSignatureModal, setShowSignatureModal] = useState(false); const [localSignature, setLocalSignature] = useState(null); const state = useMemo(() => { if (!field.inserted) { return 'empty'; } if (signature?.signatureImageAsBase64) { return 'signed-image'; } return 'signed-text'; }, [field.inserted, signature?.signatureImageAsBase64]); const onPreSign = () => { if (!providedSignature) { setShowSignatureModal(true); return false; } return true; }; /** * When the user clicks the sign button in the dialog where they enter their signature. */ const onDialogSignClick = () => { setShowSignatureModal(false); setProvidedSignature(localSignature); if (!localSignature) { return; } void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions, localSignature), actionTarget: field.type, }); }; const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => { try { const value = signature || providedSignature; if (!value) { setShowSignatureModal(true); return; } const isTypedSignature = !value.startsWith('data:image'); if (isTypedSignature && typedSignatureEnabled === false) { toast({ title: _(msg`Error`), description: _(msg`Typed signatures are not allowed. Please draw your signature.`), variant: 'destructive', }); return; } const payload: TSignFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, value, isBase64: !isTypedSignature, authOptions, }; if (onSignField) { await onSignField(payload); } else { await signFieldWithToken(payload); } await revalidate(); } catch (err) { const error = AppError.parseError(err); if (error.code === AppErrorCode.UNAUTHORIZED) { throw error; } console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while signing the document.`), variant: 'destructive', }); } }; const onRemove = async () => { try { const payload: TRemovedSignedFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, }; if (onUnsignField) { await onUnsignField(payload); return; } else { await removeSignedFieldWithToken(payload); } await revalidate(); } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while removing the signature.`), variant: 'destructive', }); } }; useLayoutEffect(() => { if (!signatureRef.current || !containerRef.current || !signature?.typedSignature) { return; } const adjustTextSize = () => { const container = containerRef.current; const text = signatureRef.current; if (!container || !text) { return; } let size = 2; text.style.fontSize = `${size}rem`; while ( (text.scrollWidth > container.clientWidth || text.scrollHeight > container.clientHeight) && size > 0.8 ) { size -= 0.1; text.style.fontSize = `${size}rem`; } setFontSize(size); }; const resizeObserver = new ResizeObserver(adjustTextSize); resizeObserver.observe(containerRef.current); adjustTextSize(); return () => resizeObserver.disconnect(); }, [signature?.typedSignature]); return ( {isLoading && (
)} {state === 'empty' && (

Signature

)} {state === 'signed-image' && signature?.signatureImageAsBase64 && ( {`Signature )} {state === 'signed-text' && (

{signature?.typedSignature}

)} Sign as {recipient.name}{' '}
({recipient.email})
setLocalSignature(value)} typedSignatureEnabled={typedSignatureEnabled} uploadSignatureEnabled={uploadSignatureEnabled} drawSignatureEnabled={drawSignatureEnabled} />
); };