Compare commits

..

4 Commits

Author SHA1 Message Date
6e2d1fb148 fix: add missing URL to email template 2023-09-27 17:44:29 +10:00
81f9b2f776 Merge branch 'feat/refresh' into feat/update-email-templates 2023-09-27 15:50:54 +10:00
334671ef85 feat: add email forgot password action
Updated email template imports
2023-09-27 15:34:16 +10:00
e5fa2f4490 feat: update email templates
Add SPM email attachment
2023-09-26 17:48:26 +10:00
41 changed files with 161 additions and 371 deletions

View File

@ -50,9 +50,7 @@ NEXT_PRIVATE_SMTP_SECURE=
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso" NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
# REQUIRED: Defines the email address to use as the from address. # REQUIRED: Defines the email address to use as the from address.
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com" NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
# OPTIONAL: The API key to use for Resend.com # OPTIONAL: The API key to use for the MailChannels proxy endpoint.
NEXT_PRIVATE_RESEND_API_KEY=
# OPTIONAL: The API key to use for MailChannels.
NEXT_PRIVATE_MAILCHANNELS_API_KEY= NEXT_PRIVATE_MAILCHANNELS_API_KEY=
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy. # OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT= NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=

View File

@ -51,7 +51,4 @@ As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION M
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward. We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
Best from Hamburg
Timur
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.** **[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**

View File

@ -1,12 +1,12 @@
--- ---
title: Merch Mania title: Shop and Limited Edition "Mania" Shirt
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can. description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
authorName: 'Timur Ercan' authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg' authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder' authorRole: 'Co-Founder'
date: 2023-09-27 date: 2023-09-27
tags: tags:
- Merch - Testing
- Rewrite - Rewrite
- Bounties - Bounties
--- ---
@ -24,13 +24,13 @@ tags:
</figcaption> </figcaption>
</figure> </figure>
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one. > TLDR; We have a fancy limited edition shirt. Contribute to Malfunction Mania to get one.
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM. We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [Issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created the limited edition shirt for Malfunction Mania. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
## Documenso Merch Shop ## Documenso Merch Shop
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso. The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors.
<figure> <figure>
<MdxNextImage <MdxNextImage
@ -45,7 +45,7 @@ The shirt will be available in our [merch shop](https://documen.so/shop) via a u
</figcaption> </figcaption>
</figure> </figure>
## How earn the shirt ## How to get the shirt
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one: If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
@ -56,9 +56,7 @@ If you have been following us, you know we are not big on formalities but highly
- Engage in discussion about the current version and its choices - Engage in discussion about the current version and its choices
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging) - Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
- Review the version with a video, stream, or screenshots and post about it - Review the version with a video, stream, or screenshots and post about it
- Review existing or create missing documentation - Review existing or create missing documenso
- ...
Best from Hamburg **[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
Timur
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.**

View File

@ -56,14 +56,6 @@ const config = {
}, },
]; ];
}, },
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
}; };
module.exports = withContentlayer(config); module.exports = withContentlayer(config);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 956 KiB

View File

@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
}, },
); );
// Todo: Handle `downloadLink`
const template = createElement(DocumentSelfSignedEmailTemplate, { const template = createElement(DocumentSelfSignedEmailTemplate, {
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
documentName: documentName, documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000', assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
}); });
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
subject: 'Document signed', subject: 'Document signed',
html: render(template), html: render(template),
text: render(template, { plainText: true }), text: render(template, { plainText: true }),
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
}); });
return documentToken; return documentToken;

View File

@ -29,14 +29,6 @@ const config = {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}', transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
}, },
}, },
async rewrites() {
return [
{
source: '/ingest/:path*',
destination: 'https://eu.posthog.com/:path*',
},
];
},
}; };
module.exports = config; module.exports = config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

@ -10,7 +10,7 @@ export default function Loading() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8"> <div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">

View File

@ -65,7 +65,10 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
Documents Documents
</Link> </Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>

