mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
## Description Support setting a document language that will control the language used for sending emails to recipients. Additional work has been done to convert all emails to using our i18n implementation so we can later add controls for sending other kinds of emails in a users target language. ## Related Issue N/A ## Changes Made - Added `<Trans>` and `msg` macros to emails - Introduced a new `renderEmailWithI18N` utility in the lib package - Updated all emails to use the `<Tailwind>` component at the top level due to rendering constraints - Updated the `i18n.server.tsx` file to not use a top level await ## Testing Performed - Configured document language and verified emails were sent in the expected language - Created a document from a template and verified that the templates language was transferred to the document
189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import { createElement } from 'react';
|
|
|
|
import { PDFDocument } from 'pdf-lib';
|
|
|
|
import { mailer } from '@documenso/email/mailer';
|
|
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
|
|
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
|
|
import { alphaid } from '@documenso/lib/universal/id';
|
|
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
import { renderEmailWithI18N } from '@documenso/lib/utils/render-email-with-i18n';
|
|
import { prisma } from '@documenso/prisma';
|
|
import {
|
|
DocumentSource,
|
|
DocumentStatus,
|
|
FieldType,
|
|
ReadStatus,
|
|
SendStatus,
|
|
SigningStatus,
|
|
} from '@documenso/prisma/client';
|
|
import { signPdf } from '@documenso/signing';
|
|
|
|
import { procedure, router } from '../trpc';
|
|
import { mapField } from './helper';
|
|
import { ZCreateSinglePlayerDocumentMutationSchema } from './schema';
|
|
|
|
export const singleplayerRouter = router({
|
|
createSinglePlayerDocument: procedure
|
|
.input(ZCreateSinglePlayerDocumentMutationSchema)
|
|
.mutation(async ({ input }) => {
|
|
try {
|
|
const { signer, fields, documentData, documentName, fieldMeta } = input;
|
|
|
|
const document = await getFile({
|
|
data: documentData.data,
|
|
type: documentData.type,
|
|
});
|
|
|
|
const doc = await PDFDocument.load(document);
|
|
|
|
const createdAt = new Date();
|
|
|
|
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
|
|
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
|
|
const typedSignature = !isBase64 ? signer.signature : null;
|
|
|
|
// Update the document with the fields inserted.
|
|
for (const field of fields) {
|
|
const isSignatureField = field.type === FieldType.SIGNATURE;
|
|
|
|
await insertFieldInPDF(doc, {
|
|
...mapField(field, signer),
|
|
Signature: isSignatureField
|
|
? {
|
|
created: createdAt,
|
|
signatureImageAsBase64,
|
|
typedSignature,
|
|
// Dummy data.
|
|
id: -1,
|
|
recipientId: -1,
|
|
fieldId: -1,
|
|
}
|
|
: null,
|
|
// Dummy data.
|
|
id: -1,
|
|
secondaryId: '-1',
|
|
documentId: -1,
|
|
templateId: null,
|
|
recipientId: -1,
|
|
fieldMeta: fieldMeta || null,
|
|
});
|
|
}
|
|
|
|
const unsignedPdfBytes = await doc.save();
|
|
|
|
const signedPdfBuffer = await signPdf({ pdf: Buffer.from(unsignedPdfBytes) });
|
|
|
|
const { token } = await prisma.$transaction(
|
|
async (tx) => {
|
|
const token = alphaid();
|
|
|
|
// Fetch service user who will be the owner of the document.
|
|
const serviceUser = await tx.user.findFirstOrThrow({
|
|
where: {
|
|
email: SERVICE_USER_EMAIL,
|
|
},
|
|
});
|
|
|
|
const { id: documentDataId } = await putPdfFile({
|
|
name: `${documentName}.pdf`,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
|
|
});
|
|
|
|
// Create document.
|
|
const document = await tx.document.create({
|
|
data: {
|
|
source: DocumentSource.DOCUMENT,
|
|
title: documentName,
|
|
status: DocumentStatus.COMPLETED,
|
|
documentDataId,
|
|
userId: serviceUser.id,
|
|
createdAt,
|
|
},
|
|
});
|
|
|
|
// Create recipient.
|
|
const recipient = await tx.recipient.create({
|
|
data: {
|
|
documentId: document.id,
|
|
name: signer.name,
|
|
email: signer.email,
|
|
token,
|
|
signedAt: createdAt,
|
|
readStatus: ReadStatus.OPENED,
|
|
signingStatus: SigningStatus.SIGNED,
|
|
sendStatus: SendStatus.SENT,
|
|
},
|
|
});
|
|
|
|
// Create fields and signatures.
|
|
await Promise.all(
|
|
fields.map(async (field) => {
|
|
const insertedField = await tx.field.create({
|
|
data: {
|
|
documentId: document.id,
|
|
recipientId: recipient.id,
|
|
...mapField(field, signer),
|
|
},
|
|
});
|
|
|
|
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
|
|
await tx.signature.create({
|
|
data: {
|
|
fieldId: insertedField.id,
|
|
signatureImageAsBase64,
|
|
typedSignature,
|
|
recipientId: recipient.id,
|
|
},
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
|
|
return { document, token };
|
|
},
|
|
{
|
|
maxWait: 5000,
|
|
timeout: 30000,
|
|
},
|
|
);
|
|
|
|
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
|
documentName: documentName,
|
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
|
});
|
|
|
|
const [html, text] = await Promise.all([
|
|
renderEmailWithI18N(template),
|
|
renderEmailWithI18N(template, { plainText: true }),
|
|
]);
|
|
|
|
// Send email to signer.
|
|
await mailer.sendMail({
|
|
to: {
|
|
address: signer.email,
|
|
name: signer.name,
|
|
},
|
|
from: {
|
|
name: FROM_NAME,
|
|
address: FROM_ADDRESS,
|
|
},
|
|
subject: 'Document signed',
|
|
html,
|
|
text,
|
|
attachments: [{ content: signedPdfBuffer, filename: documentName }],
|
|
});
|
|
|
|
return token;
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
throw err;
|
|
}
|
|
}),
|
|
});
|