import { useMemo, useState } from 'react'; import { Trans } from '@lingui/react/macro'; import type { Field } from '@prisma/client'; import { FieldType, RecipientRole } from '@prisma/client'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { useNavigate } from 'react-router'; import { P, match } from 'ts-pattern'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token'; import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth'; import { ZCheckboxFieldMeta, ZDropdownFieldMeta, ZNumberFieldMeta, ZRadioFieldMeta, ZTextFieldMeta, } from '@documenso/lib/types/field-meta'; import type { CompletedField } from '@documenso/lib/types/fields'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { DocumentSigningAttachmentsPopover } from '~/components/general/document-signing/document-signing-attachments-popover'; import { DocumentSigningAutoSign } from '~/components/general/document-signing/document-signing-auto-sign'; import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field'; import { DocumentSigningDateField } from '~/components/general/document-signing/document-signing-date-field'; import { DocumentSigningDropdownField } from '~/components/general/document-signing/document-signing-dropdown-field'; import { DocumentSigningEmailField } from '~/components/general/document-signing/document-signing-email-field'; import { DocumentSigningForm } from '~/components/general/document-signing/document-signing-form'; import { DocumentSigningInitialsField } from '~/components/general/document-signing/document-signing-initials-field'; import { DocumentSigningNameField } from '~/components/general/document-signing/document-signing-name-field'; import { DocumentSigningNumberField } from '~/components/general/document-signing/document-signing-number-field'; import { DocumentSigningRadioField } from '~/components/general/document-signing/document-signing-radio-field'; import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog'; import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider'; export type DocumentSigningPageViewV1Props = { recipient: RecipientWithFields; document: DocumentAndSender; fields: Field[]; completedFields: CompletedField[]; isRecipientsTurn: boolean; allRecipients?: RecipientWithFields[]; includeSenderDetails: boolean; }; export const DocumentSigningPageViewV1 = ({ recipient, document, fields, completedFields, isRecipientsTurn, allRecipients = [], includeSenderDetails, }: DocumentSigningPageViewV1Props) => { const { documentData, documentMeta } = document; const { derivedRecipientAccessAuth, user: authUser } = useRequiredDocumentSigningAuthContext(); const hasAuthenticator = authUser?.twoFactorEnabled ? authUser.twoFactorEnabled && authUser.email === recipient.email : false; const navigate = useNavigate(); const analytics = useAnalytics(); const [selectedSignerId, setSelectedSignerId] = useState(allRecipients?.[0]?.id); const [isExpanded, setIsExpanded] = useState(false); const { mutateAsync: completeDocumentWithToken, isPending, isSuccess, } = trpc.recipient.completeDocumentWithToken.useMutation(); // 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 fieldsValidated = () => { validateFieldsInserted(fieldsRequiringValidation); }; const completeDocument = async (options: { accessAuthOptions?: TRecipientAccessAuth; nextSigner?: { email: string; name: string }; }) => { const { accessAuthOptions, nextSigner } = options; const payload = { token: recipient.token, documentId: document.id, accessAuthOptions, ...(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 (documentMeta?.redirectUrl) { window.location.href = documentMeta.redirectUrl; } else { await navigate(`/sign/${recipient.token}/complete`); } }; let senderName = document.user.name ?? ''; let senderEmail = `(${document.user.email})`; if (includeSenderDetails) { senderName = document.team?.name ?? ''; senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : ''; } const selectedSigner = allRecipients?.find((r) => r.id === selectedSignerId); const targetSigner = recipient.role === RecipientRole.ASSISTANT && selectedSigner ? selectedSigner : null; const nextRecipient = useMemo(() => { if (!documentMeta?.signingOrder || 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]); const highestPageNumber = Math.max(...fields.map((field) => field.page)); const pendingFields = fieldsRequiringValidation.filter((field) => !field.inserted); const hasPendingFields = pendingFields.length > 0; return (
{document.team.teamGlobalSettings.brandingEnabled && document.team.teamGlobalSettings.brandingLogo && ( {`${document.team.name}'s )}

{document.title}

{senderName} {senderEmail} {' '} {match(recipient.role) .with(RecipientRole.VIEWER, () => includeSenderDetails ? ( on behalf of "{document.team?.name}" has invited you to view this document ) : ( has invited you to view this document ), ) .with(RecipientRole.SIGNER, () => includeSenderDetails ? ( on behalf of "{document.team?.name}" has invited you to sign this document ) : ( has invited you to sign this document ), ) .with(RecipientRole.APPROVER, () => includeSenderDetails ? ( on behalf of "{document.team?.name}" has invited you to approve this document ) : ( has invited you to approve this document ), ) .with(RecipientRole.ASSISTANT, () => includeSenderDetails ? ( on behalf of "{document.team?.name}" has invited you to assist this document ) : ( has invited you to assist this document ), ) .otherwise(() => null)}

{match(recipient.role) .with(RecipientRole.VIEWER, () => View Document) .with(RecipientRole.SIGNER, () => Sign Document) .with(RecipientRole.APPROVER, () => Approve Document) .with(RecipientRole.ASSISTANT, () => Assist Document) .otherwise(() => null)}

{match({ hasPendingFields, isExpanded, role: recipient.role }) .with( { hasPendingFields: false, role: P.not(RecipientRole.ASSISTANT), isExpanded: false, }, () => (
completeDocument({ nextSigner }) } recipient={recipient} allowDictateNextSigner={ nextRecipient && documentMeta?.allowDictateNextSigner } defaultNextSigner={ nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined } />
), ) .with({ isExpanded: true }, () => ( )) .otherwise(() => ( ))}

{match(recipient.role) .with(RecipientRole.VIEWER, () => ( Please mark as viewed to complete. )) .with(RecipientRole.SIGNER, () => ( Please review the document before signing. )) .with(RecipientRole.APPROVER, () => ( Please review the document before approving. )) .with(RecipientRole.ASSISTANT, () => ( Complete the fields for the following signers. )) .otherwise(() => null)}


{recipient.role !== RecipientRole.ASSISTANT && ( )} {fields .filter( (field) => recipient.role !== RecipientRole.ASSISTANT || field.recipientId === selectedSigner?.id, ) .map((field) => match(field.type) .with(FieldType.SIGNATURE, () => ( )) .with(FieldType.INITIALS, () => ( )) .with(FieldType.NAME, () => ( )) .with(FieldType.DATE, () => ( )) .with(FieldType.EMAIL, () => ( )) .with(FieldType.TEXT, () => { const fieldWithMeta: FieldWithSignatureAndFieldMeta = { ...field, fieldMeta: field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null, }; return ; }) .with(FieldType.NUMBER, () => { const fieldWithMeta: FieldWithSignatureAndFieldMeta = { ...field, fieldMeta: field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null, }; return ; }) .with(FieldType.RADIO, () => { const fieldWithMeta: FieldWithSignatureAndFieldMeta = { ...field, fieldMeta: field.fieldMeta ? ZRadioFieldMeta.parse(field.fieldMeta) : null, }; return ; }) .with(FieldType.CHECKBOX, () => { const fieldWithMeta: FieldWithSignatureAndFieldMeta = { ...field, fieldMeta: field.fieldMeta ? ZCheckboxFieldMeta.parse(field.fieldMeta) : null, }; return ; }) .with(FieldType.DROPDOWN, () => { const fieldWithMeta: FieldWithSignatureAndFieldMeta = { ...field, fieldMeta: field.fieldMeta ? ZDropdownFieldMeta.parse(field.fieldMeta) : null, }; return ; }) .otherwise(() => null), )}
); };