mirror of
https://github.com/documenso/documenso.git
synced 2025-11-23 21:21:37 +10:00
Compare commits
46 Commits
chore/team
...
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 | |||
| 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
|
||||||
|
|||||||
@ -68,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
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
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
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
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
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
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
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
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:
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@ -27,6 +27,7 @@
|
|||||||
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/documenso/documenso">
|
<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" />
|
<img alt="open in devcontainer" src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Enabled&color=blue&logo=visualstudiocode" />
|
||||||
</a>
|
</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>.
|
||||||
@ -130,14 +131,12 @@ git clone https://github.com/documenso/documenso
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
||||||
3. Run `npm run dx` in the root directory
|
3. Run `npm run dx` in the root directory
|
||||||
|
|
||||||
- This will spin up a postgres database and inbucket mailserver in a docker container.
|
- This will spin up a postgres database and inbucket mailserver in a docker container.
|
||||||
|
|
||||||
4. Run `npm run dev` in the root directory
|
4. Run `npm run dev` in the root directory
|
||||||
|
|
||||||
|
|
||||||
5. Want it even faster? Just use
|
5. Want it even faster? Just use
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@ -170,6 +169,7 @@ git clone https://github.com/documenso/documenso
|
|||||||
3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
|
3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
|
||||||
|
|
||||||
4. Set the following environement variables.
|
4. Set the following environement variables.
|
||||||
|
|
||||||
- NEXTAUTH_URL
|
- NEXTAUTH_URL
|
||||||
- NEXTAUTH_SECRET
|
- NEXTAUTH_SECRET
|
||||||
- NEXT_PUBLIC_WEBAPP_URL
|
- NEXT_PUBLIC_WEBAPP_URL
|
||||||
@ -179,7 +179,7 @@ git clone https://github.com/documenso/documenso
|
|||||||
- NEXT_PRIVATE_SMTP_FROM_NAME
|
- NEXT_PRIVATE_SMTP_FROM_NAME
|
||||||
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
|
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
|
||||||
|
|
||||||
5. Create the database schema by running `npm run prisma:migrate-dev -w @documenso/prisma`
|
5. Create the database schema by running `npm run prisma:migrate-dev`
|
||||||
|
|
||||||
6. Run `npm run dev` root directory to start
|
6. Run `npm run dev` root directory to start
|
||||||
|
|
||||||
@ -254,6 +254,22 @@ containers:
|
|||||||
- '::'
|
- '::'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
## Repo Activity
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -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
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;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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';
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -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
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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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>
|
||||||
{[
|
{[
|
||||||
|
|||||||
133
apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx
Normal file
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,16 +1,18 @@
|
|||||||
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();
|
||||||
|
|
||||||
@ -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(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let subscriptionProduct: Stripe.Product | null = null;
|
||||||
|
|
||||||
|
if (subscription?.planId) {
|
||||||
|
const foundSubscriptionProduct = (await stripe.products.list()).data.find(
|
||||||
|
(item) => item.default_price === subscription.planId,
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptionProduct = foundSubscriptionProduct ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have a customer record, create one as well as an empty subscription.
|
const isMissingOrInactiveOrFreePlan = !subscription || subscription.status === 'INACTIVE';
|
||||||
return createCustomer({ user });
|
|
||||||
});
|
|
||||||
|
|
||||||
let billingPortalUrl = '';
|
|
||||||
|
|
||||||
if (subscription.customerId) {
|
|
||||||
billingPortalUrl = await getPortalSession({
|
|
||||||
customerId: subscription.customerId,
|
|
||||||
returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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">
|
{!isMissingOrInactiveOrFreePlan &&
|
||||||
<LocaleDate date={subscription.periodEnd} />
|
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>
|
</span>
|
||||||
.
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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';
|
||||||
@ -125,7 +126,7 @@ export const ShareButton = ({ token, documentId }: ShareButtonProps) => {
|
|||||||
</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,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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
27
package-lock.json
generated
27
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",
|
||||||
@ -9103,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"
|
||||||
},
|
},
|
||||||
@ -9112,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"
|
||||||
},
|
},
|
||||||
@ -9130,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"
|
||||||
}
|
}
|
||||||
@ -16359,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": "*"
|
||||||
}
|
}
|
||||||
@ -19889,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": {
|
||||||
|
|||||||
12
package.json
12
package.json
@ -2,6 +2,7 @@
|
|||||||
"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",
|
||||||
@ -10,9 +11,12 @@
|
|||||||
"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",
|
"d": "npm run dx && npm run dev",
|
||||||
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev -w @documenso/prisma",
|
"dx": "npm i && npm run dx:up && npm run prisma:migrate-dev",
|
||||||
"dx:up": "docker compose -f docker/compose-services.yml up -d",
|
"dx:up": "docker compose -f docker/compose-services.yml up -d",
|
||||||
"dx:down": "docker compose -f docker/compose-services.yml down"
|
"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",
|
||||||
@ -21,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
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
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
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;
|
||||||
|
};
|
||||||
13
packages/lib/server-only/document/delete-draft-document.ts
Normal file
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 } });
|
||||||
|
};
|
||||||
@ -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 ?? '', {
|
||||||
|
|||||||
7
packages/lib/server-only/stripe/stripe.d.ts
vendored
Normal file
7
packages/lib/server-only/stripe/stripe.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
declare module 'stripe' {
|
||||||
|
namespace Stripe {
|
||||||
|
interface Product {
|
||||||
|
features?: Array<{ name: string }>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,9 +7,14 @@ import { SALT_ROUNDS } from '../../constants/auth';
|
|||||||
export type UpdatePasswordOptions = {
|
export type UpdatePasswordOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
password: string;
|
password: string;
|
||||||
|
currentPassword: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updatePassword = async ({ userId, password }: UpdatePasswordOptions) => {
|
export const updatePassword = async ({
|
||||||
|
userId,
|
||||||
|
password,
|
||||||
|
currentPassword,
|
||||||
|
}: UpdatePasswordOptions) => {
|
||||||
// Existence check
|
// Existence check
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@ -17,23 +22,29 @@ export const updatePassword = async ({ userId, password }: UpdatePasswordOptions
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hashedPassword = await hash(password, SALT_ROUNDS);
|
if (!user.password) {
|
||||||
|
throw new Error('User has no password');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentPasswordValid = await compare(currentPassword, user.password);
|
||||||
|
if (!isCurrentPasswordValid) {
|
||||||
|
throw new Error('Current password is incorrect.');
|
||||||
|
}
|
||||||
|
|
||||||
if (user.password) {
|
|
||||||
// Compare the new password with the old password
|
// Compare the new password with the old password
|
||||||
const isSamePassword = await compare(password, user.password);
|
const isSamePassword = await compare(password, user.password);
|
||||||
|
|
||||||
if (isSamePassword) {
|
if (isSamePassword) {
|
||||||
throw new Error('Your new password cannot be the same as your old password.');
|
throw new Error('Your new password cannot be the same as your old password.');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const hashedNewPassword = await hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
password: hashedPassword,
|
password: hashedNewPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
3
packages/lib/universal/stripe/to-human-price.ts
Normal file
3
packages/lib/universal/stripe/to-human-price.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const toHumanPrice = (price: number) => {
|
||||||
|
return Number(price / 100).toFixed(2);
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Made the column `customerId` on table `Subscription` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
DELETE FROM "Subscription"
|
||||||
|
WHERE "customerId" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Subscription" ADD COLUMN "cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ALTER COLUMN "customerId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");
|
||||||
@ -18,6 +18,8 @@
|
|||||||
},
|
},
|
||||||
"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": {
|
||||||
|
|||||||
@ -55,11 +55,12 @@ model Subscription {
|
|||||||
status SubscriptionStatus @default(INACTIVE)
|
status SubscriptionStatus @default(INACTIVE)
|
||||||
planId String?
|
planId String?
|
||||||
priceId String?
|
priceId String?
|
||||||
customerId String?
|
customerId String
|
||||||
periodEnd DateTime?
|
periodEnd DateTime?
|
||||||
userId Int
|
userId Int @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
cancelAtPeriodEnd Boolean @default(false)
|
||||||
|
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,16 @@ export const authRouter = router({
|
|||||||
|
|
||||||
return await createUser({ name, email, password, signature });
|
return await createUser({ name, email, password, signature });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
let message =
|
||||||
|
'We were unable to create your account. Please review the information you provided and try again.';
|
||||||
|
|
||||||
|
if (err instanceof Error && err.message === 'User already exists') {
|
||||||
|
message = 'User with this email already exists. Please use a different email address.';
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message:
|
message,
|
||||||
'We were unable to create your account. Please review the information you provided and try again.',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
||||||
|
import { deleteDraftDocument } from '@documenso/lib/server-only/document/delete-draft-document';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||||
@ -10,6 +11,7 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s
|
|||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
ZCreateDocumentMutationSchema,
|
ZCreateDocumentMutationSchema,
|
||||||
|
ZDeleteDraftDocumentMutationSchema,
|
||||||
ZGetDocumentByIdQuerySchema,
|
ZGetDocumentByIdQuerySchema,
|
||||||
ZGetDocumentByTokenQuerySchema,
|
ZGetDocumentByTokenQuerySchema,
|
||||||
ZSendDocumentMutationSchema,
|
ZSendDocumentMutationSchema,
|
||||||
@ -76,6 +78,25 @@ export const documentRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
deleteDraftDocument: authenticatedProcedure
|
||||||
|
.input(ZDeleteDraftDocumentMutationSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const { id } = input;
|
||||||
|
|
||||||
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
|
return await deleteDraftDocument({ id, userId });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'We were unable to delete this document. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
setRecipientsForDocument: authenticatedProcedure
|
setRecipientsForDocument: authenticatedProcedure
|
||||||
.input(ZSetRecipientsForDocumentMutationSchema)
|
.input(ZSetRecipientsForDocumentMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@ -61,3 +61,9 @@ export const ZSendDocumentMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
|
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
|
||||||
|
|
||||||
|
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
||||||
|
id: z.number().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
|
||||||
|
|||||||
@ -40,11 +40,12 @@ export const profileRouter = router({
|
|||||||
.input(ZUpdatePasswordMutationSchema)
|
.input(ZUpdatePasswordMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { password } = input;
|
const { password, currentPassword } = input;
|
||||||
|
|
||||||
return await updatePassword({
|
return await updatePassword({
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
password,
|
password,
|
||||||
|
currentPassword,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message =
|
let message =
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export const ZUpdateProfileMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ZUpdatePasswordMutationSchema = z.object({
|
export const ZUpdatePasswordMutationSchema = z.object({
|
||||||
|
currentPassword: z.string().min(6),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { shareLinkRouter } from './share-link-router/router';
|
|||||||
import { procedure, router } from './trpc';
|
import { procedure, router } from './trpc';
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
hello: procedure.query(() => 'Hello, world!'),
|
health: procedure.query(() => {
|
||||||
|
return { status: 'ok' };
|
||||||
|
}),
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
profile: profileRouter,
|
profile: profileRouter,
|
||||||
document: documentRouter,
|
document: documentRouter,
|
||||||
|
|||||||
1
packages/tsconfig/process-env.d.ts
vendored
1
packages/tsconfig/process-env.d.ts
vendored
@ -10,6 +10,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;
|
||||||
|
|||||||
@ -16,12 +16,13 @@ const DialogPortal = ({
|
|||||||
children,
|
children,
|
||||||
position = 'start',
|
position = 'start',
|
||||||
...props
|
...props
|
||||||
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' }) => (
|
}: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
|
||||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||||
<div
|
<div
|
||||||
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
|
className={cn('fixed inset-0 z-50 flex justify-center sm:items-center', {
|
||||||
'items-start': position === 'start',
|
'items-start': position === 'start',
|
||||||
'items-end': position === 'end',
|
'items-end': position === 'end',
|
||||||
|
'items-center': position === 'center',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -49,7 +50,9 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|||||||
|
|
||||||
const DialogContent = React.forwardRef<
|
const DialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { position?: 'start' | 'end' }
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
|
position?: 'start' | 'end' | 'center';
|
||||||
|
}
|
||||||
>(({ className, children, position = 'start', ...props }, ref) => (
|
>(({ className, children, position = 'start', ...props }, ref) => (
|
||||||
<DialogPortal position={position}>
|
<DialogPortal position={position}>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
|
|||||||
@ -97,10 +97,7 @@ export const DocumentFlowFormContainerStep = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{title}{' '}
|
Step <span>{`${step} of ${maxStep}`}</span>
|
||||||
<span>
|
|
||||||
({step}/{maxStep})
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="bg-muted relative mt-4 h-[2px] rounded-md">
|
<div className="bg-muted relative mt-4 h-[2px] rounded-md">
|
||||||
|
|||||||
103
render.yaml
Normal file
103
render.yaml
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
services:
|
||||||
|
- type: web
|
||||||
|
name: documenso-app
|
||||||
|
env: node
|
||||||
|
plan: free
|
||||||
|
buildCommand: npm i && npm run build:web
|
||||||
|
startCommand: npx prisma migrate deploy --schema packages/prisma/schema.prisma && npm run start
|
||||||
|
healthCheckPath: /api/trpc/health
|
||||||
|
|
||||||
|
envVars:
|
||||||
|
# Node Version
|
||||||
|
- key: NODE_VERSION
|
||||||
|
value: 18.17.0
|
||||||
|
|
||||||
|
- key: PORT
|
||||||
|
value: 10000
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
- key: NEXTAUTH_URL
|
||||||
|
fromService:
|
||||||
|
name: documenso-app
|
||||||
|
type: web
|
||||||
|
envVarKey: RENDER_EXTERNAL_URL
|
||||||
|
- key: NEXTAUTH_SECRET
|
||||||
|
generateValue: true
|
||||||
|
|
||||||
|
# Database
|
||||||
|
- key: NEXT_PRIVATE_DATABASE_URL
|
||||||
|
fromDatabase:
|
||||||
|
name: documenso-db
|
||||||
|
property: connectionString
|
||||||
|
|
||||||
|
- key: NEXT_PRIVATE_DIRECT_DATABASE_URL
|
||||||
|
fromDatabase:
|
||||||
|
name: documenso-db
|
||||||
|
property: connectionString
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
- key: NEXT_PUBLIC_WEBAPP_URL
|
||||||
|
fromService:
|
||||||
|
name: documenso-app
|
||||||
|
type: web
|
||||||
|
envVarKey: RENDER_EXTERNAL_URL
|
||||||
|
- key: NEXT_PUBLIC_MARKETING_URL
|
||||||
|
value: 'http://localhost:3001'
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
- key: NEXT_PRIVATE_SMTP_TRANSPORT
|
||||||
|
value: 'smtp-auth'
|
||||||
|
- key: NEXT_PRIVATE_SMTP_HOST
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_SMTP_PORT
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_SMTP_USERNAME
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_SMTP_PASSWORD
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_SMTP_FROM_NAME
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_SMTP_FROM_ADDRESS
|
||||||
|
sync: false
|
||||||
|
|
||||||
|
# Stripe
|
||||||
|
- key: NEXT_PRIVATE_STRIPE_API_KEY
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
|
||||||
|
sync: false
|
||||||
|
|
||||||
|
# Features
|
||||||
|
- key: NEXT_PUBLIC_POSTHOG_KEY
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PUBLIC_POSTHOG_HOST
|
||||||
|
value: 'https://eu.posthog.com'
|
||||||
|
- key: NEXT_PUBLIC_FEATURE_BILLING_ENABLED
|
||||||
|
sync: false
|
||||||
|
|
||||||
|
# Redis (Only required for marketing site, but added for completeness)
|
||||||
|
- key: NEXT_PRIVATE_REDIS_URL
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_REDIS_TOKEN
|
||||||
|
sync: false
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
- key: NEXT_PUBLIC_UPLOAD_TRANSPORT
|
||||||
|
value: 'database'
|
||||||
|
- key: NEXT_PRIVATE_UPLOAD_ENDPOINT
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_UPLOAD_REGION
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_UPLOAD_BUCKET
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID
|
||||||
|
sync: false
|
||||||
|
- key: NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY
|
||||||
|
sync: false
|
||||||
|
|
||||||
|
databases:
|
||||||
|
- name: documenso-db
|
||||||
|
plan: free
|
||||||
@ -34,6 +34,7 @@
|
|||||||
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
"NEXT_PUBLIC_FEATURE_BILLING_ENABLED",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
|
||||||
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
|
||||||
|
"NEXT_PUBLIC_STRIPE_FREE_PLAN_ID",
|
||||||
"NEXT_PRIVATE_DATABASE_URL",
|
"NEXT_PRIVATE_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
"NEXT_PRIVATE_DIRECT_DATABASE_URL",
|
||||||
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user