import type { DocumentDataVersion } from '@documenso/lib/types/document'; import { getDocumentDataUrl } from '@documenso/lib/utils/envelope-download'; import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; import { getRecipientColor } from '@documenso/ui/lib/recipient-colors'; import type { Field, Recipient } from '@prisma/client'; import type React from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { TEnvelope } from '../../types/envelope'; import type { FieldRenderMode } from '../../universal/field-renderer/field-renderer'; /** * The signature data for an inserted signature field. * * Loaded separately from the envelope to avoid bloating the envelope.get response * with potentially large base64 image payloads. */ export type EnvelopeRenderFieldSignature = { fieldId: number; signatureImageAsBase64: string | null; typedSignature: string | null; }; export type PageRenderData = { scale: number; pageIndex: number; pageNumber: number; pageWidth: number; pageHeight: number; imageLoadingState: ImageLoadingState; }; export type ImageLoadingState = 'loading' | 'loaded' | 'error'; type EnvelopeRenderOverrideSettings = { mode?: FieldRenderMode; showRecipientTooltip?: boolean; showRecipientSigningStatus?: boolean; }; type EnvelopeRenderItem = { id: string; title: string; order: number; envelopeId: string; /** * The PDF data to render. * * If it's a string we assume it's a URL to the PDF file. */ data: Uint8Array | string; }; type EnvelopeRenderProviderValue = { version: DocumentDataVersion; envelopeItems: EnvelopeRenderItem[]; envelopeStatus: TEnvelope['status']; envelopeType: TEnvelope['type']; currentEnvelopeItem: EnvelopeRenderItem | null; setCurrentEnvelopeItem: (envelopeItemId: string) => void; fields: Field[]; signatures: EnvelopeRenderFieldSignature[]; recipients: Pick[]; getRecipientColorKey: (recipientId: number) => TRecipientColor; renderError: boolean; setRenderError: (renderError: boolean) => void; overrideSettings?: EnvelopeRenderOverrideSettings; }; interface EnvelopeRenderProviderProps { children: React.ReactNode; /** * The envelope item version to render. */ version: DocumentDataVersion; envelope: Pick; /** * The envelope items to render. * * If data is optional then we build the URL based of the IDs. */ envelopeItems: { id: string; title: string; order: number; envelopeId: string; documentDataId: string; data?: Uint8Array | string; }[]; /** * Optional fields which are passed down to renderers for custom rendering needs. * * Only pass if the CustomRenderer you are passing in wants fields. */ fields?: Field[]; /** * Optional inserted signature data for signature fields. * * Fetched separately from the envelope to keep the envelope response lean. * If a signature field has no entry here, the renderer will fall back to * showing the field type placeholder. */ signatures?: EnvelopeRenderFieldSignature[]; /** * Optional recipient used to determine the color of the fields and hover * previews. * * Only required for generic page renderers. */ recipients?: Pick[]; /** * The token to access the envelope. * * If not provided, it will be assumed that the current user can access the document. */ token: string | undefined; /** * The presign token to access the envelope. * * If not provided, it will be assumed that the current user can access the document. */ presignToken?: string | undefined; /** * Custom override settings for generic page renderers. */ overrideSettings?: EnvelopeRenderOverrideSettings; } const EnvelopeRenderContext = createContext(null); export const useCurrentEnvelopeRender = () => { const context = useContext(EnvelopeRenderContext); if (!context) { throw new Error('useCurrentEnvelopeRender must be used within a EnvelopeRenderProvider'); } return context; }; /** * Manages fetching the data required to render an envelope and it's items. */ export const EnvelopeRenderProvider = ({ children, envelope, envelopeItems: envelopeItemsFromProps, fields, signatures, token, presignToken, recipients = [], version, overrideSettings, }: EnvelopeRenderProviderProps) => { const [renderError, setRenderError] = useState(false); const envelopeItems = useMemo( () => [...envelopeItemsFromProps] .sort((a, b) => a.order - b.order) .map((item) => { const pdfUrl = getDocumentDataUrl({ envelopeId: envelope.id, envelopeItemId: item.id, documentDataId: item.documentDataId, version, token, presignToken, }); const data = item.data || pdfUrl; return { ...item, data, }; }), [envelopeItemsFromProps, envelope.id, token, version, presignToken], ); const [currentItemId, setCurrentItemId] = useState(envelopeItems[0]?.id ?? null); const currentItem = useMemo((): EnvelopeRenderItem | null => { return envelopeItems.find((item) => item.id === currentItemId) ?? null; }, [currentItemId, envelopeItems]); const setCurrentEnvelopeItem = (envelopeItemId: string) => { const foundItem = envelopeItems.find((item) => item.id === envelopeItemId); setCurrentItemId(foundItem?.id ?? null); }; // Set the selected item to the first item if none is set. useEffect(() => { if (currentItem && !envelopeItems.some((item) => item.id === currentItem.id)) { setCurrentItemId(null); } if (!currentItem && envelopeItems.length > 0) { setCurrentEnvelopeItem(envelopeItems[0].id); } }, [currentItem, envelopeItems]); const recipientIds = useMemo(() => recipients.map((recipient) => recipient.id).sort(), [recipients]); const getRecipientColorKey = useCallback( (recipientId: number) => getRecipientColor(recipientIds.findIndex((id) => id === recipientId)), [recipientIds], ); return ( {children} ); };