Compare commits

...
Author SHA1 Message Date
Lucas Smith be94bddd40 fix: use legacy pdfjs build for older devices (#3410) 2026-09-29 16:13:39 +10:00
Lucas Smith 5a123be46c fix: embed signing completion and reload states (#3409)
Send completed/rejected events when reopening an actioned v1 embed,
show the completed page after signing in v2, and tidy the completed
page.
2026-09-29 15:44:25 +10:00
Lucas Smith 586b1f5cb5 v2.19.0 2026-09-29 14:37:03 +10:00
9 changed files with 114 additions and 41 deletions
@@ -120,20 +120,25 @@ Specific parts of the embed can be targeted with CSS classes for granular stylin
### Component Classes
| Class | Description |
| --------------------------------- | --------------------------------------------- |
| `.embed--Root` | Main container for the embedded experience |
| `.embed--DocumentContainer` | Container for the document and signing widget |
| `.embed--DocumentViewer` | Container for the document viewer |
| `.embed--DocumentWidget` | The signing widget container |
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget |
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
| `.embed--WaitingForTurn` | Waiting screen when it is not the user's turn |
| `.embed--DocumentCompleted` | Completion screen after signing |
| `.field--FieldRootContainer` | Base container for document fields |
| Class | Description |
| ---------------------------------------- | --------------------------------------------- |
| `.embed--Root` | Main container for the embedded experience |
| `.embed--DocumentContainer` | Container for the document and signing widget |
| `.embed--DocumentViewer` | Container for the document viewer |
| `.embed--DocumentWidget` | The signing widget container |
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget |
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
| `.embed--WaitingForTurn` | Waiting screen when it is not the user's turn |
| `.embed--DocumentCompleted` | Completion screen after signing |
| `.embed--DocumentCompletedCard` | Signature card on the completion screen |
| `.embed--DocumentCompletedTitle` | Title on the completion screen |
| `.embed--DocumentCompletedStatus` | Status line on the completion screen |
| `.embed--DocumentCompletedDescription` | Description text on the completion screen |
| `.embed--DocumentRejected` | Rejection screen after rejecting the document |
| `.field--FieldRootContainer` | Base container for document fields |
### Field Data Attributes
@@ -2,6 +2,7 @@ import signingCelebration from '@documenso/assets/images/signing-celebration.png
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { Trans } from '@lingui/react/macro';
import type { Signature } from '@prisma/client';
import { CheckCircle2Icon } from 'lucide-react';
export type EmbedDocumentCompletedPageProps = {
name?: string;
@@ -10,12 +11,8 @@ export type EmbedDocumentCompletedPageProps = {
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
return (
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<h3 className="font-semibold text-2xl text-foreground">
<Trans>Document Completed!</Trans>
</h3>
<div className="mt-8 w-full max-w-md">
<div className="embed--DocumentCompleted relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center overflow-hidden p-6">
<div className="embed--DocumentCompletedCard w-full max-w-sm md:max-w-md">
<SigningCard3D
className="mx-auto w-full"
name={name || 'Documenso'}
@@ -24,10 +21,19 @@ export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentComplet
/>
</div>
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
<Trans>
The document is now completed, please follow any instructions provided within the parent application.
</Trans>
<h2 className="embed--DocumentCompletedTitle mt-8 max-w-[35ch] text-center font-semibold text-2xl text-foreground leading-normal md:text-3xl">
<Trans>Document Completed</Trans>
</h2>
<div className="embed--DocumentCompletedStatus mt-4 flex items-center text-center text-documenso-700">
<CheckCircle2Icon className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>No further action is required</Trans>
</span>
</div>
<p className="embed--DocumentCompletedDescription mt-2.5 max-w-[50ch] text-center font-medium text-muted-foreground/60 text-sm md:text-base">
<Trans>Please follow any instructions provided within the parent application.</Trans>
</p>
</div>
);
@@ -55,6 +55,7 @@ export type EmbedSignDocumentV1ClientPageProps = {
completedFields: DocumentField[];
metadata?: DocumentMeta | null;
isCompleted?: boolean;
isRejected?: boolean;
hidePoweredBy?: boolean;
allowWhitelabelling?: boolean;
allRecipients?: RecipientWithFields[];
@@ -70,6 +71,7 @@ export const EmbedSignDocumentV1ClientPage = ({
completedFields,
metadata,
isCompleted,
isRejected,
hidePoweredBy = false,
allowWhitelabelling = false,
allRecipients = [],
@@ -83,7 +85,9 @@ export const EmbedSignDocumentV1ClientPage = ({
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [hasRejectedDocument, setHasRejectedDocument] = useState(recipient.signingStatus === SigningStatus.REJECTED);
const [hasRejectedDocument, setHasRejectedDocument] = useState(
isRejected ?? recipient.signingStatus === SigningStatus.REJECTED,
);
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
allRecipients.length > 0 ? allRecipients[0].id : null,
);
@@ -263,6 +267,44 @@ export const EmbedSignDocumentV1ClientPage = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!window.parent) {
return;
}
if (hasRejectedDocument) {
window.parent.postMessage(
{
action: 'document-rejected',
data: {
token,
documentId,
recipientId: recipient.id,
},
},
'*',
);
return;
}
if (hasCompletedDocument) {
window.parent.postMessage(
{
action: 'document-completed',
data: {
token,
documentId,
recipientId: recipient.id,
},
},
'*',
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (hasFinishedInit && hasDocumentLoaded && window.parent) {
window.parent.postMessage(
@@ -40,12 +40,18 @@ export const EmbedSignDocumentV2ClientPage = ({
const [isNameLocked, setIsNameLocked] = useState(false);
const [isEmailLocked, setIsEmailLocked] = useState(envelope.type === EnvelopeType.DOCUMENT && !!email);
// The signing provider's envelope data isn't refreshed on revalidation.
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
const [hasRejectedDocument, setHasRejectedDocument] = useState(isRejected);
const onDocumentCompleted = (data: {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
}) => {
setHasCompletedDocument(true);
if (window.parent) {
window.parent.postMessage(
{
@@ -112,6 +118,8 @@ export const EmbedSignDocumentV2ClientPage = ({
recipientId: number;
reason?: string;
}) => {
setHasRejectedDocument(true);
if (window.parent) {
window.parent.postMessage(
{
@@ -219,23 +227,26 @@ export const EmbedSignDocumentV2ClientPage = ({
}
}, [isRejected, envelope.id, recipient.id, recipient.token]);
if (isRejected) {
if (hasRejectedDocument) {
return <EmbedDocumentRejected />;
}
if (isCompleted) {
if (hasCompletedDocument) {
const completedSignature =
recipient.fields.find((field) => field.signature)?.signature ?? recipientSignature ?? null;
return (
<EmbedDocumentCompleted
name={fullName}
signature={
recipientSignature
completedSignature
? {
id: 1,
fieldId: 1,
recipientId: recipient.id,
created: new Date(),
signatureImageAsBase64: recipientSignature.signatureImageAsBase64,
typedSignature: recipientSignature.typedSignature,
signatureImageAsBase64: completedSignature.signatureImageAsBase64,
typedSignature: completedSignature.typedSignature,
}
: undefined
}
@@ -5,8 +5,8 @@ import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { Trans, useLingui } from '@lingui/react/macro';
import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?url';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.mjs?url';
import type React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
@@ -19,7 +19,7 @@ import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { isRecipientExpired } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { RecipientRole } from '@prisma/client';
import { RecipientRole, SigningStatus } from '@prisma/client';
import { data } from 'react-router';
import { match } from 'ts-pattern';
@@ -80,7 +80,11 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
);
}
if (isRecipientExpired(recipient)) {
const isCompleted = recipient.signingStatus === SigningStatus.SIGNED || isDocumentCompleted(document.status);
const isRejected = recipient.signingStatus === SigningStatus.REJECTED;
const hasRecipientActioned = isCompleted || isRejected;
if (!hasRecipientActioned && isRecipientExpired(recipient)) {
throw data(
{
type: 'embed-recipient-expired',
@@ -115,7 +119,7 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
);
}
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
const isRecipientsTurnToSign = hasRecipientActioned || (await getIsRecipientsTurnToSign({ token }));
if (!isRecipientsTurnToSign) {
throw data(
@@ -173,6 +177,8 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
recipient,
fields,
completedFields,
isCompleted,
isRejected,
hidePoweredBy,
allowEmbedSigningWhitelabel,
};
@@ -392,6 +398,8 @@ const EmbedSignDocumentPageV1 = ({ data }: { data: Awaited<ReturnType<typeof han
recipient,
fields,
completedFields,
isCompleted,
isRejected,
hidePoweredBy,
allowEmbedSigningWhitelabel,
} = data;
@@ -415,7 +423,8 @@ const EmbedSignDocumentPageV1 = ({ data }: { data: Awaited<ReturnType<typeof han
fields={fields}
completedFields={completedFields}
metadata={document.documentMeta}
isCompleted={isDocumentCompleted(document.status)}
isCompleted={isCompleted}
isRejected={isRejected}
hidePoweredBy={hidePoweredBy}
allowWhitelabelling={allowEmbedSigningWhitelabel}
allRecipients={allRecipients}
+1 -1
View File
@@ -106,5 +106,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.18.0"
"version": "2.19.0"
}
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.18.0",
"version": "2.19.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.18.0",
"version": "2.19.0",
"hasInstallScript": true,
"workspaces": [
"apps/*",
@@ -193,7 +193,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.18.0",
"version": "2.19.0",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.3",
"@documenso/api": "*",
+1 -1
View File
@@ -5,7 +5,7 @@
"apps/*",
"packages/*"
],
"version": "2.18.0",
"version": "2.19.0",
"scripts": {
"postinstall": "patch-package",
"build": "turbo run build",