Files
documenso/apps/remix/app/components/general/document-signing/envelope-signing-provider.tsx
David Nguyen 7f09ba72f4 feat: add envelopes (#2025)
This PR is handles the changes required to support envelopes. The new
envelope editor/signing page will be hidden during release.

The core changes here is to migrate the documents and templates model to
a centralized envelopes model.

Even though Documents and Templates are removed, from the user
perspective they will still exist as we remap envelopes to documents and
templates.
2025-10-14 21:56:36 +11:00

291 lines
8.9 KiB
TypeScript

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<Recipient, 'name' | 'email' | 'signingStatus' | 'role'>;
})[];
assistantRecipients: EnvelopeForSigningResponse['envelope']['recipients'];
assistantFields: Field[];
setSelectedAssistantRecipientId: (_value: number | null) => void;
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise<void>;
};
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(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<number | null>(
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 (
<EnvelopeSigningContext.Provider
value={{
fullName,
setFullName,
email,
setEmail,
signature,
setSignature,
envelopeData,
envelope,
showPendingFieldTooltip,
setShowPendingFieldTooltip,
recipient,
recipientFieldsRemaining,
recipientFields,
nextRecipient,
otherRecipientCompletedFields,
assistantRecipients,
assistantFields,
setSelectedAssistantRecipientId,
selectedAssistantRecipient,
selectedRecipientFields,
signField,
}}
>
{children}
</EnvelopeSigningContext.Provider>
);
};
EnvelopeSigningProvider.displayName = 'EnvelopeSigningProvider';