View File

@ -10,7 +10,7 @@ export default function DocumentSentPage() {
Documents Documents
</Link> </Link>
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl"> <h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document... Loading Document...
</h1> </h1>
</div> </div>

View File

@ -120,7 +120,6 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10" className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
variant="secondary" variant="secondary"
size="lg" size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()} onClick={() => router.back()}
> >
Cancel Cancel

26
package-lock.json generated
View File

@ -16958,29 +16958,6 @@
"node": ">=0.10.5" "node": ">=0.10.5"
} }
}, },
"node_modules/resend": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
"dependencies": {
"@react-email/render": "0.0.7",
"type-fest": "3.13.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/resend/node_modules/type-fest": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.2", "version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@ -19796,8 +19773,7 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"react-email": "^1.9.4", "react-email": "^1.9.4"
"resend": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",

View File

@ -1,7 +1,6 @@
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { MailChannelsTransport } from './transports/mailchannels'; import { MailChannelsTransport } from './transports/mailchannels';
import { ResendTransport } from './transports/resend';
const getTransport = () => { const getTransport = () => {
const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth'; const transport = process.env.NEXT_PRIVATE_SMTP_TRANSPORT ?? 'smtp-auth';
@ -15,14 +14,6 @@ const getTransport = () => {
); );
} }
if (transport === 'resend') {
return createTransport(
ResendTransport.makeTransport({
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
}),
);
}
if (transport === 'smtp-api') { if (transport === 'smtp-api') {
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) { if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
throw new Error( throw new Error(

View File

@ -19,8 +19,7 @@
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"react-email": "^1.9.4", "react-email": "^1.9.4"
"resend": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@documenso/tailwind-config": "*", "@documenso/tailwind-config": "*",

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentCompletedProps { export interface TemplateDocumentCompletedProps {
downloadLink: string; downloadLink: string;
documentName: string; documentName: string;
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
}, },
}} }}
> >
<Section> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column> <Section>
<Img <Section className="mb-4">
className="h-42 mx-auto" <Column align="center">
src={getAssetUrl('/static/document.png')} <Text className="text-base font-semibold text-[#7AC455]">
alt="Documenso" <Img
/> src={getAssetUrl('/static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Completed
</Text>
</Column> </Column>
</Section>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed
</Text>
<Text className="text-primary mb-0 text-center text-lg font-semibold"> <Text className="text-primary mb-0 text-center text-lg font-semibold">
{documentName} was signed by all signers {documentName} was signed by all signers
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
Review Review
</Button> */} </Button> */}
<Button <Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink} href={downloadLink}
> >
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" /> <Img
src={getAssetUrl('/static/download.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Download Download
</Button> </Button>
</Section> </Section>

View File

@ -0,0 +1,28 @@
import { Column, Img, Row, Section } from '@react-email/components';
export interface TemplateDocumentImageProps {
assetBaseUrl: string;
className?: string;
}
export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Section className={className}>
<Row className="table-fixed">
<Column />
<Column>
<Img className="h-42 mx-auto" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</Column>
<Column />
</Row>
</Section>
);
};
export default TemplateDocumentImage;

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentInviteProps { export interface TemplateDocumentInviteProps {
inviterName: string; inviterName: string;
inviterEmail: string; inviterEmail: string;
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
signDocumentLink, signDocumentLink,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentInviteProps) => { }: TemplateDocumentInviteProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
}, },
}} }}
> >
<Section className="mt-4"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section> <Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">

View File

