import { useEffect, useId, useLayoutEffect, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { DocumentMeta, TemplateMeta } from '@prisma/client'; import { type DocumentData, type Field, FieldType, RecipientRole, SigningStatus, } 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 type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-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 { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group'; 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 { DocumentSigningRecipientProvider } from '../general/document-signing/document-signing-recipient-provider'; import { DocumentSigningRejectDialog } from '../general/document-signing/document-signing-reject-dialog'; import { EmbedClientLoading } from './embed-client-loading'; import { EmbedDocumentCompleted } from './embed-document-completed'; import { EmbedDocumentFields } from './embed-document-fields'; import { EmbedDocumentRejected } from './embed-document-rejected'; export type EmbedSignDocumentClientPageProps = { token: string; documentId: number; documentData: DocumentData; recipient: RecipientWithFields; fields: Field[]; metadata?: DocumentMeta | TemplateMeta | null; isCompleted?: boolean; hidePoweredBy?: boolean; allowWhitelabelling?: boolean; allRecipients?: RecipientWithFields[]; }; export const EmbedSignDocumentClientPage = ({ token, documentId, documentData, recipient, fields, metadata, isCompleted, hidePoweredBy = false, allowWhitelabelling = false, allRecipients = [], }: 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 [hasRejectedDocument, setHasRejectedDocument] = useState( recipient.signingStatus === SigningStatus.REJECTED, ); const [selectedSignerId, setSelectedSignerId] = useState( allRecipients.length > 0 ? allRecipients[0].id : null, ); const [isExpanded, setIsExpanded] = useState(false); const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); const isAssistantMode = recipient.role === RecipientRole.ASSISTANT; const [throttledOnCompleteClick, isThrottled] = useThrottleFn(() => void onCompleteClick(), 500); const [pendingFields, _completedFields] = [ fields.filter((field) => field.recipientId === recipient.id && !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 assistantSignersId = useId(); 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', }); } }; const onDocumentRejected = (reason: string) => { if (window.parent) { window.parent.postMessage( { action: 'document-rejected', data: { token, documentId, recipientId: recipient.id, reason, }, }, '*', ); } setHasRejectedDocument(true); }; 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); setAllowDocumentRejection(!!data.allowDocumentRejection); 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 (hasRejectedDocument) { return ; } if (hasCompletedDocument) { return ( ); } return (
{(!hasFinishedInit || !hasDocumentLoaded) && } {allowDocumentRejection && (
)}
{/* Viewer */}
setHasDocumentLoaded(true)} />
{/* Widget */}
{/* Header */}

{isAssistantMode ? ( Assist with signing ) : ( Sign document )}

{isAssistantMode ? ( Help complete the document for other signers. ) : ( Sign the document to complete the process. )}


{/* Form */}
{isAssistantMode && (
setSelectedSignerId(Number(value))} > {allRecipients .filter((r) => r.fields.length > 0) .map((r) => (

{r.email}

{r.fields.length} {r.fields.length === 1 ? 'field' : 'fields'}
))}
)} {!isAssistantMode && ( <>
!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
)}
); };