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, TemplateMeta } from '@prisma/client'; import { type DocumentData, type Field, FieldType } from '@prisma/client'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { trpc } from '@documenso/trpc/react'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { Button } from '@documenso/ui/primitives/button'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { BrandingLogo } from '~/components/general/branding-logo'; import { injectCss } from '~/utils/css-vars'; import { ZSignDocumentEmbedDataSchema } from '../../types/embed-document-sign-schema'; 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 EmbedSignDocumentClientPageProps = { token: string; documentId: number; documentData: DocumentData; recipient: Recipient; fields: Field[]; metadata?: DocumentMeta | TemplateMeta | null; isCompleted?: boolean; hidePoweredBy?: boolean; isPlatformOrEnterprise?: boolean; }; export const EmbedSignDocumentClientPage = ({ token, documentId, documentData, recipient, fields, metadata, isCompleted, hidePoweredBy = false, isPlatformOrEnterprise = false, }: EmbedSignDocumentClientPageProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { fullName, email, signature, signatureValid, setFullName, setSignature, setSignatureValid, } = useRequiredDocumentSigningContext(); const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted); const [isExpanded, setIsExpanded] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [pendingFields, _completedFields] = [ fields.filter((field) => !field.inserted), fields.filter((field) => field.inserted), ]; const { mutateAsync: completeDocumentWithToken, isPending: isSubmitting } = trpc.recipient.completeDocumentWithToken.useMutation(); const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE); const onNextFieldClick = () => { validateFieldsInserted(fields); setShowPendingFieldTooltip(true); setIsExpanded(false); }; const onCompleteClick = async () => { try { if (hasSignatureField && !signatureValid) { return; } const valid = validateFieldsInserted(fields); if (!valid) { setShowPendingFieldTooltip(true); return; } await completeDocumentWithToken({ documentId, token, }); if (window.parent) { window.parent.postMessage( { action: 'document-completed', data: { token, documentId, recipientId: recipient.id, }, }, '*', ); } setHasCompletedDocument(true); } catch (err) { if (window.parent) { window.parent.postMessage( { action: 'document-error', data: null, }, '*', ); } 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 = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash)))); if (!isCompleted && data.name) { setFullName(data.name); } // Since a recipient can be provided a name we can lock it without requiring // a to be provided by the parent application, unlike direct templates. setIsNameLocked(!!data.lockName); if (data.darkModeDisabled) { document.documentElement.classList.add('dark-mode-disabled'); } if (isPlatformOrEnterprise) { 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

Sign the document to complete the process.


{/* Form */}
!isNameLocked && setFullName(e.target.value)} />
{ setSignature(value); }} onValidityChange={(isValid) => { setSignatureValid(isValid); }} allowTypedSignature={Boolean( metadata && 'typedSignatureEnabled' in metadata && metadata.typedSignatureEnabled, )} /> {hasSignatureField && !signatureValid && (
Signature is too small. Please provide a more complete signature.
)}
{pendingFields.length > 0 ? ( ) : ( )}
{showPendingFieldTooltip && pendingFields.length > 0 && ( Click to insert field )} {/* Fields */}
{!hidePoweredBy && (
Powered by
)}
); };