Compare commits

..

77 Commits

Author SHA1 Message Date
833584bd4f Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 21:28:41 +02:00
ca52a22bcd chore: sync 2023-09-28 14:59:41 +02:00
ba611b9adb Merge pull request #440 from PeterKwesiAnsah/chore/add-empty-recipientlist-message
chore: add empty message
2023-09-28 22:12:21 +10:00
e8643850a0 Merge pull request #397 from documenso/feat-early-adopters
feat: early adopters
2023-09-28 13:16:21 +02:00
bec7376fe8 Merge branch 'feat/refresh' into feat-early-adopters 2023-09-28 13:16:09 +02:00
7363bbc8f7 chore: sync next 2023-09-28 13:15:56 +02:00
d0f679e177 Merge pull request #430 from documenso/feat/update-email-templates
feat: update email templates
2023-09-28 20:58:22 +10:00
e9769904ab Merge pull request #453 from documenso/fix/443-validation-with-multiple-recipients
fix: add hack for root zod validation with hook form
2023-09-28 20:57:21 +10:00
a2746742a7 Merge remote-tracking branch 'origin/feat/refresh' into feat-early-adopters 2023-09-28 12:35:10 +02:00
459e3dc45b Merge pull request #400 from documenso/feat/open-early-adopters
feat: add early adopters graph
2023-09-28 12:17:03 +02:00
21fedab707 Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-28 12:16:27 +02:00
323f1974ab chore: added classname and changed typo 2023-09-28 11:13:02 +01:00
5bc9d625c2 Merge pull request #455 from documenso/fix/432-signee-doc-version-doesnt-have-sticky-signing-area
fix: resolve issues with signing document stickiness
2023-09-28 17:50:04 +10:00
4885cf5154 fix: resolve issues with signing document stickiness 2023-09-28 17:42:01 +10:00
2f66eca925 Merge pull request #454 from documenso/fix/446-cancel-cta-does-nothing-when-a-signer-opens-the-document
fix: disable cancel button when there is no window history
2023-09-28 17:15:41 +10:00
af042a62cd fix: disable cancel button when there is no window history 2023-09-28 15:45:22 +10:00
2bfc2b0c1f fix: add hack for root zod validation with hook form 2023-09-28 15:32:03 +10:00
df8bdda718 Merge pull request #450 from documenso/feat/resend-transport
feat: add resend mail transport
2023-09-28 14:07:18 +10:00
873f99ae86 fix: resolve document title inconsistency (#452) 2023-09-28 13:56:22 +10:00
17fe135027 Merge pull request #451 from documenso/fix/445-signer-name-not-persisting
fix: do not overwrite new names or emails for signers
2023-09-28 13:07:26 +10:00
2eed0ae063 feat: add posthog reverse proxy (#449) 2023-09-28 12:56:53 +10:00
f4ae0389d8 fix: do not overwrite new names or emails for signers 2023-09-28 12:35:21 +10:00
9bdff9a61f feat: add resend mail transport 2023-09-28 12:27:04 +10:00
f003a2864e chore: add empty message 2023-09-27 13:08:59 +01:00
cdb71c3a62 chore: greetings 2023-09-27 12:30:11 +02:00
39d7b3ca58 Merge branch 'feat/refresh' of https://github.com/documenso/documenso into feat/refresh 2023-09-27 12:19:14 +02:00
748c3636d5 chore: sync shop article and add missing and updated assets 2023-09-27 12:15:44 +02:00
6e2d1fb148 fix: add missing URL to email template 2023-09-27 17:44:29 +10:00
7bd847c0d3 chore: add missing migrations 2023-09-27 16:53:48 +10:00
81f9b2f776 Merge branch 'feat/refresh' into feat/update-email-templates 2023-09-27 15:50:54 +10:00
334671ef85 feat: add email forgot password action
Updated email template imports
2023-09-27 15:34:16 +10:00
a20b1e2f6a Merge pull request #350 from documenso/feat/redirect-signed-document
feat: redirect signed document to completed page
2023-09-27 15:09:07 +10:00
572f9d5ad6 chore: styling updates 2023-09-27 14:58:10 +10:00
e49c102e8c fix: lint errors 2023-09-27 09:11:56 +10:00
2348221b03 fix: faster tooltips 2023-09-27 09:09:53 +10:00
a0c327cfcf fix: share og updates 2023-09-27 09:03:53 +10:00
ddbeb9e3db fix: twitter images 2023-09-27 08:02:51 +10:00
e8205c1390 fix: better share links 2023-09-27 07:52:24 +10:00
fbb53fc8c3 chore: update mania shirt res 2023-09-26 22:06:41 +02:00
d7fed5a5dc feat: shop Article 2023-09-26 21:49:04 +02:00
3285881586 chore: typos 2023-09-26 11:39:30 +02:00
3ff61607a2 Merge pull request #370 from documenso/feat/mania
feat: malfunction mania blog post
2023-09-26 11:24:24 +02:00
2a124b03e9 Merge branch 'feat/refresh' into feat/mania 2023-09-26 11:24:13 +02:00
e1aa23bc55 chore: fix typo 2023-09-26 11:23:39 +02:00
818c5c7ba4 chore: fix lightmode logo 2023-09-26 11:19:32 +02:00
668011d1c7 Update README.md with M̸͍͚̽͒A̴̯͊͌L̴͖͖͘F̵̗̻́̅U̶̲̅͠N̸͙̰̓C̵̙̮̾T̸̜̙͌Í̷͎̯Ö̵̘̜́N̴̳͊̈ͅ ̶͔́M̸̡͐̾A̵̞̚N̴̤̏́Ǐ̸̩͂Ă̸͓͝ 2023-09-26 11:12:59 +02:00
562fd043a9 chore: typo 2023-09-26 10:31:41 +02:00
b4781c011c chore: alt text 2023-09-26 10:27:19 +02:00
5c7d3d5503 chore: update date 2023-09-26 10:26:16 +02:00
0f2b6c0ebf chore: remove unused image 2023-09-26 10:23:28 +02:00
ab5bdfeae4 chore: typo 2023-09-26 10:18:56 +02:00
e5fa2f4490 feat: update email templates
Add SPM email attachment
2023-09-26 17:48:26 +10:00
109ad190cf fix: text 2023-09-25 19:13:03 +02:00
d950634aab Merge branch 'feat/refresh' into feat/open-early-adopters 2023-09-25 09:28:04 +02:00
d0fad4775a feat: add extra info for the early adopters 2023-09-23 12:02:37 +00:00
0732aa317d chore: rename community plan to early adopters 2023-09-22 18:54:25 +02:00
fde53e355f chore: staging 2023-09-22 18:44:34 +02:00
8e0a10298e Merge branch 'feat/refresh' into feat/mania 2023-09-22 18:33:16 +02:00
eef63c9adc refactor: metrics into reusable component 2023-09-22 14:08:25 +00:00
c3d52c68e7 feat: add early adopters graph 2023-09-22 13:27:10 +00:00
190ae18edc chore: grammerly 2023-09-21 15:09:20 +02:00
82ffbeeb21 chore: links 2023-09-21 15:01:02 +02:00
9affd7b7fa Merge branch 'feat/refresh' into feat-early-adopters 2023-09-21 14:54:45 +02:00
897ba629df feat: early adopter article 2023-09-21 14:53:48 +02:00
eaa3e0a303 Merge branch 'feat/refresh' into feat/mania 2023-09-18 20:15:50 +02:00
a283c88d7f chore: update malfunction mania image 2023-09-18 20:11:27 +02:00
c2a9647c90 Merge branch 'feat/refresh' into feat/redirect-signed-document 2023-09-17 14:52:44 +00:00
02424596db fix: update for code review 2023-09-17 14:45:22 +00:00
5809109c05 chore: grammerly, finetuned bounties 2023-09-14 15:43:17 +02:00
a6400eb6c9 Merge branch 'feat/refresh' into feat/mania 2023-09-14 15:29:55 +02:00
39958ed22c Fix typo 2023-09-14 14:57:11 +02:00
7c4ba1b1ea Fix typo 2023-09-14 11:38:42 +02:00
f588897531 Fix punctuation for consistency 2023-09-14 11:33:11 +02:00
5629e08f83 Add keywords
added keywords in description for SEO optimizations
2023-09-14 11:06:48 +02:00
37394c054c Fix typo
Fix typo and minor edits for consistency
2023-09-14 11:04:10 +02:00
b7c0df67b1 feat: malfunction mania, first draft 2023-09-11 18:20:17 +02:00
e47ab838c5 chore: remove undefined check 2023-09-06 07:37:03 +00:00
90 changed files with 1030 additions and 607 deletions

View File

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

View File

@ -1,6 +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">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/ea8de2d0-8e33-4527-8a5c-40780142a2ed" alt="Documenso Logo">
</a>
<p align="center">
@ -26,37 +26,30 @@
<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>
</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 is fast and easy and should be the best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
## Recognition
@ -65,13 +58,13 @@ Signing documents digitally is fast, easy and should be best practice for every
## 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)
- 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

View File

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

View File

@ -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.**

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
"micro": "^10.0.1",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month';
import { 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>

View File

@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
import { formatMonth } from '@documenso/lib/client-only/format-month';
import { 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 (

View File

@ -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">

View File

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

View File

@ -48,9 +48,9 @@ export default function PricingPage() {
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
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>

View File

@ -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>

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

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

View File

@ -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>

View File

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

View File

@ -25,7 +25,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

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

View File

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

View File

@ -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}

View File

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

View File

@ -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,

View File

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

View File

@ -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');

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
import { SigningCard } from '@documenso/ui/components/signing-card';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import signingCelebration from '~/assets/signing-celebration.png';
@ -53,11 +53,11 @@ export default async function CompletedSigningPage({
recipient.email;
return (
<div className="flex flex-col items-center pt-24">
<div className="-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>
);
}

View File

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

View File

@ -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

View File

@ -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>

View File

@ -1,4 +1,4 @@
import { notFound } from 'next/navigation';
import { notFound, redirect } from 'next/navigation';
import { match } from 'ts-pattern';
@ -9,7 +9,7 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { FieldType } from '@documenso/prisma/client';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -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>

View File

@ -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>

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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,
});
};

433
package-lock.json generated
View File

@ -44,7 +44,7 @@
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
"micro": "^10.0.1",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-contentlayer": "^0.3.4",
"next-plausible": "^3.10.1",
@ -84,7 +84,7 @@
"lucide-react": "^0.279.0",
"luxon": "^3.4.0",
"micro": "^10.0.1",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"next-plausible": "^3.10.1",
"next-themes": "^0.2.1",
@ -2929,22 +2929,22 @@
}
},
"node_modules/@next/env": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz",
"integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ=="
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
"integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.4.tgz",
"integrity": "sha512-vI94U+D7RNgX6XypSyjeFrOzxGlZyxOplU0dVE5norIfZGn/LDjJYPHdvdsR5vN1eRtl6PDAsOHmycFEOljK5A==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.19.tgz",
"integrity": "sha512-N/O+zGb6wZQdwu6atMZHbR7T9Np5SUFUjZqCbj0sXm+MwQO35M8TazVB4otm87GkXYs2l6OPwARd3/PUWhZBVQ==",
"dependencies": {
"glob": "7.1.7"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz",
"integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz",
"integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==",
"cpu": [
"arm64"
],
@ -2957,9 +2957,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz",
"integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz",
"integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==",
"cpu": [
"x64"
],
@ -2972,9 +2972,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz",
"integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz",
"integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==",
"cpu": [
"arm64"
],
@ -2987,9 +2987,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz",
"integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz",
"integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==",
"cpu": [
"arm64"
],
@ -3002,9 +3002,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz",
"integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz",
"integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==",
"cpu": [
"x64"
],
@ -3017,9 +3017,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz",
"integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz",
"integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==",
"cpu": [
"x64"
],
@ -3032,9 +3032,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz",
"integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz",
"integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==",
"cpu": [
"arm64"
],
@ -3047,9 +3047,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz",
"integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz",
"integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==",
"cpu": [
"ia32"
],
@ -3062,9 +3062,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz",
"integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz",
"integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==",
"cpu": [
"x64"
],
@ -5411,9 +5411,9 @@
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.4.0.tgz",
"integrity": "sha512-cEjvTPU32OM9lUFegJagO0mRnIn+rbqrG89vV8/xLnLFX0DoR0r1oy5IlTga71Q7uT3Qus7qm7wgeiMT/+Irlg=="
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz",
"integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw=="
},
"node_modules/@scure/base": {
"version": "1.1.3",
@ -6080,9 +6080,9 @@
}
},
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
"integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==",
"dependencies": {
"tslib": "^2.4.0"
}
@ -7048,14 +7048,6 @@
"astring": "bin/astring"
}
},
"node_modules/asynciterator.prototype": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
"integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
"dependencies": {
"has-symbols": "^1.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -8758,19 +8750,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-data-property": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
"integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@ -8783,11 +8762,10 @@
}
},
"node_modules/define-properties": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
},
@ -9390,27 +9368,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-iterator-helpers": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz",
"integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==",
"dependencies": {
"asynciterator.prototype": "^1.0.0",
"call-bind": "^1.0.2",
"define-properties": "^1.2.1",
"es-abstract": "^1.22.1",
"es-set-tostringtag": "^2.0.1",
"function-bind": "^1.1.1",
"get-intrinsic": "^1.2.1",
"globalthis": "^1.0.3",
"has-property-descriptors": "^1.0.0",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"internal-slot": "^1.0.5",
"iterator.prototype": "^1.1.2",
"safe-array-concat": "^1.0.1"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
@ -9767,18 +9724,18 @@
}
},
"node_modules/eslint-config-next": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.4.tgz",
"integrity": "sha512-FzQGIj4UEszRX7fcRSJK6L1LrDiVZvDFW320VVntVKh3BSU8Fb9kpaoxQx0cdFgf3MQXdeSbrCXJ/5Z/NndDkQ==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.4.19.tgz",
"integrity": "sha512-WE8367sqMnjhWHvR5OivmfwENRQ1ixfNE9hZwQqNCsd+iM3KnuMc1V8Pt6ytgjxjf23D+xbesADv9x3xaKfT3g==",
"dependencies": {
"@next/eslint-plugin-next": "13.5.4",
"@rushstack/eslint-patch": "^1.3.3",
"@next/eslint-plugin-next": "13.4.19",
"@rushstack/eslint-patch": "^1.1.3",
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.31.7",
"eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
},
"peerDependencies": {
@ -10040,15 +9997,14 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.33.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"version": "7.33.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.1.tgz",
"integrity": "sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==",
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
"array.prototype.tosorted": "^1.1.1",
"doctrine": "^2.1.0",
"es-iterator-helpers": "^1.0.12",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2",
@ -11725,20 +11681,6 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-async-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
"integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-bigint": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
@ -11873,17 +11815,6 @@
"node": ">=0.10.0"
}
},
"node_modules/is-finalizationregistry": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
"integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
"dependencies": {
"call-bind": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -11892,20 +11823,6 @@
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@ -11951,14 +11868,6 @@
"node": ">=8"
}
},
"node_modules/is-map": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
"integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@ -12051,14 +11960,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-set": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
"integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@ -12146,14 +12047,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-weakmap": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
"integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@ -12165,18 +12058,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakset": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
"integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-what": {
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.15.tgz",
@ -12248,18 +12129,6 @@
"ws": "*"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
"integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
"dependencies": {
"define-properties": "^1.2.1",
"get-intrinsic": "^1.2.1",
"has-symbols": "^1.0.3",
"reflect.getprototypeof": "^1.0.4",
"set-function-name": "^2.0.1"
}
},
"node_modules/jackspeak": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz",
@ -14302,34 +14171,35 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/next": {
"version": "13.5.4",
"resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz",
"integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==",
"version": "13.4.19",
"resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz",
"integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==",
"dependencies": {
"@next/env": "13.5.4",
"@swc/helpers": "0.5.2",
"@next/env": "13.4.19",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
"postcss": "8.4.31",
"postcss": "8.4.14",
"styled-jsx": "5.1.1",
"watchpack": "2.4.0"
"watchpack": "2.4.0",
"zod": "3.21.4"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=16.14.0"
"node": ">=16.8.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "13.5.4",
"@next/swc-darwin-x64": "13.5.4",
"@next/swc-linux-arm64-gnu": "13.5.4",
"@next/swc-linux-arm64-musl": "13.5.4",
"@next/swc-linux-x64-gnu": "13.5.4",
"@next/swc-linux-x64-musl": "13.5.4",
"@next/swc-win32-arm64-msvc": "13.5.4",
"@next/swc-win32-ia32-msvc": "13.5.4",
"@next/swc-win32-x64-msvc": "13.5.4"
"@next/swc-darwin-arm64": "13.4.19",
"@next/swc-darwin-x64": "13.4.19",
"@next/swc-linux-arm64-gnu": "13.4.19",
"@next/swc-linux-arm64-musl": "13.4.19",
"@next/swc-linux-x64-gnu": "13.4.19",
"@next/swc-linux-x64-musl": "13.4.19",
"@next/swc-win32-arm64-msvc": "13.4.19",
"@next/swc-win32-ia32-msvc": "13.4.19",
"@next/swc-win32-x64-msvc": "13.4.19"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -14411,6 +14281,46 @@
"react-dom": "*"
}
},
"node_modules/next/node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
}
],
"dependencies": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@ -15188,9 +15098,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"funding": [
{
"type": "opencollective",
@ -16879,25 +16789,6 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
"integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.2.0",
"es-abstract": "^1.22.1",
"get-intrinsic": "^1.2.1",
"globalthis": "^1.0.3",
"which-builtin-type": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
@ -17067,6 +16958,29 @@
"node": ">=0.10.5"
}
},
"node_modules/resend": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
"dependencies": {
"@react-email/render": "0.0.7",
"type-fest": "3.13.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/resend/node_modules/type-fest": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@ -17388,12 +17302,12 @@
}
},
"node_modules/safe-array-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
"integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz",
"integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.2.1",
"get-intrinsic": "^1.2.0",
"has-symbols": "^1.0.3",
"isarray": "^2.0.5"
},
@ -17491,19 +17405,6 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
"integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
"dependencies": {
"define-data-property": "^1.0.1",
"functions-have-names": "^1.2.3",
"has-property-descriptors": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@ -19695,45 +19596,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-builtin-type": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
"integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
"dependencies": {
"function.prototype.name": "^1.1.5",
"has-tostringtag": "^1.0.0",
"is-async-function": "^2.0.0",
"is-date-object": "^1.0.5",
"is-finalizationregistry": "^1.0.2",
"is-generator-function": "^1.0.10",
"is-regex": "^1.1.4",
"is-weakref": "^1.0.2",
"isarray": "^2.0.5",
"which-boxed-primitive": "^1.0.2",
"which-collection": "^1.0.1",
"which-typed-array": "^1.1.9"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-collection": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
"integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
"dependencies": {
"is-map": "^2.0.1",
"is-set": "^2.0.1",
"is-weakmap": "^2.0.1",
"is-weakset": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-typed-array": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
@ -19934,7 +19796,8 @@
"dependencies": {
"@react-email/components": "^0.0.7",
"nodemailer": "^6.9.3",
"react-email": "^1.9.4"
"react-email": "^1.9.4",
"resend": "^1.1.0"
},
"devDependencies": {
"@documenso/tailwind-config": "*",
@ -19951,7 +19814,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.40.0",
"eslint-config-next": "13.5.4",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4",
@ -19979,7 +19842,7 @@
"bcrypt": "^5.1.0",
"luxon": "^3.4.0",
"nanoid": "^4.0.2",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"pdf-lib": "^1.17.1",
"react": "18.2.0",
@ -20116,7 +19979,7 @@
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "13.5.4",
"next": "13.4.19",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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>

View File

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

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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">

View File

@ -1,7 +1,9 @@
import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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

View File

@ -1,18 +1,20 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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>

View File

@ -1,7 +1,9 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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>

View File

@ -1,7 +1,9 @@
import { Img, Section, Tailwind, Text } from '@react-email/components';
import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config';
import { 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>
);

View File

@ -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>;

View File

@ -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;

View File

@ -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}
/>

View File

@ -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,

View File

@ -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,

View File

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

View File

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

View File

@ -10,7 +10,7 @@
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.40.0",
"eslint-config-next": "13.5.4",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4",
@ -18,4 +18,4 @@
"eslint-plugin-react": "^7.32.2",
"typescript": "^5.1.6"
}
}
}

View File

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

View File

@ -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 };
};

View File

@ -28,7 +28,7 @@
"bcrypt": "^5.1.0",
"luxon": "^3.4.0",
"nanoid": "^4.0.2",
"next": "13.5.4",
"next": "13.4.19",
"next-auth": "4.22.3",
"pdf-lib": "^1.17.1",
"react": "18.2.0",

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -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,
};
};

View File

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

View File

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

View File

@ -60,7 +60,7 @@
"framer-motion": "^10.12.8",
"lucide-react": "^0.279.0",
"luxon": "^3.4.2",
"next": "13.5.4",
"next": "13.4.19",
"pdfjs-dist": "3.6.172",
"react-day-picker": "^8.7.1",
"react-hook-form": "^7.45.4",

View File

@ -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}

View File

@ -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) => (

View File

@ -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()}>

View File

@ -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>;

View File

@ -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,

View File

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