feat: send custom email to signers of direct template documents (#1215)
Introduces customization options for the document completion email template to allow for custom email bodies and subjects for documents created from direct templates. ## Testing Performed - Verified correct rendering of custom email subject and body for direct template documents - Verified the all other completed email types are sent correctly
This commit is contained in:
@@ -42,7 +42,7 @@ export const DirectTemplatePageView = ({
|
|||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { email, setEmail } = useRequiredSigningContext();
|
const { email, fullName, setEmail } = useRequiredSigningContext();
|
||||||
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
|
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
const [step, setStep] = useState<DirectTemplateStep>('configure');
|
||||||
@@ -84,6 +84,7 @@ export const DirectTemplatePageView = ({
|
|||||||
try {
|
try {
|
||||||
const token = await createDocumentFromDirectTemplate({
|
const token = await createDocumentFromDirectTemplate({
|
||||||
directTemplateToken,
|
directTemplateToken,
|
||||||
|
directRecipientName: fullName,
|
||||||
directRecipientEmail: recipient.email,
|
directRecipientEmail: recipient.email,
|
||||||
templateUpdatedAt: template.updatedAt,
|
templateUpdatedAt: template.updatedAt,
|
||||||
signedFieldValues: fields.map((field) => {
|
signedFieldValues: fields.map((field) => {
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ export interface TemplateDocumentCompletedProps {
|
|||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
documentName: string;
|
documentName: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
|
customBody?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateDocumentCompleted = ({
|
export const TemplateDocumentCompleted = ({
|
||||||
downloadLink,
|
downloadLink,
|
||||||
documentName,
|
documentName,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
|
customBody,
|
||||||
}: TemplateDocumentCompletedProps) => {
|
}: TemplateDocumentCompletedProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
const getAssetUrl = (path: string) => {
|
||||||
return new URL(path, assetBaseUrl).toString();
|
return new URL(path, assetBaseUrl).toString();
|
||||||
@@ -34,7 +36,7 @@ export const TemplateDocumentCompleted = ({
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
“{documentName}” was signed by all signers
|
{customBody ?? `“${documentName}” was signed by all signers`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import type { TemplateDocumentCompletedProps } from '../template-components/temp
|
|||||||
import { TemplateDocumentCompleted } from '../template-components/template-document-completed';
|
import { TemplateDocumentCompleted } from '../template-components/template-document-completed';
|
||||||
import { TemplateFooter } from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps> & {
|
||||||
|
customBody?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const DocumentCompletedEmailTemplate = ({
|
export const DocumentCompletedEmailTemplate = ({
|
||||||
downloadLink = 'https://documenso.com',
|
downloadLink = 'https://documenso.com',
|
||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
|
customBody,
|
||||||
}: DocumentCompletedEmailTemplateProps) => {
|
}: DocumentCompletedEmailTemplateProps) => {
|
||||||
const previewText = `Completed Document`;
|
const previewText = `Completed Document`;
|
||||||
|
|
||||||
@@ -45,6 +48,7 @@ export const DocumentCompletedEmailTemplate = ({
|
|||||||
downloadLink={downloadLink}
|
downloadLink={downloadLink}
|
||||||
documentName={documentName}
|
documentName={documentName}
|
||||||
assetBaseUrl={assetBaseUrl}
|
assetBaseUrl={assetBaseUrl}
|
||||||
|
customBody={customBody}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ export const moveDocumentToTeam = async ({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(log);
|
|
||||||
|
|
||||||
return updatedDocument;
|
return updatedDocument;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { mailer } from '@documenso/email/mailer';
|
|||||||
import { render } from '@documenso/email/render';
|
import { render } from '@documenso/email/render';
|
||||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentSource } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
|
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
|
||||||
|
|
||||||
export interface SendDocumentOptions {
|
export interface SendDocumentOptions {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@@ -23,6 +25,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentData: true,
|
documentData: true,
|
||||||
|
documentMeta: true,
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
User: true,
|
User: true,
|
||||||
team: {
|
team: {
|
||||||
@@ -38,6 +41,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||||
|
|
||||||
if (document.Recipient.length === 0) {
|
if (document.Recipient.length === 0) {
|
||||||
throw new Error('Document has no recipients');
|
throw new Error('Document has no recipients');
|
||||||
}
|
}
|
||||||
@@ -106,12 +111,22 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
|
const customEmailTemplate = {
|
||||||
|
'signer.name': recipient.name,
|
||||||
|
'signer.email': recipient.email,
|
||||||
|
'document.name': document.title,
|
||||||
|
};
|
||||||
|
|
||||||
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
|
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
|
||||||
|
|
||||||
const template = createElement(DocumentCompletedEmailTemplate, {
|
const template = createElement(DocumentCompletedEmailTemplate, {
|
||||||
documentName: document.title,
|
documentName: document.title,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
|
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
|
||||||
|
customBody:
|
||||||
|
isDirectTemplate && document.documentMeta?.message
|
||||||
|
? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate)
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendMail({
|
await mailer.sendMail({
|
||||||
@@ -125,7 +140,10 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
|
|||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
||||||
},
|
},
|
||||||
subject: 'Signing Complete!',
|
subject:
|
||||||
|
isDirectTemplate && document.documentMeta?.subject
|
||||||
|
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
|
||||||
|
: 'Signing Complete!',
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
attachments: [
|
attachments: [
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { sendDocument } from '../document/send-document';
|
|||||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||||
|
|
||||||
export type CreateDocumentFromDirectTemplateOptions = {
|
export type CreateDocumentFromDirectTemplateOptions = {
|
||||||
|
directRecipientName?: string;
|
||||||
directRecipientEmail: string;
|
directRecipientEmail: string;
|
||||||
directTemplateToken: string;
|
directTemplateToken: string;
|
||||||
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
signedFieldValues: TSignFieldWithTokenMutationSchema[];
|
||||||
@@ -57,6 +58,7 @@ type CreatedDirectRecipientField = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const createDocumentFromDirectTemplate = async ({
|
export const createDocumentFromDirectTemplate = async ({
|
||||||
|
directRecipientName: initialDirectRecipientName,
|
||||||
directRecipientEmail,
|
directRecipientEmail,
|
||||||
directTemplateToken,
|
directTemplateToken,
|
||||||
signedFieldValues,
|
signedFieldValues,
|
||||||
@@ -110,7 +112,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
documentAuth: template.authOptions,
|
documentAuth: template.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const directRecipientName = user?.name;
|
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||||
|
|
||||||
// Ensure typesafety when we add more options.
|
// Ensure typesafety when we add more options.
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
||||||
@@ -132,6 +134,8 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
|
|
||||||
const metaTimezone = template.templateMeta?.timezone || DEFAULT_DOCUMENT_TIME_ZONE;
|
const metaTimezone = template.templateMeta?.timezone || DEFAULT_DOCUMENT_TIME_ZONE;
|
||||||
const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
|
const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
|
||||||
|
const metaEmailMessage = template.templateMeta?.message || '';
|
||||||
|
const metaEmailSubject = template.templateMeta?.subject || '';
|
||||||
|
|
||||||
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
// Associate, validate and map to a query every direct template recipient field with the provided fields.
|
||||||
const createDirectRecipientFieldArgs = await Promise.all(
|
const createDirectRecipientFieldArgs = await Promise.all(
|
||||||
@@ -250,6 +254,14 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
documentMeta: {
|
||||||
|
create: {
|
||||||
|
timezone: metaTimezone,
|
||||||
|
dateFormat: metaDateFormat,
|
||||||
|
message: metaEmailMessage,
|
||||||
|
subject: metaEmailSubject,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
|
|||||||
@@ -59,12 +59,18 @@ export const templateRouter = router({
|
|||||||
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
|
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { directRecipientEmail, directTemplateToken, signedFieldValues, templateUpdatedAt } =
|
const {
|
||||||
input;
|
directRecipientName,
|
||||||
|
directRecipientEmail,
|
||||||
|
directTemplateToken,
|
||||||
|
signedFieldValues,
|
||||||
|
templateUpdatedAt,
|
||||||
|
} = input;
|
||||||
|
|
||||||
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
|
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
|
||||||
|
|
||||||
return await createDocumentFromDirectTemplate({
|
return await createDocumentFromDirectTemplate({
|
||||||
|
directRecipientName,
|
||||||
directRecipientEmail,
|
directRecipientEmail,
|
||||||
directTemplateToken,
|
directTemplateToken,
|
||||||
signedFieldValues,
|
signedFieldValues,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const ZCreateTemplateMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
|
export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
|
||||||
|
directRecipientName: z.string().optional(),
|
||||||
directRecipientEmail: z.string().email(),
|
directRecipientEmail: z.string().email(),
|
||||||
directTemplateToken: z.string().min(1),
|
directTemplateToken: z.string().min(1),
|
||||||
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
|
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
|
||||||
|
|||||||
Reference in New Issue
Block a user