import { useEffect, useLayoutEffect, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { DocumentMeta, Recipient, Signature } from '@prisma/client'; import { type DocumentData, type Field, FieldType } from '@prisma/client'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { DateTime } from 'luxon'; import { useSearchParams } from 'react-router'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; 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 { isFieldUnsignedAndRequired, isRequiredField, } from '@documenso/lib/utils/advanced-fields-helpers'; import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; 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 { BrandingLogo } from '~/components/general/branding-logo'; import { ZDirectTemplateEmbedDataSchema } from '~/types/embed-direct-template-schema'; import { injectCss } from '~/utils/css-vars'; import type { DirectTemplateLocalField } from '../general/direct-template/direct-template-signing-form'; import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover'; import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider'; import { EmbedClientLoading } from './embed-client-loading'; import { EmbedDocumentCompleted } from './embed-document-completed'; import { EmbedDocumentFields } from './embed-document-fields'; export type EmbedDirectTemplateClientPageProps = { token: string; envelopeId: string; updatedAt: Date; documentData: DocumentData; recipient: Recipient; fields: Field[]; metadata?: DocumentMeta | null; hidePoweredBy?: boolean; allowWhiteLabelling?: boolean; }; export const EmbedDirectTemplateClientPage = ({ token, envelopeId, updatedAt, documentData, recipient, fields, metadata, hidePoweredBy = false, allowWhiteLabelling = false, }: EmbedDirectTemplateClientPageProps) => { const { _ } = useLingui(); const { toast } = useToast(); const [searchParams] = useSearchParams(); const { fullName, email, signature, setFullName, setEmail, setSignature } = useRequiredDocumentSigningContext(); const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasCompletedDocument, setHasCompletedDocument] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [isEmailLocked, setIsEmailLocked] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [localFields, setLocalFields] = useState(() => fields); const [pendingFields, _completedFields] = [ localFields.filter((field) => isFieldUnsignedAndRequired(field)), localFields.filter((field) => field.inserted), ]; const highestPendingPageNumber = Math.max(...pendingFields.map((field) => field.page)); const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE); const signatureValid = !hasSignatureField || (signature && signature.trim() !== ''); const { mutateAsync: createDocumentFromDirectTemplate, isPending: isSubmitting } = trpc.template.createDocumentFromDirectTemplate.useMutation(); const onSignField = (payload: TSignFieldWithTokenMutationSchema) => { setLocalFields((fields) => fields.map((field) => { if (field.id !== payload.fieldId) { return field; } const newField: DirectTemplateLocalField = structuredClone({ ...field, customText: payload.value ?? '', inserted: true, signedValue: payload, }); if (field.type === FieldType.SIGNATURE) { newField.signature = { id: 1, created: new Date(), recipientId: 1, fieldId: 1, signatureImageAsBase64: payload.value && payload.value.startsWith('data:') ? payload.value : null, typedSignature: payload.value && !payload.value.startsWith('data:') ? payload.value : null, } satisfies Signature; } if (field.type === FieldType.DATE) { newField.customText = DateTime.now() .setZone(metadata?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE) .toFormat(metadata?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT); } return newField; }), ); if (window.parent) { window.parent.postMessage( { action: 'field-signed', data: null, }, '*', ); } setShowPendingFieldTooltip(false); }; const onUnsignField = (payload: TRemovedSignedFieldWithTokenMutationSchema) => { setLocalFields((fields) => fields.map((field) => { if (field.id !== payload.fieldId) { return field; } return structuredClone({ ...field, customText: '', inserted: false, signedValue: undefined, signature: undefined, }); }), ); if (window.parent) { window.parent.postMessage( { action: 'field-unsigned', data: null, }, '*', ); } setShowPendingFieldTooltip(false); }; const onNextFieldClick = () => { validateFieldsInserted(pendingFields); setShowPendingFieldTooltip(true); setIsExpanded(false); }; const onCompleteClick = async () => { try { const valid = validateFieldsInserted(pendingFields); if (!valid) { setShowPendingFieldTooltip(true); return; } let directTemplateExternalId = searchParams?.get('externalId') || undefined; if (directTemplateExternalId) { directTemplateExternalId = decodeURIComponent(directTemplateExternalId); } const { documentId, token: documentToken, recipientId, } = await createDocumentFromDirectTemplate({ directTemplateToken: token, directTemplateExternalId, directRecipientName: fullName, directRecipientEmail: email, templateUpdatedAt: updatedAt, signedFieldValues: localFields .filter((field) => { return field.signedValue && (isRequiredField(field) || field.inserted); }) .map((field) => field.signedValue!), }); if (window.parent) { window.parent.postMessage( { action: 'document-completed', data: { token: documentToken, documentId, recipientId, }, }, '*', ); } setHasCompletedDocument(true); } catch (err) { if (window.parent) { window.parent.postMessage( { action: 'document-error', data: String(err), }, '*', ); } toast({ title: _(msg`Something went wrong`), description: _( msg`We were unable to submit this document at this time. Please try again later.`, ), variant: 'destructive', }); } }; useLayoutEffect(() => { const hash = window.location.hash.slice(1); try { const data = ZDirectTemplateEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash)))); if (data.email) { setEmail(data.email); setIsEmailLocked(!!data.lockEmail); } if (data.name) { setFullName(data.name); setIsNameLocked(!!data.lockName); } if (data.darkModeDisabled) { document.documentElement.classList.add('dark-mode-disabled'); } if (allowWhiteLabelling) { injectCss({ css: data.css, cssVars: data.cssVars, }); } } catch (err) { console.error(err); } setHasFinishedInit(true); // !: While the two setters are stable we still want to ensure we're avoiding // !: re-renders. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (hasFinishedInit && hasDocumentLoaded && window.parent) { window.parent.postMessage( { action: 'document-ready', data: null, }, '*', ); } }, [hasFinishedInit, hasDocumentLoaded]); if (hasCompletedDocument) { return ( ); } return (
{(!hasFinishedInit || !hasDocumentLoaded) && }
{/* Viewer */}
setHasDocumentLoaded(true)} />
{/* Widget */}
{/* Header */}

Sign document

{isExpanded ? ( ) : pendingFields.length > 0 ? ( ) : ( )}

Sign the document to complete the process.


{/* Form */}
!isNameLocked && setFullName(e.target.value)} />
!isEmailLocked && setEmail(e.target.value.trim())} />
{hasSignatureField && (
setSignature(v ?? '')} typedSignatureEnabled={metadata?.typedSignatureEnabled} uploadSignatureEnabled={metadata?.uploadSignatureEnabled} drawSignatureEnabled={metadata?.drawSignatureEnabled} />
)}
{pendingFields.length > 0 ? ( ) : ( )}
{showPendingFieldTooltip && pendingFields.length > 0 && ( Click to insert field )} {/* Fields */}
{!hidePoweredBy && (
Powered by
)}
); };