Compare commits

..

25 Commits

Author SHA1 Message Date
af042a62cd fix: disable cancel button when there is no window history 2023-09-28 15:45:22 +10:00
df8bdda718 Merge pull request #450 from documenso/feat/resend-transport
feat: add resend mail transport
2023-09-28 14:07:18 +10:00
873f99ae86 fix: resolve document title inconsistency (#452) 2023-09-28 13:56:22 +10:00
17fe135027 Merge pull request #451 from documenso/fix/445-signer-name-not-persisting
fix: do not overwrite new names or emails for signers
2023-09-28 13:07:26 +10:00
2eed0ae063 feat: add posthog reverse proxy (#449) 2023-09-28 12:56:53 +10:00
f4ae0389d8 fix: do not overwrite new names or emails for signers 2023-09-28 12:35:21 +10:00
9bdff9a61f feat: add resend mail transport 2023-09-28 12:27:04 +10:00
cdb71c3a62 chore: greetings 2023-09-27 12:30:11 +02:00
39d7b3ca58 Merge branch 'feat/refresh' of https://github.com/documenso/documenso into feat/refresh 2023-09-27 12:19:14 +02:00
748c3636d5 chore: sync shop article and add missing and updated assets 2023-09-27 12:15:44 +02:00
7bd847c0d3 chore: add missing migrations 2023-09-27 16:53:48 +10:00
a20b1e2f6a Merge pull request #350 from documenso/feat/redirect-signed-document
feat: redirect signed document to completed page
2023-09-27 15:09:07 +10:00
572f9d5ad6 chore: styling updates 2023-09-27 14:58:10 +10:00
e49c102e8c fix: lint errors 2023-09-27 09:11:56 +10:00
2348221b03 fix: faster tooltips 2023-09-27 09:09:53 +10:00
a0c327cfcf fix: share og updates 2023-09-27 09:03:53 +10:00
ddbeb9e3db fix: twitter images 2023-09-27 08:02:51 +10:00
e8205c1390 fix: better share links 2023-09-27 07:52:24 +10:00
fbb53fc8c3 chore: update mania shirt res 2023-09-26 22:06:41 +02:00
d7fed5a5dc feat: shop Article 2023-09-26 21:49:04 +02:00
3285881586 chore: typos 2023-09-26 11:39:30 +02:00
3ff61607a2 Merge pull request #370 from documenso/feat/mania
feat: malfunction mania blog post
2023-09-26 11:24:24 +02:00
c2a9647c90 Merge branch 'feat/refresh' into feat/redirect-signed-document 2023-09-17 14:52:44 +00:00
02424596db fix: update for code review 2023-09-17 14:45:22 +00:00
e47ab838c5 chore: remove undefined check 2023-09-06 07:37:03 +00:00
37 changed files with 413 additions and 89 deletions

View File

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

View File

@ -28,12 +28,12 @@
> 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
>
> The code be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.
> The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.
>
> The new Version will be release after the current testing phase.
> The new Version will be released after the current testing phase.
# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/manfunction-mania)
# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
<div>
<img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">

View File

@ -51,4 +51,7 @@ 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.
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.**

View File

@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso.
We redesigned the whole signing flow to make it more appealing and more convenient.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
**We call it happy minimalism.**
We paid particular attention to the moment of signing, which should be celebrated.
We paid particular attention to the moment of signing, which should be celebrated.
The image below is the final bloom of the completion celebration we added:

View File

@ -0,0 +1,64 @@
---
title: Merch Mania
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-27
tags:
- Merch
- Rewrite
- Bounties
---
<figure>
<MdxNextImage
src="/blog/mania-shirt.png"
width="1260"
height="630"
alt="Malfunctioning Mania Themed Documenso Shirt"
/>
<figcaption className="text-center">
The Limited Edition "Malfunction Mania" Shirt - Only during Malfunction Mania
</figcaption>
</figure>
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/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.
## 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.
<figure>
<MdxNextImage
src="/blog/shop.png"
width="1260"
height="630"
alt="Malfunctioning Mania Themed Documenso Shirt"
/>
<figcaption className="text-center">
Merch at Documenso is always given to those who deserve it.
</figcaption>
</figure>
## How earn 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:
- Report a bug with detailed reproduction details
- Fix a bug (you or somebody else reported)
- Analyze and describe a usability or user experience shortcoming
- Test the product in a systematic and least somewhat documented way
- 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)
- Review the version with a video, stream, or screenshots and post about it
- Review existing or create missing documentation
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 updates about Malfunction Mania.**

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -73,7 +73,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
)}
<h2 className="text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
<h2 className="relative z-10 text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
You have signed
<span className="mt-2 block">{document.title}</span>
</h2>
@ -84,17 +84,17 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
signingCelebrationImage={signingCelebration}
/>
<div className="mt-8 w-full">
<div className="relative mt-8 w-full">
<div className={cn('flex flex-col items-center', className)}>
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
{/* TODO: Hook this up */}
<Button variant="outline" className="flex-1" disabled>
<Button variant="outline" className="flex-1 bg-transparent backdrop-blur-sm" disabled>
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DocumentDownloadButton
className="flex-1"
className="flex-1 bg-transparent backdrop-blur-sm"
fileName={document.title}
documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { ImageResponse } from 'next/server';
import { ImageResponse, NextResponse } from 'next/server';
import { P, match } from 'ts-pattern';
@ -21,7 +21,7 @@ type SharePageOpenGraphImageProps = {
params: { slug: string };
};
export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) {
export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) {
const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
getAssetBuffer('/fonts/inter-semibold.ttf'),
getAssetBuffer('/fonts/inter-regular.ttf'),
@ -32,7 +32,7 @@ export default async function Image({ params: { slug } }: SharePageOpenGraphImag
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null);
if (!recipientOrSender) {
return null;
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const isRecipient = 'Signature' in recipientOrSender;

View File

@ -1,11 +1,39 @@
import { Metadata } from 'next';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { Redirect } from './redirect';
import { APP_BASE_URL } from '@documenso/lib/constants/app';
export const metadata: Metadata = {
title: 'Documenso - Share',
type SharePageProps = {
params: { slug: string };
};
export default function SharePage() {
return <Redirect />;
export function generateMetadata({ params: { slug } }: SharePageProps) {
return {
title: 'Documenso - Share',
description: 'I just signed a document with Documenso!',
openGraph: {
title: 'Documenso - Join the open source signing revolution',
description: 'I just signed with Documenso!',
type: 'website',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
},
twitter: {
site: '@documenso',
card: 'summary_large_image',
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
description: 'I just signed with Documenso!',
},
} satisfies Metadata;
}
export default function SharePage() {
const userAgent = headers().get('User-Agent') ?? '';
// https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
return null;
}
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
}

View File

@ -1,11 +0,0 @@
'use client';
import { useEffect } from 'react';
export const Redirect = () => {
useEffect(() => {
window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001';
}, []);
return null;
};

View File

@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { SigningCard } from '@documenso/ui/components/signing-card';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import signingCelebration from '~/assets/signing-celebration.png';
@ -53,11 +53,11 @@ export default async function CompletedSigningPage({
recipient.email;
return (
<div className="flex flex-col items-center pt-24">
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
{/* Card with recipient */}
<SigningCard name={recipientName} signingCelebrationImage={signingCelebration} />
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
<div className="mt-6">
<div className="relative mt-6 flex w-full flex-col items-center">
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<div className="text-documenso-700 flex items-center text-center">
@ -71,41 +71,44 @@ export default async function CompletedSigningPage({
<span className="text-sm">Waiting for others to sign</span>
</div>
))}
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have signed "{document.title}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<ShareButton documentId={document.id} token={recipient.token} />
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</p>
</div>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have signed "{document.title}"
</h2>
{match(document.status)
.with(DocumentStatus.COMPLETED, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
Everyone has signed! You will receive an Email copy of the signed document.
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
You will receive an Email copy of the signed document once everyone has signed.
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<ShareButton documentId={document.id} token={recipient.token} />
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
Check out Documenso.
</Link>
</p>
</div>
);
}

View File

@ -120,6 +120,7 @@ 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"
variant="secondary"
size="lg"
disabled={typeof window !== 'undefined' && window.history.length <= 1}
onClick={() => router.back()}
>
Cancel

View File

@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern';
@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { FieldType } from '@documenso/prisma/client';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -53,6 +53,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const user = await getServerComponentSession();
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
) {
redirect(`/sign/${token}/complete`);
}
return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">

View File

@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({
return (
<TooltipProvider>
<Tooltip>
<Tooltip delayDuration={200}>
<TooltipTrigger className="flex cursor-pointer">
{children || <StackAvatars recipients={recipients} />}
</TooltipTrigger>

26
package-lock.json generated
View File

@ -16958,6 +16958,29 @@
"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": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@ -19773,7 +19796,8 @@
"dependencies": {
"@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3",
"react-email": "^1.9.4"
"react-email": "^1.9.4",
"resend": "^1.1.0"
},
"devDependencies": {
"@documenso/tailwind-config": "*",

View File

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

View File

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

View File

@ -0,0 +1,145 @@
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,5 +1,10 @@
{
"extends": "@documenso/tsconfig/react-library.json",
"compilerOptions": {
"types": [
"@documenso/tsconfig/process-env.d.ts",
]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
"exclude": ["dist", "build", "node_modules"]
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
-- 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

@ -0,0 +1,8 @@
/*
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,3 +1,5 @@
DROP TABLE IF EXISTS "PasswordResetToken" CASCADE;
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL,

View File

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

View File

@ -148,7 +148,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
return (
<Card
className={cn(
'group mx-auto flex aspect-[21/9] w-full items-center justify-center',
'group z-10 mx-auto flex aspect-[21/9] w-full items-center justify-center',
className,
)}
degrees={-145}
@ -180,14 +180,14 @@ type SigningCardImageProps = {
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
return (
<motion.div
className="pointer-events-none absolute -inset-32 -z-10 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
className="pointer-events-none absolute -inset-32 -z-50 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
initial={{
opacity: 0,
scale: 0.8,
scale: 0.6,
}}
animate={{
scale: 1,
opacity: 0.5,
opacity: 0.6,
}}
transition={{
delay: 0.5,
@ -197,7 +197,7 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
<Image
src={signingCelebrationImage}
alt="background pattern"
className="w-full dark:invert dark:sepia"
className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
style={{
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',

View File

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