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:
Ephraim Duncan
2024-07-05 03:03:22 +00:00
committed by GitHub
parent 06b1d4835e
commit 2eee2b4d2a
8 changed files with 51 additions and 9 deletions

View File

@ -42,7 +42,7 @@ export const DirectTemplatePageView = ({
const { toast } = useToast();
const { email, setEmail } = useRequiredSigningContext();
const { email, fullName, setEmail } = useRequiredSigningContext();
const { recipient, setRecipient } = useRequiredDocumentAuthContext();
const [step, setStep] = useState<DirectTemplateStep>('configure');
@ -84,6 +84,7 @@ export const DirectTemplatePageView = ({
try {
const token = await createDocumentFromDirectTemplate({
directTemplateToken,
directRecipientName: fullName,
directRecipientEmail: recipient.email,
templateUpdatedAt: template.updatedAt,
signedFieldValues: fields.map((field) => {

View File

@ -5,12 +5,14 @@ export interface TemplateDocumentCompletedProps {
downloadLink: string;
documentName: string;
assetBaseUrl: string;
customBody?: string;
}
export const TemplateDocumentCompleted = ({
downloadLink,
documentName,
assetBaseUrl,
customBody,
}: TemplateDocumentCompletedProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -34,7 +36,7 @@ export const TemplateDocumentCompleted = ({
</Section>
<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 className="my-1 text-center text-base text-slate-400">

View File

@ -5,12 +5,15 @@ import type { TemplateDocumentCompletedProps } from '../template-components/temp
import { TemplateDocumentCompleted } from '../template-components/template-document-completed';
import { TemplateFooter } from '../template-components/template-footer';
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps> & {
customBody?: string;
};
export const DocumentCompletedEmailTemplate = ({
downloadLink = 'https://documenso.com',
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
customBody,
}: DocumentCompletedEmailTemplateProps) => {
const previewText = `Completed Document`;
@ -45,6 +48,7 @@ export const DocumentCompletedEmailTemplate = ({
downloadLink={downloadLink}
documentName={documentName}
assetBaseUrl={assetBaseUrl}
customBody={customBody}
/>
</Section>
</Container>

View File

@ -76,8 +76,6 @@ export const moveDocumentToTeam = async ({
}),
});
console.log(log);
return updatedDocument;
});
};

View File

@ -4,12 +4,14 @@ import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
import { DocumentSource } from '@documenso/prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
export interface SendDocumentOptions {
documentId: number;
@ -23,6 +25,7 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
},
include: {
documentData: true,
documentMeta: true,
Recipient: true,
User: true,
team: {
@ -38,6 +41,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
throw new Error('Document not found');
}
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
@ -106,12 +111,22 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
await Promise.all(
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 template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
customBody:
isDirectTemplate && document.documentMeta?.message
? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate)
: undefined,
});
await mailer.sendMail({
@ -125,7 +140,10 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
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),
text: render(template, { plainText: true }),
attachments: [

View File

@ -39,6 +39,7 @@ import { sendDocument } from '../document/send-document';
import { validateFieldAuth } from '../document/validate-field-auth';
export type CreateDocumentFromDirectTemplateOptions = {
directRecipientName?: string;
directRecipientEmail: string;
directTemplateToken: string;
signedFieldValues: TSignFieldWithTokenMutationSchema[];
@ -57,6 +58,7 @@ type CreatedDirectRecipientField = {
};
export const createDocumentFromDirectTemplate = async ({
directRecipientName: initialDirectRecipientName,
directRecipientEmail,
directTemplateToken,
signedFieldValues,
@ -110,7 +112,7 @@ export const createDocumentFromDirectTemplate = async ({
documentAuth: template.authOptions,
});
const directRecipientName = user?.name;
const directRecipientName = user?.name || initialDirectRecipientName;
// Ensure typesafety when we add more options.
const isAccessAuthValid = match(derivedRecipientAccessAuth)
@ -132,6 +134,8 @@ export const createDocumentFromDirectTemplate = async ({
const metaTimezone = template.templateMeta?.timezone || DEFAULT_DOCUMENT_TIME_ZONE;
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.
const createDirectRecipientFieldArgs = await Promise.all(
@ -250,6 +254,14 @@ export const createDocumentFromDirectTemplate = async ({
}),
},
},
documentMeta: {
create: {
timezone: metaTimezone,
dateFormat: metaDateFormat,
message: metaEmailMessage,
subject: metaEmailSubject,
},
},
},
include: {
Recipient: true,

View File

@ -59,12 +59,18 @@ export const templateRouter = router({
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { directRecipientEmail, directTemplateToken, signedFieldValues, templateUpdatedAt } =
input;
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
signedFieldValues,
templateUpdatedAt,
} = input;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
signedFieldValues,

View File

@ -17,6 +17,7 @@ export const ZCreateTemplateMutationSchema = z.object({
});
export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
directRecipientName: z.string().optional(),
directRecipientEmail: z.string().email(),
directTemplateToken: z.string().min(1),
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),