mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
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 { 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) => {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -76,8 +76,6 @@ export const moveDocumentToTeam = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(log);
|
||||
|
||||
return updatedDocument;
|
||||
});
|
||||
};
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
Reference in New Issue
Block a user