mirror of
https://github.com/documenso/documenso.git
synced 2026-08-15 02:53:32 +10:00
Previously attempting to complete a document which is already completed you'd get a generic error toast. Now when completing a document that you have already completed you are redirected to the completed page. Handles cases where two mutations managed to fire racing eachother.
278 lines
9.0 KiB
TypeScript
278 lines
9.0 KiB
TypeScript
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
|
import { PDF_VIEWER_CONTENT_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { FieldType } from '@prisma/client';
|
|
import { useMemo } from 'react';
|
|
import { useNavigate, useRevalidator, useSearchParams } from 'react-router';
|
|
|
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
|
|
|
import { DocumentSigningCompleteDialog } from '../document-signing/document-signing-complete-dialog';
|
|
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
|
|
|
export const EnvelopeSignerCompleteDialog = () => {
|
|
const navigate = useNavigate();
|
|
const analytics = useAnalytics();
|
|
|
|
const { t } = useLingui();
|
|
const { toast } = useToast();
|
|
const { revalidate } = useRevalidator();
|
|
|
|
const [searchParams] = useSearchParams();
|
|
|
|
const {
|
|
isDirectTemplate,
|
|
envelope,
|
|
setShowPendingFieldTooltip,
|
|
recipientFieldsRemaining,
|
|
recipient,
|
|
nextRecipient,
|
|
email,
|
|
fullName,
|
|
} = useRequiredEnvelopeSigningContext();
|
|
|
|
const { currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
|
|
|
const { onDocumentCompleted, onDocumentError } = useEmbedSigningContext() || {};
|
|
|
|
const {
|
|
mutateAsync: completeDocument,
|
|
isPending,
|
|
isSuccess,
|
|
} = trpc.recipient.completeDocumentWithToken.useMutation();
|
|
|
|
const { mutateAsync: createDocumentFromDirectTemplate } =
|
|
trpc.template.createDocumentFromDirectTemplate.useMutation();
|
|
|
|
const handleOnNextFieldClick = () => {
|
|
const nextField = recipientFieldsRemaining[0];
|
|
|
|
if (!nextField) {
|
|
setShowPendingFieldTooltip(false);
|
|
return;
|
|
}
|
|
|
|
const isEnvelopeItemSwitch = nextField.envelopeItemId !== currentEnvelopeItem?.id;
|
|
|
|
if (isEnvelopeItemSwitch) {
|
|
setCurrentEnvelopeItem(nextField.envelopeItemId);
|
|
}
|
|
|
|
setShowPendingFieldTooltip(true);
|
|
|
|
setTimeout(
|
|
() => {
|
|
const fieldTooltip = document.querySelector(`#field-tooltip`);
|
|
|
|
if (fieldTooltip) {
|
|
fieldTooltip.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
} else {
|
|
// Tooltip not in DOM (page virtualized away) — signal the PDF viewer
|
|
// to scroll to the correct page via the data attribute.
|
|
const pdfContent = document.querySelector(PDF_VIEWER_CONTENT_SELECTOR);
|
|
|
|
if (pdfContent) {
|
|
pdfContent.setAttribute('data-scroll-to-page', String(nextField.page));
|
|
}
|
|
}
|
|
},
|
|
isEnvelopeItemSwitch ? 150 : 50,
|
|
);
|
|
};
|
|
|
|
const handleOnCompleteClick = async (
|
|
nextSigner?: { name: string; email: string },
|
|
accessAuthOptions?: TRecipientAccessAuth,
|
|
recipientDetails?: { name: string; email: string },
|
|
) => {
|
|
try {
|
|
const result = await completeDocument({
|
|
token: recipient.token,
|
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
accessAuthOptions,
|
|
recipientOverride: recipientDetails,
|
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
|
});
|
|
|
|
// TSP envelopes can't be completed via the SES path; the mutation returns
|
|
// a credential-scope OAuth URL the recipient must follow to acquire a SAD
|
|
// before the sync sign mutation can run. Short-circuit here so the
|
|
// analytics / completion handlers don't run with a still-unsigned doc.
|
|
if (result.status === 'REDIRECT') {
|
|
window.location.href = result.redirectUrl;
|
|
return;
|
|
}
|
|
|
|
// The document was already completed by an earlier request (retry,
|
|
// stale tab or concurrent submission). Let the user know this click
|
|
// didn't complete the document, then continue to the completed page.
|
|
if (result.status === 'ALREADY_SIGNED') {
|
|
toast({
|
|
title: t`Document already signed`,
|
|
description: t`This document was already signed and no further action was taken.`,
|
|
});
|
|
} else {
|
|
analytics.capture('App: Recipient has completed signing', {
|
|
signerId: recipient.id,
|
|
documentId: envelope.id,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
if (onDocumentCompleted) {
|
|
onDocumentCompleted({
|
|
token: recipient.token,
|
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
recipientId: recipient.id,
|
|
envelopeId: envelope.id,
|
|
});
|
|
|
|
await revalidate();
|
|
|
|
return;
|
|
}
|
|
|
|
if (envelope.documentMeta.redirectUrl) {
|
|
window.location.href = envelope.documentMeta.redirectUrl;
|
|
} else {
|
|
window.location.href = `/sign/${recipient.token}/complete`;
|
|
}
|
|
} catch (err) {
|
|
const error = AppError.parseError(err);
|
|
|
|
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
|
|
toast({
|
|
title: t`Something went wrong`,
|
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
|
variant: 'destructive',
|
|
});
|
|
|
|
onDocumentError?.();
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Direct template completion flow.
|
|
*/
|
|
const handleDirectTemplateCompleteClick = async (
|
|
nextSigner?: { name: string; email: string },
|
|
accessAuthOptions?: TRecipientAccessAuth,
|
|
recipientDetails?: { name: string; email: string },
|
|
) => {
|
|
try {
|
|
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
|
|
|
|
if (directTemplateExternalId) {
|
|
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
|
}
|
|
|
|
if (!recipient.directToken) {
|
|
throw new Error('Recipient direct token is required');
|
|
}
|
|
|
|
const { token } = await createDocumentFromDirectTemplate({
|
|
directTemplateToken: recipient.directToken, // The direct template token is inserted into the recipient token for ease of use.
|
|
directTemplateExternalId,
|
|
directRecipientName: recipientDetails?.name || fullName,
|
|
directRecipientEmail: recipientDetails?.email || email,
|
|
templateUpdatedAt: envelope.updatedAt,
|
|
signedFieldValues: recipient.fields.map((field) => {
|
|
let value = field.customText;
|
|
let isBase64 = false;
|
|
|
|
if (field.type === FieldType.SIGNATURE && field.signature) {
|
|
value = field.signature.signatureImageAsBase64 || field.signature.typedSignature || '';
|
|
isBase64 = isBase64Image(value);
|
|
}
|
|
|
|
return {
|
|
token: '',
|
|
fieldId: field.id,
|
|
value,
|
|
isBase64,
|
|
};
|
|
}),
|
|
nextSigner,
|
|
});
|
|
|
|
const redirectUrl = envelope.documentMeta.redirectUrl;
|
|
|
|
if (onDocumentCompleted) {
|
|
await navigate({
|
|
pathname: `/embed/sign/${token}`,
|
|
search: window.location.search,
|
|
hash: window.location.hash,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
if (redirectUrl) {
|
|
window.location.href = redirectUrl;
|
|
} else {
|
|
window.location.href = `/sign/${token}/complete`;
|
|
}
|
|
} catch (err) {
|
|
console.log('err', err);
|
|
toast({
|
|
title: t`Something went wrong`,
|
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
|
variant: 'destructive',
|
|
});
|
|
|
|
onDocumentError?.();
|
|
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const recipientPayload = useMemo(() => {
|
|
if (!isDirectTemplate) {
|
|
return {
|
|
name:
|
|
recipient.name ||
|
|
fullName ||
|
|
recipient.fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
|
'',
|
|
email:
|
|
recipient.email ||
|
|
email ||
|
|
recipient.fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
|
'',
|
|
};
|
|
}
|
|
|
|
return {
|
|
name: fullName,
|
|
email: email,
|
|
};
|
|
}, [email, fullName, isDirectTemplate, recipient.email, recipient.name, recipient.fields]);
|
|
|
|
return (
|
|
<DocumentSigningCompleteDialog
|
|
isSubmitting={isPending || isSuccess}
|
|
recipientPayload={recipientPayload}
|
|
onSignatureComplete={isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick}
|
|
documentTitle={envelope.title}
|
|
fields={recipientFieldsRemaining}
|
|
fieldsValidated={handleOnNextFieldClick}
|
|
recipient={recipient}
|
|
allowDictateNextSigner={Boolean(nextRecipient && envelope.documentMeta.allowDictateNextSigner)}
|
|
disableNameInput={!isDirectTemplate && recipient.name !== ''}
|
|
defaultNextSigner={nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined}
|
|
buttonSize="sm"
|
|
position="center"
|
|
/>
|
|
);
|
|
};
|