diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 7e729262e..18221edb6 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -1,6 +1,7 @@ import { createNextRoute } from '@ts-rest/next'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError } from '@documenso/lib/errors/app-error'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; @@ -76,7 +77,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { status: 200, body: { ...document, - recipients, + recipients: recipients.map((recipient) => ({ + ...recipient, + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, + })), }, }; } catch (err) { @@ -258,6 +262,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { email: recipient.email, token: recipient.token, role: recipient.role, + + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, })), }, }; @@ -349,6 +355,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { email: recipient.email, token: recipient.token, role: recipient.role, + + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, })), }, }; @@ -428,6 +436,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { email: recipient.email, token: recipient.token, role: recipient.role, + + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, })), }, }; @@ -435,6 +445,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { sendDocument: authenticatedMiddleware(async (args, user, team) => { const { id } = args.params; + const { sendEmail = true } = args.body ?? {}; const document = await getDocumentById({ id: Number(id), userId: user.id, teamId: team?.id }); @@ -490,10 +501,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { // }); // } - await sendDocument({ + const { Recipient: recipients, ...sentDocument } = await sendDocument({ documentId: Number(id), userId: user.id, teamId: team?.id, + sendEmail, requestMetadata: extractNextApiRequestMetadata(args.req), }); @@ -501,6 +513,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { status: 200, body: { message: 'Document sent for signing successfully', + ...sentDocument, + recipients: recipients.map((recipient) => ({ + ...recipient, + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, + })), }, }; } catch (err) { @@ -585,6 +602,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { body: { ...newRecipient, documentId: Number(documentId), + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${newRecipient.token}`, }, }; } catch (err) { @@ -650,6 +668,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { body: { ...updatedRecipient, documentId: Number(documentId), + signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${updatedRecipient.token}`, }, }; }), @@ -703,6 +722,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { body: { ...deletedRecipient, documentId: Number(documentId), + signingUrl: '', }, }; }), diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index f109df348..7f82c611e 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -45,7 +45,11 @@ export type TSuccessfulGetDocumentResponseSchema = z.infer< export type TSuccessfulDocumentResponseSchema = z.infer; -export const ZSendDocumentForSigningMutationSchema = null; +export const ZSendDocumentForSigningMutationSchema = z + .object({ + sendEmail: z.boolean().optional().default(true), + }) + .or(z.literal('').transform(() => ({ sendEmail: true }))); export type TSendDocumentForSigningMutationSchema = typeof ZSendDocumentForSigningMutationSchema; @@ -89,8 +93,12 @@ export const ZCreateDocumentMutationResponseSchema = z.object({ recipients: z.array( z.object({ recipientId: z.number(), + name: z.string(), + email: z.string().email().min(1), token: z.string(), role: z.nativeEnum(RecipientRole), + + signingUrl: z.string(), }), ), }); @@ -134,6 +142,8 @@ export const ZCreateDocumentFromTemplateMutationResponseSchema = z.object({ email: z.string().email().min(1), token: z.string(), role: z.nativeEnum(RecipientRole).optional().default(RecipientRole.SIGNER), + + signingUrl: z.string(), }), ), }); @@ -187,6 +197,8 @@ export const ZGenerateDocumentFromTemplateMutationResponseSchema = z.object({ email: z.string().email().min(1), token: z.string(), role: z.nativeEnum(RecipientRole), + + signingUrl: z.string(), }), ), }); @@ -229,6 +241,8 @@ export const ZSuccessfulRecipientResponseSchema = z.object({ readStatus: z.nativeEnum(ReadStatus), signingStatus: z.nativeEnum(SigningStatus), sendStatus: z.nativeEnum(SendStatus), + + signingUrl: z.string(), }); export type TSuccessfulRecipientResponseSchema = z.infer; @@ -279,9 +293,11 @@ export const ZSuccessfulResponseSchema = z.object({ export type TSuccessfulResponseSchema = z.infer; -export const ZSuccessfulSigningResponseSchema = z.object({ - message: z.string(), -}); +export const ZSuccessfulSigningResponseSchema = z + .object({ + message: z.string(), + }) + .and(ZSuccessfulGetDocumentResponseSchema); export type TSuccessfulSigningResponseSchema = z.infer; diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index 64ddb883d..fc65e8c6e 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -28,6 +28,7 @@ export type SendDocumentOptions = { documentId: number; userId: number; teamId?: number; + sendEmail?: boolean; requestMetadata?: RequestMetadata; }; @@ -35,6 +36,7 @@ export const sendDocument = async ({ documentId, userId, teamId, + sendEmail = true, requestMetadata, }: SendDocumentOptions) => { const user = await prisma.user.findFirstOrThrow({ @@ -120,98 +122,102 @@ export const sendDocument = async ({ Object.assign(document, result); } - await Promise.all( - document.Recipient.map(async (recipient) => { - if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) { - return; - } + if (sendEmail) { + await Promise.all( + document.Recipient.map(async (recipient) => { + if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) { + return; + } - const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; + const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; - const { email, name } = recipient; - const selfSigner = email === user.email; + const { email, name } = recipient; + const selfSigner = email === user.email; - const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[ - recipient.role - ].actionVerb.toLowerCase()} it.`; + const selfSignerCustomEmail = `You have initiated the document ${`"${document.title}"`} that requires you to ${RECIPIENT_ROLES_DESCRIPTION[ + recipient.role + ].actionVerb.toLowerCase()} it.`; - const customEmailTemplate = { - 'signer.name': name, - 'signer.email': email, - 'document.name': document.title, - }; + const customEmailTemplate = { + 'signer.name': name, + 'signer.email': email, + 'document.name': document.title, + }; - const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; - const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`; + const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; + const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`; - const template = createElement(DocumentInviteEmailTemplate, { - documentName: document.title, - inviterName: user.name || undefined, - inviterEmail: user.email, - assetBaseUrl, - signDocumentLink, - customBody: renderCustomEmailTemplate( - selfSigner && !customEmail?.message ? selfSignerCustomEmail : customEmail?.message || '', - customEmailTemplate, - ), - role: recipient.role, - selfSigner, - }); + const template = createElement(DocumentInviteEmailTemplate, { + documentName: document.title, + inviterName: user.name || undefined, + inviterEmail: user.email, + assetBaseUrl, + signDocumentLink, + customBody: renderCustomEmailTemplate( + selfSigner && !customEmail?.message + ? selfSignerCustomEmail + : customEmail?.message || '', + customEmailTemplate, + ), + role: recipient.role, + selfSigner, + }); - const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; + const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; - const emailSubject = selfSigner - ? `Please ${actionVerb.toLowerCase()} your document` - : `Please ${actionVerb.toLowerCase()} this document`; + const emailSubject = selfSigner + ? `Please ${actionVerb.toLowerCase()} your document` + : `Please ${actionVerb.toLowerCase()} this document`; - await prisma.$transaction( - async (tx) => { - await mailer.sendMail({ - to: { - address: email, - name, - }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: customEmail?.subject - ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) - : emailSubject, - html: render(template), - text: render(template, { plainText: true }), - }); - - await tx.recipient.update({ - where: { - id: recipient.id, - }, - data: { - sendStatus: SendStatus.SENT, - }, - }); - - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, - user, - requestMetadata, - data: { - emailType: recipientEmailType, - recipientEmail: recipient.email, - recipientName: recipient.name, - recipientRole: recipient.role, - recipientId: recipient.id, - isResending: false, + await prisma.$transaction( + async (tx) => { + await mailer.sendMail({ + to: { + address: email, + name, }, - }), - }); - }, - { timeout: 30_000 }, - ); - }), - ); + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: customEmail?.subject + ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + : emailSubject, + html: render(template), + text: render(template, { plainText: true }), + }); + + await tx.recipient.update({ + where: { + id: recipient.id, + }, + data: { + sendStatus: SendStatus.SENT, + }, + }); + + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, + documentId: document.id, + user, + requestMetadata, + data: { + emailType: recipientEmailType, + recipientEmail: recipient.email, + recipientName: recipient.name, + recipientRole: recipient.role, + recipientId: recipient.id, + isResending: false, + }, + }), + }); + }, + { timeout: 30_000 }, + ); + }), + ); + } const allRecipientsHaveNoActionToTake = document.Recipient.every( (recipient) => recipient.role === RecipientRole.CC,