mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
Merge branch 'main' into feat/public-profiles
This commit is contained in:
12
README.md
12
README.md
@ -73,6 +73,18 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
|||||||
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
<p align="left">
|
||||||
|
<a href="https://www.typescriptlang.org"><img src="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" alt="TypeScript"></a>
|
||||||
|
<a href="https://nextjs.org/"><img src="https://img.shields.io/badge/next.js-000000?style=flat-square&logo=nextdotjs&logoColor=white" alt="NextJS"></a>
|
||||||
|
<a href="https://prisma.io"><img width="122" height="20" src="http://made-with.prisma.io/indigo.svg" alt="Made with Prisma" /></a>
|
||||||
|
<a href="https://tailwindcss.com/"><img src="https://img.shields.io/badge/tailwindcss-0F172A?&logo=tailwindcss" alt="Tailwind CSS"></a>
|
||||||
|
<a href=""><img src="" alt=""></a>
|
||||||
|
<a href=""><img src="" alt=""></a>
|
||||||
|
<a href=""><img src="" alt=""></a>
|
||||||
|
<a href=""><img src="" alt=""></a>
|
||||||
|
<a href=""><img src="" alt=""></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
- [Typescript](https://www.typescriptlang.org/) - Language
|
- [Typescript](https://www.typescriptlang.org/) - Language
|
||||||
- [Next.js](https://nextjs.org/) - Framework
|
- [Next.js](https://nextjs.org/) - Framework
|
||||||
|
|||||||
@ -25,6 +25,7 @@ tags:
|
|||||||
> TLDR; We are launching direct links to templates. With direct links, a document is created from a template every time anyone signs the link. Links can be public.
|
> TLDR; We are launching direct links to templates. With direct links, a document is created from a template every time anyone signs the link. Links can be public.
|
||||||
|
|
||||||
## Sync or Async?
|
## Sync or Async?
|
||||||
|
|
||||||
> Quick refresher on Sync vs. Async: Sync means everyone has to wait for me until they can continue their work. Async means everyone can and does their work at the time that fits best.
|
> Quick refresher on Sync vs. Async: Sync means everyone has to wait for me until they can continue their work. Async means everyone can and does their work at the time that fits best.
|
||||||
|
|
||||||
Digital signing has become almost as normalized as email when doing business. While not 100% of companies are onboarded on digital signatures yet, hardly anyone is surprised when receiving a link to sign something digitally. As we got used to the user experience of sending emails, we also got used to the experience of sending document signature requests, with all the downsides:
|
Digital signing has become almost as normalized as email when doing business. While not 100% of companies are onboarded on digital signatures yet, hardly anyone is surprised when receiving a link to sign something digitally. As we got used to the user experience of sending emails, we also got used to the experience of sending document signature requests, with all the downsides:
|
||||||
@ -34,6 +35,7 @@ Digital signing has become almost as normalized as email when doing business. Wh
|
|||||||
- I need to monitor the requests I started for completion: "I sent you a link yesterday; please check it out."
|
- I need to monitor the requests I started for completion: "I sent you a link yesterday; please check it out."
|
||||||
|
|
||||||
## Introducing Direct Links
|
## Introducing Direct Links
|
||||||
|
|
||||||
Today, we are introducing a new paradigm to signing: Async Direct Signing Links. Direct links are attached to a template and can be used anytime by anyone using the link. You set up the signature experience and flow once using all existing template mechanisms and you are done. You can provide anyone with the link so they can sign whenever they need to. You can even post the link publicly if you want to maximize its reach, i.e. for sales contracts.
|
Today, we are introducing a new paradigm to signing: Async Direct Signing Links. Direct links are attached to a template and can be used anytime by anyone using the link. You set up the signature experience and flow once using all existing template mechanisms and you are done. You can provide anyone with the link so they can sign whenever they need to. You can even post the link publicly if you want to maximize its reach, i.e. for sales contracts.
|
||||||
|
|
||||||
<video
|
<video
|
||||||
@ -45,15 +47,14 @@ Today, we are introducing a new paradigm to signing: Async Direct Signing Links.
|
|||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
|
|
||||||
|
|
||||||
## Embrace Async
|
## Embrace Async
|
||||||
|
|
||||||
So, how does this help anyone? You may still need to send a signature request to people, but in the cases you don't, you are not forced to anymore. Need an NDA? Check out our standing NDA link. A customer needs an updated Form W-9? Just use the company W-9 Link; it always has the most up-to-date form. You can even go as far as publicly posting a link to a software development or design contract any potential customer can sign anytime. Can they talk to you first? Sure, but if they don't need to or already have to, they go straight to the link. The process of actively sending has gotten us used to using a sync paradigm (I send, you receive and sign, and I get the result), whereas an async one (you sign whenever it suits you, and I become active only then, if at all) is way better suited. Adding more approval and signature steps makes sure you still control the outcome, but the process becomes a lot more efficient. For example, you can grab your own copy of the early adopter's pledge here if you missed it: [documen.so/pledge](https://documen.so/pledge).
|
So, how does this help anyone? You may still need to send a signature request to people, but in the cases you don't, you are not forced to anymore. Need an NDA? Check out our standing NDA link. A customer needs an updated Form W-9? Just use the company W-9 Link; it always has the most up-to-date form. You can even go as far as publicly posting a link to a software development or design contract any potential customer can sign anytime. Can they talk to you first? Sure, but if they don't need to or already have to, they go straight to the link. The process of actively sending has gotten us used to using a sync paradigm (I send, you receive and sign, and I get the result), whereas an async one (you sign whenever it suits you, and I become active only then, if at all) is way better suited. Adding more approval and signature steps makes sure you still control the outcome, but the process becomes a lot more efficient. For example, you can grab your own copy of the early adopter's pledge here if you missed it: [documen.so/pledge](https://documen.so/pledge).
|
||||||
|
|
||||||
> Take a minute to think about every signing request you send and whether they really require you to be part of the transaction. Could they be outsourced to the recipient and only reviewed once their part is done?
|
> Take a minute to think about every signing request you send and whether they really require you to be part of the transaction. Could they be outsourced to the recipient and only reviewed once their part is done?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Coming Soon: Profiles
|
## Coming Soon: Profiles
|
||||||
|
|
||||||
The best place to put your public links will be your **Documenso profile**, which is also close to launching. We want to get a feel for how links are used and move on to profiles shortly after. Want to try out direct links? Grab a free account here to get started: [documen.so/free](https://documen.so/free).
|
The best place to put your public links will be your **Documenso profile**, which is also close to launching. We want to get a feel for how links are used and move on to profiles shortly after. Want to try out direct links? Grab a free account here to get started: [documen.so/free](https://documen.so/free).
|
||||||
|
|
||||||
As always, we want to hear from you on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments.
|
As always, we want to hear from you on [Twitter / X](https://twitter.com/eltimuro) (DMs are open) or [Discord](https://documen.so/discord) if you have any questions, ideas, or comments.
|
||||||
|
|||||||
@ -11,23 +11,26 @@ tags:
|
|||||||
- Productivity
|
- Productivity
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## Yes to Yes
|
## Yes to Yes
|
||||||
|
|
||||||
> [Check out Part 1](https://documen.so/freelance-proposal) to learn about signing freelance proposals with Documenso and getting your first yes
|
> [Check out Part 1](https://documen.so/freelance-proposal) to learn about signing freelance proposals with Documenso and getting your first yes
|
||||||
|
|
||||||
A basic rule of sales is going from "yes to yes”. Outlining the main points of working together in a proposal is a good way to get to your first yes since it reduces details and focuses on the main points of the work at hand. After being on the same page about the work and getting the first yes, it's time to draw up a formal contract. While agreeing to the proposal has some weight as well, the legal contract formalizes the commitments of both sides in an enforceable way. Having clear legal terms on payments, unexpected cases, and even dissolving the partnership helps both parties to feel assured about what to expect.
|
A basic rule of sales is going from "yes to yes”. Outlining the main points of working together in a proposal is a good way to get to your first yes since it reduces details and focuses on the main points of the work at hand. After being on the same page about the work and getting the first yes, it's time to draw up a formal contract. While agreeing to the proposal has some weight as well, the legal contract formalizes the commitments of both sides in an enforceable way. Having clear legal terms on payments, unexpected cases, and even dissolving the partnership helps both parties to feel assured about what to expect.
|
||||||
|
|
||||||
### **Digital Signatures for the Win**
|
### **Digital Signatures for the Win**
|
||||||
|
|
||||||
Digitally signing documents accelerates contract closure, enhancing both speed and security. Parties can review and sign documents within minutes, eliminating the days required for manual signatures or even weeks with traditional mail. Beyond these efficiency gains, digital signatures boost trust by making the process secure and auditable. Once signed, digital documents are immutable, and every step is logged.
|
Digitally signing documents accelerates contract closure, enhancing both speed and security. Parties can review and sign documents within minutes, eliminating the days required for manual signatures or even weeks with traditional mail. Beyond these efficiency gains, digital signatures boost trust by making the process secure and auditable. Once signed, digital documents are immutable, and every step is logged.
|
||||||
|
|
||||||
Documenso simplifies this process, allowing you to send contracts effortlessly. As an open-source solution, our product's integrity and security are verifiable by anyone, which is why thousands of users rely on Documenso for their signing needs. Discover more at [https://documen.so/open](https://documen.so/open).
|
Documenso simplifies this process, allowing you to send contracts effortlessly. As an open-source solution, our product's integrity and security are verifiable by anyone, which is why thousands of users rely on Documenso for their signing needs. Discover more at [https://documen.so/open](https://documen.so/open).
|
||||||
|
|
||||||
## Preparing the Contract
|
## Preparing the Contract
|
||||||
|
|
||||||
As a freelancer, obtaining a contract template ensures you have a standardized and professional agreement ready for new clients, helping to protect your interests and clarify project terms. While there are many good templates out there, be sure to verify that they fit your case since contracts are often very specific to a certain case. Always consider having your contract checked by a legal professional if it's a high-value transaction.
|
As a freelancer, obtaining a contract template ensures you have a standardized and professional agreement ready for new clients, helping to protect your interests and clarify project terms. While there are many good templates out there, be sure to verify that they fit your case since contracts are often very specific to a certain case. Always consider having your contract checked by a legal professional if it's a high-value transaction.
|
||||||
|
|
||||||
Here is a quick checklist of what your contract should include:
|
Here is a quick checklist of what your contract should include:
|
||||||
|
|
||||||
### Checklist
|
### Checklist
|
||||||
|
|
||||||
- Names and Addresses of you and your client
|
- Names and Addresses of you and your client
|
||||||
- Scope of Work to be performed, deadlines and deliverables
|
- Scope of Work to be performed, deadlines and deliverables
|
||||||
- Payment Terms, Payment Schedules, and Pricing
|
- Payment Terms, Payment Schedules, and Pricing
|
||||||
@ -43,8 +46,8 @@ Here is a quick checklist of what your contract should include:
|
|||||||
- Severability Clause ensuring minor errors will not endanger the whole contract
|
- Severability Clause ensuring minor errors will not endanger the whole contract
|
||||||
- The signees with name, role, and date
|
- The signees with name, role, and date
|
||||||
|
|
||||||
|
|
||||||
## Getting the Signature
|
## Getting the Signature
|
||||||
|
|
||||||
Once you have your contract ready, you can upload it and add recipients and signature fields. To add a more personal touch, consider adding a personal message to the signature request.
|
Once you have your contract ready, you can upload it and add recipients and signature fields. To add a more personal touch, consider adding a personal message to the signature request.
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
@ -91,6 +94,7 @@ The more you add to the workflow, the more important it is to keep up to date wi
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
### Conclusion
|
### Conclusion
|
||||||
|
|
||||||
Sending a contract to clients using Documenso makes the process fast and easy. Seeing if your contract was signed or even read helps you understand where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-contract) to send 5 contracts per month. Digital signing in 2024 is the best practice for professionals seeking the most efficient way to get business done.
|
Sending a contract to clients using Documenso makes the process fast and easy. Seeing if your contract was signed or even read helps you understand where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-contract) to send 5 contracts per month. Digital signing in 2024 is the best practice for professionals seeking the most efficient way to get business done.
|
||||||
|
|
||||||
Let us know what you think and what we can improve. Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
|
Let us know what you think and what we can improve. Connect with us on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments! We're always here and would love to hear from you :)
|
||||||
|
|||||||
@ -16,11 +16,13 @@ Getting new clients, or maybe even your first client, to sign with you is at the
|
|||||||
## Understanding Proposal and Contracts
|
## Understanding Proposal and Contracts
|
||||||
|
|
||||||
### 1. Initial Proposal
|
### 1. Initial Proposal
|
||||||
|
|
||||||
> Agreeing on what needs to be done and terms for payment
|
> Agreeing on what needs to be done and terms for payment
|
||||||
|
|
||||||
A proposal will include the scope of the work (what does the customer want done?), desired deliverables (documents, code, features, videos, etc.), timelines, payment terms (one-time, monthly, per hour), and prices (e.g. $60/ hour, $5k one-time). A proposal is important for both sides to be clear about the goal and the terms that apply. Customers usually decide based on the proposal if your offer is what they want.
|
A proposal will include the scope of the work (what does the customer want done?), desired deliverables (documents, code, features, videos, etc.), timelines, payment terms (one-time, monthly, per hour), and prices (e.g. $60/ hour, $5k one-time). A proposal is important for both sides to be clear about the goal and the terms that apply. Customers usually decide based on the proposal if your offer is what they want.
|
||||||
|
|
||||||
### 2. Formal Contract
|
### 2. Formal Contract
|
||||||
|
|
||||||
> After the client signs the proposal, a contract can be signed, formalizing the agreement and adding detailed legal terms.
|
> After the client signs the proposal, a contract can be signed, formalizing the agreement and adding detailed legal terms.
|
||||||
|
|
||||||
Once the terms are agreed upon, a more formal document should specify the terms of working together, especially legal details such as confidentiality, indemnification, governing law, etc. The contract provides clarity on legal details for both sides and formalizes their claims.
|
Once the terms are agreed upon, a more formal document should specify the terms of working together, especially legal details such as confidentiality, indemnification, governing law, etc. The contract provides clarity on legal details for both sides and formalizes their claims.
|
||||||
@ -28,11 +30,13 @@ Once the terms are agreed upon, a more formal document should specify the terms
|
|||||||
Sending one or even multiple documents to a potential client and having them send them back signed (in the worst case, they send it via actual mail) is time-consuming for both sides. It also introduces friction at a time when making it as easy as possible for your potential client to say yes should be your number one goal.
|
Sending one or even multiple documents to a potential client and having them send them back signed (in the worst case, they send it via actual mail) is time-consuming for both sides. It also introduces friction at a time when making it as easy as possible for your potential client to say yes should be your number one goal.
|
||||||
|
|
||||||
### **Digital Signatures for the Win**
|
### **Digital Signatures for the Win**
|
||||||
|
|
||||||
Signing documents digitally makes closing proposals and contracts faster and more secure. Each party can review and sign the documents in minutes instead of days (inserting the signatures manually via PDF editor) or even weeks (using conventional mail). Apart from the efficiency gains, signing digitally also increases trust by making the process more secure and auditable. Digitally signed documents can’t be changed after the fact, and every step of the process is logged.
|
Signing documents digitally makes closing proposals and contracts faster and more secure. Each party can review and sign the documents in minutes instead of days (inserting the signatures manually via PDF editor) or even weeks (using conventional mail). Apart from the efficiency gains, signing digitally also increases trust by making the process more secure and auditable. Digitally signed documents can’t be changed after the fact, and every step of the process is logged.
|
||||||
|
|
||||||
Documenso lets you reap these benefits by sending proposals and contracts with minimal effort. Being open source, the whole world can verify our product and how we deliver on these promises, which is why thousands of users already trust Documenso for their signing needs: [https://documen.so/open](https://documen.so/open).
|
Documenso lets you reap these benefits by sending proposals and contracts with minimal effort. Being open source, the whole world can verify our product and how we deliver on these promises, which is why thousands of users already trust Documenso for their signing needs: [https://documen.so/open](https://documen.so/open).
|
||||||
|
|
||||||
## Preparing the Proposal
|
## Preparing the Proposal
|
||||||
|
|
||||||
If you already have a proposal template, create a new version for your client and export it to PDF. If your tool doesn’t support that, your system's “PDF printer” lets you create a PDF from almost any tool by using the print function. If you do not have a template yet, you can find a lot of content and guides on the matter through a quick Google search. Here is a quick checklist of what your proposal should cover:
|
If you already have a proposal template, create a new version for your client and export it to PDF. If your tool doesn’t support that, your system's “PDF printer” lets you create a PDF from almost any tool by using the print function. If you do not have a template yet, you can find a lot of content and guides on the matter through a quick Google search. Here is a quick checklist of what your proposal should cover:
|
||||||
|
|
||||||
- A clear and concise title
|
- A clear and concise title
|
||||||
@ -48,6 +52,7 @@ If you already have a proposal template, create a new version for your client an
|
|||||||
- Summary of major terms for the coming contract
|
- Summary of major terms for the coming contract
|
||||||
|
|
||||||
## Sending the Proposal
|
## Sending the Proposal
|
||||||
|
|
||||||
If you don’t have a Documenso Account yet, you can [create one for free](https://documen.so/signup?utm_source=blog-freelancer-proposal). Once you sign up, you can upload your proposal PDF by simply dragging it into the upload area. Add your potential client as a recipient, add a signature field, and you are done! You can track the status of your proposal simply by clicking the Document in the overview. Documenso will also notify you once the proposal is signed.
|
If you don’t have a Documenso Account yet, you can [create one for free](https://documen.so/signup?utm_source=blog-freelancer-proposal). Once you sign up, you can upload your proposal PDF by simply dragging it into the upload area. Add your potential client as a recipient, add a signature field, and you are done! You can track the status of your proposal simply by clicking the Document in the overview. Documenso will also notify you once the proposal is signed.
|
||||||
|
|
||||||
<video
|
<video
|
||||||
@ -60,6 +65,7 @@ If you don’t have a Documenso Account yet, you can [create one for free](https
|
|||||||
></video>
|
></video>
|
||||||
|
|
||||||
### Conclusion
|
### Conclusion
|
||||||
|
|
||||||
Sending a proposal to potential clients using Documenso makes getting to the first “yes” fast and easy. Seeing if your proposal was signed or even read helps you to get a feel for where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-proposal) to send 5 proposals per month. Digital Signing in 2024 is the best practice for all professionals looking for the most efficient way to get business done.
|
Sending a proposal to potential clients using Documenso makes getting to the first “yes” fast and easy. Seeing if your proposal was signed or even read helps you to get a feel for where you are in the process. You can use the [Documenso Free Plan](https://app.documenso.com/signup?utm_source=blog-freelancer-proposal) to send 5 proposals per month. Digital Signing in 2024 is the best practice for all professionals looking for the most efficient way to get business done.
|
||||||
|
|
||||||
> [Check out Part 2](https://documen.so/freelance-contract) to learn about signing freelance contracts with Documenso.
|
> [Check out Part 2](https://documen.so/freelance-contract) to learn about signing freelance contracts with Documenso.
|
||||||
@ -68,4 +74,3 @@ Let us know what you think and what we can improve. Which field types are you mi
|
|||||||
|
|
||||||
Best from Hamburg\
|
Best from Hamburg\
|
||||||
Timur
|
Timur
|
||||||
|
|
||||||
|
|||||||
@ -27,9 +27,11 @@ tags:
|
|||||||
> TLDR; The Early Adopters Plan ended, and we have a new pricing. If you are an Early Adopter, reach out for a Discord community badge 🏅
|
> TLDR; The Early Adopters Plan ended, and we have a new pricing. If you are an Early Adopter, reach out for a Discord community badge 🏅
|
||||||
|
|
||||||
# The End of the Beginning
|
# The End of the Beginning
|
||||||
|
|
||||||
12 months, 13 releases, and 344 merged pull requests after announcing the Early Adopter plan in our first-ever launch week, and we hit the cap of 100. Documenso has changed and grown a lot since then. For us, this is a great milestone towards our broader mission of bringing open signing to the world since now we are joined by a community consisting of contributors and early customers all around the world.
|
12 months, 13 releases, and 344 merged pull requests after announcing the Early Adopter plan in our first-ever launch week, and we hit the cap of 100. Documenso has changed and grown a lot since then. For us, this is a great milestone towards our broader mission of bringing open signing to the world since now we are joined by a community consisting of contributors and early customers all around the world.
|
||||||
|
|
||||||
# The New Plans
|
# The New Plans
|
||||||
|
|
||||||
Starting today, we are sunsetting the Early Adopter Plan in favor of our new, more nuanced pricing model. The Early Adopter plan will succeeded by the **Individual plan**, which is still priced at $30/mo. The Individual plans will still include unlimited signatures and recipients since this aligns with our core belief of empowering our users wherever possible. If you managed to grab an Early Adopter plan, reach out on X or Discord to receive a special community badge. Early Adopters are meant to get preferential treatment where possible.
|
Starting today, we are sunsetting the Early Adopter Plan in favor of our new, more nuanced pricing model. The Early Adopter plan will succeeded by the **Individual plan**, which is still priced at $30/mo. The Individual plans will still include unlimited signatures and recipients since this aligns with our core belief of empowering our users wherever possible. If you managed to grab an Early Adopter plan, reach out on X or Discord to receive a special community badge. Early Adopters are meant to get preferential treatment where possible.
|
||||||
|
|
||||||
Previously soft-launched as part of Early Adopters, we are officially introducing the **Team Plan** to our pricing for customers requiring multi-user accounts. Priced at $50/ mo. for 5 users, this plan offers unlimited signature volume as well. Additional users can be added for $10/mo. as needed. We have carefully crafted the billing of teams to ensure that dynamic changes are accurately reflected at the end of each billing cycle, providing you with a fair-value pricing structure.
|
Previously soft-launched as part of Early Adopters, we are officially introducing the **Team Plan** to our pricing for customers requiring multi-user accounts. Priced at $50/ mo. for 5 users, this plan offers unlimited signature volume as well. Additional users can be added for $10/mo. as needed. We have carefully crafted the billing of teams to ensure that dynamic changes are accurately reflected at the end of each billing cycle, providing you with a fair-value pricing structure.
|
||||||
@ -39,6 +41,7 @@ Our **Free Plan** stays unchanged, offering coverage to casual users and an easy
|
|||||||
Check out our [new pricing page here](https://documen.so/pricing). We also updated our [open page](https://documen.so/open) to reflect the end of Early Adopters. The metric now counts active subscriptions from Individuals and Teams.
|
Check out our [new pricing page here](https://documen.so/pricing). We also updated our [open page](https://documen.so/open) to reflect the end of Early Adopters. The metric now counts active subscriptions from Individuals and Teams.
|
||||||
|
|
||||||
# API Access
|
# API Access
|
||||||
|
|
||||||
All plans include access to the API as per our philosophy, making Documenso an open platform, and allowing everyone to build on it, no matter how big or small. Besides the Free Plan's 5 signatures per month limit, the API does not have access restrictions. Even the free plan can keep using the API after using its signature volume for non-signing operations like reading, editing, and even creating documents. Since the individual plan technically allows for running a Fortune 500 company for $30/ mo., plan we are adding a fair use clause here: You are free to use the API "a lot" if you are a big organization trying to stay on the Individual Plan we will ask to have a word about upgrading (which might make sense anyway considering your requirements). Fair use excludes Early Adopters, which we consider limitless by any measure. If you need clarification on whether your case is covered under fair use, you can contact us on Discord or support@documenso.com. It's probably fine, though.
|
All plans include access to the API as per our philosophy, making Documenso an open platform, and allowing everyone to build on it, no matter how big or small. Besides the Free Plan's 5 signatures per month limit, the API does not have access restrictions. Even the free plan can keep using the API after using its signature volume for non-signing operations like reading, editing, and even creating documents. Since the individual plan technically allows for running a Fortune 500 company for $30/ mo., plan we are adding a fair use clause here: You are free to use the API "a lot" if you are a big organization trying to stay on the Individual Plan we will ask to have a word about upgrading (which might make sense anyway considering your requirements). Fair use excludes Early Adopters, which we consider limitless by any measure. If you need clarification on whether your case is covered under fair use, you can contact us on Discord or support@documenso.com. It's probably fine, though.
|
||||||
|
|
||||||
We also have a lot in the pipeline, and we are excited to share everything with you soon. A Big Shoutout to all Early Adopters. We salute you, and you will receive the preferred treatment where possible.
|
We also have a lot in the pipeline, and we are excited to share everything with you soon. A Big Shoutout to all Early Adopters. We salute you, and you will receive the preferred treatment where possible.
|
||||||
|
|||||||
@ -30,6 +30,12 @@ const SLIDES = [
|
|||||||
srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm',
|
srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm',
|
||||||
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm',
|
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Direct Link',
|
||||||
|
type: 'video',
|
||||||
|
srcLight: 'https://github.com/documenso/design/raw/main/marketing/direct-links.webm',
|
||||||
|
srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/direct-links.webm',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Webhooks',
|
label: 'Webhooks',
|
||||||
type: 'video',
|
type: 'video',
|
||||||
|
|||||||
@ -14,18 +14,34 @@ import {
|
|||||||
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
|
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
|
||||||
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
|
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
|
||||||
import {
|
import {
|
||||||
|
getUserWithAtLeastOneDocumentPerMonth,
|
||||||
|
getUserWithAtLeastOneDocumentSignedPerMonth,
|
||||||
|
getUserWithSignedDocumentMonthlyGrowth,
|
||||||
getUsersCount,
|
getUsersCount,
|
||||||
getUsersWithSubscriptionsCount,
|
getUsersWithSubscriptionsCount,
|
||||||
} from '@documenso/lib/server-only/admin/get-users-stats';
|
} from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
|
|
||||||
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
|
||||||
|
|
||||||
|
import { UserWithDocumentChart } from './user-with-document';
|
||||||
|
|
||||||
export default async function AdminStatsPage() {
|
export default async function AdminStatsPage() {
|
||||||
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([
|
const [
|
||||||
|
usersCount,
|
||||||
|
usersWithSubscriptionsCount,
|
||||||
|
docStats,
|
||||||
|
recipientStats,
|
||||||
|
userWithAtLeastOneDocumentPerMonth,
|
||||||
|
userWithAtLeastOneDocumentSignedPerMonth,
|
||||||
|
MONTHLY_USERS_SIGNED,
|
||||||
|
] = await Promise.all([
|
||||||
getUsersCount(),
|
getUsersCount(),
|
||||||
getUsersWithSubscriptionsCount(),
|
getUsersWithSubscriptionsCount(),
|
||||||
getDocumentStats(),
|
getDocumentStats(),
|
||||||
getRecipientsStats(),
|
getRecipientsStats(),
|
||||||
|
getUserWithAtLeastOneDocumentPerMonth(),
|
||||||
|
getUserWithAtLeastOneDocumentSignedPerMonth(),
|
||||||
|
getUserWithSignedDocumentMonthlyGrowth(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -43,12 +59,11 @@ export default async function AdminStatsPage() {
|
|||||||
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
|
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-1 lg:grid-cols-2">
|
<div className="mt-16 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-3xl font-semibold">Document metrics</h3>
|
<h3 className="text-3xl font-semibold">Document metrics</h3>
|
||||||
|
|
||||||
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
|
<div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
|
|
||||||
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
|
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
|
||||||
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
|
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
|
||||||
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
|
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
|
||||||
@ -58,7 +73,7 @@ export default async function AdminStatsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
|
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
|
||||||
|
|
||||||
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
|
<div className="mb-8 mt-4 grid flex-1 grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<CardMetric
|
<CardMetric
|
||||||
icon={UserSquare2}
|
icon={UserSquare2}
|
||||||
title="Total Recipients"
|
title="Total Recipients"
|
||||||
@ -70,6 +85,23 @@ export default async function AdminStatsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16">
|
||||||
|
<h3 className="text-3xl font-semibold">Charts</h3>
|
||||||
|
<div className="mt-5 grid grid-cols-2 gap-10">
|
||||||
|
<UserWithDocumentChart
|
||||||
|
data={MONTHLY_USERS_SIGNED}
|
||||||
|
title="MAU (created document)"
|
||||||
|
tooltip="Monthly Active Users: Users that created at least one Document"
|
||||||
|
/>
|
||||||
|
<UserWithDocumentChart
|
||||||
|
data={MONTHLY_USERS_SIGNED}
|
||||||
|
completed
|
||||||
|
title="MAU (had document completed)"
|
||||||
|
tooltip="Monthly Active Users: Users that had at least one of their documents completed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
import type { TooltipProps } from 'recharts';
|
||||||
|
import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent';
|
||||||
|
|
||||||
|
import type { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
|
||||||
|
|
||||||
|
export type UserWithDocumentChartProps = {
|
||||||
|
className?: string;
|
||||||
|
title: string;
|
||||||
|
data: GetUserWithDocumentMonthlyGrowth;
|
||||||
|
completed?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomTooltip = ({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
|
||||||
|
if (active && payload && payload.length) {
|
||||||
|
return (
|
||||||
|
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
|
||||||
|
<p className="">{label}</p>
|
||||||
|
<p className="text-documenso">
|
||||||
|
{`${tooltip} : `}
|
||||||
|
<span className="text-black">{payload[0].value}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserWithDocumentChart = ({
|
||||||
|
className,
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
completed = false,
|
||||||
|
tooltip,
|
||||||
|
}: UserWithDocumentChartProps) => {
|
||||||
|
const formattedData = (data: GetUserWithDocumentMonthlyGrowth, completed: boolean) => {
|
||||||
|
return [...data].reverse().map(({ month, count, signed_count }) => {
|
||||||
|
const formattedMonth = DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL');
|
||||||
|
if (completed) {
|
||||||
|
return {
|
||||||
|
month: formattedMonth,
|
||||||
|
count: Number(signed_count),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
month: formattedMonth,
|
||||||
|
count: Number(count),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
|
||||||
|
<div className="mb-6 flex h-12 px-4">
|
||||||
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<BarChart className="bg-white" data={formattedData(data, completed)}>
|
||||||
|
<XAxis dataKey="month" />
|
||||||
|
<YAxis />
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={<CustomTooltip tooltip={tooltip} />}
|
||||||
|
labelStyle={{
|
||||||
|
color: 'hsl(var(--primary-foreground))',
|
||||||
|
}}
|
||||||
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bar
|
||||||
|
dataKey="count"
|
||||||
|
fill="hsl(var(--primary))"
|
||||||
|
radius={[4, 4, 0, 0]}
|
||||||
|
maxBarSize={60}
|
||||||
|
label={tooltip}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -8,7 +8,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
|||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||||
import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document';
|
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
@ -86,14 +86,15 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
documentMeta.password = securePassword;
|
documentMeta.password = securePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [recipients, completedFields] = await Promise.all([
|
const [recipients, fields] = await Promise.all([
|
||||||
getRecipientsForDocument({
|
getRecipientsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}),
|
}),
|
||||||
getCompletedFieldsForDocument({
|
getFieldsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
|
userId: user.id,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -163,10 +164,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{document.status === DocumentStatus.PENDING && (
|
{document.status === DocumentStatus.PENDING && (
|
||||||
<DocumentReadOnlyFields
|
<DocumentReadOnlyFields fields={fields} documentMeta={documentMeta || undefined} />
|
||||||
fields={completedFields}
|
|
||||||
documentMeta={document.documentMeta || undefined}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||||
|
|||||||
@ -383,7 +383,7 @@ export const TemplateDirectLinkDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className='mt-4'>
|
<DialogFooter className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get
|
|||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
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 { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||||
|
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
@ -70,8 +71,14 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let recipientHasAccount: boolean | null = null;
|
||||||
|
|
||||||
if (!isDocumentAccessValid) {
|
if (!isDocumentAccessValid) {
|
||||||
return <SigningAuthPageView email={recipient.email} />;
|
recipientHasAccount = await getUserByEmail({ email: recipient?.email })
|
||||||
|
.then((user) => !!user)
|
||||||
|
.catch(() => false);
|
||||||
|
|
||||||
|
return <SigningAuthPageView email={recipient.email} emailHasAccount={!!recipientHasAccount} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
await viewedDocument({
|
await viewedDocument({
|
||||||
|
|||||||
@ -11,9 +11,10 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
export type SigningAuthPageViewProps = {
|
export type SigningAuthPageViewProps = {
|
||||||
email: string;
|
email: string;
|
||||||
|
emailHasAccount?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
|
export const SigningAuthPageView = ({ email, emailHasAccount }: SigningAuthPageViewProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
@ -30,7 +31,9 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await signOut({
|
await signOut({
|
||||||
callbackUrl: `/signin?email=${encodeURIComponent(encryptedEmail)}`,
|
callbackUrl: emailHasAccount
|
||||||
|
? `/signin?email=${encodeURIComponent(encryptedEmail)}`
|
||||||
|
: `/signup?email=${encodeURIComponent(encryptedEmail)}`,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
@ -59,7 +62,7 @@ export const SigningAuthPageView = ({ email }: SigningAuthPageViewProps) => {
|
|||||||
onClick={async () => handleChangeAccount(email)}
|
onClick={async () => handleChangeAccount(email)}
|
||||||
loading={isSigningOut}
|
loading={isSigningOut}
|
||||||
>
|
>
|
||||||
Login
|
{emailHasAccount ? 'Login' : 'Sign up'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { EyeOffIcon } from 'lucide-react';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,19 +11,19 @@ import {
|
|||||||
} from '@documenso/lib/constants/date-formats';
|
} from '@documenso/lib/constants/date-formats';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { DocumentMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta } from '@documenso/prisma/client';
|
||||||
import { FieldType } from '@documenso/prisma/client';
|
import { FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||||
|
|
||||||
export type DocumentReadOnlyFieldsProps = {
|
export type DocumentReadOnlyFieldsProps = {
|
||||||
fields: CompletedField[];
|
fields: DocumentField[];
|
||||||
documentMeta?: DocumentMeta;
|
documentMeta?: DocumentMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,30 +54,33 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
}
|
}
|
||||||
contentProps={{
|
contentProps={{
|
||||||
className: 'flex w-fit flex-col py-2.5 text-sm',
|
className: 'relative flex w-fit flex-col p-2.5 text-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>
|
<p className="font-semibold">
|
||||||
<span className="font-semibold">
|
{field.Recipient.signingStatus === SigningStatus.SIGNED ? 'Signed' : 'Pending'}{' '}
|
||||||
|
{FRIENDLY_FIELD_TYPE[field.type].toLowerCase()} field
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
{field.Recipient.name
|
{field.Recipient.name
|
||||||
? `${field.Recipient.name} (${field.Recipient.email})`
|
? `${field.Recipient.name} (${field.Recipient.email})`
|
||||||
: field.Recipient.email}{' '}
|
: field.Recipient.email}{' '}
|
||||||
</span>
|
|
||||||
inserted a {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
|
||||||
className="mt-2.5 h-6 text-xs focus:outline-none focus-visible:ring-0"
|
|
||||||
onClick={() => handleHideField(field.secondaryId)}
|
onClick={() => handleHideField(field.secondaryId)}
|
||||||
|
title="Hide field"
|
||||||
>
|
>
|
||||||
Hide field
|
<EyeOffIcon className="h-3 w-3" />
|
||||||
</Button>
|
</button>
|
||||||
</PopoverHover>
|
</PopoverHover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground break-all text-sm">
|
<div className="text-muted-foreground break-all text-sm">
|
||||||
{match(field)
|
{field.Recipient.signingStatus === SigningStatus.SIGNED &&
|
||||||
|
match(field)
|
||||||
.with({ type: FieldType.SIGNATURE }, (field) =>
|
.with({ type: FieldType.SIGNATURE }, (field) =>
|
||||||
field.Signature?.signatureImageAsBase64 ? (
|
field.Signature?.signatureImageAsBase64 ? (
|
||||||
<img
|
<img
|
||||||
@ -103,6 +107,18 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
|
|||||||
)
|
)
|
||||||
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
|
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
|
||||||
.exhaustive()}
|
.exhaustive()}
|
||||||
|
|
||||||
|
{field.Recipient.signingStatus === SigningStatus.NOT_SIGNED && (
|
||||||
|
<p
|
||||||
|
className={cn('text-muted-foreground text-lg duration-200', {
|
||||||
|
'font-signature sm:text-xl md:text-2xl lg:text-3xl':
|
||||||
|
field.type === FieldType.SIGNATURE ||
|
||||||
|
field.type === FieldType.FREE_SIGNATURE,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{FRIENDLY_FIELD_TYPE[field.type]}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FieldRootContainer>
|
</FieldRootContainer>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -292,7 +292,9 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
|
|||||||
await unseedUser(user.id);
|
await unseedUser(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', async ({ page }) => {
|
test('[DOCUMENT_FLOW]: should not be able to create a document without signatures', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
const user = await seedUser();
|
const user = await seedUser();
|
||||||
const document = await seedBlankDocument(user);
|
const document = await seedBlankDocument(user);
|
||||||
|
|
||||||
@ -323,43 +325,9 @@ test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', asyn
|
|||||||
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Add subject and send
|
await expect(
|
||||||
await expect(page.getByRole('heading', { name: 'Add Subject' })).toBeVisible();
|
page.getByRole('dialog').getByText('No signature field found').first(),
|
||||||
await page.getByRole('button', { name: 'Send' }).click();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.waitForURL('/documents');
|
|
||||||
|
|
||||||
// Assert document was created
|
|
||||||
await expect(page.getByRole('link', { name: documentTitle })).toBeVisible();
|
|
||||||
await page.getByRole('link', { name: documentTitle }).click();
|
|
||||||
await page.waitForURL(/\/documents\/\d+/);
|
|
||||||
|
|
||||||
// Start signing process
|
|
||||||
const url = page.url().split('/');
|
|
||||||
const documentId = url[url.length - 1];
|
|
||||||
|
|
||||||
const { token } = await getRecipientByEmail({
|
|
||||||
email: 'user1@example.com',
|
|
||||||
documentId: Number(documentId),
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.goto(`/sign/${token}`);
|
|
||||||
await page.waitForURL(`/sign/${token}`);
|
|
||||||
|
|
||||||
// Check if document has been viewed
|
|
||||||
const { status } = await getDocumentByToken(token);
|
|
||||||
expect(status).toBe(DocumentStatus.PENDING);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
|
||||||
await expect(page.getByRole('dialog').getByText('Complete Signing').first()).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
|
||||||
|
|
||||||
await page.waitForURL(`/sign/${token}/complete`);
|
|
||||||
await expect(page.getByText('Document Signed')).toBeVisible();
|
|
||||||
|
|
||||||
// Check if document has been signed
|
|
||||||
const { status: completedStatus } = await getDocumentByToken(token);
|
|
||||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
|
||||||
|
|
||||||
await unseedUser(user.id);
|
await unseedUser(user.id);
|
||||||
});
|
});
|
||||||
@ -449,6 +417,9 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
|
|||||||
await page.getByPlaceholder('Email').fill('user1@example.com');
|
await page.getByPlaceholder('Email').fill('user1@example.com');
|
||||||
await page.getByPlaceholder('Name').fill('User 1');
|
await page.getByPlaceholder('Name').fill('User 1');
|
||||||
|
|
||||||
|
await page.getByRole('combobox').click();
|
||||||
|
await page.getByLabel('Needs to approve').getByText('Needs to approve').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|
||||||
// Add fields
|
// Add fields
|
||||||
@ -480,8 +451,8 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a
|
|||||||
expect(status).toBe(DocumentStatus.PENDING);
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
await expect(page.getByRole('dialog').getByText('Complete Signing').first()).toBeVisible();
|
await expect(page.getByRole('dialog').getByText('Complete Approval').first()).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Sign' }).click();
|
await page.getByRole('button', { name: 'Approve' }).click();
|
||||||
|
|
||||||
await page.waitForURL('https://documenso.com');
|
await page.waitForURL('https://documenso.com');
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,9 @@ import { getPricesByPlan } from './get-prices-by-plan';
|
|||||||
* Returns the Stripe prices of items that affect the amount of documents a user can create.
|
* Returns the Stripe prices of items that affect the amount of documents a user can create.
|
||||||
*/
|
*/
|
||||||
export const getDocumentRelatedPrices = async () => {
|
export const getDocumentRelatedPrices = async () => {
|
||||||
return await getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
|
return await getPricesByPlan([
|
||||||
|
STRIPE_PLAN_TYPE.REGULAR,
|
||||||
|
STRIPE_PLAN_TYPE.COMMUNITY,
|
||||||
|
STRIPE_PLAN_TYPE.ENTERPRISE,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type Stripe from 'stripe';
|
import type Stripe from 'stripe';
|
||||||
|
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
||||||
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
// Utility type to handle usage of the `expand` option.
|
// Utility type to handle usage of the `expand` option.
|
||||||
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
|
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
|
||||||
|
|||||||
@ -6,5 +6,9 @@ import { getPricesByPlan } from './get-prices-by-plan';
|
|||||||
* Returns the prices of items that count as the account's primary plan.
|
* Returns the prices of items that count as the account's primary plan.
|
||||||
*/
|
*/
|
||||||
export const getPrimaryAccountPlanPrices = async () => {
|
export const getPrimaryAccountPlanPrices = async () => {
|
||||||
return await getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
|
return await getPricesByPlan([
|
||||||
|
STRIPE_PLAN_TYPE.REGULAR,
|
||||||
|
STRIPE_PLAN_TYPE.COMMUNITY,
|
||||||
|
STRIPE_PLAN_TYPE.ENTERPRISE,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const getUsersCount = async () => {
|
export const getUsersCount = async () => {
|
||||||
return await prisma.user.count();
|
return await prisma.user.count();
|
||||||
@ -16,3 +18,65 @@ export const getUsersWithSubscriptionsCount = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserWithAtLeastOneDocumentPerMonth = async () => {
|
||||||
|
return await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
Document: {
|
||||||
|
some: {
|
||||||
|
createdAt: {
|
||||||
|
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserWithAtLeastOneDocumentSignedPerMonth = async () => {
|
||||||
|
return await prisma.user.count({
|
||||||
|
where: {
|
||||||
|
Document: {
|
||||||
|
some: {
|
||||||
|
status: {
|
||||||
|
equals: DocumentStatus.COMPLETED,
|
||||||
|
},
|
||||||
|
completedAt: {
|
||||||
|
gte: DateTime.now().minus({ months: 1 }).toJSDate(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetUserWithDocumentMonthlyGrowth = Array<{
|
||||||
|
month: string;
|
||||||
|
count: number;
|
||||||
|
signed_count: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type GetUserWithDocumentMonthlyGrowthQueryResult = Array<{
|
||||||
|
month: Date;
|
||||||
|
count: bigint;
|
||||||
|
signed_count: bigint;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const getUserWithSignedDocumentMonthlyGrowth = async () => {
|
||||||
|
const result = await prisma.$queryRaw<GetUserWithDocumentMonthlyGrowthQueryResult>`
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('month', "Document"."createdAt") AS "month",
|
||||||
|
COUNT(DISTINCT "Document"."userId") as "count",
|
||||||
|
COUNT(DISTINCT CASE WHEN "Document"."status" = 'COMPLETED' THEN "Document"."userId" END) as "signed_count"
|
||||||
|
FROM "Document"
|
||||||
|
GROUP BY "month"
|
||||||
|
ORDER BY "month" DESC
|
||||||
|
LIMIT 12
|
||||||
|
`;
|
||||||
|
|
||||||
|
return result.map((row) => ({
|
||||||
|
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
|
||||||
|
count: Number(row.count),
|
||||||
|
signed_count: Number(row.signed_count),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|||||||
@ -5,13 +5,7 @@ import type { RequestMetadata } from '@documenso/lib/universal/extract-request-m
|
|||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import {
|
import { DocumentStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||||
DocumentSource,
|
|
||||||
DocumentStatus,
|
|
||||||
RecipientRole,
|
|
||||||
SendStatus,
|
|
||||||
SigningStatus,
|
|
||||||
} from '@documenso/prisma/client';
|
|
||||||
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
import { WebhookTriggerEvents } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { jobsClient } from '../../jobs/client';
|
import { jobsClient } from '../../jobs/client';
|
||||||
@ -71,8 +65,6 @@ export const sendDocument = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const customEmail = document?.documentMeta;
|
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new Error('Document not found');
|
throw new Error('Document not found');
|
||||||
}
|
}
|
||||||
@ -87,8 +79,6 @@ export const sendDocument = async ({
|
|||||||
|
|
||||||
const { documentData } = document;
|
const { documentData } = document;
|
||||||
|
|
||||||
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
|
||||||
|
|
||||||
if (!documentData.data) {
|
if (!documentData.data) {
|
||||||
throw new Error('Document data not found');
|
throw new Error('Document data not found');
|
||||||
}
|
}
|
||||||
@ -98,6 +88,7 @@ export const sendDocument = async ({
|
|||||||
|
|
||||||
const prefilled = await insertFormValuesInPdf({
|
const prefilled = await insertFormValuesInPdf({
|
||||||
pdf: Buffer.from(file),
|
pdf: Buffer.from(file),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
formValues: document.formValues as Record<string, string | number | boolean>,
|
formValues: document.formValues as Record<string, string | number | boolean>,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,6 +110,31 @@ export const sendDocument = async ({
|
|||||||
Object.assign(document, result);
|
Object.assign(document, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
||||||
|
// decide if we want to enforce this for API & templates.
|
||||||
|
// const fields = await getFieldsForDocument({
|
||||||
|
// documentId: documentId,
|
||||||
|
// userId: userId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const fieldsWithSignerEmail = fields.map((field) => ({
|
||||||
|
// ...field,
|
||||||
|
// signerEmail:
|
||||||
|
// document.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// const everySignerHasSignature = document?.Recipient.every(
|
||||||
|
// (recipient) =>
|
||||||
|
// recipient.role !== RecipientRole.SIGNER ||
|
||||||
|
// fieldsWithSignerEmail.some(
|
||||||
|
// (field) => field.type === 'SIGNATURE' && field.signerEmail === recipient.email,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (!everySignerHasSignature) {
|
||||||
|
// throw new Error('Some signers have not been assigned a signature field.');
|
||||||
|
// }
|
||||||
|
|
||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
document.Recipient.map(async (recipient) => {
|
document.Recipient.map(async (recipient) => {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsFo
|
|||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
|
signingStatus: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,6 +5,8 @@ export interface GetFieldsForDocumentOptions {
|
|||||||
userId: number;
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DocumentField = Awaited<ReturnType<typeof getFieldsForDocument>>[number];
|
||||||
|
|
||||||
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForDocumentOptions) => {
|
||||||
const fields = await prisma.field.findMany({
|
const fields = await prisma.field.findMany({
|
||||||
where: {
|
where: {
|
||||||
@ -26,6 +28,16 @@ export const getFieldsForDocument = async ({ documentId, userId }: GetFieldsForD
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
Signature: true,
|
||||||
|
Recipient: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
signingStatus: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -481,7 +481,9 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||||
recipientName: directRecipientEmail,
|
recipientName: directRecipientEmail,
|
||||||
recipientRole: directTemplateRecipient.role,
|
recipientRole: directTemplateRecipient.role,
|
||||||
documentLink: `${formatDocumentsPath(document.team?.url)}/${document.id}`,
|
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${
|
||||||
|
document.id
|
||||||
|
}`,
|
||||||
documentName: document.title,
|
documentName: document.title,
|
||||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -354,6 +354,9 @@ export const seedPendingDocumentWithFullFields = async ({
|
|||||||
...updateDocumentOptions,
|
...updateDocumentOptions,
|
||||||
status: DocumentStatus.PENDING,
|
status: DocumentStatus.PENDING,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import {
|
|||||||
DocumentFlowFormContainerStep,
|
DocumentFlowFormContainerStep,
|
||||||
} from './document-flow-root';
|
} from './document-flow-root';
|
||||||
import { FieldItem } from './field-item';
|
import { FieldItem } from './field-item';
|
||||||
|
import { MissingSignatureFieldDialog } from './missing-signature-field-dialog';
|
||||||
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
|
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
|
||||||
|
|
||||||
const fontCaveat = Caveat({
|
const fontCaveat = Caveat({
|
||||||
@ -66,6 +67,8 @@ export const AddFieldsFormPartial = ({
|
|||||||
canGoBack = false,
|
canGoBack = false,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
}: AddFieldsFormProps) => {
|
}: AddFieldsFormProps) => {
|
||||||
|
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
|
||||||
|
|
||||||
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
|
||||||
const { currentStep, totalSteps, previousStep } = useStep();
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
const canRenderBackButtonAsRemove =
|
const canRenderBackButtonAsRemove =
|
||||||
@ -317,6 +320,22 @@ export const AddFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
}, [recipientsByRole]);
|
}, [recipientsByRole]);
|
||||||
|
|
||||||
|
const handleGoNextClick = () => {
|
||||||
|
const everySignerHasSignature = recipientsByRole.SIGNER.every((signer) =>
|
||||||
|
localFields.some(
|
||||||
|
(field) =>
|
||||||
|
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
|
||||||
|
field.signerEmail === signer.email,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!everySignerHasSignature) {
|
||||||
|
setIsMissingSignatureDialogVisible(true);
|
||||||
|
} else {
|
||||||
|
void onFormSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
@ -602,9 +621,14 @@ export const AddFieldsFormPartial = ({
|
|||||||
documentFlow.onBackStep?.();
|
documentFlow.onBackStep?.();
|
||||||
}}
|
}}
|
||||||
goBackLabel={canRenderBackButtonAsRemove ? 'Remove' : undefined}
|
goBackLabel={canRenderBackButtonAsRemove ? 'Remove' : undefined}
|
||||||
onGoNextClick={() => void onFormSubmit()}
|
onGoNextClick={handleGoNextClick}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
|
|
||||||
|
<MissingSignatureFieldDialog
|
||||||
|
isOpen={isMissingSignatureDialogVisible}
|
||||||
|
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DialogClose } from '@radix-ui/react-dialog';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
|
export type MissingSignatureFieldDialogProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MissingSignatureFieldDialog = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
}: MissingSignatureFieldDialogProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg" position="center">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>No signature field found</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<p className="mt-2">
|
||||||
|
Some signers have not been assigned a signature field. Please assign at least 1
|
||||||
|
signature field to each signer before proceeding.
|
||||||
|
</p>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -44,6 +44,8 @@
|
|||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--warning: 54 96% 45%;
|
--warning: 54 96% 45%;
|
||||||
|
|
||||||
|
--gold: 47.9 95.8% 53.1%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -83,6 +85,8 @@
|
|||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
--warning: 54 96% 45%;
|
--warning: 54 96% 45%;
|
||||||
|
|
||||||
|
--gold: 47.9 95.8% 53.1%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user