@ -1,7 +1,9 @@
import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentPendingProps { export interface TemplateDocumentPendingProps {
documentName: string; documentName: string;
assetBaseUrl: string; assetBaseUrl: string;
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
}, },
}} }}
> >
<Section> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column> <Section>
<Img <Section className="mb-4">
className="h-42 mx-auto" <Column align="center">
src={getAssetUrl('/static/document.png')} <Text className="text-base font-semibold text-blue-500">
alt="Documenso" <Img
/> src={getAssetUrl('/static/clock.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Waiting for others
</Text>
</Column> </Column>
</Section>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Waiting for others
</Text>
<Text className="text-primary mb-0 text-center text-lg font-semibold"> <Text className="text-primary mb-0 text-center text-lg font-semibold">
{documentName} has been signed {documentName} has been signed

View File

@ -1,18 +1,20 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentSelfSignedProps { export interface TemplateDocumentSelfSignedProps {
downloadLink: string;
documentName: string; documentName: string;
assetBaseUrl: string; assetBaseUrl: string;
} }
export const TemplateDocumentSelfSigned = ({ export const TemplateDocumentSelfSigned = ({
downloadLink,
documentName, documentName,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentSelfSignedProps) => { }: TemplateDocumentSelfSignedProps) => {
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
const getAssetUrl = (path: string) => { const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString(); return new URL(path, assetBaseUrl).toString();
}; };
@ -27,39 +29,59 @@ export const TemplateDocumentSelfSigned = ({
}, },
}} }}
> >
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Section className="flex-row items-center justify-center"> <Section className="flex-row items-center justify-center">
<div className="flex items-center justify-center p-4"> <Section>
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" /> <Column align="center">
</div> <Text className="text-base font-semibold text-[#7AC455]">
<Img
src={getAssetUrl('/static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Completed
</Text>
</Column>
</Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]"> <Text className="text-primary mb-0 mt-6 text-center text-lg font-semibold">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed
</Text>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
You have signed {documentName} You have signed {documentName}
</Text> </Text>
<Text className="my-1 text-center text-base text-slate-400"> <Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
Check out our plans to access the full suite of features. Create a{' '}
<Link
href={signUpUrl}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time.
</Text> </Text>
<Section className="mb-6 mt-8 text-center"> <Section className="mb-6 mt-8 text-center">
<Button <Button
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" href={signUpUrl}
className="mr-4 rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
>
<Img
src={getAssetUrl('/static/user-plus.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Create account
</Button>
<Button
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href="https://documenso.com/pricing" href="https://documenso.com/pricing"
> >
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" /> <Img
src={getAssetUrl('/static/review.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
View plans View plans
</Button> </Button>
<Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink}
>
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
Download
</Button>
</Section> </Section>
</Section> </Section>
</Tailwind> </Tailwind>

View File

@ -1,7 +1,9 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export type TemplateForgotPasswordProps = { export type TemplateForgotPasswordProps = {
resetPasswordLink: string; resetPasswordLink: string;
assetBaseUrl: string; assetBaseUrl: string;
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
resetPasswordLink, resetPasswordLink,
assetBaseUrl, assetBaseUrl,
}: TemplateForgotPasswordProps) => { }: TemplateForgotPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
}, },
}} }}
> >
<Section className="mt-4 flex-row items-center justify-center"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Forgot your password? Forgot your password?
</Text> </Text>

View File

@ -1,7 +1,9 @@
import { Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateResetPasswordProps { export interface TemplateResetPasswordProps {
userName: string; userName: string;
userEmail: string; userEmail: string;
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
} }
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => { export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
}, },
}} }}
> >
<Section className="mt-4 flex-row items-center justify-center"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Password updated! Password updated!
</Text> </Text>
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
<Text className="my-1 text-center text-base text-slate-400"> <Text className="my-1 text-center text-base text-slate-400">
Your password has been updated. Your password has been updated.
</Text> </Text>
<Section className="mb-6 mt-8 text-center">
<Button
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
>
Sign In
</Button>
</Section>
</Section> </Section>
</Tailwind> </Tailwind>
); );

View File

@ -15,7 +15,7 @@ import {
TemplateDocumentCompleted, TemplateDocumentCompleted,
TemplateDocumentCompletedProps, TemplateDocumentCompletedProps,
} from '../template-components/template-document-completed'; } from '../template-components/template-document-completed';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>; export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;

View File

