Compare commits

..

61 Commits

Author SHA1 Message Date
4d4b011146 chore: add new team members 2023-10-03 14:04:29 +02:00
97dfacd133 fix: Error in Pricing Page Validation for Signup Now Modal (#497)
* fix: signup modal validation on close
* fix: restore auto focus input
2023-10-03 18:26:33 +11:00
87a5bab734 fix: signature text overflow truncated for longer signature texts (#489) 2023-10-03 16:58:11 +11:00
fa61bb660e Merge pull request #438 from documenso/chore/team
chore: and now his watch has ended
2023-10-01 15:41:52 +11:00
83dd079e03 fix: remove unused imports 2023-10-01 15:24:34 +11:00
f7933d8a4d Merge pull request #477 from Hallidayo/button-texts
feat: auth button loading texts
2023-10-01 13:42:19 +11:00
4bd0cfd283 capitalise 2023-09-30 17:00:43 +01:00
86f39f3824 text changes on isSubmitting 2023-09-30 16:50:52 +01:00
94216f5219 Merge pull request #474 from adithyaakrishna/fix/scripts 2023-09-29 21:56:13 +00:00
a2e6187dae fix: typo in script
Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
2023-09-30 01:42:09 +05:30
15e3926ce4 Merge pull request #463 from documenso/docs/minor-readme-updatess
docs: readme updates.
2023-09-30 00:48:38 +10:00
69738d7ed3 Merge branch 'feat/refresh' into docs/minor-readme-updatess 2023-09-30 00:48:30 +10:00
649620c1c9 fix: update compose scripts 2023-09-30 00:47:50 +10:00
99df006019 fix: further readme updates 2023-09-30 00:43:45 +10:00
be40e28f47 Update README.md 2023-09-29 14:42:26 +02:00
8899d82e1b Merge pull request #462 from adithyaakrishna/issue#461
fix: sharp corners of signing card
2023-09-29 18:12:00 +10:00
0527641a4f fix: dogfood resend transport 2023-09-29 12:17:41 +10:00
54e32af1d4 docs: clearer readme? maybe? 2023-09-28 22:48:02 +00:00
b1645ec09a fix: sharp corners of signing card 2023-09-29 02:01:04 +05:30
5b9ce55a6d Merge pull request #457 from documenso/feat-early-adopters
chore: sync
2023-09-28 21:29:09 +02:00
833584bd4f Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 21:28:41 +02:00
ca52a22bcd chore: sync 2023-09-28 14:59:41 +02:00
ba611b9adb Merge pull request #440 from PeterKwesiAnsah/chore/add-empty-recipientlist-message
chore: add empty message
2023-09-28 22:12:21 +10:00
e8643850a0 Merge pull request #397 from documenso/feat-early-adopters
feat: early adopters
2023-09-28 13:16:21 +02:00
bec7376fe8 Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 13:16:09 +02:00
7363bbc8f7 chore: sync next 2023-09-28 13:15:56 +02:00
d0f679e177 Merge pull request #430 from documenso/feat/update-email-templates
feat: update email templates
2023-09-28 20:58:22 +10:00
e9769904ab Merge pull request #453 from documenso/fix/443-validation-with-multiple-recipients
fix: add hack for root zod validation with hook form
2023-09-28 20:57:21 +10:00
a2746742a7 Merge remote-tracking branch 'origin/feat/refresh' into feat-early-adopters 2023-09-28 12:35:10 +02:00
459e3dc45b Merge pull request #400 from documenso/feat/open-early-adopters
feat: add early adopters graph
2023-09-28 12:17:03 +02:00
21fedab707 Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-28 12:16:27 +02:00
323f1974ab chore: added classname and changed typo 2023-09-28 11:13:02 +01:00
5bc9d625c2 Merge pull request #455 from documenso/fix/432-signee-doc-version-doesnt-have-sticky-signing-area
fix: resolve issues with signing document stickiness
2023-09-28 17:50:04 +10:00
4885cf5154 fix: resolve issues with signing document stickiness 2023-09-28 17:42:01 +10:00
2f66eca925 Merge pull request #454 from documenso/fix/446-cancel-cta-does-nothing-when-a-signer-opens-the-document
fix: disable cancel button when there is no window history
2023-09-28 17:15:41 +10:00
af042a62cd fix: disable cancel button when there is no window history 2023-09-28 15:45:22 +10:00
2bfc2b0c1f fix: add hack for root zod validation with hook form 2023-09-28 15:32:03 +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
9bdff9a61f feat: add resend mail transport 2023-09-28 12:27:04 +10:00
f003a2864e chore: add empty message 2023-09-27 13:08:59 +01:00
ab26af19b8 Merge branch 'feat/refresh' into chore/team 2023-09-27 13:09:26 +02:00
dc512600dc chore: and now his watch has ended 2023-09-27 13:08:19 +02: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
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
d950634aab Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-25 09:28:04 +02:00
d0fad4775a feat: add extra info for the early adopters 2023-09-23 12:02:37 +00:00
0732aa317d chore: rename community plan to early adopters 2023-09-22 18:54:25 +02:00
eef63c9adc refactor: metrics into reusable component 2023-09-22 14:08:25 +00:00
c3d52c68e7 feat: add early adopters graph 2023-09-22 13:27:10 +00:00
190ae18edc chore: grammerly 2023-09-21 15:09:20 +02:00
82ffbeeb21 chore: links 2023-09-21 15:01:02 +02:00
9affd7b7fa Merge branch 'feat/refresh' into feat-early-adopters 2023-09-21 14:54:45 +02:00
897ba629df feat: early adopter article 2023-09-21 14:53:48 +02:00
80 changed files with 761 additions and 393 deletions

View File

@ -50,7 +50,9 @@ 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 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= 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=

245
README.md
View File

@ -1,8 +1,6 @@
<p align="center" style="margin-top: 120px"> <img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<a href="https://github.com/documenso/documenso">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/ea8de2d0-8e33-4527-8a5c-40780142a2ed" alt="Documenso Logo">
</a>
<p align="center" style="margin-top: 20px">
<p align="center"> <p align="center">
The Open Source DocuSign Alternative. The Open Source DocuSign Alternative.
<br> <br>
@ -16,24 +14,29 @@
<a href="https://github.com/documenso/documenso/issues">Issues</a> <a href="https://github.com/documenso/documenso/issues">Issues</a>
· ·
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a> <a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
·
<a href="https://documen.so/launches">Upcoming Launches</a>
</p> </p>
</p> </p>
<p align="center"> <p align="center">
<a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a> <a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a>
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a> <a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a> <a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a> <a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
</a>
</p> </p>
> 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>. > 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
> >
> The code can 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 released 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/malfunction-mania) # Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
<div> <div>
<img style="display: block; height: 120px; width: 24%" <img style="display: block; height: 120px; width: 24%"
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)"> src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
@ -49,19 +52,27 @@
## About this project ## About this project
Signing documents digitally is fast and easy and should be the best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. Signing documents digitally should be fast and easy and should be the best practice for every document signed worldwide.
This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work.
Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood.
Join us in creating the next generation of open trust infrastructure.
## Recognition ## Recognition
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The&#0032;open&#0032;source&#0032;DocuSign&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <p align="center">
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The&#0032;open&#0032;source&#0032;DocuSign&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p>
## Community and Next Steps 🎯 ## Community and Next Steps 🎯
We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon. We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it - Check out the first source code release in this repository and test it
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions) - Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions)
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members - Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members
- ⭐ the repository to help us raise awareness - ⭐ the repository to help us raise awareness
- Spread the word on Twitter that Documenso is working towards a more open signing tool - Spread the word on Twitter that Documenso is working towards a more open signing tool
@ -77,62 +88,70 @@ Contact us if you are interested in our Enterprise plan for large organizations
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a> <a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Activity ## Tech Stack
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg) - [Typescript](https://www.typescriptlang.org/) - Language
- [Next.js](https://nextjs.org/) - Framework
- [Prisma](https://www.prisma.io/) - ORM
- [Tailwind](https://tailwindcss.com/) - CSS
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
- [NextAuth.js](https://next-auth.js.org/) - Authentication
- [react-email](https://react.email/) - Email Templates
- [tRPC](https://trpc.io/) - API
- [Node SignPDF](https://github.com/vbuch/node-signpdf) - Digital Signature
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
- [Stripe](https://stripe.com/) - Payments
- [Vercel](https://vercel.com) - Hosting
# Tech <!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
Documenso is built using awesome open source tech including: ## Local Development
- [Typescript](https://www.typescriptlang.org/) ### Requirements
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
- [Postgres SQL (Database)](https://www.postgresql.org/)
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
- [Tailwind CSS (Styling)](https://tailwindcss.com/)
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
- Check out `/package.json` and `/apps/web/package.json` for more
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
# Getting Started To run Documenso locally you will need
## Requirements - Node.js
- Postgres SQL Database
- Docker (optional)
To run Documenso locally you need ### Developer Quickstart
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- Node Package Manager NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine. > **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
Want to get up and running quickly? Follow these steps: Want to get up and running quickly? Follow these steps:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. 1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh ```sh
git clone https://github.com/documenso/documenso git clone https://github.com/documenso/documenso
``` ```
- Set up your `.env` file using the recommendations in the `.env.example` file. 2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively just run `cp .env.example .env` to get started with our handpicked defaults.
- Run `npm run dx` in the root directory
- This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory
- Want it even faster? Just use
```sh
npm run d
```
That's it! You should now be able to access the app at http://localhost:3000
Incoming mail will be available at http://localhost:9000 3. Run `npm run dx` in the root directory
Your database will also be available on port `54320`. You can connect to it using your favorite database client. - This will spin up a postgres database and inbucket mailserver in a docker container.
4. Run `npm run dev` in the root directory
5. Want it even faster? Just use
```sh
npm run d
```
#### Access Points for Your Application
1. **App** - http://localhost:3000
2. **Incoming Mail Access** - http://localhost:9000
3. **Database Connection Details**
- **Port**: 54320
- **Connection**: Use your favorite database client to connect using the provided port.
## Developer Setup ## Developer Setup
@ -140,33 +159,37 @@ Your database will also be available on port `54320`. You can connect to it usin
Follow these steps to setup documenso on you local machine: Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. 1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso ```sh
``` git clone https://github.com/documenso/documenso
- Run `npm i` in root directory ```
- Rename `.env.example` to `.env`
- Set DATABASE_URL value in .env file 2. Run `npm i` in root directory
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommended) 3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
- Create the database scheme by running `db-migrate:dev`
- Setup your mail provider 4. Set the following environement variables.
- Set `SENDGRID_API_KEY` value in .env file - NEXTAUTH_URL
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/). - NEXTAUTH_SECRET
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP - NEXT_PUBLIC_WEBAPP_URL
\_ - NEXT_PUBLIC_MARKETING_URL
* variables` in your .env - NEXT_PRIVATE_DATABASE_URL
- Run `npm run dev` root directory to start - NEXT_PRIVATE_DIRECT_DATABASE_URL
- Register a new user at http://localhost:3000/signup - NEXT_PRIVATE_SMTP_FROM_NAME
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
5. Create the database schema by running `npm run prisma:migrate-dev -w @documenso/prisma`
6. Run `npm run dev` root directory to start
7. Register a new user at http://localhost:3000/signup
--- ---
- Optional: Seed the database using `npm run db-seed` to create a test user and document - Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
- Optional: Create your own signing certificate - Optional: Create your own signing certificate
- A demo certificate is provided in `/app/web/resources/certificate.p12` - To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](./SIGNING.md)**.
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
### Run in Gitpod ### Run in Gitpod
@ -174,81 +197,39 @@ Follow these steps to setup documenso on you local machine:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso)
## Updating ### Run in DevContainer
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client We support DevContainers for VSCode. [Click here to get started.](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso)
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
```
- This is not necessary on first clone.
# Creating your own signing certificate ## Docker
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one: 🚧 Docker containers and images are current in progress. We are actively working on bringing a simple docker build and publish pipeline for Documenso.
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key: ## Self Hosting
`openssl genrsa -out private.key 2048`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate `/apps/web/resources/certificate.p12`
# Docker
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- cd into `docker` directory
- Make `build.sh` executable by running `chmod +x build.sh`
- Run `./build.sh` to start building the docker image.
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
```
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
```
Command Breakdown:
- `-d` - Let's you run the container in background
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
- `-v` - Volume let's you persist the data
- `--name` - Name of the container
- `documenso:latest` - Image you have built
# Deployment
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates! We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
## Railway > Please note the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
### Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX) [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render ### Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
# Troubleshooting ## Troubleshooting
## I'm not receiving any emails when using the developer quickstart ### I'm not receiving any emails when using the developer quickstart
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view. When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500. The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
## Support IPv6 ### Support IPv6
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the Next.js start command
For local docker run For local docker run
@ -270,5 +251,9 @@ containers:
- start - start
- -- - --
- -H - -H
- "::" - '::'
``` ```
## Repo Activity
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg)

55
SIGNING.md Normal file
View File

@ -0,0 +1,55 @@
# Creating your own signing certificate
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
`openssl genrsa -out private.key 2048`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate `/apps/web/resources/certificate.p12`
## Docker
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- cd into `docker` directory
- Make `build.sh` executable by running `chmod +x build.sh`
- Run `./build.sh` to start building the docker image.
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
```
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
```
Command Breakdown:
- `-d` - Let's you run the container in background
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
- `-v` - Volume let's you persist the data
- `--name` - Name of the container
- `documenso:latest` - Image you have built
## Deployment
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
## Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)

View File

@ -0,0 +1,67 @@
---
title: The Early Adopters Plan
description: Launch Week Day 4 and we are still early! Early enough for you to get a sweet deal for supporting Documenso's Mission. Join the movement and get a shiny early adopter account in the process.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-09-28
tags:
- Paid Plan
- Metrics
- Open Startup
---
<figure>
<MdxNextImage
src="/blog/early.png"
width="1260"
height="630"
alt="XKCD: Bug"
/>
<figcaption className="text-center">
"Being early is, uh, good." -Unknown
</figcaption>
</figure>
> TLDR; we have 100 early adopter accounts available at a great price. [Secure yours now](https://documenso.com/pricing).
## Community-Driven Development
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.
As we want to be a highly community-driven product, we want to capture the thoughts and ideas of said community in two ways:
1. Everything we plan (i.e. documen.so/roadmap) and build (i.e. documen.so/repo) is public and open to comments and suggestions from everyone by design. We plan to have discussions, streams, and articles showcasing what we're up to, encouraging feedback and iterations on our plans.
2. We're looking for 100 die-hard early adopters to get more deep hands-on feedback. If you want to be among the first to use and help shape Documenso, we have a special offer for you:
## The Early Adopter Plan
- All first 100 signups are entitled to the early adopter plan.
- The plan includes everything we build in the next 12 months and unlimited<sup>1</sup> signatures
- This plan is priced at $30/mo.
- No matter what we add, the price is guaranteed for life as a thank you for supporting Documenso's mission<sup>2</sup>. And we plan to add a lot.
- This also includes unlimited users<sup>3</sup> as part of the upcoming support for teams.
- If you already claimed an early adopter account in the past and canceled, we are happy to reactivate your account with the early adopter pricing. Reach out to support@documenso.com
## Being an Early Adopter
Being eligible for the early adopter plan has no formal requirements like giving feedback or being active in the community. In good faith, we assume you sign the Documenso Supporter Pledge to be part of this. And if you want to use the newest version quietly, that's fine, too. Everything the die-hard community brings up will be carefully considered and prioritized <sup>4</sup>. Also, there will obviously be limited edition merch available for the first 100.
Documenso currently runs the community reviewed 0.9.1 version. Getting from here to the globally loved and adopted signing tool we all deserve will take a lot of work, and we want you on board to help us create it. Join us in shaping the future of open signing and having fun doing it. [Malfunction Mania](https://documenso.com/blog/malfunction-mania) and releasing 1.0 will go a long way.
## Extending our open metrics
As part of our ongoing effort to be open and transparent in our doing, we are adding "Early Adopters" to our [/open page](https://documenso.com/open) page. After we exceed the early adopter slots, this metric will transition to "Customers". When no more early adopter seats can be claimed, the early adopter plan will transition to a standard paid plan. It will still be priced at $30/mo., but will no longer include upcoming features or unlimited seats. You can [claim your early adopter account here](https://documenso.com/pricing).
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
Best from Hamburg\
Timur
\
[1] Excluding signatures, we have to pay for i.e. third-party QES
[2] The Documenso Manifest
[3] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com. But it should be fine if you don't plan to onboard a huge enterprise.
[4] We won't be able to build everything everyone asks for. But we firmly plan to listen and build in a way everyone's requirements are met as well as possible.

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. 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: Shop and Limited Edition "Mania" Shirt title: Merch Mania
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:
- Testing - Merch
- 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 to get one. > 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 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. 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 ## 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. 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> <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 to get the shirt ## 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: 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,7 +56,9 @@ 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 documenso - Review existing or create missing documentation
- ...
**[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.** 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); 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: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { StargazersType } from './page'; export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
data: T;
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues'; metricKey: keyof T[string];
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
data: StargazersType;
metricKey: MetricsDataKey;
title: string; title: string;
label: string; label: string;
chartHeight?: number; chartHeight?: number;
extraInfo?: JSX.Element;
}; };
export const GithubMetric = ({ export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
className, className,
data, data,
metricKey, metricKey,
title, title,
label, label,
chartHeight = 400, chartHeight = 400,
extraInfo,
...props ...props
}: GithubMetricProps) => { }: BarMetricProps<T>) => {
const formattedData = Object.keys(data) const formattedData = Object.keys(data)
.map((key) => ({ .map((key) => ({
month: formatMonth(key), month: formatMonth(key),
@ -36,7 +35,10 @@ export const GithubMetric = ({
return ( return (
<div className={cn('flex flex-col', className)} {...props}> <div className={cn('flex flex-col', className)} {...props}>
<h3 className="px-4 text-lg font-semibold">{title}</h3> <div className="flex items-center px-4">
<h3 className="text-lg font-semibold">{title}</h3>
<span>{extraInfo}</span>
</div>
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow"> <div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
<ResponsiveContainer width="100%" height={chartHeight}> <ResponsiveContainer width="100%" height={chartHeight}>
@ -50,7 +52,7 @@ export const GithubMetric = ({
formatter={(value) => [Number(value), label]} formatter={(value) => [Number(value), label]}
cursor={{ fill: 'hsl(var(--primary) / 10%)' }} cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
/> />
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} /> <Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>

View File

@ -23,22 +23,6 @@ export const TEAM_MEMBERS = [
engagement: 'Part-Time', engagement: 'Part-Time',
joinDate: 'June 6th, 2023', joinDate: 'June 6th, 2023',
}, },
{
name: 'Florent Merian',
role: 'Marketer - III',
salary: 'Project-Based',
location: 'France',
engagement: 'Full-Time',
joinDate: 'July 10th, 2023',
},
{
name: 'Thilo Konzok',
role: 'Designer',
salary: 'Project-Based',
location: 'Germany',
engagement: 'Full-Time',
joinDate: 'April 26th, 2023',
},
{ {
name: 'David Nguyen', name: 'David Nguyen',
role: 'Software Engineer - III', role: 'Software Engineer - III',
@ -47,6 +31,22 @@ export const TEAM_MEMBERS = [
engagement: 'Full-Time', engagement: 'Full-Time',
joinDate: 'July 26th, 2023', joinDate: 'July 26th, 2023',
}, },
{
name: 'Catalin-Marinel Pit',
role: 'Software Engineer - II',
salary: 80_000,
location: 'Romania',
engagement: 'Full-Time',
joinDate: 'September 4th, 2023',
},
{
name: 'Gowdhama Rajan B',
role: 'Designer - III',
salary: 100_000,
location: 'India',
engagement: 'Full-Time',
joinDate: 'October 9th, 2023',
},
]; ];
export const FUNDING_RAISED = [ export const FUNDING_RAISED = [

View File

@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month'; import { formatMonth } from '@documenso/lib/client-only/format-month';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data'; export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
data: Record<string, string | number>[];
};
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>; export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
const formattedData = data.map((item) => ({
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
const formattedData = FUNDING_RAISED.map((item) => ({
amount: Number(item.amount), amount: Number(item.amount),
date: formatMonth(item.date), date: formatMonth(item.date as string),
})); }));
return ( return (

View File

@ -1,12 +1,14 @@
import { z } from 'zod'; import { z } from 'zod';
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
import { MetricCard } from '~/app/(marketing)/open/metric-card'; import { MetricCard } from '~/app/(marketing)/open/metric-card';
import { SalaryBands } from '~/app/(marketing)/open/salary-bands'; import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
import { BarMetric } from './bar-metrics';
import { CapTable } from './cap-table'; import { CapTable } from './cap-table';
import { FundingRaised } from './funding-raised'; import { FundingRaised } from './funding-raised';
import { GithubMetric } from './gh-metrics';
import { TeamMembers } from './team-members'; import { TeamMembers } from './team-members';
import { OpenPageTooltip } from './tooltip';
export const revalidate = 3600; export const revalidate = 3600;
@ -29,9 +31,16 @@ const ZStargazersLiveResponse = z.record(
}), }),
); );
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>; const ZEarlyAdoptersResponse = z.record(
z.object({
id: z.number(),
time: z.string().datetime(),
earlyAdopters: z.number(),
}),
);
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse; export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
export default async function OpenPage() { export default async function OpenPage() {
const { const {
@ -65,6 +74,14 @@ export default async function OpenPage() {
.then(async (res) => res.json()) .then(async (res) => res.json())
.then((res) => ZStargazersLiveResponse.parse(res)); .then((res) => ZStargazersLiveResponse.parse(res));
const EARLY_ADOPTERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe', {
headers: {
accept: 'application/json',
},
})
.then(async (res) => res.json())
.then((res) => ZEarlyAdoptersResponse.parse(res));
return ( return (
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12"> <div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
@ -107,10 +124,20 @@ export default async function OpenPage() {
<SalaryBands className="col-span-12 lg:col-span-6" /> <SalaryBands className="col-span-12 lg:col-span-6" />
<FundingRaised className="col-span-12 lg:col-span-6" /> <FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
<CapTable className="col-span-12 lg:col-span-6" /> <CapTable className="col-span-12 lg:col-span-6" />
<GithubMetric
<BarMetric<EarlyAdoptersType>
data={EARLY_ADOPTERS_DATA}
metricKey="earlyAdopters"
title="Early Adopters"
label="Early Adopters"
className="col-span-12 lg:col-span-6"
extraInfo={<OpenPageTooltip />}
/>
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="stars" metricKey="stars"
title="Github: Total Stars" title="Github: Total Stars"
@ -118,29 +145,31 @@ export default async function OpenPage() {
className="col-span-12 lg:col-span-6" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric <BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="mergedPRs" metricKey="mergedPRs"
title="Github: Total Merged PRs" title="Github: Total Merged PRs"
label="Merged PRs" label="Merged PRs"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="forks" metricKey="forks"
title="Github: Total Forks" title="Github: Total Forks"
label="Forks" label="Forks"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<GithubMetric
<BarMetric<StargazersType>
data={STARGAZERS_DATA} data={STARGAZERS_DATA}
metricKey="openIssues" metricKey="openIssues"
title="Github: Total Open Issues" title="Github: Total Open Issues"
label="Open Issues" label="Open Issues"
chartHeight={300} chartHeight={300}
className="col-span-12 lg:col-span-4" className="col-span-12 lg:col-span-6"
/> />
<div className="col-span-12 mt-12 flex flex-col items-center justify-center"> <div className="col-span-12 mt-12 flex flex-col items-center justify-center">

View File

@ -0,0 +1,40 @@
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
export function OpenPageTooltip() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
className="ml-2 mt-2.5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
></path>
</svg>
</TooltipTrigger>
<TooltipContent>
<p>
August and earlier: Active subscribers. September and beyond: Numbers of active
subscriptions.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@ -48,9 +48,9 @@ export default function PricingPage() {
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed"> <AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
You can self-host Documenso for free or use our ready-to-use hosted version. The You can self-host Documenso for free or use our ready-to-use hosted version. The
hosted version comes with additional support, painless scalability and more. Early hosted version comes with additional support, painless scalability and more. Early
adopters of the community plan will get access to all features we build this year, for adopters will get access to all features we build this year, for no additional cost!
no additional cost! Forever! Yes, that includes multiple users per account later. If Forever! Yes, that includes multiple users per account later. If you want Documenso
you want Documenso for your enterprise, we are happy to talk about your needs. for your enterprise, we are happy to talk about your needs.
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>

View File

@ -40,7 +40,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
className="rounded-full bg-transparent backdrop-blur-sm" className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Early Adopters Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -55,8 +55,8 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
reset,
} = useForm<TClaimPlanDialogFormSchema>({ } = useForm<TClaimPlanDialogFormSchema>({
mode: 'onBlur',
defaultValues: { defaultValues: {
name: params?.get('name') ?? '', name: params?.get('name') ?? '',
email: params?.get('email') ?? '', email: params?.get('email') ?? '',
@ -91,6 +91,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
} }
}; };
useEffect(() => {
if (!isSubmitting && !open) {
reset();
}
}, [open]);
return ( return (
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}> <Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
@ -139,7 +145,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
</div> </div>
<Button type="submit" size="lg" loading={isSubmitting}> <Button type="submit" size="lg" loading={isSubmitting}>
Claim the Community Plan ( Claim the early adopters Plan (
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */} {/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID {planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly' ? 'Monthly'

View File

@ -114,7 +114,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
className="rounded-full bg-transparent backdrop-blur-sm" className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick} onClick={onSignUpClick}
> >
Get the Community Plan Get the Early Adopters Plan
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs"> <span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever! $30/mo. forever!
</span> </span>

View File

@ -119,7 +119,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
data-plan="community" data-plan="community"
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]" className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
> >
<p className="text-foreground text-4xl font-medium">Community</p> <p className="text-foreground text-4xl font-medium">Early Adopters</p>
<div className="text-primary mt-2.5 text-xl font-medium"> <div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>} {period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
@ -136,12 +136,22 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</ClaimPlanDialog> </ClaimPlanDialog>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</p> <p className="text-foreground py-4 font-medium">
{' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank">
The Early Adopter Deal:
</a>
</p>
<p className="text-foreground py-4">Join the movement</p> <p className="text-foreground py-4">Join the movement</p>
<p className="text-foreground py-4">Simple signing solution</p> <p className="text-foreground py-4">Simple signing solution</p>
<p className="text-foreground py-4">Email and Slack assistance</p> <p className="text-foreground py-4">Email, Discord and Slack assistance</p>
<p className="text-foreground py-4"> <p className="text-foreground py-4">
<strong>Includes all upcoming features</strong> <strong>
{' '}
<a href="https://documenso.com/blog/early-adopters" target="_blank">
Includes all upcoming features
</a>
</strong>
</p> </p>
<p className="text-foreground py-4">Fixed, straightforward pricing</p> <p className="text-foreground py-4">Fixed, straightforward pricing</p>
</div> </div>
@ -168,7 +178,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
</Link> </Link>
<div className="mt-8 flex w-full flex-col divide-y"> <div className="mt-8 flex w-full flex-col divide-y">
<p className="text-foreground py-4 font-medium">Everything in Community, plus:</p> <p className="text-foreground py-4 font-medium">Everything in Early Adopters, plus:</p>
<p className="text-foreground py-4">Custom Subdomain</p> <p className="text-foreground py-4">Custom Subdomain</p>
<p className="text-foreground py-4">Compliance Check</p> <p className="text-foreground py-4">Compliance Check</p>
<p className="text-foreground py-4">Guaranteed Uptime</p> <p className="text-foreground py-4">Guaranteed Uptime</p>

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

@ -189,7 +189,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5" className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3> <h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
<p className="text-muted-foreground mt-2 text-xs"> <p className="text-muted-foreground mt-2 text-xs">
with Timur Ercan & Lucas Smith from Documenso with Timur Ercan & Lucas Smith from Documenso
</p> </p>

View File

@ -29,6 +29,14 @@ 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

@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
}; };
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) { export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
if (!isAdmin(user)) { if (!isAdmin(user)) {
redirect('/documents'); redirect('/documents');

View File

@ -10,7 +10,7 @@ export default function Loading() {
Documents Documents
</Link> </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... 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

@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
redirect('/documents'); redirect('/documents');
} }
const session = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const document = await getDocumentById({ const document = await getDocumentById({
id: documentId, id: documentId,
userId: session.id, userId: user.id,
}).catch(() => null); }).catch(() => null);
if (!document || !document.documentData) { if (!document || !document.documentData) {
@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
const [recipients, fields] = await Promise.all([ const [recipients, fields] = await Promise.all([
await getRecipientsForDocument({ await getRecipientsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
await getFieldsForDocument({ await getFieldsForDocument({
documentId, documentId,
userId: session.id, userId: user.id,
}), }),
]); ]);
@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
Documents Documents
</Link> </Link>
<h1 <h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>
@ -90,7 +87,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
<EditDocumentForm <EditDocumentForm
className="mt-8" className="mt-8"
document={document} document={document}
user={session} user={user}
recipients={recipients} recipients={recipients}
fields={fields} fields={fields}
dataUrl={documentDataUrl} dataUrl={documentDataUrl}

View File

@ -10,7 +10,7 @@ export default function DocumentSentPage() {
Documents Documents
</Link> </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... Loading Document...
</h1> </h1>
</div> </div>

View File

@ -25,7 +25,7 @@ export type DocumentsPageProps = {
}; };
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) { export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const stats = await getStats({ const stats = await getStats({
user, user,

View File

@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({
redirect('/signin'); redirect('/signin');
} }
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<NextAuthProvider session={session}> <NextAuthProvider session={session}>

View File

@ -12,7 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { LocaleDate } from '~/components/formatter/locale-date'; import { LocaleDate } from '~/components/formatter/locale-date';
export default async function BillingSettingsPage() { export default async function BillingSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
const isBillingEnabled = await getServerComponentFlag('app_billing'); const isBillingEnabled = await getServerComponentFlag('app_billing');

View File

@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { PasswordForm } from '~/components/forms/password'; import { PasswordForm } from '~/components/forms/password';
export default async function PasswordSettingsPage() { export default async function PasswordSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<div> <div>

View File

@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { ProfileForm } from '~/components/forms/profile'; import { ProfileForm } from '~/components/forms/profile';
export default async function ProfileSettingsPage() { export default async function ProfileSettingsPage() {
const user = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
return ( return (
<div> <div>

View File

@ -53,7 +53,7 @@ export default async function CompletedSigningPage({
recipient.email; recipient.email;
return ( return (
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44"> <div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
{/* Card with recipient */} {/* Card with recipient */}
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} /> <SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />

View File

@ -106,7 +106,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent position="end">
<DialogHeader> <DialogHeader>
<DialogTitle>Share</DialogTitle> <DialogTitle>Share</DialogTitle>
@ -119,7 +119,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
<span className="font-medium text-blue-400">@documenso</span> <span className="font-medium text-blue-400">@documenso</span>
. Check it out! . Check it out!
<span className="mt-2 block" /> <span className="mt-2 block" />
<span className="font-medium text-blue-400"> <span className="break-all font-medium text-blue-400">
{window.location.origin}/share/{shareLink?.slug || '...'} {window.location.origin}/share/{shareLink?.slug || '...'}
</span> </span>
</div> </div>

View File

@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
@ -27,6 +28,7 @@ export type SigningFormProps = {
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => { export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession();
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
@ -60,7 +62,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
return ( return (
<form <form
className={cn( className={cn(
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6', 'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
{
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
},
)} )}
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
> >
@ -120,6 +126,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" 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

View File

@ -10,11 +10,11 @@ export type SigningLayoutProps = {
}; };
export default async function SigningLayout({ children }: SigningLayoutProps) { export default async function SigningLayout({ children }: SigningLayoutProps) {
const user = await getServerComponentSession(); const { user, session } = await getServerComponentSession();
return ( return (
<NextAuthProvider> <NextAuthProvider session={session}>
<div className="min-h-screen overflow-hidden"> <div className="min-h-screen">
{user && <AuthenticatedHeader user={user} />} {user && <AuthenticatedHeader user={user} />}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main> <main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>

View File

@ -51,7 +51,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.then((buffer) => Buffer.from(buffer).toString('base64')) .then((buffer) => Buffer.from(buffer).toString('base64'))
.then((data) => `data:application/pdf;base64,${data}`); .then((data) => `data:application/pdf;base64,${data}`);
const user = await getServerComponentSession(); const { user } = await getServerComponentSession();
if ( if (
document.status === DocumentStatus.COMPLETED || document.status === DocumentStatus.COMPLETED ||
@ -62,7 +62,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
return ( return (
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}> <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"> <div className="mx-auto w-full max-w-screen-xl">
<h1 className="mt-4 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} {document.title}
</h1> </h1>

View File

@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button';
import NotFoundPartial from '~/components/partials/not-found'; import NotFoundPartial from '~/components/partials/not-found';
export default async function NotFound() { export default async function NotFound() {
const session = await getServerComponentSession(); const { session } = await getServerComponentSession();
return ( return (
<NotFoundPartial> <NotFoundPartial>

View File

@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & {
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => { export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
await setFieldsForDocument({ await setFieldsForDocument({
userId, userId: user.id,
documentId, documentId,
fields: fields.map((field) => ({ fields: fields.map((field) => ({
id: field.nativeId, id: field.nativeId,

View File

@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & {
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => { export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
await setRecipientsForDocument({ await setRecipientsForDocument({
userId, userId: user.id,
documentId, documentId,
recipients: signers.map((signer) => ({ recipients: signers.map((signer) => ({
id: signer.nativeId, id: signer.nativeId,

View File

@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => { export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
'use server'; 'use server';
const { id: userId } = await getRequiredServerComponentSession(); const { user } = await getRequiredServerComponentSession();
if (email.message || email.subject) { if (email.message || email.subject) {
await upsertDocumentMeta({ await upsertDocumentMeta({
@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc
} }
return await sendDocument({ return await sendDocument({
userId, userId: user.id,
documentId, documentId,
}); });
}; };

View File

@ -73,7 +73,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
</div> </div>
<Button size="lg" loading={isSubmitting}> <Button size="lg" loading={isSubmitting}>
Reset Password {isSubmitting ? 'Sending Reset Email...' : 'Reset Password'}
</Button> </Button>
</form> </form>
); );

View File

@ -166,7 +166,7 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
</div> </div>
<Button size="lg" loading={isSubmitting}> <Button size="lg" loading={isSubmitting}>
Reset Password {isSubmitting ? 'Resetting Password...' : 'Reset Password'}
</Button> </Button>
</form> </form>
); );

View File

@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Eye, EyeOff, Loader } from 'lucide-react'; import { Eye, EyeOff } from 'lucide-react';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc'; import { FcGoogle } from 'react-icons/fc';
@ -147,9 +147,13 @@ export const SignInForm = ({ className }: SignInFormProps) => {
<FormErrorMessage className="mt-1.5" error={errors.password} /> <FormErrorMessage className="mt-1.5" error={errors.password} />
</div> </div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90"> <Button
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />} size="lg"
Sign In loading={isSubmitting}
disabled={isSubmitting}
className="dark:bg-documenso dark:hover:opacity-90"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button> </Button>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase"> <div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">

View File

@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Eye, EyeOff, Loader } from 'lucide-react'; import { Eye, EyeOff } from 'lucide-react';
import { signIn } from 'next-auth/react'; import { signIn } from 'next-auth/react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
@ -157,9 +157,13 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
<FormErrorMessage className="mt-1.5" error={errors.signature} /> <FormErrorMessage className="mt-1.5" error={errors.signature} />
</div> </div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90"> <Button
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />} size="lg"
Sign Up loading={isSubmitting}
disabled={isSubmitting}
className="dark:bg-documenso dark:hover:opacity-90"
>
{isSubmitting ? 'Signing up...' : 'Sign Up'}
</Button> </Button>
</form> </form>
); );

View File

@ -0,0 +1,19 @@
name: documenso-services
services:
database:
image: postgres:15
container_name: database
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 54320:5432
inbucket:
image: inbucket/inbucket
container_name: mailserver
ports:
- 9000:9000
- 2500:2500
- 1100:1100

38
package-lock.json generated
View File

@ -1868,6 +1868,17 @@
"resolved": "apps/marketing", "resolved": "apps/marketing",
"link": true "link": true
}, },
"node_modules/@documenso/nodemailer-resend": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-1.0.0.tgz",
"integrity": "sha512-rG+jBbBEsVJUBU6v/2hb+OQD1m3Lhn49TOzQjln73zzL1B/sZsHhYOKpNPlTX0/FafCP7P9fKerndEeIKn54Vw==",
"dependencies": {
"resend": "^1.1.0"
},
"peerDependencies": {
"nodemailer": "^6.9.3"
}
},
"node_modules/@documenso/prettier-config": { "node_modules/@documenso/prettier-config": {
"resolved": "packages/prettier-config", "resolved": "packages/prettier-config",
"link": true "link": true
@ -16958,6 +16969,29 @@
"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",
@ -19771,9 +19805,11 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@documenso/nodemailer-resend": "1.0.0",
"@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

@ -8,7 +8,11 @@
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"", "format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
"prepare": "husky install", "prepare": "husky install",
"commitlint": "commitlint --edit", "commitlint": "commitlint --edit",
"clean": "turbo run clean && rimraf node_modules" "clean": "turbo run clean && rimraf node_modules",
"d": "npm run dx && npm run dev",
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev -w @documenso/prisma",
"dx:up": "docker compose -f docker/compose-services.yml up -d",
"dx:down": "docker compose -f docker/compose-services.yml down"
}, },
"engines": { "engines": {
"npm": ">=8.6.0", "npm": ">=8.6.0",

View File

@ -1,5 +1,7 @@
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { ResendTransport } from '@documenso/nodemailer-resend';
import { MailChannelsTransport } from './transports/mailchannels'; import { MailChannelsTransport } from './transports/mailchannels';
const getTransport = () => { const getTransport = () => {
@ -14,6 +16,14 @@ 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

@ -17,9 +17,11 @@
"worker:test": "tsup worker/index.ts --format esm" "worker:test": "tsup worker/index.ts --format esm"
}, },
"dependencies": { "dependencies": {
"@documenso/nodemailer-resend": "1.0.0",
"@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,5 +1,10 @@
{ {
"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,3 +1,5 @@
import { APP_BASE_URL } from './app';
/** /**
* The flag name for global session recording feature flag. * 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 { 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 = process.env.NEXT_PUBLIC_POSTHOG_HOST; const postHogHost = `${APP_BASE_URL}/ingest`;
if (!postHogKey || !postHogHost) { if (!postHogKey || !postHogHost) {
return null; return null;

View File

@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS); const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) { if (!session || !session.user?.email) {
return null; return { user: null, session: null };
} }
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
@ -24,14 +24,14 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
}, },
}); });
return user; return { user, session };
}; };
export const getServerComponentSession = async () => { export const getServerComponentSession = async () => {
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS); const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
if (!session || !session.user?.email) { if (!session || !session.user?.email) {
return null; return { user: null, session: null };
} }
const user = await prisma.user.findFirstOrThrow({ const user = await prisma.user.findFirstOrThrow({
@ -40,15 +40,15 @@ export const getServerComponentSession = async () => {
}, },
}); });
return user; return { user, session };
}; };
export const getRequiredServerComponentSession = async () => { export const getRequiredServerComponentSession = async () => {
const session = await getServerComponentSession(); const { user, session } = await getServerComponentSession();
if (!session) { if (!user || !session) {
throw new Error('No session found'); throw new Error('No session found');
} }
return session; return { user, session };
}; };

View File

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

View File

@ -17,7 +17,7 @@ import { alphaid } from '../id';
export const getPresignPostUrl = async (fileName: string, contentType: string) => { export const getPresignPostUrl = async (fileName: string, contentType: string) => {
const client = getS3Client(); const client = getS3Client();
const user = await getServerComponentSession(); const { user } = await getServerComponentSession();
// Get the basename and extension for the file // Get the basename and extension for the file
const { name, ext } = path.parse(fileName); const { name, ext } = path.parse(fileName);

View File

@ -3,7 +3,7 @@ import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getServerSession } from '@documenso/lib/next-auth/get-server-session'; import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => { export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
const session = await getServerSession({ req, res }); const { session, user } = await getServerSession({ req, res });
if (!session) { if (!session) {
return { return {
@ -12,9 +12,16 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
}; };
} }
if (!user) {
return {
session: null,
user: null,
};
}
return { return {
session, session,
user: session, user,
}; };
}; };

View File

@ -27,7 +27,9 @@ 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' | '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_API_KEY?: string;
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string; NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;

View File

@ -118,7 +118,7 @@ export const SigningCard3D = ({ className, name, signingCelebrationImage }: Sign
style={{ perspective: 800 }} style={{ perspective: 800 }}
> >
<motion.div <motion.div
className="bg-background w-full [--sheen-color:180_180_180] dark:[--sheen-color:200_200_200]" className="bg-background w-full rounded-lg [--sheen-color:180_180_180] dark:[--sheen-color:200_200_200]"
ref={cardRef} ref={cardRef}
style={{ style={{
perspective: '800', perspective: '800',

View File

@ -11,9 +11,19 @@ const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger; const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => ( const DialogPortal = ({
className,
children,
position = 'start',
...props
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => (
<DialogPrimitive.Portal className={cn(className)} {...props}> <DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center"> <div
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
'items-start': position === 'start',
'items-end': position === 'end',
})}
>
{children} {children}
</div> </div>
</DialogPrimitive.Portal> </DialogPrimitive.Portal>
@ -39,14 +49,14 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
>(({ className, children, ...props }, ref) => ( >(({ className, children, position = 'start', ...props }, ref) => (
<DialogPortal> <DialogPortal position={position}>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'bg-background animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg', 'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
className, className,
)} )}
{...props} {...props}

View File

@ -351,7 +351,11 @@ export const AddFieldsFormPartial = ({
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command> <Command>
<CommandInput /> <CommandInput />
<CommandEmpty /> <CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
No recipient matching this description was found.
</span>
</CommandEmpty>
<CommandGroup> <CommandGroup>
{recipients.map((recipient, index) => ( {recipients.map((recipient, index) => (
@ -421,7 +425,7 @@ export const AddFieldsFormPartial = ({
<CardContent className="flex flex-col items-center justify-center px-6 py-4"> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p <p
className={cn( className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-3xl font-medium', 'text-muted-foreground group-data-[selected]:text-foreground w-full truncate text-3xl font-medium',
fontCaveat.className, fontCaveat.className,
)} )}
> >

View File

@ -126,12 +126,12 @@ export const AddSignersFormPartial = ({
<AnimatePresence> <AnimatePresence>
{signers.map((signer, index) => ( {signers.map((signer, index) => (
<motion.div <motion.div
key={signer.formId} key={signer.id}
data-native-id={signer.nativeId} data-native-id={signer.nativeId}
className="flex flex-wrap items-end gap-x-4" className="flex flex-wrap items-end gap-x-4"
> >
<div className="flex-1"> <div className="flex-1">
<Label htmlFor={`signer-${signer.formId}-email`}> <Label htmlFor={`signer-${signer.id}-email`}>
Email Email
<span className="text-destructive ml-1 inline-block font-medium">*</span> <span className="text-destructive ml-1 inline-block font-medium">*</span>
</Label> </Label>
@ -141,7 +141,7 @@ export const AddSignersFormPartial = ({
name={`signers.${index}.email`} name={`signers.${index}.email`}
render={({ field }) => ( render={({ field }) => (
<Input <Input
id={`signer-${signer.formId}-email`} id={`signer-${signer.id}-email`}
type="email" type="email"
className="bg-background mt-2" className="bg-background mt-2"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)} disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
@ -153,14 +153,14 @@ export const AddSignersFormPartial = ({
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Label htmlFor={`signer-${signer.formId}-name`}>Name</Label> <Label htmlFor={`signer-${signer.id}-name`}>Name</Label>
<Controller <Controller
control={control} control={control}
name={`signers.${index}.name`} name={`signers.${index}.name`}
render={({ field }) => ( render={({ field }) => (
<Input <Input
id={`signer-${signer.formId}-name`} id={`signer-${signer.id}-name`}
type="text" type="text"
className="bg-background mt-2" className="bg-background mt-2"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)} disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
@ -195,7 +195,11 @@ export const AddSignersFormPartial = ({
</AnimatePresence> </AnimatePresence>
</div> </div>
<FormErrorMessage className="mt-2" error={errors.signers} /> <FormErrorMessage
className="mt-2"
// Dirty hack to handle errors when .root is populated for an array type
error={'signers__root' in errors && errors['signers__root']}
/>
<div className="mt-4"> <div className="mt-4">
<Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}> <Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}>

View File

@ -1,19 +1,24 @@
import { z } from 'zod'; import { z } from 'zod';
export const ZAddSignersFormSchema = z.object({ export const ZAddSignersFormSchema = z
signers: z .object({
.array( signers: z.array(
z.object({ z.object({
formId: z.string().min(1), formId: z.string().min(1),
nativeId: z.number().optional(), nativeId: z.number().optional(),
email: z.string().min(1).email(), email: z.string().min(1).email(),
name: z.string(), name: z.string(),
}), }),
) ),
.refine((signers) => { })
const emails = signers.map((signer) => signer.email); .refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
return new Set(emails).size === emails.length; return new Set(emails).size === emails.length;
}, 'Signers must have unique emails'), },
}); // Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
);
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>; export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;

View File

@ -4,13 +4,17 @@ import { cn } from '@documenso/ui/lib/utils';
export type FormErrorMessageProps = { export type FormErrorMessageProps = {
className?: string; className?: string;
error: { message?: string } | undefined; error: { message?: string } | undefined | unknown;
};
const isErrorWithMessage = (error: unknown): error is { message?: string } => {
return typeof error === 'object' && error !== null && 'message' in error;
}; };
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => { export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
return ( return (
<AnimatePresence> <AnimatePresence>
{error && ( {isErrorWithMessage(error) && (
<motion.p <motion.p
initial={{ initial={{
opacity: 0, opacity: 0,

View File

@ -50,6 +50,7 @@
"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",