diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index e6cbd6fd4..813458062 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -151,7 +151,7 @@ export const EditDocumentForm = ({ }; const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { - const { subject, message, timezone, dateFormat } = data.meta; + const { subject, message, timezone, dateFormat, redirectUrl } = data.meta; try { await sendDocument({ @@ -159,8 +159,9 @@ export const EditDocumentForm = ({ meta: { subject, message, - timezone, dateFormat, + timezone, + redirectUrl, }, }); diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 7105baafd..7e6cf26b8 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -26,9 +26,10 @@ export type SigningFormProps = { document: Document; recipient: Recipient; fields: Field[]; + redirectUrl?: string | null; }; -export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { +export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => { const router = useRouter(); const analytics = useAnalytics(); const { data: session } = useSession(); @@ -74,7 +75,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = timestamp: new Date().toISOString(), }); - router.push(`/sign/${recipient.token}/complete`); + redirectUrl ? router.push(redirectUrl) : router.push(`/sign/${recipient.token}/complete`); }; return ( diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 7e025593c..9a7e8acbe 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -8,7 +8,6 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; -import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; @@ -49,15 +48,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp viewedDocument({ token }).catch(() => null), ]); - const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); - if (!document || !document.documentData || !recipient) { return notFound(); } const truncatedTitle = truncateTitle(document.title); - const { documentData } = document; + const { documentData, documentMeta } = document; const { user } = await getServerComponentSession(); @@ -65,7 +62,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp document.status === DocumentStatus.COMPLETED || recipient.signingStatus === SigningStatus.SIGNED ) { - redirect(`/sign/${token}/complete`); + documentMeta?.redirectUrl + ? redirect(documentMeta.redirectUrl) + : redirect(`/sign/${token}/complete`); } if (documentMeta?.password) { @@ -133,7 +132,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
- +
diff --git a/package-lock.json b/package-lock.json index aae034c57..c4d4c1e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14503,6 +14503,7 @@ "version": "6.9.7", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.7.tgz", "integrity": "sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -19495,14 +19496,14 @@ "@react-email/section": "0.0.10", "@react-email/tailwind": "0.0.9", "@react-email/text": "0.0.6", - "nodemailer": "^6.9.3", + "nodemailer": "^6.9.9", "react-email": "^1.9.5", "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", - "@types/nodemailer": "^6.4.8", + "@types/nodemailer": "^6.4.14", "tsup": "^7.1.0" } }, @@ -19520,6 +19521,14 @@ "node": ">=16.0.0" } }, + "packages/email/node_modules/nodemailer": { + "version": "6.9.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.9.tgz", + "integrity": "sha512-dexTll8zqQoVJEZPwQAKzxxtFn0qTnjdQTchoU6Re9BUUGBJiOy3YMn/0ShTW6J5M0dfQ1NeDeRTTl4oIWgQMA==", + "engines": { + "node": ">=6.0.0" + } + }, "packages/eslint-config": { "name": "@documenso/eslint-config", "version": "0.0.0", diff --git a/packages/email/package.json b/packages/email/package.json index d41a4c24c..984ea3d4c 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -35,14 +35,14 @@ "@react-email/section": "0.0.10", "@react-email/tailwind": "0.0.9", "@react-email/text": "0.0.6", - "nodemailer": "^6.9.3", + "nodemailer": "^6.9.9", "react-email": "^1.9.5", "resend": "^2.0.0" }, "devDependencies": { "@documenso/tailwind-config": "*", "@documenso/tsconfig": "*", - "@types/nodemailer": "^6.4.8", + "@types/nodemailer": "^6.4.14", "tsup": "^7.1.0" } } diff --git a/packages/lib/constants/url-regex.ts b/packages/lib/constants/url-regex.ts new file mode 100644 index 000000000..259ce070d --- /dev/null +++ b/packages/lib/constants/url-regex.ts @@ -0,0 +1,2 @@ +export const URL_REGEX = + /^(https?):\/\/(?:www\.)?[a-zA-Z0-9-]+\.[a-zA-Z0-9()]{2,}(?:\/[a-zA-Z0-9-._?&=/]*)?$/i; diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts index 3e6cd75be..7bd6d93cc 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -9,6 +9,7 @@ export type CreateDocumentMetaOptions = { timezone?: string; password?: string; dateFormat?: string; + redirectUrl?: string; userId: number; }; @@ -20,6 +21,7 @@ export const upsertDocumentMeta = async ({ documentId, userId, password, + redirectUrl, }: CreateDocumentMetaOptions) => { await prisma.document.findFirstOrThrow({ where: { @@ -48,17 +50,19 @@ export const upsertDocumentMeta = async ({ create: { subject, message, + password, dateFormat, timezone, - password, documentId, + redirectUrl, }, update: { subject, message, - dateFormat, password, + dateFormat, timezone, + redirectUrl, }, }); }; diff --git a/packages/lib/server-only/document/duplicate-document-by-id.ts b/packages/lib/server-only/document/duplicate-document-by-id.ts index 5ca848bb3..4e6a7bd87 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -39,6 +39,7 @@ export const duplicateDocumentById = async ({ dateFormat: true, password: true, timezone: true, + redirectUrl: true, }, }, }, diff --git a/packages/lib/server-only/document/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts index 62c8a5ca1..18f9a5161 100644 --- a/packages/lib/server-only/document/get-document-by-token.ts +++ b/packages/lib/server-only/document/get-document-by-token.ts @@ -27,6 +27,7 @@ export const getDocumentAndSenderByToken = async ({ include: { User: true, documentData: true, + documentMeta: true, }, }); diff --git a/packages/prisma/migrations/20240206111230_add_document_meta_redirect_url/migration.sql b/packages/prisma/migrations/20240206111230_add_document_meta_redirect_url/migration.sql new file mode 100644 index 000000000..0eb8a1175 --- /dev/null +++ b/packages/prisma/migrations/20240206111230_add_document_meta_redirect_url/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "DocumentMeta" ADD COLUMN "redirectUrl" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index fc128efc1..ff2d12319 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -199,6 +199,7 @@ model DocumentMeta { dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text documentId Int @unique document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + redirectUrl String? } enum ReadStatus { diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 5940d971d..d0ff48941 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -215,13 +215,14 @@ export const documentRouter = router({ try { const { documentId, meta } = input; - if (meta.message || meta.subject || meta.timezone || meta.dateFormat) { + if (meta.message || meta.subject || meta.timezone || meta.dateFormat || meta.redirectUrl) { await upsertDocumentMeta({ documentId, subject: meta.subject, message: meta.message, dateFormat: meta.dateFormat, timezone: meta.timezone, + redirectUrl: meta.redirectUrl, userId: ctx.user.id, }); } diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index f8d008f50..fceb6413f 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; +import { URL_REGEX } from '@documenso/lib/constants/url-regex'; import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client'; export const ZGetDocumentByIdQuerySchema = z.object({ @@ -73,6 +74,12 @@ export const ZSendDocumentMutationSchema = z.object({ message: z.string(), timezone: z.string(), dateFormat: z.string(), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }), }), }); diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index b3bbcbe92..40e42e3b3 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Info } from 'lucide-react'; import { Controller, useForm } from 'react-hook-form'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; @@ -23,6 +25,7 @@ import { SelectTrigger, SelectValue, } from '@documenso/ui/primitives/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Combobox } from '../combobox'; import { FormErrorMessage } from '../form/form-error-message'; @@ -30,7 +33,7 @@ import { Input } from '../input'; import { Label } from '../label'; import { useStep } from '../stepper'; import { Textarea } from '../textarea'; -import type { TAddSubjectFormSchema } from './add-subject.types'; +import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types'; import { DocumentFlowFormContainerActions, DocumentFlowFormContainerContent, @@ -69,8 +72,10 @@ export const AddSubjectFormPartial = ({ message: document.documentMeta?.message ?? '', timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + redirectUrl: document.documentMeta?.redirectUrl ?? '', }, }, + resolver: zodResolver(ZAddSubjectFormSchema), }); const onFormSubmit = handleSubmit(onSubmit); @@ -163,64 +168,94 @@ export const AddSubjectFormPartial = ({ - {hasDateField && ( - - - - Advanced Options - + + + + Advanced Options + - -
- + + {hasDateField && ( + <> +
+ - ( - + + + - - {DATE_FORMATS.map((format) => ( - - {format.label} - - ))} - - - )} - /> + + {DATE_FORMATS.map((format) => ( + + {format.label} + + ))} + + + )} + /> +
+ +
+ + + ( + value && onChange(value)} + disabled={documentHasBeenSent} + /> + )} + /> +
+ + )} + +
+
+
+ + + + + +
- -
- - - ( - value && onChange(value)} - disabled={documentHasBeenSent} - /> - )} - /> -
- - - - )} +
+
+ +
diff --git a/packages/ui/primitives/document-flow/add-subject.types.ts b/packages/ui/primitives/document-flow/add-subject.types.ts index ea14f4c0f..fd4175368 100644 --- a/packages/ui/primitives/document-flow/add-subject.types.ts +++ b/packages/ui/primitives/document-flow/add-subject.types.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import { URL_REGEX } from '@documenso/lib/constants/url-regex'; export const ZAddSubjectFormSchema = z.object({ meta: z.object({ @@ -9,6 +10,12 @@ export const ZAddSubjectFormSchema = z.object({ message: z.string(), timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE), dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT), + redirectUrl: z + .string() + .optional() + .refine((value) => value === undefined || URL_REGEX.test(value), { + message: 'Please enter a valid URL', + }), }), });