import { createContext, useContext, useMemo, useState } from 'react'; import { type Field, FieldType, type Recipient, RecipientRole, SigningStatus, } from '@prisma/client'; import { isBase64Image } from '@documenso/lib/constants/signatures'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { trpc } from '@documenso/trpc/react'; import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types'; export type EnvelopeSigningContextValue = { fullName: string; setFullName: (_value: string) => void; email: string; setEmail: (_value: string) => void; signature: string | null; setSignature: (_value: string | null) => void; showPendingFieldTooltip: boolean; setShowPendingFieldTooltip: (_value: boolean) => void; envelopeData: EnvelopeForSigningResponse; envelope: EnvelopeForSigningResponse['envelope']; recipient: EnvelopeForSigningResponse['recipient']; recipientFieldsRemaining: Field[]; recipientFields: Field[]; selectedRecipientFields: Field[]; nextRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null; otherRecipientCompletedFields: (Field & { recipient: Pick; })[]; assistantRecipients: EnvelopeForSigningResponse['envelope']['recipients']; assistantFields: Field[]; setSelectedAssistantRecipientId: (_value: number | null) => void; selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null; signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise; }; const EnvelopeSigningContext = createContext(null); export const useEnvelopeSigningContext = () => { return useContext(EnvelopeSigningContext); }; export const useRequiredEnvelopeSigningContext = () => { const context = useEnvelopeSigningContext(); if (!context) { throw new Error('Signing context is required'); } return context; }; export interface EnvelopeSigningProviderProps { fullName?: string | null; email?: string | null; signature?: string | null; envelopeData: EnvelopeForSigningResponse; children: React.ReactNode; } export const EnvelopeSigningProvider = ({ fullName: initialFullName, email: initialEmail, signature: initialSignature, envelopeData: initialEnvelopeData, children, }: EnvelopeSigningProviderProps) => { const [envelopeData, setEnvelopeData] = useState(initialEnvelopeData); const { envelope, recipient } = envelopeData; const [fullName, setFullName] = useState(initialFullName || ''); const [email, setEmail] = useState(initialEmail || ''); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const { mutateAsync: completeDocument, isPending, isSuccess, } = trpc.recipient.completeDocumentWithToken.useMutation(); const { mutateAsync: signEnvelopeField } = trpc.envelope.field.sign.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, onSuccess: (data) => { console.log('signEnvelopeField', data); const newRecipientFields = envelopeData.recipient.fields.map((field) => field.id === data.signedField.id ? data.signedField : field, ); setEnvelopeData((prev) => ({ ...prev, recipient: { ...prev.recipient, fields: newRecipientFields, }, })); }, }); // Ensure the user signature doesn't show up if it's not allowed. const [signature, setSignature] = useState( (() => { const sig = initialSignature || ''; const isBase64 = isBase64Image(sig); if ( !sig && (envelope.documentMeta.uploadSignatureEnabled || envelope.documentMeta.drawSignatureEnabled) && envelopeData.recipientSignature?.signatureImageAsBase64 ) { return envelopeData.recipientSignature.signatureImageAsBase64; } if ( !sig && envelope.documentMeta.typedSignatureEnabled && envelopeData.recipientSignature?.typedSignature ) { return envelopeData.recipientSignature.typedSignature; } if ( isBase64 && (envelope.documentMeta.uploadSignatureEnabled || envelope.documentMeta.drawSignatureEnabled) ) { return sig; } if (!isBase64 && envelope.documentMeta.typedSignatureEnabled) { return sig; } return null; })(), ); /** * Assistant recipients are those that have a signing order after the assistant. */ const assistantRecipients = recipient.role === RecipientRole.ASSISTANT ? envelope.recipients.filter((r) => (r.signingOrder ?? 0) > (recipient.signingOrder ?? 0)) : []; /** * Assistant fields are those fulfill all of the following: * - From recipients that have not signed * - After the assistant signing order * - Are not signature fields */ const assistantFields = recipient.role === RecipientRole.ASSISTANT ? assistantRecipients .filter((r) => r.signingStatus !== SigningStatus.SIGNED) .map((r) => r.fields.filter((field) => field.type !== FieldType.SIGNATURE)) .flat() : []; /** * The recipient that the assistant has currently selected to sign on behalf of. */ const [selectedAssistantRecipientId, setSelectedAssistantRecipientId] = useState( assistantRecipients[0]?.id || null, ); const selectedAssistantRecipient = useMemo(() => { return envelope.recipients.find((r) => r.id === selectedAssistantRecipientId) || null; }, [envelope.recipients, selectedAssistantRecipientId]); /** * The fields that are still required to be signed by the current recipient. */ const recipientFieldsRemaining = useMemo(() => { return envelopeData.recipient.fields.filter((field) => isFieldUnsignedAndRequired(field)); }, [envelopeData.recipient.fields]); /** * All the fields for the current recipient. */ const recipientFields = useMemo(() => { return envelopeData.recipient.fields; }, [envelopeData.recipient.fields]); const selectedRecipientFields = useMemo(() => { return recipientFields.filter((field) => field.recipientId === selectedAssistantRecipient?.id); }, [recipientFields, selectedAssistantRecipient]); /** * Fields that have been completed by other recipients. */ const otherRecipientCompletedFields = envelope.recipients .filter(({ signingStatus }) => signingStatus === SigningStatus.SIGNED) .flatMap((recipient) => recipient.fields.map((field) => ({ ...field, recipient: { name: recipient.name, email: recipient.email, signingStatus: recipient.signingStatus, role: recipient.role, }, })), ) .filter((field) => field.inserted); const nextRecipient = useMemo(() => { if ( !envelope.documentMeta.signingOrder || envelope.documentMeta.signingOrder !== 'SEQUENTIAL' ) { return null; } const sortedRecipients = envelope.recipients.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] : null; }, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]); const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => { console.log('insertField', fieldId, fieldValue); await signEnvelopeField({ token: envelopeData.recipient.token, fieldId, fieldValue, authOptions: undefined, }); }; return ( {children} ); }; EnvelopeSigningProvider.displayName = 'EnvelopeSigningProvider';