diff --git a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
index ed2597cb3..a8ac432f6 100644
--- a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
+++ b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
@@ -1,10 +1,8 @@
-import { useEffect } from 'react';
-
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
-import { CheckCircle2, Clock8, DownloadIcon } from 'lucide-react';
-import { Link, useRevalidator } from 'react-router';
+import { CheckCircle2, Clock8, DownloadIcon, Loader2 } from 'lucide-react';
+import { Link } from 'react-router';
import { match } from 'ts-pattern';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
@@ -18,7 +16,7 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env';
-import type { Document } from '@documenso/prisma/types/document-legacy-schema';
+import { trpc } from '@documenso/trpc/react';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { cn } from '@documenso/ui/lib/utils';
@@ -120,6 +118,24 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
returnToHomePath,
} = loaderData;
+ // Poll signing status every few seconds
+ const { data: signingStatusData } = trpc.envelope.signingStatus.useQuery(
+ {
+ token: recipient?.token || '',
+ },
+ {
+ refetchInterval: 3000,
+ initialData: match(document?.status)
+ .with(DocumentStatus.COMPLETED, () => ({ status: 'COMPLETED' }) as const)
+ .with(DocumentStatus.REJECTED, () => ({ status: 'REJECTED' }) as const)
+ .with(DocumentStatus.PENDING, () => ({ status: 'PENDING' }) as const)
+ .otherwise(() => ({ status: 'PENDING' }) as const),
+ },
+ );
+
+ // Use signing status from query if available, otherwise fall back to document status
+ const signingStatus = signingStatusData?.status ?? 'PENDING';
+
if (!isDocumentAccessValid) {
return ;
}
@@ -161,8 +177,8 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
{recipient.role === RecipientRole.APPROVER && Document Approved}
- {match({ status: document.status, deletedAt: document.deletedAt })
- .with({ status: DocumentStatus.COMPLETED }, () => (
+ {match({ status: signingStatus, deletedAt: document.deletedAt })
+ .with({ status: 'COMPLETED' }, () => (
@@ -170,6 +186,14 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
))
+ .with({ status: 'PROCESSING' }, () => (
+
+
+
+ Processing document
+
+
+ ))
.with({ deletedAt: null }, () => (
@@ -187,14 +211,22 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
))}
- {match({ status: document.status, deletedAt: document.deletedAt })
- .with({ status: DocumentStatus.COMPLETED }, () => (
+ {match({ status: signingStatus, deletedAt: document.deletedAt })
+ .with({ status: 'COMPLETED' }, () => (
Everyone has signed! You will receive an Email copy of the signed document.
))
+ .with({ status: 'PROCESSING' }, () => (
+
+
+ All recipients have signed. The document is being processed and you will receive
+ an Email copy shortly.
+
+
+ ))
.with({ deletedAt: null }, () => (
@@ -218,7 +250,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
className="w-full max-w-none md:flex-1"
/>
- {isDocumentCompleted(document.status) && (
+ {isDocumentCompleted(document) && (
-
-
);
}
-
-export type PollUntilDocumentCompletedProps = {
- document: Pick;
-};
-
-export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentCompletedProps) => {
- const { revalidate } = useRevalidator();
-
- useEffect(() => {
- if (isDocumentCompleted(document.status)) {
- return;
- }
-
- const interval = setInterval(() => {
- if (window.document.hasFocus()) {
- void revalidate();
- }
- }, 5000);
-
- return () => clearInterval(interval);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [document.status]);
-
- return <>>;
-};
diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts
index c4662c2ab..e11709eb2 100644
--- a/packages/trpc/server/envelope-router/router.ts
+++ b/packages/trpc/server/envelope-router/router.ts
@@ -25,6 +25,7 @@ import { redistributeEnvelopeRoute } from './redistribute-envelope';
import { setEnvelopeFieldsRoute } from './set-envelope-fields';
import { setEnvelopeRecipientsRoute } from './set-envelope-recipients';
import { signEnvelopeFieldRoute } from './sign-envelope-field';
+import { signingStatusEnvelopeRoute } from './signing-status-envelope';
import { updateEnvelopeRoute } from './update-envelope';
import { updateEnvelopeItemsRoute } from './update-envelope-items';
import { useEnvelopeRoute } from './use-envelope';
@@ -72,4 +73,5 @@ export const envelopeRouter = router({
duplicate: duplicateEnvelopeRoute,
distribute: distributeEnvelopeRoute,
redistribute: redistributeEnvelopeRoute,
+ signingStatus: signingStatusEnvelopeRoute,
});
diff --git a/packages/trpc/server/envelope-router/signing-status-envelope.ts b/packages/trpc/server/envelope-router/signing-status-envelope.ts
new file mode 100644
index 000000000..f29a622ce
--- /dev/null
+++ b/packages/trpc/server/envelope-router/signing-status-envelope.ts
@@ -0,0 +1,82 @@
+import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { prisma } from '@documenso/prisma';
+
+import { maybeAuthenticatedProcedure } from '../trpc';
+import {
+ ZSigningStatusEnvelopeRequestSchema,
+ ZSigningStatusEnvelopeResponseSchema,
+} from './signing-status-envelope.types';
+
+// Internal route - not intended for public API usage
+export const signingStatusEnvelopeRoute = maybeAuthenticatedProcedure
+ .input(ZSigningStatusEnvelopeRequestSchema)
+ .output(ZSigningStatusEnvelopeResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { token } = input;
+
+ ctx.logger.info({
+ input: {
+ token,
+ },
+ });
+
+ const envelope = await prisma.envelope.findFirst({
+ where: {
+ type: EnvelopeType.DOCUMENT,
+ recipients: {
+ some: {
+ token,
+ },
+ },
+ },
+ include: {
+ recipients: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ signingStatus: true,
+ role: true,
+ },
+ },
+ },
+ });
+
+ if (!envelope) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Envelope not found',
+ });
+ }
+
+ // Check if envelope is rejected
+ if (envelope.status === DocumentStatus.REJECTED) {
+ return {
+ status: 'REJECTED',
+ };
+ }
+
+ if (envelope.status === DocumentStatus.COMPLETED) {
+ return {
+ status: 'COMPLETED',
+ };
+ }
+
+ const isComplete =
+ envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) ||
+ envelope.recipients.every(
+ (recipient) =>
+ recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
+ );
+
+ if (isComplete) {
+ return {
+ status: 'PROCESSING',
+ };
+ }
+
+ return {
+ status: 'PENDING',
+ };
+ });
diff --git a/packages/trpc/server/envelope-router/signing-status-envelope.types.ts b/packages/trpc/server/envelope-router/signing-status-envelope.types.ts
new file mode 100644
index 000000000..ccdb7e632
--- /dev/null
+++ b/packages/trpc/server/envelope-router/signing-status-envelope.types.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+export const EnvelopeSigningStatus = z.enum(['PENDING', 'PROCESSING', 'COMPLETED', 'REJECTED']);
+
+export const ZSigningStatusEnvelopeRequestSchema = z.object({
+ token: z.string().describe('The recipient token to check the signing status for'),
+});
+
+export const ZSigningStatusEnvelopeResponseSchema = z.object({
+ status: EnvelopeSigningStatus.describe('The current signing status of the envelope'),
+});
+
+export type TSigningStatusEnvelopeRequest = z.infer;
+export type TSigningStatusEnvelopeResponse = z.infer;