mirror of
https://github.com/documenso/documenso.git
synced 2026-07-13 22:37:24 +10:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e74c2c708c | |||
| 7849ffbc2d | |||
| 0490fd78b1 | |||
| 16481bc34e |
@@ -19,6 +19,7 @@ import * as z from 'zod';
|
|||||||
|
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -129,18 +130,44 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
const distributionMethod = watch('meta.distributionMethod');
|
const distributionMethod = watch('meta.distributionMethod');
|
||||||
|
|
||||||
|
const recipientsWithIndex = useMemo(
|
||||||
|
() =>
|
||||||
|
envelope.recipients.map((recipient, index) => ({
|
||||||
|
...recipient,
|
||||||
|
index,
|
||||||
|
})),
|
||||||
|
[envelope.recipients],
|
||||||
|
);
|
||||||
|
|
||||||
const recipientsMissingSignatureFields = useMemo(
|
const recipientsMissingSignatureFields = useMemo(
|
||||||
() =>
|
() =>
|
||||||
envelope.recipients.filter(
|
recipientsWithIndex.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.SIGNER &&
|
recipient.role === RecipientRole.SIGNER &&
|
||||||
!envelope.fields.some(
|
!envelope.fields.some(
|
||||||
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
(field) => field.type === FieldType.SIGNATURE && field.recipientId === recipient.id,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
[envelope.recipients, envelope.fields],
|
[recipientsWithIndex, envelope.fields],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of recipients who must have an email due to having auth enabled.
|
||||||
|
*/
|
||||||
|
const recipientsMissingRequiredEmail = useMemo(() => {
|
||||||
|
return recipientsWithIndex.filter((recipient) => {
|
||||||
|
const auth = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) &&
|
||||||
|
!recipient.email
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [recipientsWithIndex, envelope.authOptions]);
|
||||||
|
|
||||||
const invalidEnvelopeCode = useMemo(() => {
|
const invalidEnvelopeCode = useMemo(() => {
|
||||||
if (recipientsMissingSignatureFields.length > 0) {
|
if (recipientsMissingSignatureFields.length > 0) {
|
||||||
return 'MISSING_SIGNATURES';
|
return 'MISSING_SIGNATURES';
|
||||||
@@ -150,8 +177,12 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
return 'MISSING_RECIPIENTS';
|
return 'MISSING_RECIPIENTS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipientsMissingRequiredEmail.length > 0) {
|
||||||
|
return 'MISSING_REQUIRED_EMAIL';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [envelope.recipients, envelope.fields, recipientsMissingSignatureFields]);
|
}, [envelope.recipients, recipientsMissingRequiredEmail, recipientsMissingSignatureFields]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
const onFormSubmit = async ({ meta }: TEnvelopeDistributeFormSchema) => {
|
||||||
try {
|
try {
|
||||||
@@ -444,7 +475,22 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
<ul className="ml-2 mt-1 list-inside list-disc">
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
{recipientsMissingSignatureFields.map((recipient) => (
|
{recipientsMissingSignatureFields.map((recipient) => (
|
||||||
<li key={recipient.id}>{recipient.email}</li>
|
<li key={recipient.id}>
|
||||||
|
{recipient.email || recipient.name || `Recipient ${recipient.index + 1}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
))
|
||||||
|
.with('MISSING_REQUIRED_EMAIL', () => (
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>The following recipients require an email address:</Trans>
|
||||||
|
|
||||||
|
<ul className="ml-2 mt-1 list-inside list-disc">
|
||||||
|
{recipientsMissingRequiredEmail.map((recipient) => (
|
||||||
|
<li key={recipient.id}>
|
||||||
|
{recipient.email || recipient.name || `Recipient ${recipient.index + 1}`}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
@@ -65,7 +66,7 @@ const ZAddRecipientsForNewDocumentSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
email: z.string().email(),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
@@ -349,7 +350,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="distributeDocument"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Send document</Trans>
|
<Trans>Send document</Trans>
|
||||||
@@ -358,7 +359,7 @@ export function TemplateUseDialog({
|
|||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
The document will be immediately sent to recipients if this
|
The document will be immediately sent to recipients if this
|
||||||
@@ -378,7 +379,7 @@ export function TemplateUseDialog({
|
|||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="distributeDocument"
|
htmlFor="distributeDocument"
|
||||||
>
|
>
|
||||||
<Trans>Create as pending</Trans>
|
<Trans>Create as pending</Trans>
|
||||||
@@ -386,7 +387,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Create the document as pending and ready to sign.
|
Create the document as pending and ready to sign.
|
||||||
@@ -432,7 +433,7 @@ export function TemplateUseDialog({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
className="ml-2 flex items-center text-sm text-muted-foreground"
|
||||||
htmlFor="useCustomDocument"
|
htmlFor="useCustomDocument"
|
||||||
>
|
>
|
||||||
<Trans>Upload custom document</Trans>
|
<Trans>Upload custom document</Trans>
|
||||||
@@ -440,7 +441,7 @@ export function TemplateUseDialog({
|
|||||||
<TooltipTrigger type="button">
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
<TooltipContent className="z-[99999] max-w-md space-y-2 p-4 text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<Trans>
|
<Trans>
|
||||||
Upload a custom document to use instead of the template's default
|
Upload a custom document to use instead of the template's default
|
||||||
@@ -470,19 +471,19 @@ export function TemplateUseDialog({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className="border-border bg-card hover:bg-accent/10 flex items-center gap-4 rounded-lg border p-4 transition-colors"
|
className="flex items-center gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/10"
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg">
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||||
<FileTextIcon className="text-primary h-5 w-5" />
|
<FileTextIcon className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h4 className="text-foreground truncate text-sm font-medium">
|
<h4 className="truncate text-sm font-medium text-foreground">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||||
{field.value ? (
|
{field.value ? (
|
||||||
<div>
|
<div>
|
||||||
<Trans>
|
<Trans>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ZDocumentMetaDateFormatSchema,
|
ZDocumentMetaDateFormatSchema,
|
||||||
ZDocumentMetaLanguageSchema,
|
ZDocumentMetaLanguageSchema,
|
||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
import { DocumentDistributionMethod } from '@documenso/prisma/generated/types';
|
||||||
|
|
||||||
// Define the schema for configuration
|
// Define the schema for configuration
|
||||||
@@ -55,7 +56,7 @@ export const ZConfigureTemplateEmbedFormSchema = ZConfigureEmbedFormSchema.exten
|
|||||||
nativeId: z.number().optional(),
|
nativeId: z.number().optional(),
|
||||||
formId: z.string(),
|
formId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.union([z.string().length(0), z.string().email('Invalid email address')]),
|
email: ZRecipientEmailSchema,
|
||||||
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
role: z.enum(['SIGNER', 'CC', 'APPROVER', 'VIEWER', 'ASSISTANT']),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
disabled: z.boolean().optional(),
|
disabled: z.boolean().optional(),
|
||||||
|
|||||||
+17
-17
@@ -57,7 +57,7 @@ export type DocumentSigningCompleteDialogProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
directTemplatePayload?: {
|
recipientPayload?: {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
@@ -89,7 +89,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
recipient,
|
recipient,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
allowDictateNextSigner = false,
|
allowDictateNextSigner = false,
|
||||||
directTemplatePayload,
|
recipientPayload,
|
||||||
defaultNextSigner,
|
defaultNextSigner,
|
||||||
buttonSize = 'lg',
|
buttonSize = 'lg',
|
||||||
position,
|
position,
|
||||||
@@ -113,11 +113,11 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const directRecipientForm = useForm<TDirectRecipientFormSchema>({
|
const recipientForm = useForm<TDirectRecipientFormSchema>({
|
||||||
resolver: zodResolver(ZDirectRecipientFormSchema),
|
resolver: zodResolver(ZDirectRecipientFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: directTemplatePayload?.name ?? '',
|
name: recipientPayload?.name ?? '',
|
||||||
email: directTemplatePayload?.email ?? '',
|
email: recipientPayload?.email ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,16 +145,16 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||||
try {
|
try {
|
||||||
let directRecipient: { name: string; email: string } | undefined;
|
let recipientOverridePayload: { name: string; email: string } | undefined;
|
||||||
|
|
||||||
if (directTemplatePayload && !directTemplatePayload.email) {
|
if (recipientPayload && !recipientPayload.email) {
|
||||||
const isFormValid = await directRecipientForm.trigger();
|
const isFormValid = await recipientForm.trigger();
|
||||||
|
|
||||||
if (!isFormValid) {
|
if (!isFormValid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
directRecipient = directRecipientForm.getValues();
|
recipientOverridePayload = recipientForm.getValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if 2FA is required
|
// Check if 2FA is required
|
||||||
@@ -168,7 +168,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
? { name: data.name, email: data.email }
|
? { name: data.name, email: data.email }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
await onSignatureComplete(nextSigner, data.accessAuthOptions, directRecipient);
|
await onSignatureComplete(nextSigner, data.accessAuthOptions, recipientOverridePayload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = AppError.parseError(error);
|
const err = AppError.parseError(error);
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
<Trans>Are you sure?</Trans>
|
<Trans>Are you sure?</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
<div className="max-w-[50ch] text-muted-foreground">
|
||||||
{match(recipient.role)
|
{match(recipient.role)
|
||||||
.with(RecipientRole.VIEWER, () => (
|
.with(RecipientRole.VIEWER, () => (
|
||||||
<span className="inline-flex flex-wrap">
|
<span className="inline-flex flex-wrap">
|
||||||
@@ -250,19 +250,19 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="border-border bg-muted/50 rounded-lg border p-4 text-center">
|
<div className="rounded-lg border border-border bg-muted/50 p-4 text-center">
|
||||||
<p className="text-muted-foreground text-sm font-medium">{documentTitle}</p>
|
<p className="text-sm font-medium text-muted-foreground">{documentTitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showTwoFactorForm && (
|
{!showTwoFactorForm && (
|
||||||
<>
|
<>
|
||||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||||
{directTemplatePayload && !directTemplatePayload.email && (
|
{recipientPayload && !recipientPayload.email && (
|
||||||
<Form {...directRecipientForm}>
|
<Form {...recipientForm}>
|
||||||
<div className="mb-4 flex flex-col gap-4">
|
<div className="mb-4 flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-4 md:flex-row">
|
<div className="flex flex-col gap-4 md:flex-row">
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
@@ -284,7 +284,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={directRecipientForm.control}
|
control={recipientForm.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex-1">
|
<FormItem className="flex-1">
|
||||||
|
|||||||
+14
-32
@@ -8,7 +8,6 @@ import {
|
|||||||
type SensorAPI,
|
type SensorAPI,
|
||||||
} from '@hello-pangea/dnd';
|
} from '@hello-pangea/dnd';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
ZRecipientActionAuthTypesSchema,
|
ZRecipientActionAuthTypesSchema,
|
||||||
ZRecipientAuthOptionsSchema,
|
ZRecipientAuthOptionsSchema,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@@ -65,10 +65,7 @@ const ZEnvelopeRecipientsForm = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
formId: z.string().min(1),
|
formId: z.string().min(1),
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z
|
email: ZRecipientEmailSchema,
|
||||||
.string()
|
|
||||||
.email({ message: msg`Invalid email`.id })
|
|
||||||
.min(1),
|
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
@@ -201,12 +198,13 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
keyName: 'nativeId',
|
keyName: 'nativeId',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptySigners = useCallback(
|
const emptySignerIndex = watchedSigners.findIndex(
|
||||||
() => form.getValues('signers').filter((signer) => signer.email === ''),
|
(signer) =>
|
||||||
[form],
|
!signer.name &&
|
||||||
|
!signer.email &&
|
||||||
|
envelope.fields.filter((field) => field.recipientId === signer.id).length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const emptySignerIndex = watchedSigners.findIndex((signer) => !signer.name && !signer.email);
|
|
||||||
const isUserAlreadyARecipient = watchedSigners.some(
|
const isUserAlreadyARecipient = watchedSigners.some(
|
||||||
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
(signer) => signer.email.toLowerCase() === user?.email?.toLowerCase(),
|
||||||
);
|
);
|
||||||
@@ -460,21 +458,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formValueSigners = formValues.signers || [];
|
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse(formValues);
|
||||||
|
|
||||||
// Remove the last signer if it's empty.
|
|
||||||
const nonEmptyRecipients = formValueSigners.filter((signer, i) => {
|
|
||||||
if (i === formValueSigners.length - 1 && signer.email === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
|
|
||||||
...formValues,
|
|
||||||
signers: nonEmptyRecipients,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!validatedFormValues.success) {
|
if (!validatedFormValues.success) {
|
||||||
return;
|
return;
|
||||||
@@ -570,7 +554,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<div className="bg-accent/50 -mt-2 mb-2 space-y-4 rounded-md p-4">
|
<div className="-mt-2 mb-2 space-y-4 rounded-md bg-accent/50 p-4">
|
||||||
{organisation.organisationClaim.flags.cfr21 && (
|
{organisation.organisationClaim.flags.cfr21 && (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -618,9 +602,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={isSubmitting || hasDocumentBeenSent}
|
||||||
isSubmitting || hasDocumentBeenSent || emptySigners().length !== 0
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
@@ -634,7 +616,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="text-muted-foreground ml-1 cursor-help">
|
<span className="ml-1 cursor-help text-muted-foreground">
|
||||||
<HelpCircleIcon className="h-3.5 w-3.5" />
|
<HelpCircleIcon className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -679,7 +661,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="text-muted-foreground ml-1 cursor-help">
|
<span className="ml-1 cursor-help text-muted-foreground">
|
||||||
<HelpCircleIcon className="h-3.5 w-3.5" />
|
<HelpCircleIcon className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -732,7 +714,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
className={cn('py-1', {
|
className={cn('py-1', {
|
||||||
'bg-widget-foreground pointer-events-none rounded-md pt-2':
|
'pointer-events-none rounded-md bg-widget-foreground pt-2':
|
||||||
snapshot.isDragging,
|
snapshot.isDragging,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -806,7 +788,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showAdvancedSettings && index === 0 && (
|
{!showAdvancedSettings && index === 0 && (
|
||||||
<FormLabel required>
|
<FormLabel>
|
||||||
<Trans>Email</Trans>
|
<Trans>Email</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
}: EnvelopeRecipientSelectorProps) => {
|
}: EnvelopeRecipientSelectorProps) => {
|
||||||
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -49,7 +54,7 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
|
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
|
||||||
getRecipientColorStyles(
|
getRecipientColorStyles(
|
||||||
Math.max(
|
Math.max(
|
||||||
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
recipients.findIndex((r) => r.id === selectedRecipient?.id),
|
||||||
@@ -59,16 +64,12 @@ export const EnvelopeRecipientSelector = ({
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedRecipient?.email && (
|
{selectedRecipient && (
|
||||||
<span className="flex-1 truncate text-left">
|
<span className="flex-1 truncate text-left">
|
||||||
{selectedRecipient?.name} ({selectedRecipient?.email})
|
{getRecipientLabel(selectedRecipient)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedRecipient?.email && (
|
|
||||||
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -154,6 +155,11 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
[fields, recipients],
|
[fields, recipients],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getRecipientLabel = useCallback(
|
||||||
|
(recipient: Recipient) => extractRecipientLabel(recipient, recipients),
|
||||||
|
[recipients],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Command
|
<Command
|
||||||
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
value={selectedRecipient ? selectedRecipient.id.toString() : undefined}
|
||||||
@@ -162,21 +168,21 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
<CommandInput placeholder={placeholder} />
|
<CommandInput placeholder={placeholder} />
|
||||||
|
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
<span className="text-muted-foreground inline-block px-4">
|
<span className="inline-block px-4 text-muted-foreground">
|
||||||
<Trans>No recipient matching this description was found.</Trans>
|
<Trans>No recipient matching this description was found.</Trans>
|
||||||
</span>
|
</span>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
|
||||||
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
|
||||||
<CommandGroup key={roleIndex}>
|
<CommandGroup key={roleIndex}>
|
||||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
|
||||||
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
{t(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{roleRecipients.length === 0 && (
|
{roleRecipients.length === 0 && (
|
||||||
<div
|
<div
|
||||||
key={`${role}-empty`}
|
key={`${role}-empty`}
|
||||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
|
||||||
>
|
>
|
||||||
<Trans>No recipients with this role</Trans>
|
<Trans>No recipients with this role</Trans>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,18 +211,12 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn('text-foreground/70 truncate', {
|
className={cn('truncate text-foreground/70', {
|
||||||
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
'text-foreground/80': recipient.id === selectedRecipient?.id,
|
||||||
'opacity-50': isRecipientDisabled(recipient.id),
|
'opacity-50': isRecipientDisabled(recipient.id),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recipient.name && (
|
{getRecipientLabel(recipient)}
|
||||||
<span title={`${recipient.name} (${recipient.email})`}>
|
|
||||||
{recipient.name} ({recipient.email})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center justify-center">
|
<div className="ml-auto flex items-center justify-center">
|
||||||
@@ -234,7 +234,7 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
<Info className="z-50 ml-2 h-4 w-4" />
|
<Info className="z-50 ml-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
<TooltipContent className="max-w-xs text-muted-foreground">
|
||||||
<Trans>
|
<Trans>
|
||||||
This document has already been sent to this recipient. You can no longer
|
This document has already been sent to this recipient. You can no longer
|
||||||
edit this recipient.
|
edit this recipient.
|
||||||
@@ -250,3 +250,22 @@ export const EnvelopeRecipientSelectorCommand = ({
|
|||||||
</Command>
|
</Command>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractRecipientLabel = (recipient: Recipient, recipients: Recipient[]) => {
|
||||||
|
if (recipient.name && recipient.email) {
|
||||||
|
return `${recipient.name} (${recipient.email})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.name) {
|
||||||
|
return recipient.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.email) {
|
||||||
|
return recipient.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
|
||||||
|
const index = recipients.indexOf(recipient);
|
||||||
|
|
||||||
|
return `Recipient ${index + 1}`;
|
||||||
|
};
|
||||||
|
|||||||
+14
-3
@@ -80,12 +80,14 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const handleOnCompleteClick = async (
|
const handleOnCompleteClick = async (
|
||||||
nextSigner?: { name: string; email: string },
|
nextSigner?: { name: string; email: string },
|
||||||
accessAuthOptions?: TRecipientAccessAuth,
|
accessAuthOptions?: TRecipientAccessAuth,
|
||||||
|
recipientDetails?: { name: string; email: string },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await completeDocument({
|
await completeDocument({
|
||||||
token: recipient.token,
|
token: recipient.token,
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
|
recipientOverride: recipientDetails,
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,9 +207,18 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const directTemplatePayload = useMemo(() => {
|
const recipientPayload = useMemo(() => {
|
||||||
if (!isDirectTemplate) {
|
if (!isDirectTemplate) {
|
||||||
return;
|
return {
|
||||||
|
name:
|
||||||
|
recipient.name ||
|
||||||
|
recipient.fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
|
'',
|
||||||
|
email:
|
||||||
|
recipient.email ||
|
||||||
|
recipient.fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||||
|
'',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -219,7 +230,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
return (
|
return (
|
||||||
<DocumentSigningCompleteDialog
|
<DocumentSigningCompleteDialog
|
||||||
isSubmitting={isPending}
|
isSubmitting={isPending}
|
||||||
directTemplatePayload={directTemplatePayload}
|
recipientPayload={recipientPayload}
|
||||||
onSignatureComplete={
|
onSignatureComplete={
|
||||||
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
isDirectTemplate ? handleDirectTemplateCompleteClick : handleOnCompleteClick
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { pick } from 'remeda';
|
|||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||||
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import {
|
||||||
DocumentDistributionMethod,
|
DocumentDistributionMethod,
|
||||||
@@ -23,7 +24,9 @@ import type {
|
|||||||
TCreateEnvelopePayload,
|
TCreateEnvelopePayload,
|
||||||
TCreateEnvelopeResponse,
|
TCreateEnvelopeResponse,
|
||||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||||
|
import type { TDistributeEnvelopeRequest } from '@documenso/trpc/server/envelope-router/distribute-envelope.types';
|
||||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||||
|
import type { TUpdateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/update-envelope-recipients.types';
|
||||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||||
|
|
||||||
@@ -557,4 +560,543 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
userEmail: userA.email,
|
userEmail: userA.email,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Empty recipient tests', () => {
|
||||||
|
test('Create template envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Template with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Test Recipient',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
expect(createRecipientsRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Test Recipient');
|
||||||
|
|
||||||
|
// Get envelope items to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient with empty email
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
expect(createFieldsRes.status()).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create document envelope with empty email recipient', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Empty Email Recipient',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Document Recipient No Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
expect(recipient.email).toBe('');
|
||||||
|
expect(recipient.name).toBe('Document Recipient No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update recipient to have empty email', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Update Recipient Email Test',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Test User',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Get the envelope to get recipient ID
|
||||||
|
const getRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getRes.json();
|
||||||
|
const recipientId = envelope.recipients[0].id;
|
||||||
|
|
||||||
|
// Update recipient to have empty email
|
||||||
|
const updateRequest: TUpdateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: recipientId,
|
||||||
|
email: '',
|
||||||
|
name: 'Updated Name No Email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRes = await request.post(`${baseUrl}/envelope/recipient/update-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: updateRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateRes.ok()).toBeTruthy();
|
||||||
|
const updateResponse = await updateRes.json();
|
||||||
|
const updatedRecipient = updateResponse.data[0];
|
||||||
|
|
||||||
|
expect(updatedRecipient.email).toBe('');
|
||||||
|
expect(updatedRecipient.name).toBe('Updated Name No Email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed recipients with and without emails', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.TEMPLATE,
|
||||||
|
title: 'Mixed Recipients Test',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create multiple recipients, some with email, some without
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: userA.email,
|
||||||
|
name: 'Recipient With Email',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 1',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: userB.email,
|
||||||
|
name: 'Another With Email',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Without Email 2',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
expect(recipients.length).toBe(4);
|
||||||
|
expect(recipients[0].email).toBe(userA.email.toLowerCase());
|
||||||
|
expect(recipients[1].email).toBe('');
|
||||||
|
expect(recipients[2].email).toBe(userB.email.toLowerCase());
|
||||||
|
expect(recipients[3].email).toBe('');
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${response.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for all recipients including those without emails
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: response.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipients', async ({ request }) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document for Distribution with Empty Email',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipients with empty emails
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient One',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient Two',
|
||||||
|
role: RecipientRole.APPROVER,
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipients = recipientsResponse.data;
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create fields for recipients
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: recipients.map((recipient, index) => ({
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 0 + index,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Distribute the envelope
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
} satisfies TDistributeEnvelopeRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(distributeRes.ok()).toBeTruthy();
|
||||||
|
expect(distributeRes.status()).toBe(200);
|
||||||
|
|
||||||
|
const distributeResponse = await distributeRes.json();
|
||||||
|
expect(distributeResponse.success).toBe(true);
|
||||||
|
expect(distributeResponse.id).toBe(createResponse.id);
|
||||||
|
expect(distributeResponse.recipients).toHaveLength(2);
|
||||||
|
|
||||||
|
// Verify recipients have empty emails and signing URLs
|
||||||
|
expect(distributeResponse.recipients[0].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[0].signingUrl).toBeTruthy();
|
||||||
|
expect(distributeResponse.recipients[1].email).toBe('');
|
||||||
|
expect(distributeResponse.recipients[1].signingUrl).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Distribute envelope with empty email recipient and auth requirements fails', async ({
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const payload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Document with Auth Requirements',
|
||||||
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: 'example.pdf',
|
||||||
|
data: fs.readFileSync(path.join(__dirname, '../../../../../assets/example.pdf')),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', new File([file.data], file.name, { type: 'application/pdf' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const createRes = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRes.ok()).toBeTruthy();
|
||||||
|
const createResponse = (await createRes.json()) as TCreateEnvelopeResponse;
|
||||||
|
|
||||||
|
// Create recipient with empty email and TWO_FACTOR_AUTH action auth
|
||||||
|
const createRecipientsRequest: TCreateEnvelopeRecipientsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
email: '',
|
||||||
|
name: 'Recipient With Auth',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
accessAuth: [DocumentAccessAuth.TWO_FACTOR_AUTH],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRecipientsRes = await request.post(`${baseUrl}/envelope/recipient/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createRecipientsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createRecipientsRes.ok()).toBeTruthy();
|
||||||
|
const recipientsResponse = await createRecipientsRes.json();
|
||||||
|
const recipient = recipientsResponse.data[0];
|
||||||
|
|
||||||
|
// Get envelope to assign fields
|
||||||
|
const getEnvelopeRes = await request.get(`${baseUrl}/envelope/${createResponse.id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope: TGetEnvelopeResponse = await getEnvelopeRes.json();
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
|
||||||
|
// Create field for the recipient
|
||||||
|
const createFieldsRequest = {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 100,
|
||||||
|
positionY: 100,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFieldsRes = await request.post(`${baseUrl}/envelope/field/create-many`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: createFieldsRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createFieldsRes.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Try to distribute the envelope - should fail
|
||||||
|
const distributeRes = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: createResponse.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expect distribution to fail
|
||||||
|
expect(distributeRes.ok()).toBeFalsy();
|
||||||
|
expect(distributeRes.status()).toBe(400);
|
||||||
|
|
||||||
|
const errorResponse = await distributeRes.json();
|
||||||
|
expect(errorResponse.message).toContain('requires an email');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/cli
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -77,7 +78,8 @@ export const run = async ({
|
|||||||
const recipientsToNotify = envelope.recipients.filter(
|
const recipientsToNotify = envelope.recipients.filter(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||||
recipient.signingStatus !== SigningStatus.REJECTED,
|
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
);
|
);
|
||||||
|
|
||||||
await io.runTask('send-cancellation-emails', async () => {
|
await io.runTask('send-cancellation-emails', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
|||||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
import type { JobRunIO } from '../../client/_internal/job';
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email';
|
||||||
@@ -79,8 +80,8 @@ export const run = async ({
|
|||||||
|
|
||||||
const recipientReference = recipientName || recipientEmail;
|
const recipientReference = recipientName || recipientEmail;
|
||||||
|
|
||||||
// Don't send notification if the owner is the one who signed
|
// Don't send notification if the owner is the one who signed.
|
||||||
if (owner.email === recipientEmail) {
|
if (owner.email === recipientEmail || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client';
|
|||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
import DocumentRejectedEmail from '@documenso/email/templates/document-rejected';
|
||||||
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
import DocumentRejectionConfirmedEmail from '@documenso/email/templates/document-rejection-confirmed';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -85,36 +86,38 @@ export const run = async ({
|
|||||||
const i18n = await getI18nInstance(emailLanguage);
|
const i18n = await getI18nInstance(emailLanguage);
|
||||||
|
|
||||||
// Send confirmation email to the recipient who rejected
|
// Send confirmation email to the recipient who rejected
|
||||||
await io.runTask('send-rejection-confirmation-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
await io.runTask('send-rejection-confirmation-email', async () => {
|
||||||
recipientName: recipient.name,
|
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
|
||||||
documentName: envelope.title,
|
recipientName: recipient.name,
|
||||||
documentOwnerName: envelope.user.name || envelope.user.email,
|
documentName: envelope.title,
|
||||||
reason: recipient.rejectionReason || '',
|
documentOwnerName: envelope.user.name || envelope.user.email,
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
reason: recipient.rejectionReason || '',
|
||||||
});
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||||
|
});
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
|
||||||
renderEmailWithI18N(recipientTemplate, {
|
renderEmailWithI18N(recipientTemplate, {
|
||||||
lang: emailLanguage,
|
lang: emailLanguage,
|
||||||
branding,
|
branding,
|
||||||
plainText: true,
|
plainText: true,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Send notification email to document owner
|
// Send notification email to document owner
|
||||||
await io.runTask('send-owner-notification-email', async () => {
|
await io.runTask('send-owner-notification-email', async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
import DocumentInviteEmailTemplate from '@documenso/email/templates/document-invite';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -177,31 +178,33 @@ export const run = async ({
|
|||||||
includeSenderDetails: settings.includeSenderDetails,
|
includeSenderDetails: settings.includeSenderDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
await io.runTask('send-signing-email', async () => {
|
if (isRecipientEmailValidForSending(recipient)) {
|
||||||
const [html, text] = await Promise.all([
|
await io.runTask('send-signing-email', async () => {
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
const [html, text] = await Promise.all([
|
||||||
renderEmailWithI18N(template, {
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||||
lang: emailLanguage,
|
renderEmailWithI18N(template, {
|
||||||
branding,
|
lang: emailLanguage,
|
||||||
plainText: true,
|
branding,
|
||||||
}),
|
plainText: true,
|
||||||
]);
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
to: {
|
to: {
|
||||||
name: recipient.name,
|
name: recipient.name,
|
||||||
address: recipient.email,
|
address: recipient.email,
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
from: senderEmail,
|
||||||
replyTo: replyToEmail,
|
replyTo: replyToEmail,
|
||||||
subject: renderCustomEmailTemplate(
|
subject: renderCustomEmailTemplate(
|
||||||
documentMeta?.subject || emailSubject,
|
documentMeta?.subject || emailSubject,
|
||||||
customEmailTemplate,
|
customEmailTemplate,
|
||||||
),
|
),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
await io.runTask('update-recipient', async () => {
|
await io.runTask('update-recipient', async () => {
|
||||||
await prisma.recipient.update({
|
await prisma.recipient.update({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EnvelopeType } from '@prisma/client';
|
|||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
||||||
|
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||||
@@ -69,6 +70,12 @@ export const send2FATokenEmail = async ({ token, envelopeId }: Send2FATokenEmail
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: 'Recipient is missing email address',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
||||||
envelopeId,
|
envelopeId,
|
||||||
email: recipient.email,
|
email: recipient.email,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -64,14 +65,18 @@ export const adminSuperDeleteDocument = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).documentDeleted;
|
).documentDeleted;
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
// if the document is pending, send cancellation emails to all recipients
|
// if the document is pending, send cancellation emails to all recipients
|
||||||
if (
|
if (
|
||||||
status === DocumentStatus.PENDING &&
|
status === DocumentStatus.PENDING &&
|
||||||
envelope.recipients.length > 0 &&
|
recipientsToNotify.length > 0 &&
|
||||||
isDocumentDeletedEmailEnabled
|
isDocumentDeletedEmailEnabled
|
||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
DocumentSigningOrder,
|
DocumentSigningOrder,
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
EnvelopeType,
|
EnvelopeType,
|
||||||
|
FieldType,
|
||||||
RecipientRole,
|
RecipientRole,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
@@ -43,6 +44,14 @@ export type CompleteDocumentWithTokenOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Override the recipient information. This will only work if the recipient
|
||||||
|
* does not have a name or email set.
|
||||||
|
*/
|
||||||
|
recipientOverride?: {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeDocumentWithToken = async ({
|
export const completeDocumentWithToken = async ({
|
||||||
@@ -52,6 +61,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
|
recipientOverride,
|
||||||
}: CompleteDocumentWithTokenOptions) => {
|
}: CompleteDocumentWithTokenOptions) => {
|
||||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -116,6 +126,35 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
throw new Error(`Recipient ${recipient.id} has unsigned fields`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let recipientName = recipient.name;
|
||||||
|
let recipientEmail = recipient.email;
|
||||||
|
|
||||||
|
// Only trim the name if it's been derived.
|
||||||
|
if (!recipientName) {
|
||||||
|
recipientName = (
|
||||||
|
recipientOverride?.name ||
|
||||||
|
fields.find((field) => field.type === FieldType.NAME)?.customText ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only trim the email if it's been derived.
|
||||||
|
if (!recipient.email) {
|
||||||
|
recipientEmail = (
|
||||||
|
recipientOverride?.email ||
|
||||||
|
fields.find((field) => field.type === FieldType.EMAIL)?.customText ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipientEmail) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||||
|
message: 'Recipient email is required',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check ACCESS AUTH 2FA validation during document completion
|
// Check ACCESS AUTH 2FA validation during document completion
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
@@ -129,6 +168,12 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!recipient.email.trim()) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isValid = await isRecipientAuthorized({
|
const isValid = await isRecipientAuthorized({
|
||||||
type: 'ACCESS_2FA',
|
type: 'ACCESS_2FA',
|
||||||
documentAuthOptions: envelope.authOptions,
|
documentAuthOptions: envelope.authOptions,
|
||||||
@@ -176,9 +221,43 @@ export const completeDocumentWithToken = async ({
|
|||||||
data: {
|
data: {
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
signedAt: new Date(),
|
signedAt: new Date(),
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (recipientEmail !== recipient.email || recipientName !== recipient.name) {
|
||||||
|
await tx.documentAuditLog.create({
|
||||||
|
data: createDocumentAuditLogData({
|
||||||
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
user: {
|
||||||
|
name: recipientName,
|
||||||
|
email: recipientEmail,
|
||||||
|
},
|
||||||
|
requestMetadata,
|
||||||
|
data: {
|
||||||
|
recipientEmail: recipient.email,
|
||||||
|
recipientName: recipient.name,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
recipientRole: recipient.role,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||||
|
from: recipient.name,
|
||||||
|
to: recipientName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||||
|
from: recipient.email,
|
||||||
|
to: recipientEmail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const authOptions = extractDocumentAuthMethods({
|
const authOptions = extractDocumentAuthMethods({
|
||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
@@ -189,13 +268,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
recipientEmail: recipient.email,
|
recipientEmail: recipientEmail,
|
||||||
recipientName: recipient.name,
|
recipientName: recipientName,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
actionAuth: authOptions.derivedRecipientActionAuth,
|
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||||
@@ -204,13 +283,15 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await jobs.triggerJob({
|
if (recipientEmail) {
|
||||||
name: 'send.recipient.signed.email',
|
await jobs.triggerJob({
|
||||||
payload: {
|
name: 'send.recipient.signed.email',
|
||||||
documentId: legacyDocumentId,
|
payload: {
|
||||||
recipientId: recipient.id,
|
documentId: legacyDocumentId,
|
||||||
},
|
recipientId: recipient.id,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const pendingRecipients = await prisma.recipient.findMany({
|
const pendingRecipients = await prisma.recipient.findMany({
|
||||||
select: {
|
select: {
|
||||||
@@ -247,8 +328,8 @@ export const completeDocumentWithToken = async ({
|
|||||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
user: {
|
user: {
|
||||||
name: recipient.name,
|
name: recipientName,
|
||||||
email: recipient.email,
|
email: recipientEmail,
|
||||||
},
|
},
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { ApiRequestMetadata } from '../../universal/extract-request-metadat
|
|||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getMemberRoles } from '../team/get-member-roles';
|
import { getMemberRoles } from '../team/get-member-roles';
|
||||||
@@ -209,7 +210,7 @@ const handleDocumentOwnerDelete = async ({
|
|||||||
// Send cancellation emails to recipients.
|
// Send cancellation emails to recipients.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
envelope.recipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -118,7 +119,7 @@ export const resendDocument = async ({
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
recipientsToRemind.map(async (recipient) => {
|
recipientsToRemind.map(async (recipient) => {
|
||||||
if (recipient.role === RecipientRole.CC) {
|
if (recipient.role === RecipientRole.CC || !isRecipientEmailValidForSending(recipient)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { formatDocumentsPath } from '../../utils/teams';
|
import { formatDocumentsPath } from '../../utils/teams';
|
||||||
@@ -176,8 +177,12 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||||
|
isRecipientEmailValidForSending(recipient),
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
envelope.recipients.map(async (recipient) => {
|
recipientsToNotify.map(async (recipient) => {
|
||||||
const customEmailTemplate = {
|
const customEmailTemplate = {
|
||||||
'signer.name': recipient.name,
|
'signer.name': recipient.name,
|
||||||
'signer.email': recipient.email,
|
'signer.email': recipient.email,
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ import {
|
|||||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||||
import { isDocumentCompleted } from '../../utils/document';
|
import { isDocumentCompleted } from '../../utils/document';
|
||||||
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
import { toCheckboxCustomText, toRadioCustomText } from '../../utils/fields';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@@ -128,6 +130,24 @@ export const sendDocument = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that recipients with auth requirements have a valid email.
|
||||||
|
envelope.recipients.forEach((recipient) => {
|
||||||
|
const auth = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
recipient.role !== RecipientRole.CC &&
|
||||||
|
(auth.recipientAccessAuthRequired || auth.recipientActionAuthRequired) &&
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
|
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||||
|
message: `Recipient ${recipient.id} requires an email because they have auth requirements.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
||||||
// decide if we want to enforce this for API & templates.
|
// decide if we want to enforce this for API & templates.
|
||||||
// const fields = await getFieldsForDocument({
|
// const fields = await getFieldsForDocument({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
|
|
||||||
@@ -69,6 +70,11 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
|
|||||||
|
|
||||||
const { email, name } = recipient;
|
const { email, name } = recipient;
|
||||||
|
|
||||||
|
// Skip sending email if recipient has no email address
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
const template = createElement(DocumentPendingEmailTemplate, {
|
const template = createElement(DocumentPendingEmailTemplate, {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
@@ -142,7 +142,8 @@ export const deleteEnvelopeRecipient = async ({
|
|||||||
if (
|
if (
|
||||||
recipientToDelete.sendStatus === SendStatus.SENT &&
|
recipientToDelete.sendStatus === SendStatus.SENT &&
|
||||||
isRecipientRemovedEmailEnabled &&
|
isRecipientRemovedEmailEnabled &&
|
||||||
envelope.type === EnvelopeType.DOCUMENT
|
envelope.type === EnvelopeType.DOCUMENT &&
|
||||||
|
isRecipientEmailValidForSending(recipientToDelete)
|
||||||
) {
|
) {
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { canRecipientBeModified } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
@@ -294,10 +294,14 @@ export const setDocumentRecipients = async ({
|
|||||||
envelope.documentMeta,
|
envelope.documentMeta,
|
||||||
).recipientRemoved;
|
).recipientRemoved;
|
||||||
|
|
||||||
// Send emails to deleted recipients.
|
// Send emails to deleted recipients who have emails.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
removedRecipients.map(async (recipient) => {
|
removedRecipients.map(async (recipient) => {
|
||||||
if (recipient.sendStatus !== SendStatus.SENT || !isRecipientRemovedEmailEnabled) {
|
if (
|
||||||
|
recipient.sendStatus !== SendStatus.SENT ||
|
||||||
|
!isRecipientRemovedEmailEnabled ||
|
||||||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
|
||||||
@@ -110,3 +111,13 @@ export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
|
|||||||
documentId: true,
|
documentId: true,
|
||||||
templateId: true,
|
templateId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZRecipientEmailSchema = z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.email({ message: msg`Invalid email`.id })
|
||||||
|
.max(254),
|
||||||
|
]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Envelope } from '@prisma/client';
|
import type { Envelope } from '@prisma/client';
|
||||||
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
import { extractLegacyIds } from '../universal/id';
|
import { extractLegacyIds } from '../universal/id';
|
||||||
@@ -58,3 +59,7 @@ export const mapRecipientToLegacyRecipient = (
|
|||||||
...legacyId,
|
...legacyId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isRecipientEmailValidForSending = (recipient: Pick<Recipient, 'email'>) => {
|
||||||
|
return z.string().email().safeParse(recipient.email).success;
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
documentDataId: z.string(),
|
documentDataId: z.string(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
|
||||||
@@ -21,22 +21,11 @@ import {
|
|||||||
ZFieldPageYSchema,
|
ZFieldPageYSchema,
|
||||||
ZFieldWidthSchema,
|
ZFieldWidthSchema,
|
||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZDocumentTitleSchema } from '../document-router/schema';
|
import { ZDocumentTitleSchema } from '../document-router/schema';
|
||||||
|
|
||||||
const ZFieldSchema = z.object({
|
|
||||||
id: z.number().optional(),
|
|
||||||
type: z.nativeEnum(FieldType),
|
|
||||||
pageNumber: ZFieldPageNumberSchema,
|
|
||||||
pageX: ZFieldPageXSchema,
|
|
||||||
pageY: ZFieldPageYSchema,
|
|
||||||
width: ZFieldWidthSchema,
|
|
||||||
height: ZFieldHeightSchema,
|
|
||||||
fieldMeta: ZFieldMetaSchema.optional(),
|
|
||||||
envelopeItemId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
@@ -44,7 +33,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z.union([z.string().length(0), z.string().email()]),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import {
|
|||||||
ZDocumentTitleSchema,
|
ZDocumentTitleSchema,
|
||||||
ZDocumentVisibilitySchema,
|
ZDocumentVisibilitySchema,
|
||||||
} from '../document-router/schema';
|
} from '../document-router/schema';
|
||||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../trpc';
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
|
import { ZCreateEnvelopeRecipientSchema } from './envelope-recipients/create-envelope-recipients.types';
|
||||||
|
|
||||||
export const createEnvelopeMeta: TrpcRouteMeta = {
|
export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||||
openapi: {
|
openapi: {
|
||||||
@@ -54,7 +54,7 @@ export const ZCreateEnvelopePayloadSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
ZCreateRecipientSchema.extend({
|
ZCreateEnvelopeRecipientSchema.extend({
|
||||||
fields: ZEnvelopeFieldAndMetaSchema.and(
|
fields: ZEnvelopeFieldAndMetaSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
identifier: z
|
identifier: z
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,15 @@
|
|||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZEnvelopeRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import {
|
||||||
|
ZRecipientAccessAuthTypesSchema,
|
||||||
|
ZRecipientActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import {
|
||||||
|
ZEnvelopeRecipientLiteSchema,
|
||||||
|
ZRecipientEmailSchema,
|
||||||
|
} from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZCreateRecipientSchema } from '../../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../../trpc';
|
import type { TrpcRouteMeta } from '../../trpc';
|
||||||
|
|
||||||
export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
||||||
@@ -15,9 +22,18 @@ export const createEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ZCreateEnvelopeRecipientSchema = z.object({
|
||||||
|
email: ZRecipientEmailSchema,
|
||||||
|
name: z.string().max(255),
|
||||||
|
role: z.nativeEnum(RecipientRole),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
|
||||||
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZCreateEnvelopeRecipientsRequestSchema = z.object({
|
export const ZCreateEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
data: ZCreateRecipientSchema.array(),
|
data: ZCreateEnvelopeRecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateEnvelopeRecipientsResponseSchema = z.object({
|
export const ZCreateEnvelopeRecipientsResponseSchema = z.object({
|
||||||
|
|||||||
+17
-3
@@ -1,8 +1,12 @@
|
|||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import {
|
||||||
|
ZRecipientAccessAuthTypesSchema,
|
||||||
|
ZRecipientActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { ZUpdateRecipientSchema } from '../../recipient-router/schema';
|
|
||||||
import type { TrpcRouteMeta } from '../../trpc';
|
import type { TrpcRouteMeta } from '../../trpc';
|
||||||
|
|
||||||
export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
||||||
@@ -15,9 +19,19 @@ export const updateEnvelopeRecipientsMeta: TrpcRouteMeta = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ZUpdateEnvelopeRecipientSchema = z.object({
|
||||||
|
id: z.number().describe('The ID of the recipient to update.'),
|
||||||
|
email: ZRecipientEmailSchema,
|
||||||
|
name: z.string().max(255).optional(),
|
||||||
|
role: z.nativeEnum(RecipientRole).optional(),
|
||||||
|
signingOrder: z.number().optional(),
|
||||||
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema).default([]).optional(),
|
||||||
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).default([]).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRecipientsRequestSchema = z.object({
|
export const ZUpdateEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
data: ZUpdateRecipientSchema.array(),
|
data: ZUpdateEnvelopeRecipientSchema.array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdateEnvelopeRecipientsResponseSchema = z.object({
|
export const ZUpdateEnvelopeRecipientsResponseSchema = z.object({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EnvelopeType, RecipientRole } from '@prisma/client';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
|
||||||
import { ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
import { ZRecipientEmailSchema, ZRecipientLiteSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
||||||
envelopeId: z.string(),
|
envelopeId: z.string(),
|
||||||
@@ -10,7 +10,7 @@ export const ZSetEnvelopeRecipientsRequestSchema = z.object({
|
|||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().optional(),
|
id: z.number().optional(),
|
||||||
email: z.string().toLowerCase().email().min(1).max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255),
|
name: z.string().max(255),
|
||||||
role: z.nativeEnum(RecipientRole),
|
role: z.nativeEnum(RecipientRole),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
|
|
||||||
import { zodFormData } from '../../utils/zod-form-data';
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
import type { TrpcRouteMeta } from '../trpc';
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
@@ -40,7 +41,7 @@ export const ZUseEnvelopePayloadSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().describe('The ID of the recipient in the template.'),
|
id: z.number().describe('The ID of the recipient in the template.'),
|
||||||
email: z.string().email().max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255).optional(),
|
name: z.string().max(255).optional(),
|
||||||
signingOrder: z.number().optional(),
|
signingOrder: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ export const recipientRouter = router({
|
|||||||
completeDocumentWithToken: procedure
|
completeDocumentWithToken: procedure
|
||||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { token, documentId, accessAuthOptions, nextSigner } = input;
|
const { token, documentId, accessAuthOptions, nextSigner, recipientOverride } = input;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
@@ -577,6 +577,7 @@ export const recipientRouter = router({
|
|||||||
},
|
},
|
||||||
accessAuthOptions,
|
accessAuthOptions,
|
||||||
nextSigner,
|
nextSigner,
|
||||||
|
recipientOverride,
|
||||||
userId: ctx.user?.id,
|
userId: ctx.user?.id,
|
||||||
requestMetadata: ctx.metadata.requestMetadata,
|
requestMetadata: ctx.metadata.requestMetadata,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -171,6 +171,12 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
|||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
recipientOverride: z
|
||||||
|
.object({
|
||||||
|
email: z.string().trim().toLowerCase().email().max(254).optional(),
|
||||||
|
name: z.string().max(255).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '@documenso/lib/types/document-meta';
|
} from '@documenso/lib/types/document-meta';
|
||||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { ZRecipientEmailSchema } from '@documenso/lib/types/recipient';
|
||||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||||
import {
|
import {
|
||||||
ZTemplateLiteSchema,
|
ZTemplateLiteSchema,
|
||||||
@@ -100,7 +101,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().describe('The ID of the recipient in the template.'),
|
id: z.number().describe('The ID of the recipient in the template.'),
|
||||||
email: z.string().email().max(254),
|
email: ZRecipientEmailSchema,
|
||||||
name: z.string().max(255).optional(),
|
name: z.string().max(255).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user