import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client'; import { Loader, LucideChevronDown, LucideChevronUp, X } from 'lucide-react'; import { P, match } from 'ts-pattern'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import PDFViewer from '@documenso/ui/primitives/pdf-viewer'; import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider'; import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog'; import { EmbedDocumentFields } from '../embed-document-fields'; interface MultiSignDocumentSigningViewProps { token: string; recipientId: number; onBack: () => void; onDocumentCompleted?: (data: { token: string; documentId: number; recipientId: number }) => void; onDocumentRejected?: (data: { token: string; documentId: number; recipientId: number; reason: string; }) => void; onDocumentError?: () => void; onDocumentReady?: () => void; isNameLocked?: boolean; allowDocumentRejection?: boolean; } export const MultiSignDocumentSigningView = ({ token, recipientId, onBack, onDocumentCompleted, onDocumentRejected, onDocumentError, onDocumentReady, isNameLocked = false, allowDocumentRejection = false, }: MultiSignDocumentSigningViewProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { fullName, email, signature, setFullName, setSignature } = useRequiredDocumentSigningContext(); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const { data: document, isLoading } = trpc.embeddingPresign.getMultiSignDocument.useQuery( { token }, { staleTime: 0, }, ); const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation(); const { mutateAsync: removeSignedFieldWithToken } = trpc.field.removeSignedFieldWithToken.useMutation(); const { mutateAsync: completeDocumentWithToken } = trpc.recipient.completeDocumentWithToken.useMutation(); const hasSignatureField = document?.fields.some((field) => field.type === FieldType.SIGNATURE); const [pendingFields, completedFields] = [ document?.fields.filter((field) => field.recipient.signingStatus !== SigningStatus.SIGNED) ?? [], document?.fields.filter((field) => field.recipient.signingStatus === SigningStatus.SIGNED) ?? [], ]; const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page)); const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? []; const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => { try { await signFieldWithToken(payload); } 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 onUnsignField = async (payload: TRemovedSignedFieldWithTokenMutationSchema) => { try { await removeSignedFieldWithToken(payload); } catch (err) { const error = AppError.parseError(err); if (error.code === AppErrorCode.UNAUTHORIZED) { throw error; } console.error(err); } }; const onDocumentComplete = async () => { try { setIsSubmitting(true); await completeDocumentWithToken({ documentId: document!.id, token, }); onBack(); onDocumentCompleted?.({ token, documentId: document!.id, recipientId, }); } catch (err) { onDocumentError?.(); toast({ title: 'Error', description: 'Failed to complete the document. Please try again.', variant: 'destructive', }); } finally { setIsSubmitting(false); } }; const onNextFieldClick = () => { setShowPendingFieldTooltip(true); setIsExpanded(false); }; const onRejected = (reason: string) => { if (onDocumentRejected && document) { onDocumentRejected({ token, documentId: document.id, recipientId, reason, }); } }; return (
{match({ isLoading, document }) .with({ isLoading: true }, () => (

Loading document...

)) .with({ isLoading: false, document: undefined }, () => (

Failed to load document

)) .with({ document: P.nonNullable }, ({ document }) => ( <>

{document.title}

{allowDocumentRejection && (
)}
{ setHasDocumentLoaded(true); onDocumentReady?.(); }} />
{/* Widget */} {document.status !== DocumentStatus.COMPLETED && (
{/* Header */}

Sign document

Sign the document to complete the process.


{/* Form */}
{ <>
!isNameLocked && setFullName(e.target.value)} />
{hasSignatureField && (
setSignature(v ?? '')} typedSignatureEnabled={ document.documentMeta?.typedSignatureEnabled } uploadSignatureEnabled={ document.documentMeta?.uploadSignatureEnabled } drawSignatureEnabled={ document.documentMeta?.drawSignatureEnabled } />
)} }
{uninsertedFields.length > 0 ? ( ) : ( )}
)}
{hasDocumentLoaded && ( {showPendingFieldTooltip && pendingFields.length > 0 && ( Click to insert field )} )} {/* Fields */} {hasDocumentLoaded && ( )} {/* Completed fields */} {document.status !== DocumentStatus.COMPLETED && ( )} )) .otherwise(() => null)}
); };