@ -18,7 +18,7 @@ import {
TemplateDocumentInvite, TemplateDocumentInvite,
TemplateDocumentInviteProps, TemplateDocumentInviteProps,
} from '../template-components/template-document-invite'; } from '../template-components/template-document-invite';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & { export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
customBody?: string; customBody?: string;

View File

@ -15,12 +15,11 @@ import {
TemplateDocumentSelfSigned, TemplateDocumentSelfSigned,
TemplateDocumentSelfSignedProps, TemplateDocumentSelfSignedProps,
} from '../template-components/template-document-self-signed'; } from '../template-components/template-document-self-signed';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps; export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
export const DocumentSelfSignedEmailTemplate = ({ export const DocumentSelfSignedEmailTemplate = ({
downloadLink = 'https://documenso.com',
documentName = 'Open Source Pledge.pdf', documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002', assetBaseUrl = 'http://localhost:3002',
}: DocumentSelfSignedTemplateProps) => { }: DocumentSelfSignedTemplateProps) => {
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
/> />
<TemplateDocumentSelfSigned <TemplateDocumentSelfSigned
downloadLink={downloadLink}
documentName={documentName} documentName={documentName}
assetBaseUrl={assetBaseUrl} assetBaseUrl={assetBaseUrl}
/> />

View File

@ -11,7 +11,7 @@ import {
import config from '@documenso/tailwind-config'; import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
import { import {
TemplateForgotPassword, TemplateForgotPassword,
TemplateForgotPasswordProps, TemplateForgotPasswordProps,

View File

@ -14,7 +14,7 @@ import {
import config from '@documenso/tailwind-config'; import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
import { import {
TemplateResetPassword, TemplateResetPassword,
TemplateResetPasswordProps, TemplateResetPasswordProps,

View File

@ -1,145 +0,0 @@
import { type SentMessageInfo, type Transport } from 'nodemailer';
import type Mail from 'nodemailer/lib/mailer';
import type MailMessage from 'nodemailer/lib/mailer/mail-message';
import { Resend } from 'resend';
const VERSION = '1.0.0';
type ResendTransportOptions = {
apiKey: string;
};
type ResendResponseError = {
statusCode: number;
name: string;
message: string;
};
const isResendResponseError = (error: unknown): error is ResendResponseError => {
// We could use Zod here, but it's not worth the extra bundle size
return (
typeof error === 'object' &&
error !== null &&
'statusCode' in error &&
typeof error.statusCode === 'number' &&
'name' in error &&
typeof error.name === 'string' &&
'message' in error &&
typeof error.message === 'string'
);
};
/**
* Transport for sending email via the Resend SDK.
*/
export class ResendTransport implements Transport<SentMessageInfo> {
public name = 'ResendMailTransport';
public version = VERSION;
private _client: Resend;
private _options: ResendTransportOptions;
public static makeTransport(options: Partial<ResendTransportOptions>) {
return new ResendTransport(options);
}
constructor(options: Partial<ResendTransportOptions>) {
const { apiKey = '' } = options;
this._options = {
apiKey,
};
this._client = new Resend(apiKey);
}
public send(mail: MailMessage, callback: (_err: Error | null, _info: SentMessageInfo) => void) {
if (!mail.data.to || !mail.data.from) {
return callback(new Error('Missing required fields "to" or "from"'), null);
}
this._client
.sendEmail({
subject: mail.data.subject ?? '',
from: this.toResendFromAddress(mail.data.from),
to: this.toResendAddresses(mail.data.to),
cc: this.toResendAddresses(mail.data.cc),
bcc: this.toResendAddresses(mail.data.bcc),
html: mail.data.html?.toString() || '',
text: mail.data.text?.toString() || '',
attachments: this.toResendAttachments(mail.data.attachments),
})
.then((response) => {
if (isResendResponseError(response)) {
throw new Error(`[${response.statusCode}]: ${response.name} ${response.message}`);
}
callback(null, response);
})
.catch((error) => {
callback(error, null);
});
}
private toResendAddresses(addresses: Mail.Options['to']) {
if (!addresses) {
return [];
}
if (typeof addresses === 'string') {
return [addresses];
}
if (Array.isArray(addresses)) {
return addresses.map((address) => {
if (typeof address === 'string') {
return address;
}
return address.address;
});
}
return [addresses.address];
}
private toResendFromAddress(address: Mail.Options['from']) {
if (!address) {
return '';
}
if (typeof address === 'string') {
return address;
}
return `${address.name} <${address.address}>`;
}
private toResendAttachments(attachments: Mail.Options['attachments']) {
if (!attachments) {
return [];
}
return attachments.map((attachment) => {
if (!attachment.filename || !attachment.content) {
throw new Error('Attachment is missing filename or content');
}
if (typeof attachment.content === 'string') {
return {
filename: attachment.filename,
content: Buffer.from(attachment.content),
};
}
if (attachment.content instanceof Buffer) {
return {
filename: attachment.filename,
content: attachment.content,
};
}
throw new Error('Attachment content must be a string or a buffer');
});
}
}

View File

@ -1,10 +1,5 @@
{ {
"extends": "@documenso/tsconfig/react-library.json", "extends": "@documenso/tsconfig/react-library.json",
"compilerOptions": {
"types": [
"@documenso/tsconfig/process-env.d.ts",
]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"], "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
} }

View File

@ -1,5 +1,3 @@
import { APP_BASE_URL } from './app';
/** /**
* The flag name for global session recording feature flag. * The flag name for global session recording feature flag.
*/ */
@ -25,7 +23,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
*/ */
export function extractPostHogConfig(): { key: string; host: string } | null { export function extractPostHogConfig(): { key: string; host: string } | null {
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY; const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const postHogHost = `${APP_BASE_URL}/ingest`; const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
if (!postHogKey || !postHogHost) { if (!postHogKey || !postHogHost) {
return null; return null;

View File

@ -14,6 +14,7 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
}); });
if (!recipient) { if (!recipient) {
console.warn(`No recipient found for token ${token}`);
return; return;
} }

View File

@ -57,13 +57,12 @@ export const setRecipientsForDocument = async ({
return { return {
...recipient, ...recipient,
_persisted: existing, ...existing,
}; };
}) })
.filter((recipient) => { .filter((recipient) => {
return ( return (
recipient._persisted?.sendStatus !== SendStatus.SENT && recipient.sendStatus !== SendStatus.SENT && recipient.signingStatus !== SigningStatus.SIGNED
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
); );
}); });
@ -73,7 +72,7 @@ export const setRecipientsForDocument = async ({
linkedRecipients.map((recipient) => linkedRecipients.map((recipient) =>
prisma.recipient.upsert({ prisma.recipient.upsert({
where: { where: {
id: recipient._persisted?.id ?? -1, id: recipient.id ?? -1,
documentId, documentId,
}, },
update: { update: {

View File

@ -1,15 +0,0 @@
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
-- AddForeignKey
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,8 +0,0 @@
/*
Warnings:
- Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL;

View File

@ -1,5 +1,3 @@
DROP TABLE IF EXISTS "PasswordResetToken" CASCADE;
-- CreateTable -- CreateTable
CREATE TABLE "PasswordResetToken" ( CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,

View File

@ -27,9 +27,7 @@ declare namespace NodeJS {
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string; NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string; NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api'; NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
NEXT_PRIVATE_RESEND_API_KEY?: string;
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string; NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string; NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;

View File

@ -50,7 +50,6 @@
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS", "NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING", "NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
"NEXT_PRIVATE_SMTP_TRANSPORT", "NEXT_PRIVATE_SMTP_TRANSPORT",
"NEXT_PRIVATE_RESEND_API_KEY",
"NEXT_PRIVATE_MAILCHANNELS_API_KEY", "NEXT_PRIVATE_MAILCHANNELS_API_KEY",
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT", "NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
"NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN", "NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN",