Compare commits
107 Commits
fix/445-si
...
feat/strip
| Author | SHA1 | Date | |
|---|---|---|---|
| ede9eb052d | |||
| 4d5275f915 | |||
| 01e6367b72 | |||
| 565602f8e1 | |||
| e0271cace3 | |||
| cc8c4b8297 | |||
| a287aab4f4 | |||
| b5ed703553 | |||
| f49880125a | |||
| 8380c357d9 | |||
| 4e010c5624 | |||
| f53cdbace9 | |||
| b4d04e2ce9 | |||
| 2470aeee1f | |||
| fd07b47325 | |||
| 9257a05831 | |||
| 1faa6f2944 | |||
| 5584bbe9ca | |||
| cc65537ea3 | |||
| 04a80b7c03 | |||
| c71a89d1b7 | |||
| e2abfd2312 | |||
| 49d55227e8 | |||
| 0dadec3b8d | |||
| e2d8591d66 | |||
| eac7aa84b0 | |||
| bd941202c8 | |||
| b854f0eedc | |||
| 1814bd4167 | |||
| b6f9d70fec | |||
| 7c54913bf5 | |||
| e8d5044ac5 | |||
| ddf097ede3 | |||
| 1bad85e1d6 | |||
| 68458b50d2 | |||
| e00f28cf87 | |||
| 4cc34ec50a | |||
| 693249916d | |||
| 381a248543 | |||
| f637381198 | |||
| 071335cc66 | |||
| 4d4b011146 | |||
| 97dfacd133 | |||
| 87a5bab734 | |||
| fa61bb660e | |||
| 83dd079e03 | |||
| f7933d8a4d | |||
| 4bd0cfd283 | |||
| 86f39f3824 | |||
| 94216f5219 | |||
| a2e6187dae | |||
| 15e3926ce4 | |||
| 69738d7ed3 | |||
| 649620c1c9 | |||
| 99df006019 | |||
| be40e28f47 | |||
| 8899d82e1b | |||
| 0527641a4f | |||
| 54e32af1d4 | |||
| b1645ec09a | |||
| 5b9ce55a6d | |||
| 833584bd4f | |||
| ca52a22bcd | |||
| ba611b9adb | |||
| e8643850a0 | |||
| bec7376fe8 | |||
| 7363bbc8f7 | |||
| d0f679e177 | |||
| e9769904ab | |||
| a2746742a7 | |||
| 459e3dc45b | |||
| 21fedab707 | |||
| 323f1974ab | |||
| 5bc9d625c2 | |||
| 4885cf5154 | |||
| 2f66eca925 | |||
| af042a62cd | |||
| 2bfc2b0c1f | |||
| df8bdda718 | |||
| 873f99ae86 | |||
| 17fe135027 | |||
| 2eed0ae063 | |||
| 9bdff9a61f | |||
| f003a2864e | |||
| ab26af19b8 | |||
| dc512600dc | |||
| cdb71c3a62 | |||
| 39d7b3ca58 | |||
| 748c3636d5 | |||
| 6e2d1fb148 | |||
| 81f9b2f776 | |||
| 334671ef85 | |||
| e5fa2f4490 | |||
| d950634aab | |||
| d0fad4775a | |||
| 0732aa317d | |||
| eef63c9adc | |||
| c3d52c68e7 | |||
| 190ae18edc | |||
| 82ffbeeb21 | |||
| 9affd7b7fa | |||
| 897ba629df | |||
| 4d485940ea | |||
| cbe118b74f | |||
| de9116e9b2 | |||
| 027a588604 | |||
| 773566f193 |
@ -9,10 +9,5 @@ npm install
|
|||||||
# Copy the env file
|
# Copy the env file
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Source the env file, export the variables
|
|
||||||
set -a
|
|
||||||
source .env
|
|
||||||
set +a
|
|
||||||
|
|
||||||
# Run the migrations
|
# Run the migrations
|
||||||
npm run -w @documenso/prisma prisma:migrate-dev
|
npm run prisma:migrate-dev
|
||||||
|
|||||||
@ -50,7 +50,9 @@ NEXT_PRIVATE_SMTP_SECURE=
|
|||||||
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
NEXT_PRIVATE_SMTP_FROM_NAME="No Reply @ Documenso"
|
||||||
# REQUIRED: Defines the email address to use as the from address.
|
# REQUIRED: Defines the email address to use as the from address.
|
||||||
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@documenso.com"
|
||||||
# OPTIONAL: The API key to use for the MailChannels proxy endpoint.
|
# OPTIONAL: The API key to use for Resend.com
|
||||||
|
NEXT_PRIVATE_RESEND_API_KEY=
|
||||||
|
# OPTIONAL: The API key to use for MailChannels.
|
||||||
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
NEXT_PRIVATE_MAILCHANNELS_API_KEY=
|
||||||
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
# OPTIONAL: The endpoint to use for the MailChannels API if using a proxy.
|
||||||
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
NEXT_PRIVATE_MAILCHANNELS_ENDPOINT=
|
||||||
@ -66,6 +68,7 @@ NEXT_PRIVATE_STRIPE_API_KEY=
|
|||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID=
|
||||||
|
|
||||||
# [[FEATURES]]
|
# [[FEATURES]]
|
||||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||||
|
|||||||
50
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Create a bug report to help us improve
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--- Please provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
|
||||||
|
<!--- Please provide a clear and concise description of the problem. -->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
<!--- Please provide step-by-step instructions to reproduce the issue. -->
|
||||||
|
<!--- Include code snippets, error messages, and any other relevant information. -->
|
||||||
|
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!--- Describe what you expected to happen. -->
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
|
||||||
|
<!--- Describe what is currently happening. -->
|
||||||
|
|
||||||
|
## Screenshots (optional)
|
||||||
|
|
||||||
|
<!--- If applicable, add screenshots to help explain the issue. -->
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
<!--- Please provide information about your environment, such as operating system, browser, version, etc. -->
|
||||||
|
|
||||||
|
- OS: [e.g., Windows 10]
|
||||||
|
- Browser: [e.g., Chrome, Firefox]
|
||||||
|
- Version: [e.g., 2.0.1]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--- Please check the boxes that apply to this issue report. -->
|
||||||
|
<!--- You can add or remove items as needed. -->
|
||||||
|
|
||||||
|
- [ ] I have searched the existing issues to make sure this is not a duplicate.
|
||||||
|
- [ ] I have provided steps to reproduce the issue.
|
||||||
|
- [ ] I have included relevant environment information.
|
||||||
|
- [ ] I have included any relevant screenshots.
|
||||||
|
- [ ] I understand that this is a voluntary contribution and that there is no guarantee of resolution.
|
||||||
41
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new idea or enhancement for this project
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--- Please provide a clear and concise title for your feature request -->
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
|
||||||
|
<!--- Describe the feature you are requesting in detail. -->
|
||||||
|
<!--- Explain what problem it solves or what value it adds to the project. -->
|
||||||
|
|
||||||
|
## Use Case
|
||||||
|
|
||||||
|
<!--- Provide a scenario or use case where this feature would be beneficial. -->
|
||||||
|
<!--- Explain how users would interact with this feature and why it's important. -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!--- If you have an idea of how this feature could be implemented, describe it here. -->
|
||||||
|
<!--- Include any technical details, UI/UX considerations, or design suggestions. -->
|
||||||
|
|
||||||
|
## Alternatives (optional)
|
||||||
|
|
||||||
|
<!--- Are there any alternative ways to achieve the same goal? -->
|
||||||
|
<!--- Describe other approaches that could be considered if this feature is not implemented. -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!--- Add any additional context or information that might be relevant to the feature request. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--- Please check the boxes that apply to this feature request. -->
|
||||||
|
<!--- You can add or remove items as needed. -->
|
||||||
|
|
||||||
|
- [ ] I have searched the existing feature requests to make sure this is not a duplicate.
|
||||||
|
- [ ] I have provided a detailed description of the requested feature.
|
||||||
|
- [ ] I have explained the use case or scenario for this feature.
|
||||||
|
- [ ] I have included any relevant technical details or design suggestions.
|
||||||
|
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||||
41
.github/ISSUE_TEMPLATE/improvement.md
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: General Improvement
|
||||||
|
about: Suggest a minor enhancement or improvement for this project
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--- Please provide a clear and concise title for your improvement suggestion -->
|
||||||
|
|
||||||
|
## Improvement Description
|
||||||
|
|
||||||
|
<!--- Describe the improvement you are suggesting in detail. -->
|
||||||
|
<!--- Explain what specific aspect of the project it addresses or enhances. -->
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
<!--- Explain why this improvement would be beneficial. -->
|
||||||
|
<!--- Share any context, pain points, or reasons for suggesting this change. -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!--- If you have a suggestion for how this improvement could be implemented, describe it here. -->
|
||||||
|
<!--- Include any technical details, design suggestions, or other relevant information. -->
|
||||||
|
|
||||||
|
## Alternatives (optional)
|
||||||
|
|
||||||
|
<!--- Are there any alternative approaches to achieve the same improvement? -->
|
||||||
|
<!--- Describe other ways to address the issue or enhance the project. -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!--- Add any additional context or information that might be relevant to the improvement suggestion. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--- Please check the boxes that apply to this improvement suggestion. -->
|
||||||
|
<!--- You can add or remove items as needed. -->
|
||||||
|
|
||||||
|
- [ ] I have searched the existing issues and improvement suggestions to avoid duplication.
|
||||||
|
- [ ] I have provided a clear description of the improvement being suggested.
|
||||||
|
- [ ] I have explained the rationale behind this improvement.
|
||||||
|
- [ ] I have included any relevant technical details or design suggestions.
|
||||||
|
- [ ] I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||||
49
.github/PULL_REQUEST_TEMPLATE/generic.md
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
name: Pull Request
|
||||||
|
about: Submit changes to the project for review and inclusion
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!--- Describe the changes introduced by this pull request. -->
|
||||||
|
<!--- Explain what problem it solves or what feature/fix it adds. -->
|
||||||
|
|
||||||
|
## Related Issue
|
||||||
|
|
||||||
|
<!--- If this pull request is related to a specific issue, reference it here using #issue_number. -->
|
||||||
|
<!--- For example, "Fixes #123" or "Addresses #456". -->
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
<!--- Provide a summary of the changes made in this pull request. -->
|
||||||
|
<!--- Include any relevant technical details or architecture changes. -->
|
||||||
|
|
||||||
|
- Change 1
|
||||||
|
- Change 2
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Testing Performed
|
||||||
|
|
||||||
|
<!--- Describe the testing that you have performed to validate these changes. -->
|
||||||
|
<!--- Include information about test cases, testing environments, and results. -->
|
||||||
|
|
||||||
|
- Tested feature X in scenario Y.
|
||||||
|
- Ran unit tests for component Z.
|
||||||
|
- Tested on browsers A, B, and C.
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--- Please check the boxes that apply to this pull request. -->
|
||||||
|
<!--- You can add or remove items as needed. -->
|
||||||
|
|
||||||
|
- [ ] I have tested these changes locally and they work as expected.
|
||||||
|
- [ ] I have added/updated tests that prove the effectiveness of these changes.
|
||||||
|
- [ ] I have updated the documentation to reflect these changes, if applicable.
|
||||||
|
- [ ] I have followed the project's coding style guidelines.
|
||||||
|
- [ ] I have addressed the code review feedback from the previous submission, if applicable.
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
<!--- Provide any additional context or notes for the reviewers. -->
|
||||||
|
<!--- This might include details about design decisions, potential concerns, or anything else relevant. -->
|
||||||
40
.github/PULL_REQUEST_TEMPLATE/test-addition.md
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Test Addition
|
||||||
|
about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!--- Provide a clear and concise description of the new test you are adding. -->
|
||||||
|
<!--- Explain the purpose of the test and what it aims to validate. -->
|
||||||
|
|
||||||
|
## Related Issue
|
||||||
|
|
||||||
|
<!--- If this test addition is related to a specific issue, reference it here using #issue_number. -->
|
||||||
|
<!--- For example, "Fixes #123" or "Addresses #456". -->
|
||||||
|
|
||||||
|
## Test Details
|
||||||
|
|
||||||
|
<!--- Describe the details of the test you're adding. -->
|
||||||
|
<!--- Include information about inputs, expected outputs, and any specific scenarios. -->
|
||||||
|
|
||||||
|
- Test Name: Name of the test
|
||||||
|
- Type: [Unit / E2E]
|
||||||
|
- Description: Brief description of what the test checks
|
||||||
|
- Inputs: What inputs the test uses (if applicable)
|
||||||
|
- Expected Output: What output or behavior the test expects
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--- Please check the boxes that apply to this pull request. -->
|
||||||
|
<!--- You can add or remove items as needed. -->
|
||||||
|
|
||||||
|
- [ ] I have written the new test and ensured it works as intended.
|
||||||
|
- [ ] I have added necessary documentation to explain the purpose of the test.
|
||||||
|
- [ ] I have followed the project's testing guidelines and coding style.
|
||||||
|
- [ ] I have addressed any review feedback from previous submissions, if applicable.
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
<!--- Provide any additional context or notes for the reviewers. -->
|
||||||
|
<!--- This might include explanations about the testing approach or any potential concerns. -->
|
||||||
6
.github/dependabot.yml
vendored
@ -9,7 +9,7 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "ci dependencies"
|
- "ci dependencies"
|
||||||
- "ci"
|
- "ci"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/apps/marketing"
|
directory: "/apps/marketing"
|
||||||
@ -19,7 +19,7 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- "npm dependencies"
|
||||||
- "frontend"
|
- "frontend"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 0
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/apps/web"
|
directory: "/apps/web"
|
||||||
@ -29,4 +29,4 @@ updates:
|
|||||||
labels:
|
labels:
|
||||||
- "npm dependencies"
|
- "npm dependencies"
|
||||||
- "frontend"
|
- "frontend"
|
||||||
open-pull-requests-limit: 2
|
open-pull-requests-limit: 0
|
||||||
|
|||||||
55
.gitpod.yml
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
tasks:
|
||||||
|
- init: |
|
||||||
|
npm i &&
|
||||||
|
npm run dx:up &&
|
||||||
|
cp .env.example .env &&
|
||||||
|
set -a; source .env &&
|
||||||
|
export NEXTAUTH_URL="$(gp url 3000)" &&
|
||||||
|
export NEXT_PUBLIC_WEBAPP_URL="$(gp url 3000)" &&
|
||||||
|
export NEXT_PUBLIC_MARKETING_URL="$(gp url 3001)"
|
||||||
|
command: npm run d
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
visibility: public
|
||||||
|
onOpen: open-preview
|
||||||
|
- port: 3001
|
||||||
|
visibility: public
|
||||||
|
onOpen: open-preview
|
||||||
|
- port: 9000
|
||||||
|
visibility: public
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 1100
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 2500
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 54320
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
master: true
|
||||||
|
pullRequests: true
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
addCheck: true
|
||||||
|
addComment: true
|
||||||
|
addBadge: true
|
||||||
|
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- aaron-bond.better-comments
|
||||||
|
- bradlc.vscode-tailwindcss
|
||||||
|
- dbaeumer.vscode-eslint
|
||||||
|
- esbenp.prettier-vscode
|
||||||
|
- mikestead.dotenv
|
||||||
|
- unifiedjs.vscode-mdx
|
||||||
|
- GitHub.copilot-chat
|
||||||
|
- GitHub.copilot-labs
|
||||||
|
- GitHub.copilot
|
||||||
|
- GitHub.vscode-pull-request-github
|
||||||
|
- Prisma.prisma
|
||||||
|
- VisualStudioExptTeam.vscodeintellicode
|
||||||
126
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
support@documenso.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
@ -5,20 +5,36 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
|
|||||||
## Before getting started
|
## Before getting started
|
||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
||||||
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
- Select an issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
||||||
- Consider the results from the discussion in the issue
|
- Consider the results from the discussion on the issue
|
||||||
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
|
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
|
||||||
|
|
||||||
|
## Taking issues
|
||||||
|
|
||||||
|
Before taking an issue, ensure that:
|
||||||
|
|
||||||
|
- The issue has been assigned the public label
|
||||||
|
- The issue is clearly defined and understood
|
||||||
|
- No one has been assigned to the issue
|
||||||
|
- No one has expressed intention to work on it
|
||||||
|
|
||||||
|
You can then:
|
||||||
|
|
||||||
|
1. Comment on the issue with your intention to work on it
|
||||||
|
2. Begin work on the issue
|
||||||
|
|
||||||
|
Always feel free to ask questions or seek clarification on the issue.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Discord](https://documen.so/discord).
|
The development branch is <code>main</code>. All pull requests should be made against this branch. If you need help getting started, [join us on Discord](https://documen.so/discord).
|
||||||
|
|
||||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
||||||
own GitHub account and then
|
own GitHub account and then
|
||||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
2. Create a new branch:
|
2. Create a new branch:
|
||||||
|
|
||||||
- Create a new branch (include the issue id and somthing readable):
|
- Create a new branch (include the issue id and something readable):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git checkout -b doc-999-my-feature-or-fix
|
git checkout -b doc-999-my-feature-or-fix
|
||||||
@ -29,7 +45,7 @@ The development branch is <code>main</code>. All pull request should be made aga
|
|||||||
## Building
|
## Building
|
||||||
|
|
||||||
> **Note**
|
> **Note**
|
||||||
> Please be sure that you can make a full production build before pushing code or creating PRs.
|
> Please ensure you can make a full production build before pushing code or creating PRs.
|
||||||
|
|
||||||
You can build the project with:
|
You can build the project with:
|
||||||
|
|
||||||
|
|||||||
257
README.md
@ -1,8 +1,6 @@
|
|||||||
<p align="center" style="margin-top: 120px">
|
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
|
||||||
<a href="https://github.com/documenso/documenso">
|
|
||||||
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/ea8de2d0-8e33-4527-8a5c-40780142a2ed" alt="Documenso Logo">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
<p align="center" style="margin-top: 20px">
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The Open Source DocuSign Alternative.
|
The Open Source DocuSign Alternative.
|
||||||
<br>
|
<br>
|
||||||
@ -16,6 +14,8 @@
|
|||||||
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
|
<a href="https://github.com/documenso/documenso/milestones">Roadmap</a>
|
||||||
|
·
|
||||||
|
<a href="https://documen.so/launches">Upcoming Launches</a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -24,16 +24,20 @@
|
|||||||
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
|
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
|
||||||
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||||
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||||
|
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
|
||||||
|
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
|
||||||
|
</a>
|
||||||
|
<a href="code_of_conduct.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
|
> 🦺 Documenso 1.0 is deployed to our <a href="https://documen.so/staging" target="_blank">Staging Environment</a>.
|
||||||
>
|
>
|
||||||
> The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.
|
> The code can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.
|
||||||
>
|
>
|
||||||
> The new Version will be released after the current testing phase.
|
> The new version will be released after the current testing phase.
|
||||||
|
|
||||||
|
|
||||||
# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
|
# Join us in testing Documenso 1.0 during [MALFUNCTION MANIA](https://documenso.com/blog/malfunction-mania)
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<img style="display: block; height: 120px; width: 24%"
|
<img style="display: block; height: 120px; width: 24%"
|
||||||
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
|
src="https://github.com/documenso/documenso/assets/1309312/67e08c98-c153-4115-aa2d-77979bb12c94)">
|
||||||
@ -49,19 +53,27 @@
|
|||||||
|
|
||||||
## About this project
|
## About this project
|
||||||
|
|
||||||
Signing documents digitally is fast and easy and should be the best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
|
Signing documents digitally should be fast and easy and should be the best practice for every document signed worldwide.
|
||||||
|
|
||||||
|
This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work.
|
||||||
|
|
||||||
|
Documenso aims to be the world's most trusted document-signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood.
|
||||||
|
|
||||||
|
Join us in creating the next generation of open trust infrastructure.
|
||||||
|
|
||||||
## Recognition
|
## Recognition
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The open source DocuSign alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<p align="center">
|
||||||
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The open source DocuSign alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Community and Next Steps 🎯
|
## Community and Next Steps 🎯
|
||||||
|
|
||||||
We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
|
We're currently working on a redesign of the application, including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
|
||||||
|
|
||||||
- Check out the first source code release in this repository and test it
|
- Check out the first source code release in this repository and test it
|
||||||
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
|
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions)
|
||||||
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members
|
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members
|
||||||
- ⭐ the repository to help us raise awareness
|
- ⭐ the repository to help us raise awareness
|
||||||
- Spread the word on Twitter that Documenso is working towards a more open signing tool
|
- Spread the word on Twitter that Documenso is working towards a more open signing tool
|
||||||
@ -77,62 +89,68 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
|||||||
|
|
||||||
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||||
|
|
||||||
## Activity
|
## Tech Stack
|
||||||
|
|
||||||

|
- [Typescript](https://www.typescriptlang.org/) - Language
|
||||||
|
- [Next.js](https://nextjs.org/) - Framework
|
||||||
|
- [Prisma](https://www.prisma.io/) - ORM
|
||||||
|
- [Tailwind](https://tailwindcss.com/) - CSS
|
||||||
|
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
|
||||||
|
- [NextAuth.js](https://next-auth.js.org/) - Authentication
|
||||||
|
- [react-email](https://react.email/) - Email Templates
|
||||||
|
- [tRPC](https://trpc.io/) - API
|
||||||
|
- [Node SignPDF](https://github.com/vbuch/node-signpdf) - Digital Signature
|
||||||
|
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
||||||
|
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
||||||
|
- [Stripe](https://stripe.com/) - Payments
|
||||||
|
- [Vercel](https://vercel.com) - Hosting
|
||||||
|
|
||||||
# Tech
|
<!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
|
||||||
|
|
||||||
Documenso is built using awesome open source tech including:
|
## Local Development
|
||||||
|
|
||||||
- [Typescript](https://www.typescriptlang.org/)
|
### Requirements
|
||||||
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
||||||
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
|
|
||||||
- [Postgres SQL (Database)](https://www.postgresql.org/)
|
|
||||||
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
|
|
||||||
- [Tailwind CSS (Styling)](https://tailwindcss.com/)
|
|
||||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
|
||||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
|
||||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
|
||||||
- Check out `/package.json` and `/apps/web/package.json` for more
|
|
||||||
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
|
||||||
|
|
||||||
# Getting Started
|
To run Documenso locally you will need
|
||||||
|
|
||||||
## Requirements
|
- Node.js
|
||||||
|
- Postgres SQL Database
|
||||||
|
- Docker (optional)
|
||||||
|
|
||||||
To run Documenso locally you need
|
### Developer Quickstart
|
||||||
|
|
||||||
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
|
|
||||||
- Node Package Manager NPM - included in Node.js
|
|
||||||
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
|
|
||||||
|
|
||||||
## Developer Quickstart
|
|
||||||
|
|
||||||
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
|
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
|
||||||
|
|
||||||
Want to get up and running quickly? Follow these steps:
|
Want to get up and running quickly? Follow these steps:
|
||||||
|
|
||||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/documenso/documenso
|
git clone https://github.com/documenso/documenso
|
||||||
```
|
```
|
||||||
|
|
||||||
- Set up your `.env` file using the recommendations in the `.env.example` file.
|
2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively just run `cp .env.example .env` to get started with our handpicked defaults.
|
||||||
- Run `npm run dx` in the root directory
|
|
||||||
- This will spin up a postgres database and inbucket mail server in docker containers.
|
|
||||||
- Run `npm run dev` in the root directory
|
|
||||||
- Want it even faster? Just use
|
|
||||||
```sh
|
|
||||||
npm run d
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it! You should now be able to access the app at http://localhost:3000
|
3. Run `npm run dx` in the root directory
|
||||||
|
|
||||||
Incoming mail will be available at http://localhost:9000
|
- This will spin up a postgres database and inbucket mailserver in a docker container.
|
||||||
|
|
||||||
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
|
4. Run `npm run dev` in the root directory
|
||||||
|
|
||||||
|
5. Want it even faster? Just use
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Access Points for Your Application
|
||||||
|
|
||||||
|
1. **App** - http://localhost:3000
|
||||||
|
2. **Incoming Mail Access** - http://localhost:9000
|
||||||
|
|
||||||
|
3. **Database Connection Details**
|
||||||
|
- **Port**: 54320
|
||||||
|
- **Connection**: Use your favorite database client to connect using the provided port.
|
||||||
|
|
||||||
## Developer Setup
|
## Developer Setup
|
||||||
|
|
||||||
@ -140,33 +158,38 @@ Your database will also be available on port `54320`. You can connect to it usin
|
|||||||
|
|
||||||
Follow these steps to setup documenso on you local machine:
|
Follow these steps to setup documenso on you local machine:
|
||||||
|
|
||||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
```sh
|
|
||||||
git clone https://github.com/documenso/documenso
|
```sh
|
||||||
```
|
git clone https://github.com/documenso/documenso
|
||||||
- Run `npm i` in root directory
|
```
|
||||||
- Rename `.env.example` to `.env`
|
|
||||||
- Set DATABASE_URL value in .env file
|
2. Run `npm i` in root directory
|
||||||
- You can use the provided test database url (may be wiped at any point)
|
|
||||||
- Or setup a local postgres sql instance (recommended)
|
3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
|
||||||
- Create the database scheme by running `db-migrate:dev`
|
|
||||||
- Setup your mail provider
|
4. Set the following environement variables.
|
||||||
- Set `SENDGRID_API_KEY` value in .env file
|
|
||||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
- NEXTAUTH_URL
|
||||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
|
- NEXTAUTH_SECRET
|
||||||
\_
|
- NEXT_PUBLIC_WEBAPP_URL
|
||||||
* variables` in your .env
|
- NEXT_PUBLIC_MARKETING_URL
|
||||||
- Run `npm run dev` root directory to start
|
- NEXT_PRIVATE_DATABASE_URL
|
||||||
- Register a new user at http://localhost:3000/signup
|
- NEXT_PRIVATE_DIRECT_DATABASE_URL
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_NAME
|
||||||
|
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
|
||||||
|
|
||||||
|
5. Create the database schema by running `npm run prisma:migrate-dev`
|
||||||
|
|
||||||
|
6. Run `npm run dev` root directory to start
|
||||||
|
|
||||||
|
7. Register a new user at http://localhost:3000/signup
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- Optional: Seed the database using `npm run db-seed` to create a test user and document
|
- Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document
|
||||||
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
|
|
||||||
|
|
||||||
- Optional: Create your own signing certificate
|
- Optional: Create your own signing certificate
|
||||||
- A demo certificate is provided in `/app/web/resources/certificate.p12`
|
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](./SIGNING.md)**.
|
||||||
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
|
|
||||||
|
|
||||||
### Run in Gitpod
|
### Run in Gitpod
|
||||||
|
|
||||||
@ -174,81 +197,39 @@ Follow these steps to setup documenso on you local machine:
|
|||||||
|
|
||||||
[](https://gitpod.io/#https://github.com/documenso/documenso)
|
[](https://gitpod.io/#https://github.com/documenso/documenso)
|
||||||
|
|
||||||
## Updating
|
### Run in DevContainer
|
||||||
|
|
||||||
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client
|
We support DevContainers for VSCode. [Click here to get started.](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso)
|
||||||
- You can do this by running the generate command in `/packages/prisma`:
|
|
||||||
```sh
|
|
||||||
npx prisma generate
|
|
||||||
```
|
|
||||||
- This is not necessary on first clone.
|
|
||||||
|
|
||||||
# Creating your own signing certificate
|
## Docker
|
||||||
|
|
||||||
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
🚧 Docker containers and images are current in progress. We are actively working on bringing a simple docker build and publish pipeline for Documenso.
|
||||||
|
|
||||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
|
## Self Hosting
|
||||||
|
|
||||||
`openssl genrsa -out private.key 2048`
|
|
||||||
|
|
||||||
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
|
|
||||||
|
|
||||||
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
|
||||||
|
|
||||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
|
||||||
|
|
||||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
|
|
||||||
|
|
||||||
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
|
||||||
|
|
||||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
|
||||||
5. Place the certificate `/apps/web/resources/certificate.p12`
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
|
|
||||||
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
|
|
||||||
|
|
||||||
Want to create a production ready docker image? Follow these steps:
|
|
||||||
|
|
||||||
- cd into `docker` directory
|
|
||||||
- Make `build.sh` executable by running `chmod +x build.sh`
|
|
||||||
- Run `./build.sh` to start building the docker image.
|
|
||||||
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Command Breakdown:
|
|
||||||
- `-d` - Let's you run the container in background
|
|
||||||
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
|
|
||||||
- `-v` - Volume let's you persist the data
|
|
||||||
- `--name` - Name of the container
|
|
||||||
- `documenso:latest` - Image you have built
|
|
||||||
|
|
||||||
# Deployment
|
|
||||||
|
|
||||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||||
|
|
||||||
## Railway
|
> Please note the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
|
||||||
[](https://railway.app/template/DjrRRX)
|
[](https://railway.app/template/DjrRRX)
|
||||||
|
|
||||||
## Render
|
### Render
|
||||||
|
|
||||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||||
|
|
||||||
# Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
## I'm not receiving any emails when using the developer quickstart
|
### I'm not receiving any emails when using the developer quickstart
|
||||||
|
|
||||||
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
|
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
|
||||||
|
|
||||||
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
|
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
|
||||||
|
|
||||||
## Support IPv6
|
### Support IPv6
|
||||||
|
|
||||||
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
|
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the Next.js start command
|
||||||
|
|
||||||
For local docker run
|
For local docker run
|
||||||
|
|
||||||
@ -270,5 +251,25 @@ containers:
|
|||||||
- start
|
- start
|
||||||
- --
|
- --
|
||||||
- -H
|
- -H
|
||||||
- "::"
|
- '::'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### I can't see environment variables in my package scripts
|
||||||
|
|
||||||
|
Wrap your package script with the `with:env` script like such:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run with:env -- npm run myscript
|
||||||
|
```
|
||||||
|
|
||||||
|
The same can be done when using `npx` for one of bin scripts:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run with:env -- npx myscript
|
||||||
|
```
|
||||||
|
|
||||||
|
This will load environment variables from your `.env` and `.env.local` files.
|
||||||
|
|
||||||
|
## Repo Activity
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
55
SIGNING.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Creating your own signing certificate
|
||||||
|
|
||||||
|
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||||
|
|
||||||
|
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
|
||||||
|
|
||||||
|
`openssl genrsa -out private.key 2048`
|
||||||
|
|
||||||
|
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
|
||||||
|
|
||||||
|
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
||||||
|
|
||||||
|
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
||||||
|
|
||||||
|
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
|
||||||
|
|
||||||
|
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
||||||
|
|
||||||
|
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||||
|
5. Place the certificate `/apps/web/resources/certificate.p12`
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
|
||||||
|
|
||||||
|
Want to create a production ready docker image? Follow these steps:
|
||||||
|
|
||||||
|
- cd into `docker` directory
|
||||||
|
- Make `build.sh` executable by running `chmod +x build.sh`
|
||||||
|
- Run `./build.sh` to start building the docker image.
|
||||||
|
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Command Breakdown:
|
||||||
|
|
||||||
|
- `-d` - Let's you run the container in background
|
||||||
|
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
|
||||||
|
- `-v` - Volume let's you persist the data
|
||||||
|
- `--name` - Name of the container
|
||||||
|
- `documenso:latest` - Image you have built
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||||
|
|
||||||
|
## Railway
|
||||||
|
|
||||||
|
[](https://railway.app/template/DjrRRX)
|
||||||
|
|
||||||
|
## Render
|
||||||
|
|
||||||
|
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||||
67
apps/marketing/content/blog/early-adopters.mdx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: The Early Adopters Plan
|
||||||
|
description: Launch Week Day 4 and we are still early! Early enough for you to get a sweet deal for supporting Documenso's Mission. Join the movement and get a shiny early adopter account in the process.
|
||||||
|
authorName: 'Timur Ercan'
|
||||||
|
authorImage: '/blog/blog-author-timur.jpeg'
|
||||||
|
authorRole: 'Co-Founder'
|
||||||
|
date: 2023-09-28
|
||||||
|
tags:
|
||||||
|
- Paid Plan
|
||||||
|
- Metrics
|
||||||
|
- Open Startup
|
||||||
|
---
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<MdxNextImage
|
||||||
|
src="/blog/early.png"
|
||||||
|
width="1260"
|
||||||
|
height="630"
|
||||||
|
alt="XKCD: Bug"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<figcaption className="text-center">
|
||||||
|
"Being early is, uh, good." -Unknown
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
> TLDR; we have 100 early adopter accounts available at a great price. [Secure yours now](https://documenso.com/pricing).
|
||||||
|
|
||||||
|
## Community-Driven Development
|
||||||
|
|
||||||
|
As we ramp up hiring and development speed for Documenso, I want to discuss how we plan to build its core version.
|
||||||
|
As we want to be a highly community-driven product, we want to capture the thoughts and ideas of said community in two ways:
|
||||||
|
|
||||||
|
1. Everything we plan (i.e. documen.so/roadmap) and build (i.e. documen.so/repo) is public and open to comments and suggestions from everyone by design. We plan to have discussions, streams, and articles showcasing what we're up to, encouraging feedback and iterations on our plans.
|
||||||
|
2. We're looking for 100 die-hard early adopters to get more deep hands-on feedback. If you want to be among the first to use and help shape Documenso, we have a special offer for you:
|
||||||
|
|
||||||
|
## The Early Adopter Plan
|
||||||
|
|
||||||
|
- All first 100 signups are entitled to the early adopter plan.
|
||||||
|
- The plan includes everything we build in the next 12 months and unlimited<sup>1</sup> signatures
|
||||||
|
- This plan is priced at $30/mo.
|
||||||
|
- No matter what we add, the price is guaranteed for life as a thank you for supporting Documenso's mission<sup>2</sup>. And we plan to add a lot.
|
||||||
|
- This also includes unlimited users<sup>3</sup> as part of the upcoming support for teams.
|
||||||
|
- If you already claimed an early adopter account in the past and canceled, we are happy to reactivate your account with the early adopter pricing. Reach out to support@documenso.com
|
||||||
|
|
||||||
|
## Being an Early Adopter
|
||||||
|
|
||||||
|
Being eligible for the early adopter plan has no formal requirements like giving feedback or being active in the community. In good faith, we assume you sign the Documenso Supporter Pledge to be part of this. And if you want to use the newest version quietly, that's fine, too. Everything the die-hard community brings up will be carefully considered and prioritized <sup>4</sup>. Also, there will obviously be limited edition merch available for the first 100.
|
||||||
|
Documenso currently runs the community reviewed 0.9.1 version. Getting from here to the globally loved and adopted signing tool we all deserve will take a lot of work, and we want you on board to help us create it. Join us in shaping the future of open signing and having fun doing it. [Malfunction Mania](https://documenso.com/blog/malfunction-mania) and releasing 1.0 will go a long way.
|
||||||
|
|
||||||
|
## Extending our open metrics
|
||||||
|
|
||||||
|
As part of our ongoing effort to be open and transparent in our doing, we are adding "Early Adopters" to our [/open page](https://documenso.com/open) page. After we exceed the early adopter slots, this metric will transition to "Customers". When no more early adopter seats can be claimed, the early adopter plan will transition to a standard paid plan. It will still be priced at $30/mo., but will no longer include upcoming features or unlimited seats. You can [claim your early adopter account here](https://documenso.com/pricing).
|
||||||
|
|
||||||
|
If you have any questions or comments, please reach out on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord).
|
||||||
|
|
||||||
|
Best from Hamburg\
|
||||||
|
Timur
|
||||||
|
|
||||||
|
\
|
||||||
|
[1] Excluding signatures, we have to pay for i.e. third-party QES
|
||||||
|
|
||||||
|
[2] The Documenso Manifest
|
||||||
|
|
||||||
|
[3] Within reason. If you are unsure what that means, feel free to contact hi@documenso.com. But it should be fine if you don't plan to onboard a huge enterprise.
|
||||||
|
|
||||||
|
[4] We won't be able to build everything everyone asks for. But we firmly plan to listen and build in a way everyone's requirements are met as well as possible.
|
||||||
@ -51,4 +51,7 @@ As Documenso 1.0 just hit the staging environment, we're calling a MALFUNCTION M
|
|||||||
|
|
||||||
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
|
We don't have a specific end date for Malfunction Mania. We plan to move the staging version into the production environment by the end of the month once we're happy with the results. Bug reports and fixes are, of course, always welcome going forward.
|
||||||
|
|
||||||
|
Best from Hamburg
|
||||||
|
Timur
|
||||||
|
|
||||||
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: Shop and Limited Edition "Mania" Shirt
|
title: Merch Mania
|
||||||
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
|
description: Happy Launch Week Day 3. The limited edition "Malfunction Mania" shirt is here. Grab it, while you can.
|
||||||
authorName: 'Timur Ercan'
|
authorName: 'Timur Ercan'
|
||||||
authorImage: '/blog/blog-author-timur.jpeg'
|
authorImage: '/blog/blog-author-timur.jpeg'
|
||||||
authorRole: 'Co-Founder'
|
authorRole: 'Co-Founder'
|
||||||
date: 2023-09-27
|
date: 2023-09-27
|
||||||
tags:
|
tags:
|
||||||
- Testing
|
- Merch
|
||||||
- Rewrite
|
- Rewrite
|
||||||
- Bounties
|
- Bounties
|
||||||
---
|
---
|
||||||
@ -24,13 +24,13 @@ tags:
|
|||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
> TLDR; We have a fancy limited edition shirt. Contribute to Malfunction Mania to get one.
|
> TLDR; We have a fancy limited edition shirt. Contribute to [Malfunction Mania](https://documenso.com/blog/malfunction-mania) to get one.
|
||||||
|
|
||||||
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [Issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created the limited edition shirt for Malfunction Mania. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
|
We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) yesterday, and the first [issues](https://github.com/documenso/documenso/issues) are coming in. As mentioned, there will be dollar bounties, but we also wanted to celebrate entering the final stage of version 1.0 with something special. This is why we created this limited edition shirt. It will only be available during the runtime of Malfunction Mania. We have yet to set an exact end date, the next event in October, however, is looming, ready to end MM.
|
||||||
|
|
||||||
## Documenso Merch Shop
|
## Documenso Merch Shop
|
||||||
|
|
||||||
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors.
|
The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso.
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<MdxNextImage
|
<MdxNextImage
|
||||||
@ -45,7 +45,7 @@ The shirt will be available in our [merch shop](https://documen.so/shop) via a u
|
|||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## How to get the shirt
|
## How earn the shirt
|
||||||
|
|
||||||
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
|
If you have been following us, you know we are not big on formalities but highly value rewarding merit. That being said, any worthwhile contribution has a chance to get one. To inspire, here are a few ideas on how to contribute to securing one:
|
||||||
|
|
||||||
@ -56,7 +56,9 @@ If you have been following us, you know we are not big on formalities but highly
|
|||||||
- Engage in discussion about the current version and its choices
|
- Engage in discussion about the current version and its choices
|
||||||
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
|
- Raise awareness for Malfunction Mania and try out the [version currently in staging](https://documen.so/staging)
|
||||||
- Review the version with a video, stream, or screenshots and post about it
|
- Review the version with a video, stream, or screenshots and post about it
|
||||||
- Review existing or create missing documenso
|
- Review existing or create missing documentation
|
||||||
- ...
|
|
||||||
|
|
||||||
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest about Malfunction Mania.**
|
Best from Hamburg
|
||||||
|
Timur
|
||||||
|
|
||||||
|
**[Follow Documenso on Twitter / X](https://documen.so/tw) and [join the Discord server](https://documen.so/discord) to get the latest updates about Malfunction Mania.**
|
||||||
|
|||||||
@ -56,6 +56,14 @@ const config = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/ingest/:path*',
|
||||||
|
destination: 'https://eu.posthog.com/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withContentlayer(config);
|
module.exports = withContentlayer(config);
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
"react-confetti": "^6.1.0",
|
"react-confetti": "^6.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.11.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
|
|||||||
1
apps/marketing/process-env.d.ts
vendored
@ -7,6 +7,7 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
BIN
apps/marketing/public/blog/blog-author-lucas.png
Normal file
|
After Width: | Height: | Size: 1007 KiB |
BIN
apps/marketing/public/blog/blog-banner-rewrite.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
apps/marketing/public/blog/designsystem.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
apps/marketing/public/blog/early.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 956 KiB After Width: | Height: | Size: 258 KiB |
@ -7,26 +7,25 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
|||||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { StargazersType } from './page';
|
export type BarMetricProps<T extends Record<string, unknown>> = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
data: T;
|
||||||
export type MetricsDataKey = 'stars' | 'forks' | 'mergedPRs' | 'openIssues';
|
metricKey: keyof T[string];
|
||||||
export type GithubMetricProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
data: StargazersType;
|
|
||||||
metricKey: MetricsDataKey;
|
|
||||||
title: string;
|
title: string;
|
||||||
label: string;
|
label: string;
|
||||||
chartHeight?: number;
|
chartHeight?: number;
|
||||||
|
extraInfo?: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GithubMetric = ({
|
export const BarMetric = <T extends Record<string, Record<keyof T[string], unknown>>>({
|
||||||
className,
|
className,
|
||||||
data,
|
data,
|
||||||
metricKey,
|
metricKey,
|
||||||
title,
|
title,
|
||||||
label,
|
label,
|
||||||
chartHeight = 400,
|
chartHeight = 400,
|
||||||
|
extraInfo,
|
||||||
...props
|
...props
|
||||||
}: GithubMetricProps) => {
|
}: BarMetricProps<T>) => {
|
||||||
const formattedData = Object.keys(data)
|
const formattedData = Object.keys(data)
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
month: formatMonth(key),
|
month: formatMonth(key),
|
||||||
@ -36,7 +35,10 @@ export const GithubMetric = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col', className)} {...props}>
|
<div className={cn('flex flex-col', className)} {...props}>
|
||||||
<h3 className="px-4 text-lg font-semibold">{title}</h3>
|
<div className="flex items-center px-4">
|
||||||
|
<h3 className="text-lg font-semibold">{title}</h3>
|
||||||
|
<span>{extraInfo}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
<div className="border-border mt-2.5 flex flex-1 items-center justify-center rounded-2xl border pr-2 shadow-sm hover:shadow">
|
||||||
<ResponsiveContainer width="100%" height={chartHeight}>
|
<ResponsiveContainer width="100%" height={chartHeight}>
|
||||||
@ -50,7 +52,7 @@ export const GithubMetric = ({
|
|||||||
formatter={(value) => [Number(value), label]}
|
formatter={(value) => [Number(value), label]}
|
||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey={metricKey} fill="hsl(var(--primary))" label={label} />
|
<Bar dataKey={metricKey as string} fill="hsl(var(--primary))" label={label} />{' '}
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -23,22 +23,6 @@ export const TEAM_MEMBERS = [
|
|||||||
engagement: 'Part-Time',
|
engagement: 'Part-Time',
|
||||||
joinDate: 'June 6th, 2023',
|
joinDate: 'June 6th, 2023',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Florent Merian',
|
|
||||||
role: 'Marketer - III',
|
|
||||||
salary: 'Project-Based',
|
|
||||||
location: 'France',
|
|
||||||
engagement: 'Full-Time',
|
|
||||||
joinDate: 'July 10th, 2023',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Thilo Konzok',
|
|
||||||
role: 'Designer',
|
|
||||||
salary: 'Project-Based',
|
|
||||||
location: 'Germany',
|
|
||||||
engagement: 'Full-Time',
|
|
||||||
joinDate: 'April 26th, 2023',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'David Nguyen',
|
name: 'David Nguyen',
|
||||||
role: 'Software Engineer - III',
|
role: 'Software Engineer - III',
|
||||||
@ -47,6 +31,22 @@ export const TEAM_MEMBERS = [
|
|||||||
engagement: 'Full-Time',
|
engagement: 'Full-Time',
|
||||||
joinDate: 'July 26th, 2023',
|
joinDate: 'July 26th, 2023',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Catalin-Marinel Pit',
|
||||||
|
role: 'Software Engineer - II',
|
||||||
|
salary: 80_000,
|
||||||
|
location: 'Romania',
|
||||||
|
engagement: 'Full-Time',
|
||||||
|
joinDate: 'September 4th, 2023',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gowdhama Rajan B',
|
||||||
|
role: 'Designer - III',
|
||||||
|
salary: 100_000,
|
||||||
|
location: 'India',
|
||||||
|
engagement: 'Full-Time',
|
||||||
|
joinDate: 'October 9th, 2023',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const FUNDING_RAISED = [
|
export const FUNDING_RAISED = [
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recha
|
|||||||
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
import { formatMonth } from '@documenso/lib/client-only/format-month';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
data: Record<string, string | number>[];
|
||||||
|
};
|
||||||
|
|
||||||
export type FundingRaisedProps = HTMLAttributes<HTMLDivElement>;
|
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||||
|
const formattedData = data.map((item) => ({
|
||||||
export const FundingRaised = ({ className, ...props }: FundingRaisedProps) => {
|
|
||||||
const formattedData = FUNDING_RAISED.map((item) => ({
|
|
||||||
amount: Number(item.amount),
|
amount: Number(item.amount),
|
||||||
date: formatMonth(item.date),
|
date: formatMonth(item.date as string),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FUNDING_RAISED } from '~/app/(marketing)/open/data';
|
||||||
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
import { MetricCard } from '~/app/(marketing)/open/metric-card';
|
||||||
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
import { SalaryBands } from '~/app/(marketing)/open/salary-bands';
|
||||||
|
|
||||||
|
import { BarMetric } from './bar-metrics';
|
||||||
import { CapTable } from './cap-table';
|
import { CapTable } from './cap-table';
|
||||||
import { FundingRaised } from './funding-raised';
|
import { FundingRaised } from './funding-raised';
|
||||||
import { GithubMetric } from './gh-metrics';
|
|
||||||
import { TeamMembers } from './team-members';
|
import { TeamMembers } from './team-members';
|
||||||
|
import { OpenPageTooltip } from './tooltip';
|
||||||
|
|
||||||
export const revalidate = 3600;
|
export const revalidate = 3600;
|
||||||
|
|
||||||
@ -29,9 +31,16 @@ const ZStargazersLiveResponse = z.record(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
const ZEarlyAdoptersResponse = z.record(
|
||||||
|
z.object({
|
||||||
|
id: z.number(),
|
||||||
|
time: z.string().datetime(),
|
||||||
|
earlyAdopters: z.number(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// const ZOpenPullRequestsResponse = ZMergedPullRequestsResponse;
|
export type StargazersType = z.infer<typeof ZStargazersLiveResponse>;
|
||||||
|
export type EarlyAdoptersType = z.infer<typeof ZEarlyAdoptersResponse>;
|
||||||
|
|
||||||
export default async function OpenPage() {
|
export default async function OpenPage() {
|
||||||
const {
|
const {
|
||||||
@ -65,6 +74,14 @@ export default async function OpenPage() {
|
|||||||
.then(async (res) => res.json())
|
.then(async (res) => res.json())
|
||||||
.then((res) => ZStargazersLiveResponse.parse(res));
|
.then((res) => ZStargazersLiveResponse.parse(res));
|
||||||
|
|
||||||
|
const EARLY_ADOPTERS_DATA = await fetch('https://stargrazer-live.onrender.com/api/stats/stripe', {
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (res) => res.json())
|
||||||
|
.then((res) => ZEarlyAdoptersResponse.parse(res));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
<div className="mx-auto mt-6 max-w-screen-lg sm:mt-12">
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
@ -107,10 +124,20 @@ export default async function OpenPage() {
|
|||||||
|
|
||||||
<SalaryBands className="col-span-12 lg:col-span-6" />
|
<SalaryBands className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
<FundingRaised className="col-span-12 lg:col-span-6" />
|
<FundingRaised data={FUNDING_RAISED} className="col-span-12 lg:col-span-6" />
|
||||||
|
|
||||||
<CapTable className="col-span-12 lg:col-span-6" />
|
<CapTable className="col-span-12 lg:col-span-6" />
|
||||||
<GithubMetric
|
|
||||||
|
<BarMetric<EarlyAdoptersType>
|
||||||
|
data={EARLY_ADOPTERS_DATA}
|
||||||
|
metricKey="earlyAdopters"
|
||||||
|
title="Early Adopters"
|
||||||
|
label="Early Adopters"
|
||||||
|
className="col-span-12 lg:col-span-6"
|
||||||
|
extraInfo={<OpenPageTooltip />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BarMetric<StargazersType>
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="stars"
|
metricKey="stars"
|
||||||
title="Github: Total Stars"
|
title="Github: Total Stars"
|
||||||
@ -118,29 +145,31 @@ export default async function OpenPage() {
|
|||||||
className="col-span-12 lg:col-span-6"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GithubMetric
|
<BarMetric<StargazersType>
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="mergedPRs"
|
metricKey="mergedPRs"
|
||||||
title="Github: Total Merged PRs"
|
title="Github: Total Merged PRs"
|
||||||
label="Merged PRs"
|
label="Merged PRs"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-4"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
<GithubMetric
|
|
||||||
|
<BarMetric<StargazersType>
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="forks"
|
metricKey="forks"
|
||||||
title="Github: Total Forks"
|
title="Github: Total Forks"
|
||||||
label="Forks"
|
label="Forks"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-4"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
<GithubMetric
|
|
||||||
|
<BarMetric<StargazersType>
|
||||||
data={STARGAZERS_DATA}
|
data={STARGAZERS_DATA}
|
||||||
metricKey="openIssues"
|
metricKey="openIssues"
|
||||||
title="Github: Total Open Issues"
|
title="Github: Total Open Issues"
|
||||||
label="Open Issues"
|
label="Open Issues"
|
||||||
chartHeight={300}
|
chartHeight={300}
|
||||||
className="col-span-12 lg:col-span-4"
|
className="col-span-12 lg:col-span-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
<div className="col-span-12 mt-12 flex flex-col items-center justify-center">
|
||||||
|
|||||||
40
apps/marketing/src/app/(marketing)/open/tooltip.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
|
export function OpenPageTooltip() {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
className="ml-2 mt-2.5"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
August and earlier: Active subscribers. September and beyond: Numbers of active
|
||||||
|
subscriptions.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -48,9 +48,9 @@ export default function PricingPage() {
|
|||||||
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
<AccordionContent className="text-muted-foreground max-w-prose text-sm leading-relaxed">
|
||||||
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
You can self-host Documenso for free or use our ready-to-use hosted version. The
|
||||||
hosted version comes with additional support, painless scalability and more. Early
|
hosted version comes with additional support, painless scalability and more. Early
|
||||||
adopters of the community plan will get access to all features we build this year, for
|
adopters will get access to all features we build this year, for no additional cost!
|
||||||
no additional cost! Forever! Yes, that includes multiple users per account later. If
|
Forever! Yes, that includes multiple users per account later. If you want Documenso
|
||||||
you want Documenso for your enterprise, we are happy to talk about your needs.
|
for your enterprise, we are happy to talk about your needs.
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Github } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
import { usePlausible } from 'next-plausible';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
|||||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||||
onClick={onSignUpClick}
|
onClick={onSignUpClick}
|
||||||
>
|
>
|
||||||
Get the Community Plan
|
Get the Early Adopters Plan
|
||||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||||
$30/mo. forever!
|
$30/mo. forever!
|
||||||
</span>
|
</span>
|
||||||
@ -52,7 +52,7 @@ export const Callout = ({ starCount }: CalloutProps) => {
|
|||||||
onClick={() => event('view-github')}
|
onClick={() => event('view-github')}
|
||||||
>
|
>
|
||||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||||
<Github className="mr-2 h-5 w-5" />
|
<LuGithub className="mr-2 h-5 w-5" />
|
||||||
Star on Github
|
Star on Github
|
||||||
{starCount && starCount > 0 && (
|
{starCount && starCount > 0 && (
|
||||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
@ -55,8 +55,8 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
|
reset,
|
||||||
} = useForm<TClaimPlanDialogFormSchema>({
|
} = useForm<TClaimPlanDialogFormSchema>({
|
||||||
mode: 'onBlur',
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: params?.get('name') ?? '',
|
name: params?.get('name') ?? '',
|
||||||
email: params?.get('email') ?? '',
|
email: params?.get('email') ?? '',
|
||||||
@ -91,6 +91,12 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSubmitting && !open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isSubmitting && setOpen(value)}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
@ -139,7 +145,7 @@ export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialog
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" size="lg" loading={isSubmitting}>
|
<Button type="submit" size="lg" loading={isSubmitting}>
|
||||||
Claim the Community Plan (
|
Claim the early adopters Plan (
|
||||||
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
{/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
|
||||||
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||||
? 'Monthly'
|
? 'Monthly'
|
||||||
|
|||||||
@ -5,17 +5,20 @@ import { HTMLAttributes } from 'react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Github, MessagesSquare, Moon, Sun, Twitter } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
import { FaXTwitter } from 'react-icons/fa6';
|
||||||
|
import { LiaDiscord } from 'react-icons/lia';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
export type FooterProps = HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
const SOCIAL_LINKS = [
|
const SOCIAL_LINKS = [
|
||||||
{ href: 'https://twitter.com/documenso', icon: <Twitter className="h-6 w-6" /> },
|
{ href: 'https://twitter.com/documenso', icon: <FaXTwitter className="h-6 w-6" /> },
|
||||||
{ href: 'https://github.com/documenso/documenso', icon: <Github className="h-6 w-6" /> },
|
{ href: 'https://github.com/documenso/documenso', icon: <LuGithub className="h-6 w-6" /> },
|
||||||
{ href: 'https://documen.so/discord', icon: <MessagesSquare className="h-6 w-6" /> },
|
{ href: 'https://documen.so/discord', icon: <LiaDiscord className="h-7 w-7" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FOOTER_LINKS = [
|
const FOOTER_LINKS = [
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Variants, motion } from 'framer-motion';
|
import { Variants, motion } from 'framer-motion';
|
||||||
import { Github } from 'lucide-react';
|
|
||||||
import { usePlausible } from 'next-plausible';
|
import { usePlausible } from 'next-plausible';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
@ -114,7 +114,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
className="rounded-full bg-transparent backdrop-blur-sm"
|
className="rounded-full bg-transparent backdrop-blur-sm"
|
||||||
onClick={onSignUpClick}
|
onClick={onSignUpClick}
|
||||||
>
|
>
|
||||||
Get the Community Plan
|
Get the Early Adopters Plan
|
||||||
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
<span className="bg-primary dark:text-background -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
|
||||||
$30/mo. forever!
|
$30/mo. forever!
|
||||||
</span>
|
</span>
|
||||||
@ -122,7 +122,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
|
|
||||||
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
|
||||||
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
|
||||||
<Github className="mr-2 h-5 w-5" />
|
<LuGithub className="mr-2 h-5 w-5" />
|
||||||
Star on Github
|
Star on Github
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { motion, useReducedMotion } from 'framer-motion';
|
import { motion, useReducedMotion } from 'framer-motion';
|
||||||
import { Github, MessagesSquare, Twitter } from 'lucide-react';
|
import { FaXTwitter } from 'react-icons/fa6';
|
||||||
|
import { LiaDiscord } from 'react-icons/lia';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
|
|
||||||
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
import { Sheet, SheetContent } from '@documenso/ui/primitives/sheet';
|
||||||
|
|
||||||
@ -111,7 +113,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-foreground hover:text-foreground/80"
|
className="text-foreground hover:text-foreground/80"
|
||||||
>
|
>
|
||||||
<Twitter className="h-6 w-6" />
|
<FaXTwitter className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@ -119,7 +121,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-foreground hover:text-foreground/80"
|
className="text-foreground hover:text-foreground/80"
|
||||||
>
|
>
|
||||||
<Github className="h-6 w-6" />
|
<LuGithub className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
@ -127,7 +129,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-foreground hover:text-foreground/80"
|
className="text-foreground hover:text-foreground/80"
|
||||||
>
|
>
|
||||||
<MessagesSquare className="h-6 w-6" />
|
<LiaDiscord className="h-7 w-7" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
data-plan="community"
|
data-plan="community"
|
||||||
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
|
className="border-primary bg-background shadow-foreground/5 flex flex-col items-center justify-center rounded-lg border-2 px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380]"
|
||||||
>
|
>
|
||||||
<p className="text-foreground text-4xl font-medium">Community</p>
|
<p className="text-foreground text-4xl font-medium">Early Adopters</p>
|
||||||
<div className="text-primary mt-2.5 text-xl font-medium">
|
<div className="text-primary mt-2.5 text-xl font-medium">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
|
||||||
@ -136,12 +136,22 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
</ClaimPlanDialog>
|
</ClaimPlanDialog>
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
<div className="mt-8 flex w-full flex-col divide-y">
|
||||||
<p className="text-foreground py-4 font-medium">Documenso Early Adopter Deal:</p>
|
<p className="text-foreground py-4 font-medium">
|
||||||
|
{' '}
|
||||||
|
<a href="https://documenso.com/blog/early-adopters" target="_blank">
|
||||||
|
The Early Adopter Deal:
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<p className="text-foreground py-4">Join the movement</p>
|
<p className="text-foreground py-4">Join the movement</p>
|
||||||
<p className="text-foreground py-4">Simple signing solution</p>
|
<p className="text-foreground py-4">Simple signing solution</p>
|
||||||
<p className="text-foreground py-4">Email and Slack assistance</p>
|
<p className="text-foreground py-4">Email, Discord and Slack assistance</p>
|
||||||
<p className="text-foreground py-4">
|
<p className="text-foreground py-4">
|
||||||
<strong>Includes all upcoming features</strong>
|
<strong>
|
||||||
|
{' '}
|
||||||
|
<a href="https://documenso.com/blog/early-adopters" target="_blank">
|
||||||
|
Includes all upcoming features
|
||||||
|
</a>
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
|
<p className="text-foreground py-4">Fixed, straightforward pricing</p>
|
||||||
</div>
|
</div>
|
||||||
@ -168,7 +178,7 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-col divide-y">
|
<div className="mt-8 flex w-full flex-col divide-y">
|
||||||
<p className="text-foreground py-4 font-medium">Everything in Community, plus:</p>
|
<p className="text-foreground py-4 font-medium">Everything in Early Adopters, plus:</p>
|
||||||
<p className="text-foreground py-4">Custom Subdomain</p>
|
<p className="text-foreground py-4">Custom Subdomain</p>
|
||||||
<p className="text-foreground py-4">Compliance Check</p>
|
<p className="text-foreground py-4">Compliance Check</p>
|
||||||
<p className="text-foreground py-4">Guaranteed Uptime</p>
|
<p className="text-foreground py-4">Guaranteed Uptime</p>
|
||||||
|
|||||||
@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Todo: Handle `downloadLink`
|
|
||||||
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
const template = createElement(DocumentSelfSignedEmailTemplate, {
|
||||||
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
|
|
||||||
documentName: documentName,
|
documentName: documentName,
|
||||||
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
|
||||||
});
|
});
|
||||||
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
|
|||||||
subject: 'Document signed',
|
subject: 'Document signed',
|
||||||
html: render(template),
|
html: render(template),
|
||||||
text: render(template, { plainText: true }),
|
text: render(template, { plainText: true }),
|
||||||
|
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
|
||||||
});
|
});
|
||||||
|
|
||||||
return documentToken;
|
return documentToken;
|
||||||
|
|||||||
@ -189,7 +189,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
className="bg-foreground/5 col-span-12 flex flex-col rounded-2xl p-6 lg:col-span-5"
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
|
<h3 className="text-2xl font-semibold">Sign up for the early adopters plan</h3>
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
<p className="text-muted-foreground mt-2 text-xs">
|
||||||
with Timur Ercan & Lucas Smith from Documenso
|
with Timur Ercan & Lucas Smith from Documenso
|
||||||
</p>
|
</p>
|
||||||
@ -377,7 +377,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
|
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
|
||||||
<DialogContent>
|
<DialogContent position="center">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add your signature</DialogTitle>
|
<DialogTitle>Add your signature</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -391,6 +391,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
|
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="aspect-video w-full rounded-md border"
|
className="aspect-video w-full rounded-md border"
|
||||||
|
defaultValue={signatureDataUrl || ''}
|
||||||
onChange={setDraftSignatureDataUrl}
|
onChange={setDraftSignatureDataUrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,14 @@ const config = {
|
|||||||
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/ingest/:path*',
|
||||||
|
destination: 'https://eu.posthog.com/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
|
|||||||
1
apps/web/process-env.d.ts
vendored
@ -7,6 +7,7 @@ declare namespace NodeJS {
|
|||||||
|
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string;
|
||||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string;
|
||||||
|
|
||||||
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
NEXT_PRIVATE_STRIPE_API_KEY: string;
|
||||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|||||||
BIN
apps/web/public/static/user-plus.png
Normal file
|
After Width: | Height: | Size: 367 B |
@ -12,7 +12,7 @@ export type AdminSectionLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (!isAdmin(user)) {
|
if (!isAdmin(user)) {
|
||||||
redirect('/documents');
|
redirect('/documents');
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function Loading() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
|
||||||
|
|||||||
@ -30,11 +30,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
redirect('/documents');
|
redirect('/documents');
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const document = await getDocumentById({
|
const document = await getDocumentById({
|
||||||
id: documentId,
|
id: documentId,
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
if (!document || !document.documentData) {
|
if (!document || !document.documentData) {
|
||||||
@ -50,11 +50,11 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
const [recipients, fields] = await Promise.all([
|
const [recipients, fields] = await Promise.all([
|
||||||
await getRecipientsForDocument({
|
await getRecipientsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
}),
|
}),
|
||||||
await getFieldsForDocument({
|
await getFieldsForDocument({
|
||||||
documentId,
|
documentId,
|
||||||
userId: session.id,
|
userId: user.id,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -65,10 +65,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
className="mt-4 max-w-xs truncate text-2xl font-semibold md:text-3xl"
|
|
||||||
title={document.title}
|
|
||||||
>
|
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@ -90,7 +87,7 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
<EditDocumentForm
|
<EditDocumentForm
|
||||||
className="mt-8"
|
className="mt-8"
|
||||||
document={document}
|
document={document}
|
||||||
user={session}
|
user={user}
|
||||||
recipients={recipients}
|
recipients={recipients}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
dataUrl={documentDataUrl}
|
dataUrl={documentDataUrl}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default function DocumentSentPage() {
|
|||||||
Documents
|
Documents
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="mt-4 max-w-xs grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
Loading Document...
|
Loading Document...
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -32,6 +34,8 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
|
||||||
|
|
||||||
|
import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog';
|
||||||
|
|
||||||
export type DataTableActionDropdownProps = {
|
export type DataTableActionDropdownProps = {
|
||||||
row: Document & {
|
row: Document & {
|
||||||
User: Pick<User, 'id' | 'name' | 'email'>;
|
User: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
@ -44,6 +48,8 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [, copyToClipboard] = useCopyToClipboard();
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -59,6 +65,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
// const isPending = row.status === DocumentStatus.PENDING;
|
// const isPending = row.status === DocumentStatus.PENDING;
|
||||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||||
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||||
|
const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT;
|
||||||
|
|
||||||
const onShareClick = async () => {
|
const onShareClick = async () => {
|
||||||
const { slug } = await createOrGetShareLink({
|
const { slug } = await createOrGetShareLink({
|
||||||
@ -147,7 +154,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
Void
|
Void
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem disabled>
|
<DropdownMenuItem onClick={() => setDeleteDialogOpen(true)} disabled={!isDocumentDeletable}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -168,6 +175,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
Share
|
Share
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
||||||
|
{isDocumentDeletable && (
|
||||||
|
<DeleteDraftDocumentDialog
|
||||||
|
id={row.id}
|
||||||
|
open={isDeleteDialogOpen}
|
||||||
|
onOpenChange={setDeleteDialogOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
type DeleteDraftDocumentDialogProps = {
|
||||||
|
id: number;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (_open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteDraftDocumentDialog = ({
|
||||||
|
id,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DeleteDraftDocumentDialogProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { mutateAsync: deleteDocument, isLoading } =
|
||||||
|
trpcReact.document.deleteDraftDocument.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
router.refresh();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Document deleted',
|
||||||
|
description: 'Your document has been successfully deleted.',
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDraftDelete = async () => {
|
||||||
|
try {
|
||||||
|
await deleteDocument({ id });
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description: 'This document could not be deleted at this time. Please try again.',
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Do you want to delete this document?</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
Please note that this action is irreversible. Once confirmed, your document will be
|
||||||
|
permanently deleted.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="button" loading={isLoading} onClick={onDraftDelete} className="flex-1">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -25,7 +25,7 @@ export type DocumentsPageProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
export default async function DocumentsPage({ searchParams = {} }: DocumentsPageProps) {
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const stats = await getStats({
|
const stats = await getStats({
|
||||||
user,
|
user,
|
||||||
@ -66,7 +66,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
|
|||||||
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
<div className="mt-12 flex flex-wrap items-center justify-between gap-x-4 gap-y-8">
|
||||||
<h1 className="text-4xl font-semibold">Documents</h1>
|
<h1 className="text-4xl font-semibold">Documents</h1>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-6 overflow-hidden">
|
<div className="-m-1 flex flex-wrap gap-x-4 gap-y-6 overflow-hidden p-1">
|
||||||
<Tabs defaultValue={status} className="overflow-x-auto">
|
<Tabs defaultValue={status} className="overflow-x-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
{[
|
{[
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export default async function AuthenticatedDashboardLayout({
|
|||||||
redirect('/signin');
|
redirect('/signin');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider session={session}>
|
||||||
|
|||||||
133
apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
|
||||||
|
import { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
||||||
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
|
import { toHumanPrice } from '@documenso/lib/universal/stripe/to-human-price';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { createCheckout } from './create-checkout.action';
|
||||||
|
|
||||||
|
type Interval = keyof PriceIntervals;
|
||||||
|
|
||||||
|
const INTERVALS: Interval[] = ['day', 'week', 'month', 'year'];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const isInterval = (value: unknown): value is Interval => INTERVALS.includes(value as Interval);
|
||||||
|
|
||||||
|
const FRIENDLY_INTERVALS: Record<Interval, string> = {
|
||||||
|
day: 'Daily',
|
||||||
|
week: 'Weekly',
|
||||||
|
month: 'Monthly',
|
||||||
|
year: 'Yearly',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MotionCard = motion(Card);
|
||||||
|
|
||||||
|
export type BillingPlansProps = {
|
||||||
|
prices: PriceIntervals;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BillingPlans = ({ prices }: BillingPlansProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const isMounted = useIsMounted();
|
||||||
|
|
||||||
|
const [interval, setInterval] = useState<Interval>('month');
|
||||||
|
const [isFetchingCheckoutSession, setIsFetchingCheckoutSession] = useState(false);
|
||||||
|
|
||||||
|
const onSubscribeClick = async (priceId: string) => {
|
||||||
|
try {
|
||||||
|
setIsFetchingCheckoutSession(true);
|
||||||
|
|
||||||
|
const url = await createCheckout({ priceId });
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new Error('Unable to create session');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url);
|
||||||
|
} catch (_err) {
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description: 'An error occurred while trying to create a checkout session.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingCheckoutSession(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Tabs value={interval} onValueChange={(value) => isInterval(value) && setInterval(value)}>
|
||||||
|
<TabsList>
|
||||||
|
{INTERVALS.map(
|
||||||
|
(interval) =>
|
||||||
|
prices[interval].length > 0 && (
|
||||||
|
<TabsTrigger key={interval} className="min-w-[150px]" value={interval}>
|
||||||
|
{FRIENDLY_INTERVALS[interval]}
|
||||||
|
</TabsTrigger>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="mt-8 grid gap-8 lg:grid-cols-2 2xl:grid-cols-3">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{prices[interval].map((price) => (
|
||||||
|
<MotionCard
|
||||||
|
key={price.id}
|
||||||
|
initial={{ opacity: isMounted ? 0 : 1, y: isMounted ? 20 : 0 }}
|
||||||
|
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
|
||||||
|
exit={{ opacity: 0, transition: { duration: 0.3 } }}
|
||||||
|
>
|
||||||
|
<CardContent className="flex h-full flex-col p-6">
|
||||||
|
<CardTitle>{price.product.name}</CardTitle>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground mt-2 text-lg font-medium">
|
||||||
|
${toHumanPrice(price.unit_amount ?? 0)} {price.currency.toUpperCase()}{' '}
|
||||||
|
<span className="text-xs">per {interval}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground mt-1.5 text-sm">
|
||||||
|
{price.product.description}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{price.product.features && price.product.features.length > 0 && (
|
||||||
|
<div className="text-muted-foreground mt-4">
|
||||||
|
<div className="text-sm font-medium">Includes:</div>
|
||||||
|
|
||||||
|
<ul className="mt-1 divide-y text-sm">
|
||||||
|
{price.product.features.map((feature, index) => (
|
||||||
|
<li key={index} className="py-2">
|
||||||
|
{feature.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
loading={isFetchingCheckoutSession}
|
||||||
|
onClick={() => void onSubscribeClick(price.id)}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</MotionCard>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { createBillingPortal } from './create-billing-portal.action';
|
||||||
|
|
||||||
|
export const BillingPortalButton = () => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [isFetchingPortalUrl, setIsFetchingPortalUrl] = useState(false);
|
||||||
|
|
||||||
|
const handleFetchPortalUrl = async () => {
|
||||||
|
if (isFetchingPortalUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingPortalUrl(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionUrl = await createBillingPortal();
|
||||||
|
|
||||||
|
if (!sessionUrl) {
|
||||||
|
throw new Error('NO_SESSION');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(sessionUrl, '_blank');
|
||||||
|
} catch (e) {
|
||||||
|
let description =
|
||||||
|
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.';
|
||||||
|
|
||||||
|
if (e.message === 'CUSTOMER_NOT_FOUND') {
|
||||||
|
description =
|
||||||
|
'You do not currently have a customer record, this should not happen. Please contact support for assistance.';
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Something went wrong',
|
||||||
|
description,
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFetchingPortalUrl(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={async () => handleFetchPortalUrl()} loading={isFetchingPortalUrl}>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getStripeCustomerByEmail,
|
||||||
|
getStripeCustomerById,
|
||||||
|
} from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
|
||||||
|
export const createBillingPortal = async () => {
|
||||||
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
|
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
let stripeCustomer: Stripe.Customer | null = null;
|
||||||
|
|
||||||
|
// Find the Stripe customer for the current user subscription.
|
||||||
|
if (existingSubscription) {
|
||||||
|
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
||||||
|
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
throw new Error('Missing Stripe customer for subscription');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to check if a Stripe customer already exists for the current user email.
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
stripeCustomer = await getStripeCustomerByEmail(user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Stripe customer if it does not exist for the current user.
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
stripeCustomer = await stripe.customers.create({
|
||||||
|
name: user.name ?? undefined,
|
||||||
|
email: user.email,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPortalSession({
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session';
|
||||||
|
import {
|
||||||
|
getStripeCustomerByEmail,
|
||||||
|
getStripeCustomerById,
|
||||||
|
} from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
|
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
||||||
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
|
import { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
|
|
||||||
|
export type CreateCheckoutOptions = {
|
||||||
|
priceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => {
|
||||||
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
|
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
|
||||||
|
|
||||||
|
let stripeCustomer: Stripe.Customer | null = null;
|
||||||
|
|
||||||
|
// Find the Stripe customer for the current user subscription.
|
||||||
|
if (existingSubscription) {
|
||||||
|
stripeCustomer = await getStripeCustomerById(existingSubscription.customerId);
|
||||||
|
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
throw new Error('Missing Stripe customer for subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPortalSession({
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to check if a Stripe customer already exists for the current user email.
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
stripeCustomer = await getStripeCustomerByEmail(user.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Stripe customer if it does not exist for the current user.
|
||||||
|
if (!stripeCustomer) {
|
||||||
|
stripeCustomer = await stripe.customers.create({
|
||||||
|
name: user.name ?? undefined,
|
||||||
|
email: user.email,
|
||||||
|
metadata: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCheckoutSession({
|
||||||
|
customerId: stripeCustomer.id,
|
||||||
|
priceId,
|
||||||
|
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,18 +1,20 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
|
import { match } from 'ts-pattern';
|
||||||
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
|
|
||||||
|
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
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 { Stripe, stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||||
|
|
||||||
|
import { BillingPlans } from './billing-plans';
|
||||||
|
import { BillingPortalButton } from './billing-portal-button';
|
||||||
|
|
||||||
export default async function BillingSettingsPage() {
|
export default async function BillingSettingsPage() {
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const isBillingEnabled = await getServerComponentFlag('app_billing');
|
const isBillingEnabled = await getServerComponentFlag('app_billing');
|
||||||
|
|
||||||
@ -21,57 +23,75 @@ export default async function BillingSettingsPage() {
|
|||||||
redirect('/settings/profile');
|
redirect('/settings/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
|
const [subscription, prices] = await Promise.all([
|
||||||
if (sub) {
|
getSubscriptionByUserId({ userId: user.id }),
|
||||||
return sub;
|
getPricesByInterval(),
|
||||||
}
|
]);
|
||||||
|
|
||||||
// If we don't have a customer record, create one as well as an empty subscription.
|
let subscriptionProduct: Stripe.Product | null = null;
|
||||||
return createCustomer({ user });
|
|
||||||
});
|
|
||||||
|
|
||||||
let billingPortalUrl = '';
|
if (subscription?.planId) {
|
||||||
|
const foundSubscriptionProduct = (await stripe.products.list()).data.find(
|
||||||
|
(item) => item.default_price === subscription.planId,
|
||||||
|
);
|
||||||
|
|
||||||
if (subscription.customerId) {
|
subscriptionProduct = foundSubscriptionProduct ?? null;
|
||||||
billingPortalUrl = await getPortalSession({
|
|
||||||
customerId: subscription.customerId,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMissingOrInactiveOrFreePlan = !subscription || subscription.status === 'INACTIVE';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium">Billing</h3>
|
<h3 className="text-lg font-medium">Billing</h3>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-sm">
|
<div className="text-muted-foreground mt-2 text-sm">
|
||||||
Your subscription is{' '}
|
{isMissingOrInactiveOrFreePlan && (
|
||||||
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
|
<p>
|
||||||
{subscription?.periodEnd && (
|
You are currently on the <span className="font-semibold">Free Plan</span>.
|
||||||
<>
|
</p>
|
||||||
{' '}
|
|
||||||
Your next payment is due on{' '}
|
|
||||||
<span className="font-semibold">
|
|
||||||
<LocaleDate date={subscription.periodEnd} />
|
|
||||||
</span>
|
|
||||||
.
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
|
{!isMissingOrInactiveOrFreePlan &&
|
||||||
|
match(subscription.status)
|
||||||
|
.with('ACTIVE', () => (
|
||||||
|
<p>
|
||||||
|
{subscriptionProduct ? (
|
||||||
|
<span>
|
||||||
|
You are currently subscribed to{' '}
|
||||||
|
<span className="font-semibold">{subscriptionProduct.name}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>You currently have an active plan</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subscription.periodEnd && (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
which is set to{' '}
|
||||||
|
{subscription.cancelAtPeriodEnd ? (
|
||||||
|
<span>
|
||||||
|
end on{' '}
|
||||||
|
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
automatically renew on{' '}
|
||||||
|
<LocaleDate className="font-semibold" date={subscription.periodEnd} />.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
))
|
||||||
|
.with('PAST_DUE', () => (
|
||||||
|
<p>Your current plan is past due. Please update your payment information.</p>
|
||||||
|
))
|
||||||
|
.otherwise(() => null)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr className="my-4" />
|
<hr className="my-4" />
|
||||||
|
|
||||||
{billingPortalUrl && (
|
{isMissingOrInactiveOrFreePlan ? <BillingPlans prices={prices} /> : <BillingPortalButton />}
|
||||||
<Button asChild>
|
|
||||||
<Link href={billingPortalUrl}>Manage Subscription</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!billingPortalUrl && (
|
|
||||||
<p className="text-muted-foreground max-w-[60ch] text-base">
|
|
||||||
You do not currently have a customer record, this should not happen. Please contact
|
|
||||||
support for assistance.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { PasswordForm } from '~/components/forms/password';
|
import { PasswordForm } from '~/components/forms/password';
|
||||||
|
|
||||||
export default async function PasswordSettingsPage() {
|
export default async function PasswordSettingsPage() {
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { ProfileForm } from '~/components/forms/profile';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
export default async function ProfileSettingsPage() {
|
export default async function ProfileSettingsPage() {
|
||||||
const user = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export default async function CompletedSigningPage({
|
|||||||
recipient.email;
|
recipient.email;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
|
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
|
||||||
{/* Card with recipient */}
|
{/* Card with recipient */}
|
||||||
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
|
<SigningCard3D name={recipientName} signingCelebrationImage={signingCelebration} />
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { HTMLAttributes, useState } from 'react';
|
import { HTMLAttributes, useState } from 'react';
|
||||||
|
|
||||||
import { Copy, Share, Twitter } from 'lucide-react';
|
import { Copy, Share } from 'lucide-react';
|
||||||
|
import { FaXTwitter } from 'react-icons/fa6';
|
||||||
|
|
||||||
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -106,7 +107,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent position="end">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Share</DialogTitle>
|
<DialogTitle>Share</DialogTitle>
|
||||||
|
|
||||||
@ -119,13 +120,13 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
|||||||
<span className="font-medium text-blue-400">@documenso</span>
|
<span className="font-medium text-blue-400">@documenso</span>
|
||||||
. Check it out!
|
. Check it out!
|
||||||
<span className="mt-2 block" />
|
<span className="mt-2 block" />
|
||||||
<span className="font-medium text-blue-400">
|
<span className="break-all font-medium text-blue-400">
|
||||||
{window.location.origin}/share/{shareLink?.slug || '...'}
|
{window.location.origin}/share/{shareLink?.slug || '...'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
|
<Button variant="outline" className="mt-4" onClick={onTweetClick}>
|
||||||
<Twitter className="mr-2 h-4 w-4" />
|
<FaXTwitter className="mr-2 h-4 w-4" />
|
||||||
Tweet
|
Tweet
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
|
||||||
@ -27,6 +28,7 @@ export type SigningFormProps = {
|
|||||||
|
|
||||||
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
export const SigningForm = ({ document, recipient, fields }: SigningFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
|
||||||
|
|
||||||
@ -60,7 +62,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky flex h-full flex-col rounded-xl border px-4 py-6',
|
||||||
|
{
|
||||||
|
'top-20 max-h-[min(68rem,calc(100vh-6rem))]': session,
|
||||||
|
'top-4 max-h-[min(68rem,calc(100vh-2rem))]': !session,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
@ -120,6 +126,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
|||||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -10,11 +10,11 @@ export type SigningLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function SigningLayout({ children }: SigningLayoutProps) {
|
export default async function SigningLayout({ children }: SigningLayoutProps) {
|
||||||
const user = await getServerComponentSession();
|
const { user, session } = await getServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider>
|
<NextAuthProvider session={session}>
|
||||||
<div className="min-h-screen overflow-hidden">
|
<div className="min-h-screen">
|
||||||
{user && <AuthenticatedHeader user={user} />}
|
{user && <AuthenticatedHeader user={user} />}
|
||||||
|
|
||||||
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
|
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">{children}</main>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
.then((buffer) => Buffer.from(buffer).toString('base64'))
|
.then((buffer) => Buffer.from(buffer).toString('base64'))
|
||||||
.then((data) => `data:application/pdf;base64,${data}`);
|
.then((data) => `data:application/pdf;base64,${data}`);
|
||||||
|
|
||||||
const user = await getServerComponentSession();
|
const { user } = await getServerComponentSession();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
document.status === DocumentStatus.COMPLETED ||
|
document.status === DocumentStatus.COMPLETED ||
|
||||||
@ -62,7 +62,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
||||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto w-full max-w-screen-xl">
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
{document.title}
|
{document.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import NotFoundPartial from '~/components/partials/not-found';
|
import NotFoundPartial from '~/components/partials/not-found';
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
const session = await getServerComponentSession();
|
const { session } = await getServerComponentSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotFoundPartial>
|
<NotFoundPartial>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Github,
|
|
||||||
Key,
|
Key,
|
||||||
LogOut,
|
LogOut,
|
||||||
User as LucideUser,
|
User as LucideUser,
|
||||||
@ -16,6 +15,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { signOut } from 'next-auth/react';
|
import { signOut } from 'next-auth/react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
|
import { LuGithub } from 'react-icons/lu';
|
||||||
|
|
||||||
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
|
||||||
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
@ -130,7 +130,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
|
<Link href="https://github.com/documenso/documenso" className="cursor-pointer">
|
||||||
<Github className="mr-2 h-4 w-4" />
|
<LuGithub className="mr-2 h-4 w-4" />
|
||||||
Star on Github
|
Star on Github
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@ -11,10 +11,10 @@ export type AddFieldsActionInput = TAddFieldsFormSchema & {
|
|||||||
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
|
export const addFields = async ({ documentId, fields }: AddFieldsActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { id: userId } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
await setFieldsForDocument({
|
await setFieldsForDocument({
|
||||||
userId,
|
userId: user.id,
|
||||||
documentId,
|
documentId,
|
||||||
fields: fields.map((field) => ({
|
fields: fields.map((field) => ({
|
||||||
id: field.nativeId,
|
id: field.nativeId,
|
||||||
|
|||||||
@ -11,10 +11,10 @@ export type AddSignersActionInput = TAddSignersFormSchema & {
|
|||||||
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
|
export const addSigners = async ({ documentId, signers }: AddSignersActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { id: userId } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
await setRecipientsForDocument({
|
await setRecipientsForDocument({
|
||||||
userId,
|
userId: user.id,
|
||||||
documentId,
|
documentId,
|
||||||
recipients: signers.map((signer) => ({
|
recipients: signers.map((signer) => ({
|
||||||
id: signer.nativeId,
|
id: signer.nativeId,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export type CompleteDocumentActionInput = TAddSubjectFormSchema & {
|
|||||||
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
export const completeDocument = async ({ documentId, email }: CompleteDocumentActionInput) => {
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
const { id: userId } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
if (email.message || email.subject) {
|
if (email.message || email.subject) {
|
||||||
await upsertDocumentMeta({
|
await upsertDocumentMeta({
|
||||||
@ -23,7 +23,7 @@ export const completeDocument = async ({ documentId, email }: CompleteDocumentAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await sendDocument({
|
return await sendDocument({
|
||||||
userId,
|
userId: user.id,
|
||||||
documentId,
|
documentId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" loading={isSubmitting}>
|
<Button size="lg" loading={isSubmitting}>
|
||||||
Reset Password
|
{isSubmitting ? 'Sending Reset Email...' : 'Reset Password'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { FormErrorMessage } from '../form/form-error-message';
|
|||||||
|
|
||||||
export const ZPasswordFormSchema = z
|
export const ZPasswordFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
currentPassword: z.string().min(6).max(72),
|
||||||
password: z.string().min(6).max(72),
|
password: z.string().min(6).max(72),
|
||||||
repeatedPassword: z.string().min(6).max(72),
|
repeatedPassword: z.string().min(6).max(72),
|
||||||
})
|
})
|
||||||
@ -40,6 +41,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -48,6 +50,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TPasswordFormSchema>({
|
} = useForm<TPasswordFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
|
currentPassword: '',
|
||||||
password: '',
|
password: '',
|
||||||
repeatedPassword: '',
|
repeatedPassword: '',
|
||||||
},
|
},
|
||||||
@ -56,9 +59,10 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
|
|
||||||
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ password }: TPasswordFormSchema) => {
|
const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
|
currentPassword,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,6 +96,39 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
|
|||||||
className={cn('flex w-full flex-col gap-y-4', className)}
|
className={cn('flex w-full flex-col gap-y-4', className)}
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="current-password" className="text-muted-foreground">
|
||||||
|
Current Password
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id="current-password"
|
||||||
|
type={showCurrentPassword ? 'text' : 'password'}
|
||||||
|
minLength={6}
|
||||||
|
maxLength={72}
|
||||||
|
autoComplete="current-password"
|
||||||
|
className="bg-background mt-2 pr-10"
|
||||||
|
{...register('currentPassword')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="button"
|
||||||
|
className="absolute right-0 top-0 flex h-full items-center justify-center pr-3"
|
||||||
|
aria-label={showCurrentPassword ? 'Mask password' : 'Reveal password'}
|
||||||
|
onClick={() => setShowCurrentPassword((show) => !show)}
|
||||||
|
>
|
||||||
|
{showCurrentPassword ? (
|
||||||
|
<EyeOff className="text-muted-foreground h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Eye className="text-muted-foreground h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormErrorMessage className="mt-1.5" error={errors.currentPassword} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="password" className="text-muted-foreground">
|
<Label htmlFor="password" className="text-muted-foreground">
|
||||||
Password
|
Password
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" loading={isSubmitting}>
|
<Button size="lg" loading={isSubmitting}>
|
||||||
Reset Password
|
{isSubmitting ? 'Resetting Password...' : 'Reset Password'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Eye, EyeOff, Loader } from 'lucide-react';
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
@ -147,9 +147,13 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
<FormErrorMessage className="mt-1.5" error={errors.password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">
|
<Button
|
||||||
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
size="lg"
|
||||||
Sign In
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="dark:bg-documenso dark:hover:opacity-90"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Signing in...' : 'Sign In'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Eye, EyeOff, Loader } from 'lucide-react';
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@ -157,9 +157,13 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
<FormErrorMessage className="mt-1.5" error={errors.signature} />
|
<FormErrorMessage className="mt-1.5" error={errors.signature} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">
|
<Button
|
||||||
{isSubmitting && <Loader className="mr-2 h-5 w-5 animate-spin" />}
|
size="lg"
|
||||||
Sign Up
|
loading={isSubmitting}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="dark:bg-documenso dark:hover:opacity-90"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Signing up...' : 'Sign Up'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { buffer } from 'micro';
|
import { buffer } from 'micro';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf';
|
||||||
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf';
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
ReadStatus,
|
ReadStatus,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
|
SubscriptionStatus,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
|
||||||
@ -54,6 +56,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
);
|
);
|
||||||
log('event-type:', event.type);
|
log('event-type:', event.type);
|
||||||
|
|
||||||
|
if (event.type === 'customer.subscription.updated') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const subscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await handleCustomerSubscriptionUpdated(subscription);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Webhook received',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === 'checkout.session.completed') {
|
if (event.type === 'checkout.session.completed') {
|
||||||
// This is required since we don't want to create a guard for every event type
|
// This is required since we don't want to create a guard for every event type
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
@ -195,3 +209,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
message: 'Unhandled webhook event',
|
message: 'Unhandled webhook event',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCustomerSubscriptionUpdated = async (subscription: Stripe.Subscription) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const { plan } = subscription as unknown as Stripe.SubscriptionItem;
|
||||||
|
|
||||||
|
const customerId =
|
||||||
|
typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
|
||||||
|
|
||||||
|
const status = match(subscription.status)
|
||||||
|
.with('active', () => SubscriptionStatus.ACTIVE)
|
||||||
|
.with('past_due', () => SubscriptionStatus.PAST_DUE)
|
||||||
|
.otherwise(() => SubscriptionStatus.INACTIVE);
|
||||||
|
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId: customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
planId: plan.id,
|
||||||
|
status,
|
||||||
|
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||||
|
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
19
docker/compose-services.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: documenso-services
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: database
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=documenso
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=documenso
|
||||||
|
ports:
|
||||||
|
- 54320:5432
|
||||||
|
|
||||||
|
inbucket:
|
||||||
|
image: inbucket/inbucket
|
||||||
|
container_name: mailserver
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 2500:2500
|
||||||
|
- 1100:1100
|
||||||
65
package-lock.json
generated
@ -15,8 +15,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.3.1",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.3.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"react-confetti": "^6.1.0",
|
"react-confetti": "^6.1.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.11.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
@ -95,7 +95,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-icons": "^4.8.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"sharp": "0.32.5",
|
"sharp": "0.32.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
@ -1868,6 +1868,17 @@
|
|||||||
"resolved": "apps/marketing",
|
"resolved": "apps/marketing",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@documenso/nodemailer-resend": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-rG+jBbBEsVJUBU6v/2hb+OQD1m3Lhn49TOzQjln73zzL1B/sZsHhYOKpNPlTX0/FafCP7P9fKerndEeIKn54Vw==",
|
||||||
|
"dependencies": {
|
||||||
|
"resend": "^1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"nodemailer": "^6.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@documenso/prettier-config": {
|
"node_modules/@documenso/prettier-config": {
|
||||||
"resolved": "packages/prettier-config",
|
"resolved": "packages/prettier-config",
|
||||||
"link": true
|
"link": true
|
||||||
@ -9092,7 +9103,6 @@
|
|||||||
"version": "16.3.1",
|
"version": "16.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -9101,13 +9111,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv-cli": {
|
"node_modules/dotenv-cli": {
|
||||||
"version": "7.2.1",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.3.0.tgz",
|
||||||
"integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
|
"integrity": "sha512-314CA4TyK34YEJ6ntBf80eUY+t1XaFLyem1k9P0sX1gn30qThZ5qZr/ZwE318gEnzyYP9yj9HJk6SqwE0upkfw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.3.0",
|
||||||
"dotenv-expand": "^10.0.0",
|
"dotenv-expand": "^10.0.0",
|
||||||
"minimist": "^1.2.6"
|
"minimist": "^1.2.6"
|
||||||
},
|
},
|
||||||
@ -9119,7 +9128,6 @@
|
|||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
|
||||||
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
|
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@ -16348,9 +16356,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "4.10.1",
|
"version": "4.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
|
||||||
"integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==",
|
"integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
@ -16958,6 +16966,29 @@
|
|||||||
"node": ">=0.10.5"
|
"node": ">=0.10.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resend": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resend/-/resend-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-it8TIDVT+/gAiJsUlv2tdHuvzwCCv4Zwu+udDqIm/dIuByQwe68TtFDcPccxqpSVVrNCBxxXLzsdT1tsV+P3GA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-email/render": "0.0.7",
|
||||||
|
"type-fest": "3.13.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resend/node_modules/type-fest": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.2",
|
"version": "1.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||||
@ -19771,9 +19802,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@documenso/nodemailer-resend": "1.0.0",
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4"
|
"react-email": "^1.9.4",
|
||||||
|
"resend": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
@ -19853,6 +19886,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.3.1",
|
"@prisma/client": "5.3.1",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"dotenv-cli": "^7.3.0",
|
||||||
"prisma": "5.3.1"
|
"prisma": "5.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
14
package.json
@ -2,13 +2,21 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
|
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
|
||||||
"start": "cd apps && cd web && next start",
|
"start": "cd apps && cd web && next start",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"commitlint": "commitlint --edit",
|
"commitlint": "commitlint --edit",
|
||||||
"clean": "turbo run clean && rimraf node_modules"
|
"clean": "turbo run clean && rimraf node_modules",
|
||||||
|
"d": "npm run dx && npm run dev",
|
||||||
|
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev",
|
||||||
|
"dx:up": "docker compose -f docker/compose-services.yml up -d",
|
||||||
|
"dx:down": "docker compose -f docker/compose-services.yml down",
|
||||||
|
"prisma:migrate-dev": "npm run with:env -- npm run prisma:migrate-dev -w @documenso/prisma",
|
||||||
|
"prisma:migrate-deploy": "npm run with:env -- npm run prisma:migrate-deploy -w @documenso/prisma",
|
||||||
|
"with:env": "dotenv -e .env -e .env.local --"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">=8.6.0",
|
"npm": ">=8.6.0",
|
||||||
@ -17,8 +25,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.3.1",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.3.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
|
|||||||
31
packages/ee/server-only/stripe/get-checkout-session.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
|
export type GetCheckoutSessionOptions = {
|
||||||
|
customerId: string;
|
||||||
|
priceId: string;
|
||||||
|
returnUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCheckoutSession = async ({
|
||||||
|
customerId,
|
||||||
|
priceId,
|
||||||
|
returnUrl,
|
||||||
|
}: GetCheckoutSessionOptions) => {
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
customer: customerId,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success_url: `${returnUrl}?success=true`,
|
||||||
|
cancel_url: `${returnUrl}?canceled=true`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return session.url;
|
||||||
|
};
|
||||||
19
packages/ee/server-only/stripe/get-customer.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
|
export const getStripeCustomerByEmail = async (email: string) => {
|
||||||
|
const foundStripeCustomers = await stripe.customers.list({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundStripeCustomers.data[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStripeCustomerById = async (stripeCustomerId: string) => {
|
||||||
|
try {
|
||||||
|
const stripeCustomer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
|
return !stripeCustomer.deleted ? stripeCustomer : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
40
packages/ee/server-only/stripe/get-prices-by-interval.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
|
// Utility type to handle usage of the `expand` option.
|
||||||
|
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
|
||||||
|
|
||||||
|
export type PriceIntervals = Record<Stripe.Price.Recurring.Interval, PriceWithProduct[]>;
|
||||||
|
|
||||||
|
export const getPricesByInterval = async () => {
|
||||||
|
const { data: prices } = await stripe.prices.search({
|
||||||
|
query: `active:'true' type:'recurring'`,
|
||||||
|
expand: ['data.product'],
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervals: PriceIntervals = {
|
||||||
|
day: [],
|
||||||
|
week: [],
|
||||||
|
month: [],
|
||||||
|
year: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add each price to the correct interval.
|
||||||
|
for (const price of prices) {
|
||||||
|
if (price.recurring?.interval) {
|
||||||
|
// We use `expand` to get the product, but it's not typed as part of the Price type.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
intervals[price.recurring.interval].push(price as PriceWithProduct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order all prices by unit_amount.
|
||||||
|
intervals.day.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
||||||
|
intervals.week.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
||||||
|
intervals.month.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
||||||
|
intervals.year.sort((a, b) => Number(a.unit_amount) - Number(b.unit_amount));
|
||||||
|
|
||||||
|
return intervals;
|
||||||
|
};
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import { createTransport } from 'nodemailer';
|
import { createTransport } from 'nodemailer';
|
||||||
|
|
||||||
|
import { ResendTransport } from '@documenso/nodemailer-resend';
|
||||||
|
|
||||||
import { MailChannelsTransport } from './transports/mailchannels';
|
import { MailChannelsTransport } from './transports/mailchannels';
|
||||||
|
|
||||||
const getTransport = () => {
|
const getTransport = () => {
|
||||||
@ -14,6 +16,14 @@ const getTransport = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transport === 'resend') {
|
||||||
|
return createTransport(
|
||||||
|
ResendTransport.makeTransport({
|
||||||
|
apiKey: process.env.NEXT_PRIVATE_RESEND_API_KEY || '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (transport === 'smtp-api') {
|
if (transport === 'smtp-api') {
|
||||||
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
if (!process.env.NEXT_PRIVATE_SMTP_HOST || !process.env.NEXT_PRIVATE_SMTP_APIKEY) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -17,9 +17,11 @@
|
|||||||
"worker:test": "tsup worker/index.ts --format esm"
|
"worker:test": "tsup worker/index.ts --format esm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@documenso/nodemailer-resend": "1.0.0",
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"react-email": "^1.9.4"
|
"react-email": "^1.9.4",
|
||||||
|
"resend": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
|
|||||||
BIN
packages/email/static/user-plus.png
Normal file
|
After Width: | Height: | Size: 367 B |
@ -1,7 +1,9 @@
|
|||||||
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentCompletedProps {
|
export interface TemplateDocumentCompletedProps {
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
documentName: string;
|
documentName: string;
|
||||||
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section>
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
<Section>
|
||||||
<Img
|
<Section className="mb-4">
|
||||||
className="h-42 mx-auto"
|
<Column align="center">
|
||||||
src={getAssetUrl('/static/document.png')}
|
<Text className="text-base font-semibold text-[#7AC455]">
|
||||||
alt="Documenso"
|
<Img
|
||||||
/>
|
src={getAssetUrl('/static/completed.png')}
|
||||||
|
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||||
|
/>
|
||||||
|
Completed
|
||||||
|
</Text>
|
||||||
</Column>
|
</Column>
|
||||||
|
</Section>
|
||||||
<Column />
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
|
||||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
|
||||||
Completed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
“{documentName}” was signed by all signers
|
“{documentName}” was signed by all signers
|
||||||
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
|
|||||||
Review
|
Review
|
||||||
</Button> */}
|
</Button> */}
|
||||||
<Button
|
<Button
|
||||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
href={downloadLink}
|
href={downloadLink}
|
||||||
>
|
>
|
||||||
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/download.png')}
|
||||||
|
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||||
|
/>
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { Column, Img, Row, Section } from '@react-email/components';
|
||||||
|
|
||||||
|
export interface TemplateDocumentImageProps {
|
||||||
|
assetBaseUrl: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => {
|
||||||
|
const getAssetUrl = (path: string) => {
|
||||||
|
return new URL(path, assetBaseUrl).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section className={className}>
|
||||||
|
<Row className="table-fixed">
|
||||||
|
<Column />
|
||||||
|
|
||||||
|
<Column>
|
||||||
|
<Img className="h-42 mx-auto" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column />
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TemplateDocumentImage;
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentInviteProps {
|
export interface TemplateDocumentInviteProps {
|
||||||
inviterName: string;
|
inviterName: string;
|
||||||
inviterEmail: string;
|
inviterEmail: string;
|
||||||
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
|
|||||||
signDocumentLink,
|
signDocumentLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateDocumentInviteProps) => {
|
}: TemplateDocumentInviteProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
|
||||||
<Img
|
|
||||||
className="h-42 mx-auto"
|
|
||||||
src={getAssetUrl('/static/document.png')}
|
|
||||||
alt="Documenso"
|
|
||||||
/>
|
|
||||||
</Column>
|
|
||||||
|
|
||||||
<Column />
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components';
|
import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentPendingProps {
|
export interface TemplateDocumentPendingProps {
|
||||||
documentName: string;
|
documentName: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section>
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<Row className="table-fixed">
|
|
||||||
<Column />
|
|
||||||
|
|
||||||
<Column>
|
<Section>
|
||||||
<Img
|
<Section className="mb-4">
|
||||||
className="h-42 mx-auto"
|
<Column align="center">
|
||||||
src={getAssetUrl('/static/document.png')}
|
<Text className="text-base font-semibold text-blue-500">
|
||||||
alt="Documenso"
|
<Img
|
||||||
/>
|
src={getAssetUrl('/static/clock.png')}
|
||||||
|
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||||
|
/>
|
||||||
|
Waiting for others
|
||||||
|
</Text>
|
||||||
</Column>
|
</Column>
|
||||||
|
</Section>
|
||||||
<Column />
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
|
|
||||||
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
|
||||||
Waiting for others
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
||||||
“{documentName}” has been signed
|
“{documentName}” has been signed
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateDocumentSelfSignedProps {
|
export interface TemplateDocumentSelfSignedProps {
|
||||||
downloadLink: string;
|
|
||||||
documentName: string;
|
documentName: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateDocumentSelfSigned = ({
|
export const TemplateDocumentSelfSigned = ({
|
||||||
downloadLink,
|
|
||||||
documentName,
|
documentName,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateDocumentSelfSignedProps) => {
|
}: TemplateDocumentSelfSignedProps) => {
|
||||||
|
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
|
||||||
|
|
||||||
const getAssetUrl = (path: string) => {
|
const getAssetUrl = (path: string) => {
|
||||||
return new URL(path, assetBaseUrl).toString();
|
return new URL(path, assetBaseUrl).toString();
|
||||||
};
|
};
|
||||||
@ -27,39 +29,59 @@ export const TemplateDocumentSelfSigned = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
|
|
||||||
<Section className="flex-row items-center justify-center">
|
<Section className="flex-row items-center justify-center">
|
||||||
<div className="flex items-center justify-center p-4">
|
<Section>
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
<Column align="center">
|
||||||
</div>
|
<Text className="text-base font-semibold text-[#7AC455]">
|
||||||
|
<Img
|
||||||
|
src={getAssetUrl('/static/completed.png')}
|
||||||
|
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
|
||||||
|
/>
|
||||||
|
Completed
|
||||||
|
</Text>
|
||||||
|
</Column>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
|
<Text className="text-primary mb-0 mt-6 text-center text-lg font-semibold">
|
||||||
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
|
|
||||||
Completed
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="text-primary mb-0 text-center text-lg font-semibold">
|
|
||||||
You have signed “{documentName}”
|
You have signed “{documentName}”
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
|
||||||
Check out our plans to access the full suite of features.
|
Create a{' '}
|
||||||
|
<Link
|
||||||
|
href={signUpUrl}
|
||||||
|
target="_blank"
|
||||||
|
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
free account
|
||||||
|
</Link>{' '}
|
||||||
|
to access your signed documents at any time.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Section className="mb-6 mt-8 text-center">
|
<Section className="mb-6 mt-8 text-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
href={signUpUrl}
|
||||||
|
className="mr-4 rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={getAssetUrl('/static/user-plus.png')}
|
||||||
|
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||||
|
/>
|
||||||
|
Create account
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
||||||
href="https://documenso.com/pricing"
|
href="https://documenso.com/pricing"
|
||||||
>
|
>
|
||||||
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
<Img
|
||||||
|
src={getAssetUrl('/static/review.png')}
|
||||||
|
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
|
||||||
|
/>
|
||||||
View plans
|
View plans
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
|
|
||||||
href={downloadLink}
|
|
||||||
>
|
|
||||||
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</Section>
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Button, Img, Section, Tailwind, Text } from '@react-email/components';
|
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export type TemplateForgotPasswordProps = {
|
export type TemplateForgotPasswordProps = {
|
||||||
resetPasswordLink: string;
|
resetPasswordLink: string;
|
||||||
assetBaseUrl: string;
|
assetBaseUrl: string;
|
||||||
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
|
|||||||
resetPasswordLink,
|
resetPasswordLink,
|
||||||
assetBaseUrl,
|
assetBaseUrl,
|
||||||
}: TemplateForgotPasswordProps) => {
|
}: TemplateForgotPasswordProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { Img, Section, Tailwind, Text } from '@react-email/components';
|
import { Button, Section, Tailwind, Text } from '@react-email/components';
|
||||||
|
|
||||||
import * as config from '@documenso/tailwind-config';
|
import * as config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
|
import { TemplateDocumentImage } from './template-document-image';
|
||||||
|
|
||||||
export interface TemplateResetPasswordProps {
|
export interface TemplateResetPasswordProps {
|
||||||
userName: string;
|
userName: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tailwind
|
<Tailwind
|
||||||
config={{
|
config={{
|
||||||
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Section className="mt-4 flex-row items-center justify-center">
|
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||||
<div className="flex items-center justify-center p-4">
|
|
||||||
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<Section className="flex-row items-center justify-center">
|
||||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||||
Password updated!
|
Password updated!
|
||||||
</Text>
|
</Text>
|
||||||
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
|
|||||||
<Text className="my-1 text-center text-base text-slate-400">
|
<Text className="my-1 text-center text-base text-slate-400">
|
||||||
Your password has been updated.
|
Your password has been updated.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<Section className="mb-6 mt-8 text-center">
|
||||||
|
<Button
|
||||||
|
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||||
|
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
</Section>
|
</Section>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
TemplateDocumentCompleted,
|
TemplateDocumentCompleted,
|
||||||
TemplateDocumentCompletedProps,
|
TemplateDocumentCompletedProps,
|
||||||
} from '../template-components/template-document-completed';
|
} from '../template-components/template-document-completed';
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
TemplateDocumentInvite,
|
TemplateDocumentInvite,
|
||||||
TemplateDocumentInviteProps,
|
TemplateDocumentInviteProps,
|
||||||
} from '../template-components/template-document-invite';
|
} from '../template-components/template-document-invite';
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||||
customBody?: string;
|
customBody?: string;
|
||||||
|
|||||||
@ -15,12 +15,11 @@ import {
|
|||||||
TemplateDocumentSelfSigned,
|
TemplateDocumentSelfSigned,
|
||||||
TemplateDocumentSelfSignedProps,
|
TemplateDocumentSelfSignedProps,
|
||||||
} from '../template-components/template-document-self-signed';
|
} from '../template-components/template-document-self-signed';
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
|
|
||||||
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
|
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
|
||||||
|
|
||||||
export const DocumentSelfSignedEmailTemplate = ({
|
export const DocumentSelfSignedEmailTemplate = ({
|
||||||
downloadLink = 'https://documenso.com',
|
|
||||||
documentName = 'Open Source Pledge.pdf',
|
documentName = 'Open Source Pledge.pdf',
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
assetBaseUrl = 'http://localhost:3002',
|
||||||
}: DocumentSelfSignedTemplateProps) => {
|
}: DocumentSelfSignedTemplateProps) => {
|
||||||
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<TemplateDocumentSelfSigned
|
<TemplateDocumentSelfSigned
|
||||||
downloadLink={downloadLink}
|
|
||||||
documentName={documentName}
|
documentName={documentName}
|
||||||
assetBaseUrl={assetBaseUrl}
|
assetBaseUrl={assetBaseUrl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
import {
|
import {
|
||||||
TemplateForgotPassword,
|
TemplateForgotPassword,
|
||||||
TemplateForgotPasswordProps,
|
TemplateForgotPasswordProps,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
|
|
||||||
import config from '@documenso/tailwind-config';
|
import config from '@documenso/tailwind-config';
|
||||||
|
|
||||||
import TemplateFooter from '../template-components/template-footer';
|
import { TemplateFooter } from '../template-components/template-footer';
|
||||||
import {
|
import {
|
||||||
TemplateResetPassword,
|
TemplateResetPassword,
|
||||||
TemplateResetPasswordProps,
|
TemplateResetPasswordProps,
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "@documenso/tsconfig/react-library.json",
|
"extends": "@documenso/tsconfig/react-library.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"@documenso/tsconfig/process-env.d.ts",
|
||||||
|
]
|
||||||
|
},
|
||||||
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { APP_BASE_URL } from './app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The flag name for global session recording feature flag.
|
* The flag name for global session recording feature flag.
|
||||||
*/
|
*/
|
||||||
@ -23,7 +25,7 @@ export const LOCAL_FEATURE_FLAGS: Record<string, boolean> = {
|
|||||||
*/
|
*/
|
||||||
export function extractPostHogConfig(): { key: string; host: string } | null {
|
export function extractPostHogConfig(): { key: string; host: string } | null {
|
||||||
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
const postHogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||||
const postHogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
const postHogHost = `${APP_BASE_URL}/ingest`;
|
||||||
|
|
||||||
if (!postHogKey || !postHogHost) {
|
if (!postHogKey || !postHogHost) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
const session = await getNextAuthServerSession(req, res, NEXT_AUTH_OPTIONS);
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
if (!session || !session.user?.email) {
|
||||||
return null;
|
return { user: null, session: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@ -24,14 +24,14 @@ export const getServerSession = async ({ req, res }: GetServerSessionOptions) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return { user, session };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerComponentSession = async () => {
|
export const getServerComponentSession = async () => {
|
||||||
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
const session = await getNextAuthServerSession(NEXT_AUTH_OPTIONS);
|
||||||
|
|
||||||
if (!session || !session.user?.email) {
|
if (!session || !session.user?.email) {
|
||||||
return null;
|
return { user: null, session: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
@ -40,15 +40,15 @@ export const getServerComponentSession = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return { user, session };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRequiredServerComponentSession = async () => {
|
export const getRequiredServerComponentSession = async () => {
|
||||||
const session = await getServerComponentSession();
|
const { user, session } = await getServerComponentSession();
|
||||||
|
|
||||||
if (!session) {
|
if (!user || !session) {
|
||||||
throw new Error('No session found');
|
throw new Error('No session found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return session;
|
return { user, session };
|
||||||
};
|
};
|
||||||
|
|||||||
13
packages/lib/server-only/document/delete-draft-document.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type DeleteDraftDocumentOptions = {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDraftDocument = async ({ id, userId }: DeleteDraftDocumentOptions) => {
|
||||||
|
return await prisma.document.delete({ where: { id, userId, status: DocumentStatus.DRAFT } });
|
||||||
|
};
|
||||||
@ -14,7 +14,6 @@ export const viewedDocument = async ({ token }: ViewedDocumentOptions) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
console.warn(`No recipient found for token ${token}`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/// <reference types="./stripe.d.ts" />
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
|
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
|
||||||
|
|||||||