mirror of
https://github.com/documenso/documenso.git
synced 2026-08-24 15:22:26 +10:00
fix: surface actionable errors when completing documents (#3229)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
|
||||||
@@ -32,6 +33,7 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
|||||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||||
import { injectCss } from '~/utils/css-vars';
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
||||||
|
|
||||||
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
|
import { DocumentSigningAttachmentsPopover } from '../general/document-signing/document-signing-attachments-popover';
|
||||||
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
import { useRequiredDocumentSigningContext } from '../general/document-signing/document-signing-provider';
|
||||||
@@ -162,9 +164,12 @@ export const EmbedSignDocumentV1ClientPage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(toastMessage.title),
|
||||||
description: _(msg`We were unable to submit this document at this time. Please try again later.`),
|
description: _(toastMessage.description),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useState } from 'react';
|
|||||||
import { match, P } from 'ts-pattern';
|
import { match, P } from 'ts-pattern';
|
||||||
|
|
||||||
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
import PDFViewerLazy from '~/components/general/pdf-viewer/pdf-viewer-lazy';
|
||||||
|
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
||||||
|
|
||||||
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
||||||
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
||||||
@@ -141,9 +142,12 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
onDocumentError?.();
|
onDocumentError?.();
|
||||||
|
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(toastMessage.title),
|
||||||
description: _(msg`Failed to complete the document. Please try again.`),
|
description: _(toastMessage.description),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+17
-1
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import type { Field, Recipient } from '@prisma/client';
|
import type { Field, Recipient } from '@prisma/client';
|
||||||
@@ -27,6 +28,8 @@ import { useEmbedSigningContext } from '~/components/embed/embed-signing-context
|
|||||||
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
||||||
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
|
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
||||||
|
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentSigningCompleteDialogProps = {
|
export type DocumentSigningCompleteDialogProps = {
|
||||||
@@ -85,7 +88,8 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
position,
|
position,
|
||||||
disableNameInput = false,
|
disableNameInput = false,
|
||||||
}: DocumentSigningCompleteDialogProps) => {
|
}: DocumentSigningCompleteDialogProps) => {
|
||||||
const { t } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
|
||||||
@@ -174,6 +178,18 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This dialog owns the completion error toast for every signing surface
|
||||||
|
// so the user gets a specific, actionable message. Callers should run
|
||||||
|
// their own side effects (e.g. embeds posting document-error) and
|
||||||
|
// rethrow rather than toasting themselves.
|
||||||
|
const toastMessage = getSigningCompletionErrorMessage(err.code);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: i18n._(toastMessage.title),
|
||||||
|
description: i18n._(toastMessage.description),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
@@ -19,6 +20,8 @@ import { useId, useMemo, useState } from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { getSigningCompletionErrorMessage } from '~/utils/toast-error-messages';
|
||||||
|
|
||||||
import { AssistantConfirmationDialog, type NextSigner } from '../../dialogs/assistant-confirmation-dialog';
|
import { AssistantConfirmationDialog, type NextSigner } from '../../dialogs/assistant-confirmation-dialog';
|
||||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||||
@@ -100,9 +103,12 @@ export const DocumentSigningForm = ({
|
|||||||
try {
|
try {
|
||||||
await completeDocument({ nextSigner });
|
await completeDocument({ nextSigner });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
const toastMessage = getSigningCompletionErrorMessage(error.code);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Error`),
|
title: _(toastMessage.title),
|
||||||
description: _(msg`An error occurred while completing the document. Please try again.`),
|
description: _(toastMessage.description),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-11
@@ -148,15 +148,11 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
|
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?.();
|
onDocumentError?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rethrow so DocumentSigningCompleteDialog can handle 2FA retries and
|
||||||
|
// toast a specific completion error message.
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -224,14 +220,11 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('err', 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?.();
|
onDocumentError?.();
|
||||||
|
|
||||||
|
// Rethrow so DocumentSigningCompleteDialog can toast a specific
|
||||||
|
// completion error message.
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,51 @@ export const getDirectTemplateErrorMessage = (code: string): ToastMessageDescrip
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast messages for errors thrown while a recipient attempts to complete
|
||||||
|
* (sign) a document, so the user knows whether retrying can help and what to
|
||||||
|
* do next.
|
||||||
|
*/
|
||||||
|
export const getSigningCompletionErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||||
|
return match(code)
|
||||||
|
.with(AppErrorCode.NOT_FOUND, () => ({
|
||||||
|
title: msg`Document no longer available`,
|
||||||
|
description: msg`This document can no longer be signed. It may have been removed by the sender, or your signing access may have been revoked. Please contact the sender for a new signing link.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS, () => ({
|
||||||
|
title: msg`Some fields were not saved`,
|
||||||
|
description: msg`One or more of your required fields have not been saved. Please refresh the page, complete any empty required fields, and try again.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.RECIPIENT_OUT_OF_TURN, () => ({
|
||||||
|
title: msg`It's not your turn to sign yet`,
|
||||||
|
description: msg`This document is signed in a set order and other recipients must sign before you. You will receive an email when it is your turn.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.RECIPIENT_EXPIRED, () => ({
|
||||||
|
title: msg`Signing link expired`,
|
||||||
|
description: msg`Your signing link has expired. Please contact the sender to request a new one.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.ENVELOPE_COMPLETED, () => ({
|
||||||
|
title: msg`Document already completed`,
|
||||||
|
description: msg`This document has already been completed and no further signatures can be added.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.ENVELOPE_REJECTED, () => ({
|
||||||
|
title: msg`Document rejected`,
|
||||||
|
description: msg`This document has been rejected by a recipient and can no longer be signed.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.ENVELOPE_CANCELLED, () => ({
|
||||||
|
title: msg`Document cancelled`,
|
||||||
|
description: msg`This document has been cancelled by the sender and can no longer be signed. Please contact the sender if you believe this is a mistake.`,
|
||||||
|
}))
|
||||||
|
.with(AppErrorCode.ENVELOPE_DRAFT, () => ({
|
||||||
|
title: msg`Document not ready`,
|
||||||
|
description: msg`This document has not been sent for signing yet. Please wait for the sender to send it before signing.`,
|
||||||
|
}))
|
||||||
|
.otherwise(() => ({
|
||||||
|
title: msg`Something went wrong`,
|
||||||
|
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
export const getUploadErrorMessage = (code: string): ToastMessageDescriptor => {
|
export const getUploadErrorMessage = (code: string): ToastMessageDescriptor => {
|
||||||
return match(code)
|
return match(code)
|
||||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ export enum AppErrorCode {
|
|||||||
*/
|
*/
|
||||||
RECIPIENT_ALREADY_SIGNED = 'RECIPIENT_ALREADY_SIGNED',
|
RECIPIENT_ALREADY_SIGNED = 'RECIPIENT_ALREADY_SIGNED',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A completion request was made for a recipient that still has required
|
||||||
|
* fields which have not been inserted. Usually indicates the client's field
|
||||||
|
* state is out of sync with the server (e.g. a field insert failed to
|
||||||
|
* persist before submission).
|
||||||
|
*/
|
||||||
|
RECIPIENT_HAS_UNSIGNED_FIELDS = 'RECIPIENT_HAS_UNSIGNED_FIELDS',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A completion request was made by a recipient in a sequential signing flow
|
||||||
|
* before the preceding recipients have signed.
|
||||||
|
*/
|
||||||
|
RECIPIENT_OUT_OF_TURN = 'RECIPIENT_OUT_OF_TURN',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A signer recipient does not have a signature field assigned. Thrown when
|
* A signer recipient does not have a signature field assigned. Thrown when
|
||||||
* distributing an envelope or using a direct template where at least one
|
* distributing an envelope or using a direct template where at least one
|
||||||
@@ -99,6 +113,8 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
|||||||
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
|
[AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
|
[AppErrorCode.RECIPIENT_OUT_OF_TURN]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
||||||
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
||||||
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||||
@@ -307,6 +323,8 @@ export class AppError extends Error {
|
|||||||
AppErrorCode.ENVELOPE_LEGACY,
|
AppErrorCode.ENVELOPE_LEGACY,
|
||||||
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
||||||
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
||||||
|
AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS,
|
||||||
|
AppErrorCode.RECIPIENT_OUT_OF_TURN,
|
||||||
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
||||||
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
||||||
AppErrorCode.CSC_CERT_INVALID,
|
AppErrorCode.CSC_CERT_INVALID,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
nextSigner,
|
nextSigner,
|
||||||
recipientOverride,
|
recipientOverride,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
const envelope = await prisma.envelope.findFirst({
|
||||||
where: {
|
where: {
|
||||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
||||||
recipients: {
|
recipients: {
|
||||||
@@ -78,10 +78,23 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The most common cause is a stale signing page: the document was deleted,
|
||||||
|
// or the recipient was removed, after the link was opened. Surface a
|
||||||
|
// NOT_FOUND instead of leaking a Prisma P2025 as a 500.
|
||||||
|
if (!envelope) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Document not found for the provided signing token',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||||
|
|
||||||
if (envelope.recipients.length === 0) {
|
if (envelope.recipients.length === 0) {
|
||||||
throw new Error(`Document ${envelope.id} has no recipient with token ${token}`);
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: `Document ${envelope.id} has no recipient with the provided token`,
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [recipient] = envelope.recipients;
|
const [recipient] = envelope.recipients;
|
||||||
@@ -98,7 +111,19 @@ export const completeDocumentWithToken = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (envelope.status !== DocumentStatus.PENDING) {
|
if (envelope.status !== DocumentStatus.PENDING) {
|
||||||
throw new Error(`Document ${envelope.id} must be pending`);
|
const envelopeStatusErrorCode: Record<DocumentStatus, AppErrorCode> = {
|
||||||
|
[DocumentStatus.DRAFT]: AppErrorCode.ENVELOPE_DRAFT,
|
||||||
|
[DocumentStatus.COMPLETED]: AppErrorCode.ENVELOPE_COMPLETED,
|
||||||
|
[DocumentStatus.REJECTED]: AppErrorCode.ENVELOPE_REJECTED,
|
||||||
|
[DocumentStatus.CANCELLED]: AppErrorCode.ENVELOPE_CANCELLED,
|
||||||
|
// Unreachable: guarded by the status check above.
|
||||||
|
[DocumentStatus.PENDING]: AppErrorCode.INVALID_REQUEST,
|
||||||
|
};
|
||||||
|
|
||||||
|
throw new AppError(envelopeStatusErrorCode[envelope.status], {
|
||||||
|
message: `Document ${envelope.id} must be pending to be completed, found ${envelope.status}`,
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
assertRecipientNotExpired(recipient);
|
assertRecipientNotExpired(recipient);
|
||||||
@@ -116,7 +141,10 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isRecipientsTurn) {
|
if (!isRecipientsTurn) {
|
||||||
throw new Error(`Recipient ${recipient.id} attempted to complete the document before it was their turn`);
|
throw new AppError(AppErrorCode.RECIPIENT_OUT_OF_TURN, {
|
||||||
|
message: `Recipient ${recipient.id} attempted to complete the document before it was their turn`,
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +307,10 @@ export const completeDocumentWithToken = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fieldsContainUnsignedRequiredField(fields)) {
|
if (fieldsContainUnsignedRequiredField(fields)) {
|
||||||
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
throw new AppError(AppErrorCode.RECIPIENT_HAS_UNSIGNED_FIELDS, {
|
||||||
|
message: `Recipient ${recipient.id} has unsigned fields`,
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ export const recipientRouter = router({
|
|||||||
// can't complete via this route — they go through the CSC sync sign
|
// can't complete via this route — they go through the CSC sync sign
|
||||||
// flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL
|
// flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL
|
||||||
// for the credential-scope OAuth round-trip.
|
// for the credential-scope OAuth round-trip.
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
const envelope = await prisma.envelope.findFirst({
|
||||||
where: {
|
where: {
|
||||||
...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT),
|
...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT),
|
||||||
recipients: { some: { token } },
|
recipients: { some: { token } },
|
||||||
@@ -611,6 +611,16 @@ export const recipientRouter = router({
|
|||||||
select: { signatureLevel: true, internalVersion: true },
|
select: { signatureLevel: true, internalVersion: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The most common cause is a stale signing page: the document was
|
||||||
|
// deleted, or the recipient was removed, after the link was opened.
|
||||||
|
// Surface a NOT_FOUND instead of leaking a Prisma P2025 as a 500.
|
||||||
|
if (!envelope) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Document not found for the provided signing token',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isTspEnvelope(envelope)) {
|
if (isTspEnvelope(envelope)) {
|
||||||
return await prepareCscRecipientSigning({
|
return await prepareCscRecipientSigning({
|
||||||
recipientToken: token,
|
recipientToken: token,
|
||||||
|
|||||||
Reference in New Issue
Block a user