import { useId, useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client'; import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { AssistantConfirmationDialog, type NextSigner, } from '../../dialogs/assistant-confirmation-dialog'; import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog'; import { useRequiredDocumentSigningContext } from './document-signing-provider'; export type DocumentSigningFormProps = { document: DocumentAndSender; recipient: Recipient; fields: Field[]; redirectUrl?: string | null; isRecipientsTurn: boolean; allRecipients?: RecipientWithFields[]; setSelectedSignerId?: (id: number | null) => void; }; export const DocumentSigningForm = ({ document, recipient, fields, redirectUrl, isRecipientsTurn, allRecipients = [], setSelectedSignerId, }: DocumentSigningFormProps) => { const { sessionData } = useOptionalSession(); const user = sessionData?.user; const { _ } = useLingui(); const { toast } = useToast(); const navigate = useNavigate(); const analytics = useAnalytics(); const assistantSignersId = useId(); const { fullName, signature, setFullName, setSignature } = useRequiredDocumentSigningContext(); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false); const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false); const { mutateAsync: completeDocumentWithToken, isPending, isSuccess, } = trpc.recipient.completeDocumentWithToken.useMutation(); const assistantForm = useForm<{ selectedSignerId: number | undefined }>({ defaultValues: { selectedSignerId: undefined, }, }); // Keep the loading state going if successful since the redirect may take some time. const isSubmitting = isPending || isSuccess; const fieldsRequiringValidation = useMemo( () => fields.filter(isFieldUnsignedAndRequired), [fields], ); const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const uninsertedFields = useMemo(() => { return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted)); }, [fieldsRequiringValidation]); const uninsertedRecipientFields = useMemo(() => { return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id); }, [fieldsRequiringValidation, recipient]); const fieldsValidated = () => { setValidateUninsertedFields(true); validateFieldsInserted(fieldsRequiringValidation); }; const onAssistantFormSubmit = () => { if (uninsertedRecipientFields.length > 0) { return; } setIsConfirmationDialogOpen(true); }; const handleAssistantConfirmDialogSubmit = async (nextSigner?: NextSigner) => { setIsAssistantSubmitting(true); try { await completeDocument(undefined, nextSigner); } catch (err) { toast({ title: 'Error', description: 'An error occurred while completing the document. Please try again.', variant: 'destructive', }); setIsAssistantSubmitting(false); setIsConfirmationDialogOpen(false); } }; const completeDocument = async ( authOptions?: TRecipientActionAuth, nextSigner?: { email: string; name: string }, ) => { const payload = { token: recipient.token, documentId: document.id, authOptions, ...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}), }; await completeDocumentWithToken(payload); analytics.capture('App: Recipient has completed signing', { signerId: recipient.id, documentId: document.id, timestamp: new Date().toISOString(), }); if (redirectUrl) { window.location.href = redirectUrl; } else { await navigate(`/sign/${recipient.token}/complete`); } }; const nextRecipient = useMemo(() => { if ( !document.documentMeta?.signingOrder || document.documentMeta.signingOrder !== 'SEQUENTIAL' ) { return undefined; } const sortedRecipients = allRecipients.sort((a, b) => { // Sort by signingOrder first (nulls last), then by id if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id; if (a.signingOrder === null) return 1; if (b.signingOrder === null) return -1; if (a.signingOrder === b.signingOrder) return a.id - b.id; return a.signingOrder - b.signingOrder; }); const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id); return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1 ? sortedRecipients[currentIndex + 1] : undefined; }, [document.documentMeta?.signingOrder, allRecipients, recipient.id]); return (
{validateUninsertedFields && uninsertedFields[0] && ( Click to insert field )}

{recipient.role === RecipientRole.VIEWER && View Document} {recipient.role === RecipientRole.SIGNER && Sign Document} {recipient.role === RecipientRole.APPROVER && Approve Document} {recipient.role === RecipientRole.ASSISTANT && Assist Document}

{recipient.role === RecipientRole.VIEWER ? ( <>

Please mark as viewed to complete


{ await completeDocument(undefined, nextSigner); }} role={recipient.role} allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner} defaultNextSigner={ nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined } />
) : recipient.role === RecipientRole.ASSISTANT ? ( <>

Complete the fields for the following signers. Once reviewed, they will inform you if any modifications are needed.


( { field.onChange(value); setSelectedSignerId?.(Number(value)); }} > {allRecipients .filter((r) => r.fields.length > 0) .map((r) => (

{r.email}

{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
))}
)} />
0} isOpen={isConfirmationDialogOpen} onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)} onConfirm={handleAssistantConfirmDialogSubmit} isSubmitting={isAssistantSubmitting} allowDictateNextSigner={ nextRecipient && document.documentMeta?.allowDictateNextSigner } defaultNextSigner={ nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined } /> ) : ( <>

{recipient.role === RecipientRole.APPROVER && !hasSignatureField ? ( Please review the document before approving. ) : ( Please review the document before signing. )}


setFullName(e.target.value.trimStart())} />
{hasSignatureField && (
setSignature(v ?? '')} typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled} uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled} drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled} />
)}
{ await completeDocument(undefined, nextSigner); }} role={recipient.role} allowDictateNextSigner={ nextRecipient && document.documentMeta?.allowDictateNextSigner } defaultNextSigner={ nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined } />
)}
); };