Compare commits
98 Commits
feat/singl
...
docs/minor
| Author | SHA1 | Date | |
|---|---|---|---|
| 69738d7ed3 | |||
| 649620c1c9 | |||
| 99df006019 | |||
| be40e28f47 | |||
| 8899d82e1b | |||
| 0527641a4f | |||
| 54e32af1d4 | |||
| b1645ec09a | |||
| 5b9ce55a6d | |||
| 833584bd4f | |||
| ca52a22bcd | |||
| ba611b9adb | |||
| e8643850a0 | |||
| bec7376fe8 | |||
| 7363bbc8f7 | |||
| d0f679e177 | |||
| e9769904ab | |||
| a2746742a7 | |||
| 459e3dc45b | |||
| 21fedab707 | |||
| 323f1974ab | |||
| 5bc9d625c2 | |||
| 4885cf5154 | |||
| 2f66eca925 | |||
| af042a62cd | |||
| 2bfc2b0c1f | |||
| df8bdda718 | |||
| 873f99ae86 | |||
| 17fe135027 | |||
| 2eed0ae063 | |||
| f4ae0389d8 | |||
| 9bdff9a61f | |||
| f003a2864e | |||
| cdb71c3a62 | |||
| 39d7b3ca58 | |||
| 748c3636d5 | |||
| 6e2d1fb148 | |||
| 7bd847c0d3 | |||
| 81f9b2f776 | |||
| 334671ef85 | |||
| a20b1e2f6a | |||
| 572f9d5ad6 | |||
| e49c102e8c | |||
| 2348221b03 | |||
| a0c327cfcf | |||
| ddbeb9e3db | |||
| e8205c1390 | |||
| fbb53fc8c3 | |||
| d7fed5a5dc | |||
| 3285881586 | |||
| 3ff61607a2 | |||
| 2a124b03e9 | |||
| e1aa23bc55 | |||
| 818c5c7ba4 | |||
| 668011d1c7 | |||
| 562fd043a9 | |||
| b4781c011c | |||
| 5c7d3d5503 | |||
| 0f2b6c0ebf | |||
| ab5bdfeae4 | |||
| e5fa2f4490 | |||
| 2d2615447e | |||
| 46465acd73 | |||
| 2dea9ec3e7 | |||
| 584d5bd8ea | |||
| e4b6d42672 | |||
| 109ad190cf | |||
| 78498793fa | |||
| b2e916378d | |||
| d950634aab | |||
| a52c19355a | |||
| e67e96333b | |||
| 7aa75b0c64 | |||
| 5615627001 | |||
| 78c55875ef | |||
| d0fad4775a | |||
| 0732aa317d | |||
| fde53e355f | |||
| 8e0a10298e | |||
| eef63c9adc | |||
| c3d52c68e7 | |||
| 190ae18edc | |||
| 82ffbeeb21 | |||
| 9affd7b7fa | |||
| 897ba629df | |||
| eaa3e0a303 | |||
| a283c88d7f | |||
| c2a9647c90 | |||
| 02424596db | |||
| 5809109c05 | |||
| a6400eb6c9 | |||
| 39958ed22c | |||
| 7c4ba1b1ea | |||
| f588897531 | |||
| 5629e08f83 | |||
| 37394c054c | |||
| b7c0df67b1 | |||
| e47ab838c5 |
@ -50,7 +50,9 @@ NEXT_PRIVATE_SMTP_SECURE=
|
||||
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||
# REQUIRED: Defines the email address to use as the from address.
|
||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||
# OPTIONAL: The API key to use for the MailChannels proxy endpoint.
|
||||
# OPTIONAL: The API key to use for Resend.com
|
||||
NEXT_PRIVATE_RESEND_API_KEY=
|
||||
# OPTIONAL: The API key to use for MailChannels.
|
||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
||||
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
||||
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
||||
|
||||
6
.github/dependabot.yml
vendored
@ -9,7 +9,7 @@ updates:
|
||||
labels:
|
||||
- "ci dependencies"
|
||||
- "ci"
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/marketing"
|
||||
@ -19,7 +19,7 @@ updates:
|
||||
labels:
|
||||
- "npm dependencies"
|
||||
- "frontend"
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/apps/web"
|
||||
@ -29,4 +29,4 @@ updates:
|
||||
labels:
|
||||
- "npm dependencies"
|
||||
- "frontend"
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 2
|
||||
|
||||
270
README.md
@ -1,8 +1,6 @@
|
||||
<p align="center" style="margin-top: 120px">
|
||||
<a href="https://github.com/documenso/documenso">
|
||||
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
|
||||
</a>
|
||||
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
|
||||
|
||||
<p align="center" style="margin-top: 20px">
|
||||
<p align="center">
|
||||
The Open Source DocuSign Alternative.
|
||||
<br>
|
||||
@ -16,62 +14,68 @@
|
||||
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
|
||||
·
|
||||
<a href="https://documen.so/launches">Upcoming Launches</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<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/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://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>
|
||||
|
||||
> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.**
|
||||
> 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
|
||||
>
|
||||
> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)**
|
||||
> 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.
|
||||
|
||||
# Documenso 0.9 - Developer Preview
|
||||
# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
|
||||
|
||||
<div>
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570645-167128ee-3e39-4578-85d2-5394d9a0379c.png">
|
||||
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570651-0afd12f8-cfe3-49d1-805e-e495af963d91.png">
|
||||
src="https://github.com/documenso/documenso/assets/1309312/040cfbae-3438-4ca3-87f2-ce52c793dcaf">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570655-328d2279-058d-4a3e-b5c3-5cbd8a1f4e05.png">
|
||||
src="https://github.com/documenso/documenso/assets/1309312/72d445be-41e5-4936-bdba-87ef8e70fa09">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224571617-1f3c2811-c1ac-4d7d-b9b0-4ab183731405.png">
|
||||
src="https://github.com/documenso/documenso/assets/1309312/d7b86c0f-a755-4476-a022-a608db2c4633">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570322-b2c76ea8-7482-4043-ad97-f1221220c591.png">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570325-a8055f24-9826-4a23-b116-4fbb0577581a.png">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224570318-f724bbd9-c394-4bdc-bace-2d78af92de44.png">
|
||||
<img style="display: block; height: 120px; width: 24%"
|
||||
src="https://user-images.githubusercontent.com/1309312/224571539-f019b860-f613-4b20-86e8-4437c5784265.png">
|
||||
src=https://github.com/documenso/documenso/assets/1309312/c0f55116-ab82-433f-a266-f3fc8571d69f">
|
||||
</div>
|
||||
|
||||
> **Note**
|
||||
> This project is currently under community review and will publish it's first production release soon™.
|
||||
|
||||
## About this project
|
||||
|
||||
Signing documents digitally is fast, easy and should be 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
|
||||
|
||||
<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 open source DocuSign 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 Open Source DocuSign 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-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 open source DocuSign 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 Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</p>
|
||||
|
||||
## 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
|
||||
- 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
|
||||
- ⭐ 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
|
||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||
|
||||
## Contributing
|
||||
@ -84,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>
|
||||
|
||||
## Activity
|
||||
## Tech Stack
|
||||
|
||||

|
||||
- [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/)
|
||||
- [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.
|
||||
### Requirements
|
||||
|
||||
# Getting Started
|
||||
To run Documenso locally you will need
|
||||
|
||||
## Requirements
|
||||
- Node.js
|
||||
- Postgres SQL Database
|
||||
- Docker (optional)
|
||||
|
||||
To run Documenso locally you need
|
||||
|
||||
- [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
|
||||
### 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.
|
||||
|
||||
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
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
|
||||
- Set up your `.env` file using the recommendations in the `.env.example` file.
|
||||
- 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
|
||||
```
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
@ -147,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:
|
||||
|
||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
```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
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommended)
|
||||
- Create the database scheme by running `db-migrate:dev`
|
||||
- Setup your mail provider
|
||||
- Set `SENDGRID_API_KEY` value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
|
||||
\_
|
||||
* variables` in your .env
|
||||
- Run `npm run dev` root directory to start
|
||||
- Register a new user at http://localhost:3000/signup
|
||||
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
|
||||
```
|
||||
|
||||
2. Run `npm i` in root directory
|
||||
|
||||
3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
|
||||
|
||||
4. Set the following environement variables.
|
||||
- NEXTAUTH_URL
|
||||
- NEXTAUTH_SECRET
|
||||
- NEXT_PUBLIC_WEBAPP_URL
|
||||
- NEXT_PUBLIC_MARKETING_URL
|
||||
- NEXT_PRIVATE_DATABASE_URL
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL
|
||||
- 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: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
|
||||
|
||||
- Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document
|
||||
- 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](#creating-your-own-signing-certificate)**.
|
||||
- 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)**.
|
||||
|
||||
### Run in Gitpod
|
||||
|
||||
@ -181,81 +197,39 @@ Follow these steps to setup documenso on you local machine:
|
||||
|
||||
[](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
|
||||
- You can do this by running the generate command in `/packages/prisma`:
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
- This is not necessary on first clone.
|
||||
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)
|
||||
|
||||
# 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:
|
||||
|
||||
`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
|
||||
## Self Hosting
|
||||
|
||||
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
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
|
||||
## Render
|
||||
### Render
|
||||
|
||||
[](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.
|
||||
|
||||
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
|
||||
|
||||
@ -277,5 +251,9 @@ containers:
|
||||
- start
|
||||
- --
|
||||
- -H
|
||||
- "::"
|
||||
- '::'
|
||||
```
|
||||
|
||||
## Repo Activity
|
||||
|
||||

|
||||
|
||||
55
SIGNING.md
Normal 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
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
|
||||
## Render
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||
67
apps/marketing/content/blog/early-adopters.mdx
Normal 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.
|
||||
57
apps/marketing/content/blog/malfunction-mania.mdx
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Announcing Malfunction Mania
|
||||
description: Launch Week Day 2 Y'all! We're getting ready to release Documenso 1.0! Join in on the fun of making sure the open-source alternative to DocuSign is on point. We're calling a 'MALFUNCTION MANIA.'
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2023-09-26
|
||||
tags:
|
||||
- Testing
|
||||
- Rewrite
|
||||
- Bounties
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/mm.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="Malfunctioning Documenso Logo inbroken colors"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
We're calling a MALFUNCTION MANIA! 🚨
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; Documenso 1.0 is in the [staging environment](https://documen.so/staging). Go check it out.
|
||||
|
||||
It's been a minute since Lucas proclaimed, ["We're doing a rewrite"](https://documenso.com/blog/why-were-doing-a-rewrite), and many of you have been asking when the new version will be available. I'm happy to say that the wait has come to an end. The work on Documenso 1.0 has reached a level we feel comfortable going into the next phase with. We had a lot of community feedback, contributions, and moral support to get us this far, which is why we're excited to announce the most extensive community project yet:
|
||||
|
||||
As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION MANIA. An enormous, public testing phase, where we invite everyone to try out the new version, hunt down, report, and fix any malfunctions (aka bugs), and give feedback before release. Malfunction Mania will happen alongside our internal testing, and by combining the two, we want to ensure the best possible release we can have for Version 1.0. We know many of you have been eager to contribute; this is your chance (the first of many to come).
|
||||
|
||||
## As part of Malfunction Mania, we're offering special bug bounties
|
||||
|
||||
- We award $25 - $100 per report/ issue/ fix, depending on the severity and if the problem is already known
|
||||
- Bounties will be awarded for fixing reported or other critical issues via accepted Pull Requests (PR)
|
||||
- Just reporting issues in a reproducible way can also be awarded
|
||||
- Smaller but notable contributions like minor issues and documentation will be awarded with exclusive merch as we see fit.
|
||||
|
||||
## What you can do
|
||||
|
||||
- Head over to the [staging environment](https://documen.so/staging), check out the new version and give it a spin;
|
||||
- Check out the [source code](https://github.com/documenso/documenso) on GitHub and look it over;
|
||||
- Spin up the new version locally and try it out.
|
||||
|
||||
## How to get the bounties
|
||||
|
||||
- Report bugs by creating an issue here: [documen.so/issues](https://documen.so/issues);
|
||||
- Fix bugs by creating a Pull Request (PR);
|
||||
- Look over and add missing documentation/ Quickstarts and other useful resources.
|
||||
|
||||
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.**
|
||||
@ -20,11 +20,11 @@ Today, I'm pleased to share with you a preview of the next Documenso.
|
||||
|
||||
We redesigned the whole signing flow to make it more appealing and more convenient.
|
||||
|
||||
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
|
||||
We improved the overall look and feel by making it more elegant and appropriately playful. Focused on the task at hand, but explicitly enjoying doing it.
|
||||
|
||||
**We call it happy minimalism.**
|
||||
|
||||
We paid particular attention to the moment of signing, which should be celebrated.
|
||||
We paid particular attention to the moment of signing, which should be celebrated.
|
||||
|
||||
The image below is the final bloom of the completion celebration we added:
|
||||
|
||||
|
||||
64
apps/marketing/content/blog/shop.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Merch Mania
|
||||
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
|
||||
authorName: 'Timur Ercan'
|
||||
authorImage: '/blog/blog-author-timur.jpeg'
|
||||
authorRole: 'Co-Founder'
|
||||
date: 2023-09-27
|
||||
tags:
|
||||
- Merch
|
||||
- Rewrite
|
||||
- Bounties
|
||||
---
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/mania-shirt.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="Malfunctioning Mania Themed Documenso Shirt"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
The Limited Edition "Malfunction Mania" Shirt - Only during Malfunction Mania
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one.
|
||||
|
||||
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
|
||||
|
||||
## Documenso Merch Shop
|
||||
|
||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
|
||||
|
||||
<figure>
|
||||
<MdxNextImage
|
||||
src="/blog/shop.png"
|
||||
width="1260"
|
||||
height="630"
|
||||
alt="Malfunctioning Mania Themed Documenso Shirt"
|
||||
/>
|
||||
|
||||
<figcaption className="text-center">
|
||||
Merch at Documenso is always given to those who deserve it.
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
## How earn the shirt
|
||||
|
||||
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
|
||||
|
||||
- Report a bug with detailed reproduction details
|
||||
- Fix a bug (you or somebody else reported)
|
||||
- Analyze and describe a usability or user experience shortcoming
|
||||
- Test the product in a systematic and least somewhat documented way
|
||||
- Engage in discussion about the current version and its choices
|
||||
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
|
||||
- Review the version with a video, stream, or screenshots and post about it
|
||||
- Review existing or create missing documentation
|
||||
|
||||
Best from Hamburg
|
||||
Timur
|
||||
|
||||
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.**
|
||||
BIN
apps/marketing/example/cert.p12
Normal file
@ -56,6 +56,14 @@ const config = {
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/ingest/:path*',
|
||||
destination: 'https://eu.posthog.com/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withContentlayer(config);
|
||||
|
||||
BIN
apps/marketing/public/blog/blog-author-lucas.png
Normal file
|
After Width: | Height: | Size: 1007 KiB |
BIN
apps/marketing/public/blog/blog-banner-rewrite.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
apps/marketing/public/blog/designsystem.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
apps/marketing/public/blog/early.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
apps/marketing/public/blog/mania-shirt.png
Normal file
|
After Width: | Height: | Size: 839 KiB |
BIN
apps/marketing/public/blog/mm.png
Normal file
|
After Width: | Height: | Size: 862 KiB |
BIN
apps/marketing/public/blog/shop.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
@ -39,7 +39,7 @@ export default function ContentPage({ params }: { params: { content: string } })
|
||||
const MDXContent = useMDXComponent(post.body.code);
|
||||
|
||||
return (
|
||||
<article className="prose prose-slate mx-auto">
|
||||
<article className="prose dark:prose-invert mx-auto">
|
||||
<MDXContent components={mdxComponents} />
|
||||
</article>
|
||||
);
|
||||
|
||||
@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
import { StargazersType } from './page';
|
||||
|
||||
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues';
|
||||
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
|
||||
data: StargazersType;
|
||||
metricKey: MetricsDataKey;
|
||||
export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
|
||||
data: T;
|
||||
metricKey: keyof T[string];
|
||||
title: string;
|
||||
label: string;
|
||||
chartHeight?: number;
|
||||
extraInfo?: JSX.Element;
|
||||
};
|
||||
|
||||
export const GithubMetric = ({
|
||||
export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
|
||||
className,
|
||||
data,
|
||||
metricKey,
|
||||
title,
|
||||
label,
|
||||
chartHeight = 400,
|
||||
extraInfo,
|
||||
...props
|
||||
}: GithubMetricProps) => {
|
||||
}: BarMetricProps<T>) => {
|
||||
const formattedData = Object.keys(data)
|
||||
.map((key) => ({
|
||||
month: formatMonth(key),
|
||||
@ -36,7 +35,10 @@ export const GithubMetric = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||
@ -50,7 +52,7 @@ export const GithubMetric = ({
|
||||
formatter={(value) => [Number(value), label]}
|
||||
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>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||
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, ...props }: FundingRaisedProps) => {
|
||||
const formattedData = FUNDING_RAISED.map((item) => ({
|
||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||
const formattedData = data.map((item) => ({
|
||||
amount: Number(item.amount),
|
||||
date: formatMonth(item.date),
|
||||
date: formatMonth(item.date as string),
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
||||
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
||||
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
||||
|
||||
import { BarMetric } from './bar-metrics';
|
||||
import { CapTable } from './cap-table';
|
||||
import { FundingRaised } from './funding-raised';
|
||||
import { GithubMetric } from './gh-metrics';
|
||||
import { TeamMembers } from './team-members';
|
||||
import { OpenPageTooltip } from './tooltip';
|
||||
|
||||
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() {
|
||||
const {
|
||||
@ -65,6 +74,14 @@ export default async function OpenPage() {
|
||||
.then(async (res) => res.json())
|
||||
.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 (
|
||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||
<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" />
|
||||
|
||||
<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" />
|
||||
<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}
|
||||
metricKey="stars"
|
||||
title="Github: Total Stars"
|
||||
@ -118,29 +145,31 @@ export default async function OpenPage() {
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
|
||||
<GithubMetric
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="mergedPRs"
|
||||
title="Github: Total Merged PRs"
|
||||
label="Merged PRs"
|
||||
chartHeight={300}
|
||||
className="col-span-12 lg:col-span-4"
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
<GithubMetric
|
||||
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="forks"
|
||||
title="Github: Total Forks"
|
||||
label="Forks"
|
||||
chartHeight={300}
|
||||
className="col-span-12 lg:col-span-4"
|
||||
className="col-span-12 lg:col-span-6"
|
||||
/>
|
||||
<GithubMetric
|
||||
|
||||
<BarMetric<StargazersType>
|
||||
data={STARGAZERS_DATA}
|
||||
metricKey="openIssues"
|
||||
title="Github: Total Open Issues"
|
||||
label="Open Issues"
|
||||
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">
|
||||
|
||||
40
apps/marketing/src/app/(marketing)/open/tooltip.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -48,9 +48,9 @@ export default function PricingPage() {
|
||||
<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
|
||||
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
|
||||
no additional cost! Forever! Yes, that includes multiple users per account later. If
|
||||
you want Documenso for your enterprise, we are happy to talk about your needs.
|
||||
adopters will get access to all features we build this year, for no additional cost!
|
||||
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||
for your enterprise, we are happy to talk about your needs.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
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">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
|
||||
@ -139,7 +139,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
||||
</div>
|
||||
|
||||
<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 */}
|
||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
? 'Monthly'
|
||||
|
||||
@ -114,7 +114,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||
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">
|
||||
$30/mo. forever!
|
||||
</span>
|
||||
|
||||
@ -119,7 +119,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
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]"
|
||||
>
|
||||
<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">
|
||||
<AnimatePresence mode="wait">
|
||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
||||
@ -136,12 +136,22 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</ClaimPlanDialog>
|
||||
|
||||
<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">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">
|
||||
<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 className="text-foreground py-4">Fixed, straightforward pricing</p>
|
||||
</div>
|
||||
@ -168,7 +178,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
||||
</Link>
|
||||
|
||||
<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">Compliance Check</p>
|
||||
<p className="text-foreground py-4">Guaranteed Uptime</p>
|
||||
|
||||
@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
|
||||
},
|
||||
);
|
||||
|
||||
// Todo: Handle `downloadLink`
|
||||
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
||||
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
|
||||
documentName: documentName,
|
||||
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
||||
});
|
||||
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
|
||||
subject: 'Document signed',
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
|
||||
});
|
||||
|
||||
return documentToken;
|
||||
|
||||
@ -73,7 +73,7 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
|
||||
<ConfettiScreen duration={3000} gravity={0.075} initialVelocityY={50} wind={0.005} />
|
||||
)}
|
||||
|
||||
<h2 className="text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
|
||||
<h2 className="relative z-10 text-center text-2xl font-semibold leading-normal md:text-3xl lg:mb-2 lg:text-4xl">
|
||||
You have signed
|
||||
<span className="mt-2 block">{document.title}</span>
|
||||
</h2>
|
||||
@ -84,17 +84,17 @@ export const SinglePlayerModeSuccess = ({ className, document }: SinglePlayerMod
|
||||
signingCelebrationImage={signingCelebration}
|
||||
/>
|
||||
|
||||
<div className="mt-8 w-full">
|
||||
<div className="relative mt-8 w-full">
|
||||
<div className={cn('flex flex-col items-center', className)}>
|
||||
<div className="grid w-full max-w-sm grid-cols-2 gap-4">
|
||||
{/* TODO: Hook this up */}
|
||||
<Button variant="outline" className="flex-1" disabled>
|
||||
<Button variant="outline" className="flex-1 bg-transparent backdrop-blur-sm" disabled>
|
||||
<Share className="mr-2 h-5 w-5" />
|
||||
Share
|
||||
</Button>
|
||||
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
className="flex-1 bg-transparent backdrop-blur-sm"
|
||||
fileName={document.title}
|
||||
documentData={document.documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
|
||||
@ -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"
|
||||
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">
|
||||
with Timur Ercan & Lucas Smith from Documenso
|
||||
</p>
|
||||
|
||||
BIN
apps/web/example/cert.p12
Normal file
@ -29,6 +29,14 @@ const config = {
|
||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||
},
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/ingest/:path*',
|
||||
destination: 'https://eu.posthog.com/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
BIN
apps/web/public/static/user-plus.png
Normal file
|
After Width: | Height: | Size: 367 B |
@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
|
||||
};
|
||||
|
||||
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
if (!isAdmin(user)) {
|
||||
redirect('/documents');
|
||||
|
||||
@ -10,7 +10,7 @@ export default function Loading() {
|
||||
Documents
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
Loading Document...
|
||||
</h1>
|
||||
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||
|
||||
@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
redirect('/documents');
|
||||
}
|
||||
|
||||
const session = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: documentId,
|
||||
userId: session.id,
|
||||
userId: user.id,
|
||||
}).catch(() => null);
|
||||
|
||||
if (!document || !document.documentData) {
|
||||
@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
const [recipients, fields] = await Promise.all([
|
||||
await getRecipientsForDocument({
|
||||
documentId,
|
||||
userId: session.id,
|
||||
userId: user.id,
|
||||
}),
|
||||
await getFieldsForDocument({
|
||||
documentId,
|
||||
userId: session.id,
|
||||
userId: user.id,
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
Documents
|
||||
</Link>
|
||||
|
||||
<h1
|
||||
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
|
||||
title={document.title}
|
||||
>
|
||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
@ -90,7 +87,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
<EditDocumentForm
|
||||
className="mt-8"
|
||||
document={document}
|
||||
user={session}
|
||||
user={user}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
dataUrl={documentDataUrl}
|
||||
|
||||
@ -10,7 +10,7 @@ export default function DocumentSentPage() {
|
||||
Documents
|
||||
</Link>
|
||||
|
||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||
Loading Document...
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@ -25,7 +25,7 @@ export type DocumentsPageProps = {
|
||||
};
|
||||
|
||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const stats = await getStats({
|
||||
user,
|
||||
|
||||
@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({
|
||||
redirect('/signin');
|
||||
}
|
||||
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
return (
|
||||
<NextAuthProvider session={session}>
|
||||
|
||||
@ -12,7 +12,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
|
||||
export default async function BillingSettingsPage() {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const isBillingEnabled = await getServerComponentFlag('app_billing');
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
||||
import { PasswordForm } from '~/components/forms/password';
|
||||
|
||||
export default async function PasswordSettingsPage() {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
||||
import { ProfileForm } from '~/components/forms/profile';
|
||||
|
||||
export default async function ProfileSettingsPage() {
|
||||
const user = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ImageResponse } from 'next/server';
|
||||
import { ImageResponse, NextResponse } from 'next/server';
|
||||
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
@ -21,7 +21,7 @@ type SharePageOpenGraphImageProps = {
|
||||
params: { slug: string };
|
||||
};
|
||||
|
||||
export default async function Image({ params: { slug } }: SharePageOpenGraphImageProps) {
|
||||
export async function GET(_request: Request, { params: { slug } }: SharePageOpenGraphImageProps) {
|
||||
const [interSemiBold, interRegular, caveatRegular, shareFrameImage] = await Promise.all([
|
||||
getAssetBuffer('/fonts/inter-semibold.ttf'),
|
||||
getAssetBuffer('/fonts/inter-regular.ttf'),
|
||||
@ -32,7 +32,7 @@ export default async function Image({ params: { slug } }: SharePageOpenGraphImag
|
||||
const recipientOrSender = await getRecipientOrSenderByShareLinkSlug({ slug }).catch(() => null);
|
||||
|
||||
if (!recipientOrSender) {
|
||||
return null;
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isRecipient = 'Signature' in recipientOrSender;
|
||||
@ -1,11 +1,39 @@
|
||||
import { Metadata } from 'next';
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { Redirect } from './redirect';
|
||||
import { APP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Documenso - Share',
|
||||
type SharePageProps = {
|
||||
params: { slug: string };
|
||||
};
|
||||
|
||||
export default function SharePage() {
|
||||
return <Redirect />;
|
||||
export function generateMetadata({ params: { slug } }: SharePageProps) {
|
||||
return {
|
||||
title: 'Documenso - Share',
|
||||
description: 'I just signed a document with Documenso!',
|
||||
openGraph: {
|
||||
title: 'Documenso - Join the open source signing revolution',
|
||||
description: 'I just signed with Documenso!',
|
||||
type: 'website',
|
||||
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
|
||||
},
|
||||
twitter: {
|
||||
site: '@documenso',
|
||||
card: 'summary_large_image',
|
||||
images: [`${APP_BASE_URL}/share/${slug}/opengraph`],
|
||||
description: 'I just signed with Documenso!',
|
||||
},
|
||||
} satisfies Metadata;
|
||||
}
|
||||
|
||||
export default function SharePage() {
|
||||
const userAgent = headers().get('User-Agent') ?? '';
|
||||
|
||||
// https://stackoverflow.com/questions/47026171/how-to-detect-bots-for-open-graph-with-user-agent
|
||||
if (/bot|facebookexternalhit|WhatsApp|google|bing|duckduckbot|MetaInspector/i.test(userAgent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
redirect(process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001');
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Redirect = () => {
|
||||
useEffect(() => {
|
||||
window.location.href = process.env.NEXT_PUBLIC_MARKETING_URL ?? 'http://localhost:3001';
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { SigningCard } from '@documenso/ui/components/signing-card';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
|
||||
import signingCelebration from '~/assets/signing-celebration.png';
|
||||
|
||||
@ -53,11 +53,11 @@ export default async function CompletedSigningPage({
|
||||
recipient.email;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center pt-24">
|
||||
<div className="-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 */}
|
||||
<SigningCard name={recipientName} signingCelebrationImage={signingCelebration} />
|
||||
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative mt-6 flex w-full flex-col items-center">
|
||||
{match(document.status)
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<div className="text-documenso-700 flex items-center text-center">
|
||||
@ -71,41 +71,44 @@ export default async function CompletedSigningPage({
|
||||
<span className="text-sm">Waiting for others to sign</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||
You have signed "{document.title}"
|
||||
</h2>
|
||||
|
||||
{match(document.status)
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||
Everyone has signed! You will receive an Email copy of the signed document.
|
||||
</p>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||
You will receive an Email copy of the signed document once everyone has signed.
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||
<ShareButton documentId={document.id} token={recipient.token} />
|
||||
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-36 text-sm">
|
||||
Want to send slick signing links like this one?{' '}
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="text-documenso-700 hover:text-documenso-600"
|
||||
>
|
||||
Check out Documenso.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||
You have signed "{document.title}"
|
||||
</h2>
|
||||
|
||||
{match(document.status)
|
||||
.with(DocumentStatus.COMPLETED, () => (
|
||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||
Everyone has signed! You will receive an Email copy of the signed document.
|
||||
</p>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
|
||||
You will receive an Email copy of the signed document once everyone has signed.
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||
<ShareButton documentId={document.id} token={recipient.token} />
|
||||
|
||||
<DocumentDownloadButton
|
||||
className="flex-1"
|
||||
fileName={document.title}
|
||||
documentData={documentData}
|
||||
disabled={document.status !== DocumentStatus.COMPLETED}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground/60 mt-36 text-sm">
|
||||
Want to send slick signing links like this one?{' '}
|
||||
<Link href="https://documenso.com" className="text-documenso-700 hover:text-documenso-600">
|
||||
Check out Documenso.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes, useState } from 'react';
|
||||
|
||||
import { Share } from 'lucide-react';
|
||||
import { Copy, Share, Twitter } from 'lucide-react';
|
||||
|
||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||
@ -19,14 +28,36 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
||||
const { toast } = useToast();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const { mutateAsync: createOrGetShareLink, isLoading } =
|
||||
trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onShareClick = async () => {
|
||||
const { slug } = await createOrGetShareLink({
|
||||
token: token,
|
||||
documentId,
|
||||
});
|
||||
const {
|
||||
mutateAsync: createOrGetShareLink,
|
||||
data: shareLink,
|
||||
isLoading,
|
||||
} = trpc.shareLink.createOrGetShareLink.useMutation();
|
||||
|
||||
const onOpenChange = (nextOpen: boolean) => {
|
||||
if (nextOpen) {
|
||||
void createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
}
|
||||
|
||||
setIsOpen(nextOpen);
|
||||
};
|
||||
|
||||
const onCopyClick = async () => {
|
||||
let { slug = '' } = shareLink || {};
|
||||
|
||||
if (!slug) {
|
||||
const result = await createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
|
||||
slug = result.slug;
|
||||
}
|
||||
|
||||
await copyToClipboard(`${window.location.origin}/share/${slug}`).catch(() => null);
|
||||
|
||||
@ -34,18 +65,82 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
||||
title: 'Copied to clipboard',
|
||||
description: 'The sharing link has been copied to your clipboard.',
|
||||
});
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onTweetClick = async () => {
|
||||
let { slug = '' } = shareLink || {};
|
||||
|
||||
if (!slug) {
|
||||
const result = await createOrGetShareLink({
|
||||
token,
|
||||
documentId,
|
||||
});
|
||||
|
||||
slug = result.slug;
|
||||
}
|
||||
|
||||
window.open(
|
||||
generateTwitterIntent(
|
||||
`I just ${token ? 'signed' : 'sent'} a document with @documenso. Check it out!`,
|
||||
`${window.location.origin}/share/${slug}`,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
disabled={!token || !documentId}
|
||||
loading={isLoading}
|
||||
onClick={onShareClick}
|
||||
>
|
||||
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
||||
Share
|
||||
</Button>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!token || !documentId}
|
||||
className="flex-1"
|
||||
loading={isLoading}
|
||||
>
|
||||
{!isLoading && <Share className="mr-2 h-5 w-5" />}
|
||||
Share
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="end">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share</DialogTitle>
|
||||
|
||||
<DialogDescription className="mt-4">Share your signing experience!</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="rounded-md border p-4">
|
||||
I just {token ? 'signed' : 'sent'} a document with{' '}
|
||||
<span className="font-medium text-blue-400">@documenso</span>
|
||||
. Check it out!
|
||||
<span className="mt-2 block" />
|
||||
<span className="break-all font-medium text-blue-400">
|
||||
{window.location.origin}/share/{shareLink?.slug || '...'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
|
||||
<Twitter className="mr-2 h-4 w-4" />
|
||||
Tweet
|
||||
</Button>
|
||||
|
||||
<div className="relative flex items-center justify-center gap-x-4 py-4 text-xs uppercase">
|
||||
<div className="bg-border h-px flex-1" />
|
||||
<span className="text-muted-foreground bg-transparent">Or</span>
|
||||
<div className="bg-border h-px flex-1" />
|
||||
</div>
|
||||
|
||||
<Button variant="outline" onClick={onCopyClick}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
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) => {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||
|
||||
@ -60,7 +62,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
return (
|
||||
<form
|
||||
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)}
|
||||
>
|
||||
@ -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"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@ -10,11 +10,11 @@ export type SigningLayoutProps = {
|
||||
};
|
||||
|
||||
export default async function SigningLayout({ children }: SigningLayoutProps) {
|
||||
const user = await getServerComponentSession();
|
||||
const { user, session } = await getServerComponentSession();
|
||||
|
||||
return (
|
||||
<NextAuthProvider>
|
||||
<div className="min-h-screen overflow-hidden">
|
||||
<NextAuthProvider session={session}>
|
||||
<div className="min-h-screen">
|
||||
{user && <AuthenticatedHeader user={user} />}
|
||||
|
||||
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { notFound, redirect } from 'next/navigation';
|
||||
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
@ -51,11 +51,18 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
.then((buffer) => Buffer.from(buffer).toString('base64'))
|
||||
.then((data) => `data:application/pdf;base64,${data}`);
|
||||
|
||||
const user = await getServerComponentSession();
|
||||
const { user } = await getServerComponentSession();
|
||||
|
||||
if (
|
||||
document.status === DocumentStatus.COMPLETED ||
|
||||
recipient.signingStatus === SigningStatus.SIGNED
|
||||
) {
|
||||
redirect(`/sign/${token}/complete`);
|
||||
}
|
||||
|
||||
return (
|
||||
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<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}>
|
||||
{document.title}
|
||||
</h1>
|
||||
|
||||
@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
import NotFoundPartial from '~/components/partials/not-found';
|
||||
|
||||
export default async function NotFound() {
|
||||
const session = await getServerComponentSession();
|
||||
const { session } = await getServerComponentSession();
|
||||
|
||||
return (
|
||||
<NotFoundPartial>
|
||||
|
||||
@ -40,7 +40,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<Tooltip delayDuration={200}>
|
||||
<TooltipTrigger className="flex cursor-pointer">
|
||||
{children || <StackAvatars recipients={recipients} />}
|
||||
</TooltipTrigger>
|
||||
|
||||
@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
||||
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
|
||||
'use server';
|
||||
|
||||
const { id: userId } = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
await setFieldsForDocument({
|
||||
userId,
|
||||
userId: user.id,
|
||||
documentId,
|
||||
fields: fields.map((field) => ({
|
||||
id: field.nativeId,
|
||||
|
||||
@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & {
|
||||
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
|
||||
'use server';
|
||||
|
||||
const { id: userId } = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
await setRecipientsForDocument({
|
||||
userId,
|
||||
userId: user.id,
|
||||
documentId,
|
||||
recipients: signers.map((signer) => ({
|
||||
id: signer.nativeId,
|
||||
|
||||
@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
||||
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
||||
'use server';
|
||||
|
||||
const { id: userId } = await getRequiredServerComponentSession();
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
if (email.message || email.subject) {
|
||||
await upsertDocumentMeta({
|
||||
@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc
|
||||
}
|
||||
|
||||
return await sendDocument({
|
||||
userId,
|
||||
userId: user.id,
|
||||
documentId,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Eye, EyeOff, Loader } from 'lucide-react';
|
||||
@ -40,8 +38,6 @@ export type SignInFormProps = {
|
||||
};
|
||||
|
||||
export const SignInForm = ({ className }: SignInFormProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const { toast } = useToast();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@ -57,36 +53,29 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
||||
resolver: zodResolver(ZSignInFormSchema),
|
||||
});
|
||||
|
||||
const errorCode = searchParams?.get('error');
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
if (isErrorCode(errorCode)) {
|
||||
timeout = setTimeout(() => {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
}, [errorCode, toast]);
|
||||
|
||||
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
|
||||
try {
|
||||
await signIn('credentials', {
|
||||
const result = await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
callbackUrl: LOGIN_REDIRECT_PATH,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error && isErrorCode(result.error)) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
description: ERROR_MESSAGES[result.error],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result?.url) {
|
||||
throw new Error('An unknown error occurred');
|
||||
}
|
||||
|
||||
window.location.href = result.url;
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
|
||||
19
docker/compose-services.yml
Normal 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
|
||||
91
package-lock.json
generated
@ -1868,6 +1868,17 @@
|
||||
"resolved": "apps/marketing",
|
||||
"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": {
|
||||
"resolved": "packages/prettier-config",
|
||||
"link": true
|
||||
@ -1876,6 +1887,10 @@
|
||||
"resolved": "packages/prisma",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@documenso/signing": {
|
||||
"resolved": "packages/signing",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@documenso/tailwind-config": {
|
||||
"resolved": "packages/tailwind-config",
|
||||
"link": true
|
||||
@ -6423,6 +6438,15 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
|
||||
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A=="
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.4.tgz",
|
||||
"integrity": "sha512-08scBQriFsBbm/CuBWOXRMD1RG7ydFW01EDR6vGX27nxcj6E/jGSCOLdICNd8ETwQlLFXVBVA854RX6Y7vPSrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
@ -14370,11 +14394,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
|
||||
},
|
||||
"node_modules/node-signpdf": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-2.0.0.tgz",
|
||||
"integrity": "sha512-B6fDvD8z2v0pRntQjgPO2ARqxh0pfNwfSn6YEbP7cv6xmPgcphFwcrMA3N47LztmpVbAM3vUFUslX32L6NRYDg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"node-forge": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-sql-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz",
|
||||
@ -16926,6 +16969,29 @@
|
||||
"node": ">=0.10.5"
|
||||
}
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
|
||||
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
|
||||
"dependencies": {
|
||||
"@react-email/render": "0.0.7",
|
||||
"type-fest": "3.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resend/node_modules/type-fest": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
|
||||
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.2",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||
@ -19739,9 +19805,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@documenso/nodemailer-resend": "1.0.0",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"nodemailer": "^6.9.3",
|
||||
"react-email": "^1.9.4"
|
||||
"react-email": "^1.9.4",
|
||||
"resend": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
@ -19777,6 +19845,7 @@
|
||||
"@aws-sdk/signature-v4-crt": "^3.410.0",
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@next-auth/prisma-adapter": "1.0.7",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
@ -19827,6 +19896,26 @@
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
},
|
||||
"packages/signing": {
|
||||
"name": "@documenso/signing",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPLv3",
|
||||
"dependencies": {
|
||||
"@documenso/tsconfig": "*",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^2.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"packages/signing/node_modules/ts-pattern": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.0.5.tgz",
|
||||
"integrity": "sha512-tL0w8U/pgaacOmkb9fRlYzWEUDCfVjjv9dD4wHTgZ61MjhuMt46VNWTG747NqW6vRzoWIKABVhFSOJ82FvXrfA=="
|
||||
},
|
||||
"packages/tailwind-config": {
|
||||
"name": "@documenso/tailwind-config",
|
||||
"version": "0.0.0",
|
||||
|
||||
@ -8,7 +8,11 @@
|
||||
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
||||
"prepare": "husky install",
|
||||
"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 @documesno/prisma",
|
||||
"dx:up": "docker compose -f docker/compose-services.yml up -d",
|
||||
"dx:down": "docker compose -f docker/compose-services.yml down"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8.6.0",
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { createTransport } from 'nodemailer';
|
||||
|
||||
import { ResendTransport } from '@documenso/nodemailer-resend';
|
||||
|
||||
import { MailChannelsTransport } from './transports/mailchannels';
|
||||
|
||||
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 (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
||||
throw new Error(
|
||||
|
||||
@ -17,9 +17,11 @@
|
||||
"worker:test": "tsup worker/index.ts --format esm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/nodemailer-resend": "1.0.0",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"nodemailer": "^6.9.3",
|
||||
"react-email": "^1.9.4"
|
||||
"react-email": "^1.9.4",
|
||||
"resend": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@documenso/tailwind-config": "*",
|
||||
|
||||
BIN
packages/email/static/user-plus.png
Normal file
|
After Width: | Height: | Size: 367 B |
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentCompletedProps {
|
||||
downloadLink: string;
|
||||
documentName: string;
|
||||
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
<Section>
|
||||
<Section className="mb-4">
|
||||
<Column align="center">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</Section>
|
||||
|
||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||
“{documentName}” was signed by all signers
|
||||
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
|
||||
Review
|
||||
</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}
|
||||
>
|
||||
<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
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@ -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;
|
||||
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentInviteProps {
|
||||
inviterName: string;
|
||||
inviterEmail: string;
|
||||
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
|
||||
signDocumentLink,
|
||||
assetBaseUrl,
|
||||
}: TemplateDocumentInviteProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4">
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
</Column>
|
||||
|
||||
<Column />
|
||||
</Row>
|
||||
</Section>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section>
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
|
||||
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentPendingProps {
|
||||
documentName: string;
|
||||
assetBaseUrl: string;
|
||||
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<Row className="table-fixed">
|
||||
<Column />
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Column>
|
||||
<Img
|
||||
className="h-42 mx-auto"
|
||||
src={getAssetUrl('/static/document.png')}
|
||||
alt="Documenso"
|
||||
/>
|
||||
<Section>
|
||||
<Section className="mb-4">
|
||||
<Column align="center">
|
||||
<Text className="text-base font-semibold text-blue-500">
|
||||
<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 />
|
||||
</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>
|
||||
</Section>
|
||||
|
||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||
“{documentName}” has been signed
|
||||
|
||||
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateDocumentSelfSignedProps {
|
||||
downloadLink: string;
|
||||
documentName: string;
|
||||
assetBaseUrl: string;
|
||||
}
|
||||
|
||||
export const TemplateDocumentSelfSigned = ({
|
||||
downloadLink,
|
||||
documentName,
|
||||
assetBaseUrl,
|
||||
}: TemplateDocumentSelfSignedProps) => {
|
||||
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
|
||||
|
||||
const getAssetUrl = (path: string) => {
|
||||
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">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<Section>
|
||||
<Column align="center">
|
||||
<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]">
|
||||
<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 mt-6 text-center text-lg font-semibold">
|
||||
You have signed “{documentName}”
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
Check out our plans to access the full suite of features.
|
||||
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||
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>
|
||||
|
||||
<Section className="mb-6 mt-8 text-center">
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
</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>
|
||||
</Tailwind>
|
||||
|
||||
@ -17,7 +17,7 @@ export const TemplateFooter = ({ isDocument = true }: TemplateFooterProps) => {
|
||||
)}
|
||||
|
||||
<Text className="my-8 text-sm text-slate-400">
|
||||
Documenso
|
||||
Documenso, Inc.
|
||||
<br />
|
||||
2261 Market Street, #5211, San Francisco, CA 94114, USA
|
||||
</Text>
|
||||
|
||||
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export type TemplateForgotPasswordProps = {
|
||||
resetPasswordLink: string;
|
||||
assetBaseUrl: string;
|
||||
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
|
||||
resetPasswordLink,
|
||||
assetBaseUrl,
|
||||
}: TemplateForgotPasswordProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<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">
|
||||
Forgot your password?
|
||||
</Text>
|
||||
|
||||
@ -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 { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
export interface TemplateResetPasswordProps {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
|
||||
}
|
||||
|
||||
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Section className="mt-4 flex-row items-center justify-center">
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||
</div>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<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">
|
||||
Password updated!
|
||||
</Text>
|
||||
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
Your password has been updated.
|
||||
</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>
|
||||
</Tailwind>
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
TemplateDocumentCompleted,
|
||||
TemplateDocumentCompletedProps,
|
||||
} 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>;
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
TemplateDocumentInvite,
|
||||
TemplateDocumentInviteProps,
|
||||
} 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> & {
|
||||
customBody?: string;
|
||||
|
||||
@ -15,12 +15,11 @@ import {
|
||||
TemplateDocumentSelfSigned,
|
||||
TemplateDocumentSelfSignedProps,
|
||||
} 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 const DocumentSelfSignedEmailTemplate = ({
|
||||
downloadLink = 'https://documenso.com',
|
||||
documentName = 'Open Source Pledge.pdf',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
}: DocumentSelfSignedTemplateProps) => {
|
||||
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
|
||||
/>
|
||||
|
||||
<TemplateDocumentSelfSigned
|
||||
downloadLink={downloadLink}
|
||||
documentName={documentName}
|
||||
assetBaseUrl={assetBaseUrl}
|
||||
/>
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import {
|
||||
TemplateForgotPassword,
|
||||
TemplateForgotPasswordProps,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import TemplateFooter from '../template-components/template-footer';
|
||||
import { TemplateFooter } from '../template-components/template-footer';
|
||||
import {
|
||||
TemplateResetPassword,
|
||||
TemplateResetPasswordProps,
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"@documenso/tsconfig/process-env.d.ts",
|
||||
]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { APP_BASE_URL } from './app';
|
||||
|
||||
/**
|
||||
* The flag name for global session recording feature flag.
|
||||
*/
|
||||
@ -23,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
||||
*/
|
||||
export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
||||
const postHogHost = `${APP_BASE_URL}/ingest`;
|
||||
|
||||
if (!postHogKey || !postHogHost) {
|
||||
return null;
|
||||
|
||||
@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
||||
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
||||
|
||||
if (!session || !session.user?.email) {
|
||||
return null;
|
||||
return { user: null, session: null };
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
||||
|
||||
if (!session || !session.user?.email) {
|
||||
return null;
|
||||
return { user: null, session: null };
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@ -40,15 +40,15 @@ export const getServerComponentSession = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
return { user, session };
|
||||
};
|
||||
|
||||
export const getRequiredServerComponentSession = async () => {
|
||||
const session = await getServerComponentSession();
|
||||
const { user, session } = await getServerComponentSession();
|
||||
|
||||
if (!session) {
|
||||
if (!user || !session) {
|
||||
throw new Error('No session found');
|
||||
}
|
||||
|
||||
return session;
|
||||
return { user, session };
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"@aws-sdk/signature-v4-crt": "^3.410.0",
|
||||
"@documenso/email": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/signing": "*",
|
||||
"@next-auth/prisma-adapter": "1.0.7",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@scure/base": "^1.1.3",
|
||||
@ -38,4 +39,4 @@
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/luxon": "^3.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import { putFile } from '../../universal/upload/put-file';
|
||||
@ -71,12 +72,14 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
|
||||
|
||||
const pdfBytes = await doc.save();
|
||||
|
||||
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
|
||||
|
||||
const { name, ext } = path.parse(document.title);
|
||||
|
||||
const { data: newData } = await putFile({
|
||||
name: `${name}_signed${ext}`,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(Buffer.from(pdfBytes)),
|
||||
arrayBuffer: async () => Promise.resolve(pdfBuffer),
|
||||
});
|
||||
|
||||
await prisma.documentData.update({
|
||||
|
||||
@ -14,7 +14,6 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
console.warn(`No recipient found for token ${token}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -57,12 +57,13 @@ export const setRecipientsForDocument = async ({
|
||||
|
||||
return {
|
||||
...recipient,
|
||||
...existing,
|
||||
_persisted: existing,
|
||||
};
|
||||
})
|
||||
.filter((recipient) => {
|
||||
return (
|
||||
recipient.sendStatus !== SendStatus.SENT && recipient.signingStatus !== SigningStatus.SIGNED
|
||||
recipient._persisted?.sendStatus !== SendStatus.SENT &&
|
||||
recipient._persisted?.signingStatus !== SigningStatus.SIGNED
|
||||
);
|
||||
});
|
||||
|
||||
@ -72,7 +73,7 @@ export const setRecipientsForDocument = async ({
|
||||
linkedRecipients.map((recipient) =>
|
||||
prisma.recipient.upsert({
|
||||
where: {
|
||||
id: recipient.id ?? -1,
|
||||
id: recipient._persisted?.id ?? -1,
|
||||
documentId,
|
||||
},
|
||||
update: {
|
||||
|
||||
5
packages/lib/universal/generate-twitter-intent.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const generateTwitterIntent = (text: string, shareUrl: string) => {
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
||||
text,
|
||||
)}%0A%0A${encodeURIComponent(shareUrl)}`;
|
||||
};
|
||||
@ -17,7 +17,7 @@ import { alphaid } from '../id';
|
||||
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
|
||||
const client = getS3Client();
|
||||
|
||||
const user = await getServerComponentSession();
|
||||
const { user } = await getServerComponentSession();
|
||||
|
||||
// Get the basename and extension for the file
|
||||
const { name, ext } = path.parse(fileName);
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "PasswordResetToken" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"userId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL;
|
||||
@ -1,3 +1,5 @@
|
||||
DROP TABLE IF EXISTS "PasswordResetToken" CASCADE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PasswordResetToken" (
|
||||
"id" SERIAL NOT NULL,
|
||||
|
||||
73
packages/signing/helpers/addSigningPlaceholder.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import signer from 'node-signpdf';
|
||||
import { PDFArray, PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from 'pdf-lib';
|
||||
|
||||
export type AddSigningPlaceholderOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
|
||||
const doc = await PDFDocument.load(pdf);
|
||||
const pages = doc.getPages();
|
||||
|
||||
const byteRange = PDFArray.withContext(doc.context);
|
||||
|
||||
byteRange.push(PDFNumber.of(0));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
|
||||
|
||||
const signature = doc.context.obj({
|
||||
Type: 'Sig',
|
||||
Filter: 'Adobe.PPKLite',
|
||||
SubFilter: 'adbe.pkcs7.detached',
|
||||
ByteRange: byteRange,
|
||||
Contents: PDFHexString.fromText(' '.repeat(8192)),
|
||||
Reason: PDFString.of('Signed by Documenso'),
|
||||
M: PDFString.fromDate(new Date()),
|
||||
});
|
||||
|
||||
const signatureRef = doc.context.register(signature);
|
||||
|
||||
const widget = doc.context.obj({
|
||||
Type: 'Annot',
|
||||
Subtype: 'Widget',
|
||||
FT: 'Sig',
|
||||
Rect: [0, 0, 0, 0],
|
||||
V: signatureRef,
|
||||
T: PDFString.of('Signature1'),
|
||||
F: 4,
|
||||
P: pages[0].ref,
|
||||
});
|
||||
|
||||
const widgetRef = doc.context.register(widget);
|
||||
|
||||
let widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||
|
||||
if (widgets instanceof PDFArray) {
|
||||
widgets.push(widgetRef);
|
||||
} else {
|
||||
const newWidgets = PDFArray.withContext(doc.context);
|
||||
|
||||
newWidgets.push(widgetRef);
|
||||
|
||||
pages[0].node.set(PDFName.of('Annots'), newWidgets);
|
||||
|
||||
widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||
}
|
||||
|
||||
if (!widgets) {
|
||||
throw new Error('No widgets');
|
||||
}
|
||||
|
||||
pages[0].node.set(PDFName.of('Annots'), widgets);
|
||||
|
||||
doc.catalog.set(
|
||||
PDFName.of('AcroForm'),
|
||||
doc.context.obj({
|
||||
SigFlags: 3,
|
||||
Fields: [widgetRef],
|
||||
}),
|
||||
);
|
||||
|
||||
return Buffer.from(await doc.save({ useObjectStreams: false }));
|
||||
};
|
||||
17
packages/signing/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { signWithLocalCert } from './transports/local-cert';
|
||||
|
||||
export type SignOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signPdf = async ({ pdf }: SignOptions) => {
|
||||
const transport = process.env.NEXT_PRIVATE_SIGNING_TRANSPORT || 'local';
|
||||
|
||||
return await match(transport)
|
||||
.with('local', async () => signWithLocalCert({ pdf }))
|
||||
.otherwise(() => {
|
||||
throw new Error(`Unsupported signing transport: ${transport}`);
|
||||
});
|
||||
};
|
||||
23
packages/signing/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@documenso/signing",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"license": "AGPLv3",
|
||||
"files": [
|
||||
"transports/",
|
||||
"index.ts"
|
||||
],
|
||||
"scripts": {
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/tsconfig": "*",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^2.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"ts-pattern": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-forge": "^1.3.4"
|
||||
}
|
||||
}
|
||||
32
packages/signing/transports/local-cert.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import signer from 'node-signpdf';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { addSigningPlaceholder } from '../helpers/addSigningPlaceholder';
|
||||
|
||||
export type SignWithLocalCertOptions = {
|
||||
pdf: Buffer;
|
||||
};
|
||||
|
||||
export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
|
||||
const pdfWithPlaceholder = await addSigningPlaceholder({ pdf });
|
||||
|
||||
let p12Cert: Buffer | null = null;
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS) {
|
||||
p12Cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
|
||||
}
|
||||
|
||||
if (!p12Cert) {
|
||||
p12Cert = Buffer.from(
|
||||
fs.readFileSync(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12'),
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE) {
|
||||
return signer.sign(pdfWithPlaceholder, p12Cert, {
|
||||
passphrase: process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE,
|
||||
});
|
||||
}
|
||||
|
||||
return signer.sign(pdfWithPlaceholder, p12Cert);
|
||||
};
|
||||
8
packages/signing/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@documenso/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@documenso/tsconfig/process-env.d.ts", "@types/node"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { CreateNextContextOptions } from '@trpc/server/adapters/next';
|
||||
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
|
||||
|
||||
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
|
||||
const session = await getServerSession({ req, res });
|
||||
const { session, user } = await getServerSession({ req, res });
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
@ -12,9 +12,16 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
|
||||
};
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
session: null,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user: session,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
10
packages/tsconfig/process-env.d.ts
vendored
@ -21,7 +21,15 @@ declare namespace NodeJS {
|
||||
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID?: string;
|
||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY?: string;
|
||||
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
|
||||
NEXT_PRIVATE_SIGNING_TRANSPORT?: 'local' | 'http' | 'gcloud-hsm';
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
|
||||
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'resend' | 'smtp-auth' | 'smtp-api';
|
||||
|
||||
NEXT_PRIVATE_RESEND_API_KEY?: string;
|
||||
|
||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;
|
||||
NEXT_PRIVATE_MAILCHANNELS_DKIM_DOMAIN?: string;
|
||||
|
||||
@ -118,7 +118,7 @@ export const SigningCard3D = ({ className, name, signingCelebrationImage }: Sign
|
||||
style={{ perspective: 800 }}
|
||||
>
|
||||
<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}
|
||||
style={{
|
||||
perspective: '800',
|
||||
@ -148,7 +148,7 @@ const SigningCardContent = ({ className, name }: SigningCardContentProps) => {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'group mx-auto flex aspect-[21/9] w-full items-center justify-center',
|
||||
'group z-10 mx-auto flex aspect-[21/9] w-full items-center justify-center',
|
||||
className,
|
||||
)}
|
||||
degrees={-145}
|
||||
@ -180,14 +180,14 @@ type SigningCardImageProps = {
|
||||
const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
className="pointer-events-none absolute -inset-32 -z-10 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
|
||||
className="pointer-events-none absolute -inset-32 -z-50 flex items-center justify-center md:-inset-44 xl:-inset-60 2xl:-inset-80"
|
||||
initial={{
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
scale: 0.6,
|
||||
}}
|
||||
animate={{
|
||||
scale: 1,
|
||||
opacity: 0.5,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.5,
|
||||
@ -197,7 +197,7 @@ const SigningCardImage = ({ signingCelebrationImage }: SigningCardImageProps) =>
|
||||
<Image
|
||||
src={signingCelebrationImage}
|
||||
alt="background pattern"
|
||||
className="w-full dark:invert dark:sepia"
|
||||
className="w-full dark:brightness-150 dark:contrast-[70%] dark:invert dark:sepia"
|
||||
style={{
|
||||
mask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
WebkitMask: 'radial-gradient(rgba(255, 255, 255, 1) 0%, transparent 67%)',
|
||||
|
||||
@ -11,9 +11,19 @@ const Dialog = DialogPrimitive.Root;
|
||||
|
||||
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}>
|
||||
<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}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
@ -39,14 +49,14 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
|
||||
>(({ className, children, position = 'start', ...props }, ref) => (
|
||||
<DialogPortal position={position}>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -351,7 +351,11 @@ export const AddFieldsFormPartial = ({
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
<CommandEmpty />
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
No recipient matching this description was found.
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
|
||||
@ -126,12 +126,12 @@ export const AddSignersFormPartial = ({
|
||||
<AnimatePresence>
|
||||
{signers.map((signer, index) => (
|
||||
<motion.div
|
||||
key={signer.formId}
|
||||
key={signer.id}
|
||||
data-native-id={signer.nativeId}
|
||||
className="flex flex-wrap items-end gap-x-4"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`signer-${signer.formId}-email`}>
|
||||
<Label htmlFor={`signer-${signer.id}-email`}>
|
||||
Email
|
||||
<span className="text-destructive ml-1 inline-block font-medium">*</span>
|
||||
</Label>
|
||||
@ -141,7 +141,7 @@ export const AddSignersFormPartial = ({
|
||||
name={`signers.${index}.email`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id={`signer-${signer.formId}-email`}
|
||||
id={`signer-${signer.id}-email`}
|
||||
type="email"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
||||
@ -153,14 +153,14 @@ export const AddSignersFormPartial = ({
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`signer-${signer.formId}-name`}>Name</Label>
|
||||
<Label htmlFor={`signer-${signer.id}-name`}>Name</Label>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signers.${index}.name`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
id={`signer-${signer.formId}-name`}
|
||||
id={`signer-${signer.id}-name`}
|
||||
type="text"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
||||
@ -195,7 +195,11 @@ export const AddSignersFormPartial = ({
|
||||
</AnimatePresence>
|
||||
</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">
|
||||
<Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}>
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZAddSignersFormSchema = z.object({
|
||||
signers: z
|
||||
.array(
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
z.object({
|
||||
formId: z.string().min(1),
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
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;
|
||||
}, '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>;
|
||||
|
||||
@ -4,13 +4,17 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
export type FormErrorMessageProps = {
|
||||
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) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
{isErrorWithMessage(error) && (
|
||||
<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
|
||||