mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 20:42:34 +10:00
Compare commits
2 Commits
feat/DOC-1
...
bugfix-#71
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eebd52dd1 | |||
| f8f4c07d11 |
@ -37,13 +37,6 @@ SMTP_MAIL_PASSWORD=''
|
||||
# Sender for signing requests and completion mails.
|
||||
MAIL_FROM='documenso@localhost.com'
|
||||
|
||||
# STRIPE
|
||||
STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||
|
||||
#FEATURE FLAGS
|
||||
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true
|
||||
ALLOW_SIGNUP=true
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -13,7 +13,7 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.removeUnusedImports": false
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"spellright.language": ["de"],
|
||||
"spellright.documentTypes": ["markdown", "latex", "plaintext"]
|
||||
}
|
||||
|
||||
39
README.md
39
README.md
@ -1,9 +1,6 @@
|
||||
> We are launching on Product Hunt soon! Sign up to support the launch:
|
||||
> <center><a href="https://dub.sh/documenso-launch"><img src="https://img.shields.io/badge/Documenso%20on%20Product%20Hunt-Notify%20Me-orange" alt="Product Hunt"></a></center>
|
||||
|
||||
<p align="center" style="margin-top: 12px">
|
||||
<a href="https://github.com/documenso/documenso.com">
|
||||
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
|
||||
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
|
||||
</a>
|
||||
|
||||
<h3 align="center">Open Source Signing Infrastructure</h3>
|
||||
@ -74,14 +71,14 @@ The current project goal is to <b>[release a production ready version](https://g
|
||||
|
||||
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
# Tech
|
||||
|
||||
Documenso is built using awesome open source tech including:
|
||||
|
||||
- [Typescript](https://www.typescriptlang.org/)
|
||||
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
||||
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
||||
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
|
||||
- [Postgres SQL (Database)](https://www.postgresql.org/)
|
||||
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
|
||||
@ -89,7 +86,7 @@ Documenso is built using awesome open source tech including:
|
||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||
- Check out `/package.json` and `/apps/web/package.json` for more
|
||||
- Check out /packages.json and /apps/web/package.json for more
|
||||
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
||||
|
||||
# Getting Started
|
||||
@ -99,7 +96,7 @@ Documenso is built using awesome open source tech including:
|
||||
To run Documenso locally you need
|
||||
|
||||
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
|
||||
- Node Package Manager NPM - included in Node.js
|
||||
- Node Package Manger NPM - included in Node.js
|
||||
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
|
||||
|
||||
## Developer Quickstart
|
||||
@ -114,7 +111,7 @@ Want to get up and running quickly? Follow these steps:
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
|
||||
- Set up your `.env` file using the recommendations in the `.env.example` file.
|
||||
- Set up your .env file using the recommendations in the .env.example file.
|
||||
- Run `npm run dx` in the root directory
|
||||
- This will spin up a postgres database and inbucket mail server in docker containers.
|
||||
- Run `npm run dev` in the root directory
|
||||
@ -127,11 +124,11 @@ That's it! You should now be able to access the app at http://localhost:3000
|
||||
|
||||
Incoming mail will be available at http://localhost:9000
|
||||
|
||||
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
|
||||
Your database will also be available on port `5432`. You can connect to it using your favorite database client.
|
||||
|
||||
## Developer Setup
|
||||
|
||||
Follow these steps to setup documenso on you local machine:
|
||||
Follow these steps to setup documenso on you local machnine:
|
||||
|
||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
```sh
|
||||
@ -141,36 +138,36 @@ Follow these steps to setup documenso on you local machine:
|
||||
- Rename <code>.env.example</code> to <code>.env</code>
|
||||
- Set DATABASE_URL value in .env file
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommended)
|
||||
- Or setup a local postgres sql instance (recommened)
|
||||
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||
- Setup your mail provider
|
||||
- Set <code>SENDGRID_API_KEY</code> value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</code> in your .env
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env
|
||||
- Run <code>npm run dev</code> root directory to start
|
||||
- Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
|
||||
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
|
||||
- Optional: Upload and sign <code>apps/web/ressources/example.pdf</code> manually to test your setup
|
||||
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
|
||||
|
||||
- Optional: Create your own signing certificate
|
||||
- A demo certificate is provided in `/app/web/ressources/certificate.p12`
|
||||
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
|
||||
- A demo certificate is provided in /app/web/ressources/certificate.p12
|
||||
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
|
||||
|
||||
## Updating
|
||||
|
||||
- If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
|
||||
- You can do this by running the generate command in `/packages/prisma`:
|
||||
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
|
||||
- You can do this by running the generate command in /packages/prisma:
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
- This is not necessary on first clone.
|
||||
- This is not neccessary on first clone
|
||||
|
||||
# Creating your own signing certificate
|
||||
# Creating your own signging certificate
|
||||
|
||||
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||
For the digital signature of you documents you need a signign certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||
|
||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
|
||||
<code>openssl genrsa -out private.key 2048</code>
|
||||
|
||||
4
apps/web/.babelrc
Normal file
4
apps/web/.babelrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": []
|
||||
}
|
||||
@ -1,8 +1,3 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
||||
"extends": ["next/babel", "next/core-web-vitals"]
|
||||
}
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import { STRIPE_PLANS, fetchCheckoutSession, useSubscription } from "@documenso/lib/stripe";
|
||||
import { Button } from "@documenso/ui";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
export const BillingPlans = () => {
|
||||
const { subscription, isLoading } = useSubscription();
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!subscription &&
|
||||
STRIPE_PLANS.map((plan) => (
|
||||
<div key={plan.name} className="rounded-lg border py-4 px-6">
|
||||
<h3 className="text-center text-lg font-medium leading-6 text-gray-900">{plan.name}</h3>
|
||||
|
||||
<div className="my-4 flex justify-center">
|
||||
<Switch.Group as="div" className="flex items-center">
|
||||
<Switch
|
||||
checked={isAnnual}
|
||||
onChange={setIsAnnual}
|
||||
className={classNames(
|
||||
isAnnual ? "bg-neon-600" : "bg-gray-200",
|
||||
"focus:ring-neon-600 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
)}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
isAnnual ? "translate-x-5" : "translate-x-0",
|
||||
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<Switch.Label as="span" className="ml-3 text-sm">
|
||||
<span className="font-medium text-gray-900">Annual billing</span>{" "}
|
||||
<span className="text-gray-500">(Save $60)</span>
|
||||
</Switch.Label>
|
||||
</Switch.Group>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-center text-gray-500">
|
||||
${(isAnnual ? plan.prices.yearly.price : plan.prices.monthly.price).toFixed(2)}{" "}
|
||||
<span className="text-sm text-gray-400">{isAnnual ? "/yr" : "/mo"}</span>
|
||||
</p>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-gray-500">
|
||||
All you need for easy signing. <br></br>Includes everthing we build this year.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() =>
|
||||
fetchCheckoutSession({
|
||||
priceId: isAnnual ? plan.prices.yearly.priceId : plan.prices.monthly.priceId,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
window.location.href = res.url;
|
||||
}
|
||||
})
|
||||
}>
|
||||
Subscribe
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,51 +0,0 @@
|
||||
import { useSubscription } from "@documenso/lib/stripe"
|
||||
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||
import { SubscriptionStatus } from '@prisma/client'
|
||||
import Link from "next/link";
|
||||
|
||||
export const BillingWarning = () => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<>
|
||||
{subscription?.status === SubscriptionStatus.PAST_DUE && (
|
||||
<div className="bg-yellow-50 p-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex max-w-3xl items-start justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
<PaperAirplaneIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
Your subscription is past due.{" "}
|
||||
<Link href="/account/billing" className="text-yellow-700 underline">
|
||||
Please update your payment information to avoid any service interruptions.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subscription?.status === SubscriptionStatus.INACTIVE && (
|
||||
<div className="bg-red-50 p-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto flex max-w-3xl items-center justify-center">
|
||||
<div className="flex-shrink-0">
|
||||
<PaperAirplaneIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-red-700">
|
||||
Your subscription is inactive. You can continue to view and edit your documents,
|
||||
but you will not be able to send them or create new ones.{" "}
|
||||
<Link href="/account/billing" className="text-red-700 underline">
|
||||
You can update your payment information here
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -8,17 +8,10 @@ const stc = require("string-to-color");
|
||||
export default function FieldTypeSelector(props: any) {
|
||||
const fieldTypes = [
|
||||
{
|
||||
id: FieldType.SIGNATURE,
|
||||
name: "Signature",
|
||||
id: FieldType.SIGNATURE,
|
||||
},
|
||||
{
|
||||
id: FieldType.NAME,
|
||||
name: "Name",
|
||||
},
|
||||
{
|
||||
id: FieldType.DATE,
|
||||
name: "Date",
|
||||
},
|
||||
{ name: "Date", id: FieldType.DATE },
|
||||
];
|
||||
|
||||
const [selectedFieldType, setSelectedFieldType] = useState(fieldTypes[0].id);
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { classNames, localStorage } from "@documenso/lib";
|
||||
import { Button } from "@documenso/ui";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
export default function NameDialog(props: any) {
|
||||
const [name, setName] = useState(props.defaultName);
|
||||
|
||||
useEffect(() => {
|
||||
const nameFromStorage = localStorage.getItem("typedName");
|
||||
|
||||
if (nameFromStorage) {
|
||||
setName(nameFromStorage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Transition.Root show={props.open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={() => {
|
||||
props.setOpen(false);
|
||||
}}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<Dialog.Panel className="relative min-h-[350px] transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
|
||||
<div>
|
||||
<h4 className="text-center text-2xl font-medium">
|
||||
Enter your name in the input below!
|
||||
</h4>
|
||||
|
||||
<div className="my-3 border-b border-gray-300">
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom font-sans text-2xl leading-none"
|
||||
)}
|
||||
placeholder="Kindly type your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row-reverse items-center gap-x-4">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
props.setOpen(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3"
|
||||
disabled={!name}
|
||||
onClick={() => {
|
||||
localStorage.setItem("typedName", name);
|
||||
props.onClose({
|
||||
type: "type",
|
||||
typedSignature: name,
|
||||
});
|
||||
}}>
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { createField } from "@documenso/features/editor";
|
||||
@ -6,7 +6,6 @@ import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/a
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../logo";
|
||||
import NameDialog from "./name-dialog";
|
||||
import SignatureDialog from "./signature-dialog";
|
||||
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { FieldType } from "@prisma/client";
|
||||
@ -17,62 +16,21 @@ const PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
||||
|
||||
export default function PDFSigner(props: any) {
|
||||
const router = useRouter();
|
||||
const [signatureDialogOpen, setSignatureDialogOpen] = useState(false);
|
||||
const [nameDialogOpen, setNameDialogOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [signingDone, setSigningDone] = useState(false);
|
||||
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
|
||||
const [fields, setFields] = useState<any[]>(props.fields);
|
||||
const signatureFields = useMemo(
|
||||
() => fields.filter((field) => [FieldType.SIGNATURE].includes(field.type)),
|
||||
[fields]
|
||||
);
|
||||
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
|
||||
const [dialogField, setDialogField] = useState<any>();
|
||||
|
||||
function signField(options: {
|
||||
fieldId: string;
|
||||
type: string;
|
||||
typedSignature?: string;
|
||||
signatureImage?: string;
|
||||
}) {
|
||||
const { fieldId, type, typedSignature, signatureImage } = options;
|
||||
|
||||
const signature = {
|
||||
fieldId,
|
||||
type,
|
||||
typedSignature,
|
||||
signatureImage,
|
||||
};
|
||||
|
||||
const field = fields.find((e) => e.id == fieldId);
|
||||
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalSignatures((s) => [...s.filter((e) => e.fieldId !== fieldId), signature]);
|
||||
|
||||
setFields((prevState) => {
|
||||
const newState = [...prevState];
|
||||
const index = newState.findIndex((e) => e.id == fieldId);
|
||||
|
||||
newState[index] = {
|
||||
...newState[index],
|
||||
signature,
|
||||
};
|
||||
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
setSigningDone(checkIfSigningIsDone());
|
||||
}, [fields]);
|
||||
|
||||
function onClick(item: any) {
|
||||
if (item.type === FieldType.SIGNATURE) {
|
||||
if (item.type === "SIGNATURE") {
|
||||
setDialogField(item);
|
||||
setSignatureDialogOpen(true);
|
||||
}
|
||||
|
||||
if (item.type === FieldType.NAME) {
|
||||
setDialogField(item);
|
||||
setNameDialogOpen(true);
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,107 +43,31 @@ export default function PDFSigner(props: any) {
|
||||
|
||||
if (!dialogResult) return;
|
||||
|
||||
signField({
|
||||
const signature = {
|
||||
fieldId: dialogField.id,
|
||||
type: dialogResult.type,
|
||||
typedSignature: dialogResult.typedSignature,
|
||||
signatureImage: dialogResult.signatureImage,
|
||||
});
|
||||
};
|
||||
|
||||
setSignatureDialogOpen(false);
|
||||
setNameDialogOpen(false);
|
||||
setLocalSignatures(localSignatures.concat(signature));
|
||||
|
||||
fields.splice(
|
||||
fields.findIndex(function (i) {
|
||||
return i.id === signature.fieldId;
|
||||
}),
|
||||
1
|
||||
);
|
||||
const signedField = { ...dialogField };
|
||||
signedField.signature = signature;
|
||||
setFields((prevState) => [...prevState, signedField]);
|
||||
setOpen(false);
|
||||
setDialogField(null);
|
||||
}
|
||||
|
||||
function checkIfSigningIsDone(): boolean {
|
||||
// Check if all fields are signed..
|
||||
if (signatureFields.length > 0) {
|
||||
// If there are no fields to sign at least one signature is enough
|
||||
return fields
|
||||
.filter((field) => field.type === FieldType.SIGNATURE)
|
||||
.every((field) => field.signature);
|
||||
} else {
|
||||
// If we don't have a signature field, we need at least one free signature
|
||||
// to be able to complete signing
|
||||
const freeSignatureFields = fields.filter((field) => field.type === FieldType.FREE_SIGNATURE);
|
||||
|
||||
return freeSignatureFields.length > 0 && freeSignatureFields.every((field) => field.signature);
|
||||
}
|
||||
}
|
||||
|
||||
function addFreeSignature(e: any, page: number, recipient: any): any {
|
||||
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
|
||||
|
||||
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
setDialogField(res);
|
||||
setSignatureDialogOpen(true);
|
||||
});
|
||||
|
||||
return freeSignatureField;
|
||||
}
|
||||
|
||||
function onDeleteHandler(id: any) {
|
||||
const field = fields.find((e) => e.id == id);
|
||||
const fieldIndex = fields.map((item) => item.id).indexOf(id);
|
||||
if (fieldIndex > -1) {
|
||||
const fieldWithoutRemoved = [...fields];
|
||||
const removedField = fieldWithoutRemoved.splice(fieldIndex, 1);
|
||||
setFields(fieldWithoutRemoved);
|
||||
|
||||
const signaturesWithoutRemoved = [...localSignatures];
|
||||
const removedSignature = signaturesWithoutRemoved.splice(
|
||||
signaturesWithoutRemoved.findIndex(function (i) {
|
||||
return i.fieldId === id;
|
||||
}),
|
||||
1
|
||||
);
|
||||
|
||||
setLocalSignatures(signaturesWithoutRemoved);
|
||||
deleteField(field).catch((err) => {
|
||||
setFields(fieldWithoutRemoved.concat(removedField));
|
||||
setLocalSignatures(signaturesWithoutRemoved.concat(removedSignature));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSigningDone(checkIfSigningIsDone());
|
||||
}, [fields]);
|
||||
|
||||
useEffect(() => {
|
||||
const nameFields = fields.filter((field) => field.type === FieldType.NAME);
|
||||
|
||||
if (nameFields.length > 0) {
|
||||
nameFields.forEach((field) => {
|
||||
if (!field.signature && props.recipient?.name) {
|
||||
signField({
|
||||
fieldId: field.id,
|
||||
type: "type",
|
||||
typedSignature: props.recipient.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// We are intentionally not specifying deps here
|
||||
// because we want to run this effect on the initial render
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SignatureDialog
|
||||
open={signatureDialogOpen}
|
||||
setOpen={setSignatureDialogOpen}
|
||||
onClose={onDialogClose}
|
||||
/>
|
||||
|
||||
<NameDialog
|
||||
open={nameDialogOpen}
|
||||
setOpen={setNameDialogOpen}
|
||||
onClose={onDialogClose}
|
||||
defaultName={props.recipient?.name ?? ""}
|
||||
/>
|
||||
|
||||
<SignatureDialog open={open} setOpen={setOpen} onClose={onDialogClose} />
|
||||
<div className="bg-neon p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
@ -198,11 +80,12 @@ export default function PDFSigner(props: any) {
|
||||
: props.document.User.email}{" "}
|
||||
would like you to sign this document.
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<Button
|
||||
disabled={!signingDone}
|
||||
color="secondary"
|
||||
icon={CheckBadgeIcon}
|
||||
className="float-right"
|
||||
onClick={() => {
|
||||
signDocument(props.document, localSignatures, `${router.query.token}`).then(
|
||||
() => {
|
||||
@ -251,4 +134,52 @@ export default function PDFSigner(props: any) {
|
||||
onDelete={onDeleteHandler}></PDFViewer>
|
||||
</>
|
||||
);
|
||||
|
||||
function checkIfSigningIsDone(): boolean {
|
||||
// Check if all fields are signed..
|
||||
if (signatureFields.length > 0) {
|
||||
// If there are no fields to sign at least one signature is enough
|
||||
return fields
|
||||
.filter((field) => field.type === FieldType.SIGNATURE)
|
||||
.every((field) => field.signature);
|
||||
} else {
|
||||
return localSignatures.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
function addFreeSignature(e: any, page: number, recipient: any): any {
|
||||
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
|
||||
|
||||
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
setDialogField(res);
|
||||
setOpen(true);
|
||||
});
|
||||
|
||||
return freeSignatureField;
|
||||
}
|
||||
|
||||
function onDeleteHandler(id: any) {
|
||||
const field = fields.find((e) => e.id == id);
|
||||
const fieldIndex = fields.map((item) => item.id).indexOf(id);
|
||||
if (fieldIndex > -1) {
|
||||
const fieldWithoutRemoved = [...fields];
|
||||
const removedField = fieldWithoutRemoved.splice(fieldIndex, 1);
|
||||
setFields(fieldWithoutRemoved);
|
||||
|
||||
const signaturesWithoutRemoved = [...localSignatures];
|
||||
const removedSignature = signaturesWithoutRemoved.splice(
|
||||
signaturesWithoutRemoved.findIndex(function (i) {
|
||||
return i.fieldId === id;
|
||||
}),
|
||||
1
|
||||
);
|
||||
|
||||
setLocalSignatures(signaturesWithoutRemoved);
|
||||
deleteField(field).catch((err) => {
|
||||
setFields(fieldWithoutRemoved.concat(removedField));
|
||||
setLocalSignatures(signaturesWithoutRemoved.concat(removedSignature));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,11 +45,10 @@ export default function RecipientSelector(props: any) {
|
||||
{props?.recipients.map((recipient: any) => (
|
||||
<Listbox.Option
|
||||
key={recipient?.id}
|
||||
disabled={!recipient?.email}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-neon-dark text-white" : "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
||||
)
|
||||
}
|
||||
value={recipient}>
|
||||
@ -67,7 +66,7 @@ export default function RecipientSelector(props: any) {
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"ml-3 block truncate"
|
||||
)}>
|
||||
{`${recipient?.name} <${recipient?.email || 'unknown'}>`}
|
||||
{`${recipient?.name} <${recipient?.email}>`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -77,7 +76,7 @@ export default function RecipientSelector(props: any) {
|
||||
active ? "text-white" : "text-neon-dark",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}>
|
||||
<CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useState } from "react";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import { IconButton } from "@documenso/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { FieldType } from "@prisma/client";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
@ -47,9 +46,7 @@ export default function SignableField(props: FieldPropsType) {
|
||||
ref={nodeRef}
|
||||
className={classNames(
|
||||
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
|
||||
[FieldType.SIGNATURE, FieldType.NAME].includes(field.type)
|
||||
? "cursor-pointer hover:brightness-50"
|
||||
: "cursor-not-allowed"
|
||||
field.type === "SIGNATURE" ? "cursor-pointer hover:brightness-50" : "cursor-not-allowed"
|
||||
)}
|
||||
style={{
|
||||
background: stc(props.field.Recipient.email),
|
||||
@ -57,15 +54,10 @@ export default function SignableField(props: FieldPropsType) {
|
||||
<div hidden={field?.signature} className="my-4 font-medium">
|
||||
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
|
||||
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
|
||||
{field.type === "NAME" ? "ENTER NAME HERE" : ""}
|
||||
</div>
|
||||
<div
|
||||
hidden={!field?.signature}
|
||||
className={classNames(
|
||||
"m-auto w-auto flex-row-reverse text-center font-medium",
|
||||
field.type === FieldType.SIGNATURE && "font-qwigley text-5xl",
|
||||
field.type === FieldType.NAME && "font-sans text-3xl"
|
||||
)}>
|
||||
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
|
||||
{field?.signature?.type === "type" ? (
|
||||
<div className="my-4">{field?.signature.typedSignature}</div>
|
||||
) : (
|
||||
|
||||
@ -5,7 +5,6 @@ import { Button, IconButton } from "@documenso/ui";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { useDebouncedValue } from "../../hooks/use-debounced-value";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Type", icon: LanguageIcon, current: true },
|
||||
@ -16,9 +15,6 @@ export default function SignatureDialog(props: any) {
|
||||
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
||||
const [typedSignature, setTypedSignature] = useState("");
|
||||
const [signatureEmpty, setSignatureEmpty] = useState(true);
|
||||
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
|
||||
// we also need the debounce to avoid rendering while transitions are occuring.
|
||||
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
|
||||
let signCanvasRef: any | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
@ -89,7 +85,7 @@ export default function SignatureDialog(props: any) {
|
||||
</div>
|
||||
{isCurrentTab("Type") ? (
|
||||
<div>
|
||||
<div className="my-7 mb-3 border-b border-gray-300">
|
||||
<div className="my-8 mb-3 border-b border-gray-300">
|
||||
<input
|
||||
value={typedSignature}
|
||||
onChange={(e) => {
|
||||
@ -102,7 +98,7 @@ export default function SignatureDialog(props: any) {
|
||||
placeholder="Kindly type your name"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse items-center gap-x-3">
|
||||
<div className="float-right">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
@ -130,55 +126,47 @@ export default function SignatureDialog(props: any) {
|
||||
""
|
||||
)}
|
||||
{isCurrentTab("Draw") ? (
|
||||
<div className="" key={props.open ? "closed" : "open"}>
|
||||
{showCanvas && (
|
||||
<SignatureCanvas
|
||||
ref={(ref) => {
|
||||
signCanvasRef = ref;
|
||||
}}
|
||||
canvasProps={{
|
||||
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
|
||||
}}
|
||||
clearOnResize={true}
|
||||
onEnd={() => {
|
||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<IconButton
|
||||
className="block"
|
||||
icon={TrashIcon}
|
||||
<div className="">
|
||||
<SignatureCanvas
|
||||
ref={(ref) => {
|
||||
signCanvasRef = ref;
|
||||
}}
|
||||
canvasProps={{
|
||||
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
|
||||
}}
|
||||
clearOnResize={true}
|
||||
onEnd={() => {
|
||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
className="float-left block"
|
||||
icon={TrashIcon}
|
||||
onClick={() => {
|
||||
signCanvasRef?.clear();
|
||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||
}}></IconButton>
|
||||
<div className="float-right mt-10">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
signCanvasRef?.clear();
|
||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||
props.onClose();
|
||||
props.setOpen(false);
|
||||
setCurrent(tabs[0]);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
props.onClose({
|
||||
type: "draw",
|
||||
signatureImage: signCanvasRef.toDataURL("image/png"),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row-reverse items-center gap-x-3">
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
props.setOpen(false);
|
||||
setCurrent(tabs[0]);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
props.onClose({
|
||||
type: "draw",
|
||||
signatureImage: signCanvasRef.toDataURL("image/png"),
|
||||
});
|
||||
}}
|
||||
disabled={signatureEmpty}>
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
disabled={signatureEmpty}>
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -1,13 +1,8 @@
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import Navigation from "./navigation";
|
||||
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { BillingWarning } from "./billing-warning";
|
||||
|
||||
function useRedirectToLoginIfUnauthenticated() {
|
||||
const { data: session, status } = useSession();
|
||||
@ -35,16 +30,11 @@ function useRedirectToLoginIfUnauthenticated() {
|
||||
export default function Layout({ children }: any) {
|
||||
useRedirectToLoginIfUnauthenticated();
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-full">
|
||||
<Navigation />
|
||||
|
||||
<Navigation></Navigation>
|
||||
<main>
|
||||
<BillingWarning />
|
||||
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -111,7 +111,7 @@ export default function Login(props: any) {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="text-gray-500 hover:text-neon-700 font-medium">
|
||||
<a href="#" className="text-neon hover:text-neon font-medium">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
@ -123,7 +123,7 @@ export default function Login(props: any) {
|
||||
className="group relative flex w-full">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<LockClosedIcon
|
||||
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
|
||||
className="text-neon-dark group-hover:text-neon h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
@ -141,7 +141,7 @@ export default function Login(props: any) {
|
||||
{props.allowSignup ? (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Are you new here?{" "}
|
||||
<Link href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
|
||||
<Link href="/signup" className="text-neon hover:text-neon font-medium">
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
@ -151,7 +151,7 @@ export default function Login(props: any) {
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso will be available soon™
|
||||
Hosted Documenso will be availible soon™
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -4,11 +4,8 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { updateUser } from "@documenso/features";
|
||||
import { getUser } from "@documenso/lib/api";
|
||||
import { fetchPortalSession, isSubscriptionsEnabled, useSubscription } from "@documenso/lib/stripe";
|
||||
import { Button } from "@documenso/ui";
|
||||
import { BillingPlans } from "./billing-plans";
|
||||
import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
const subNavigation = [
|
||||
@ -23,29 +20,20 @@ const subNavigation = [
|
||||
href: "/settings/password",
|
||||
icon: KeyIcon,
|
||||
current: false,
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
|
||||
subNavigation.push({
|
||||
name: "Billing",
|
||||
href: "/settings/billing",
|
||||
icon: CreditCardIcon,
|
||||
current: false,
|
||||
});
|
||||
}
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function Setttings() {
|
||||
const session = useSession();
|
||||
const { subscription, hasSubscription } = useSubscription();
|
||||
const [user, setUser] = useState({
|
||||
email: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then((res: any) => {
|
||||
res.json().then((j: any) => {
|
||||
@ -170,7 +158,6 @@ export default function Setttings() {
|
||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
|
||||
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
|
||||
@ -184,72 +171,9 @@ export default function Setttings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
hidden={!subNavigation.at(2) || subNavigation.find((e) => e.current)?.name !== subNavigation.at(2)?.name}
|
||||
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
|
||||
{/* Billing section */}
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium leading-6 text-gray-900">Billing</h2>
|
||||
|
||||
{!isSubscriptionsEnabled() && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Subscriptions are not enabled on this instance, you have nothing to do here.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isSubscriptionsEnabled() && (
|
||||
<>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Your subscription is currently{" "}
|
||||
<strong>
|
||||
{subscription?.status &&
|
||||
subscription?.status !== SubscriptionStatus.INACTIVE
|
||||
? "Active"
|
||||
: "Inactive"}
|
||||
</strong>
|
||||
.
|
||||
</p>
|
||||
|
||||
{subscription?.status === SubscriptionStatus.PAST_DUE && (
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
Your subscription is past due. Please update your payment details to
|
||||
continue using the service without interruption.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
<BillingPlans />
|
||||
</div>
|
||||
|
||||
{subscription && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (isSubscriptionsEnabled() && subscription?.customerId) {
|
||||
fetchPortalSession({
|
||||
id: subscription.customerId,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
window.location.href = res.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
}}>
|
||||
Manage my subscription
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 max-w-[1100px]" hidden={!!user.email}>
|
||||
<div className="ph-item">
|
||||
<div className="ph-col-12">
|
||||
|
||||
@ -187,7 +187,7 @@ export default function Signup(props: { source: string }) {
|
||||
</div>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
|
||||
<Link href="/login" className="text-neon hover:text-neon font-medium">
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDebouncedValue<T>(value: T, delay: number) {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@ -1,16 +1,12 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: false,
|
||||
env: {
|
||||
IS_PULL_REQUEST: process.env.IS_PULL_REQUEST,
|
||||
RENDER_EXTERNAL_URL: process.env.RENDER_EXTERNAL_URL,
|
||||
},
|
||||
};
|
||||
|
||||
const transpileModules = require("next-transpile-modules")([
|
||||
const withTM = require("next-transpile-modules")([
|
||||
"@documenso/prisma",
|
||||
"@documenso/lib",
|
||||
"@documenso/ui",
|
||||
@ -19,10 +15,8 @@ const transpileModules = require("next-transpile-modules")([
|
||||
"@documenso/signing",
|
||||
"react-signature-canvas",
|
||||
]);
|
||||
|
||||
const plugins = [
|
||||
transpileModules
|
||||
];
|
||||
const plugins = [];
|
||||
plugins.push(withTM);
|
||||
|
||||
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
|
||||
@ -7,27 +7,36 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"db-studio": "prisma db studio",
|
||||
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook"
|
||||
"db-studio": "prisma db studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/pdf": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/filesystem": "^0.0.32",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"avatar-from-initials": "^1.0.3",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"formidable": "^3.2.5",
|
||||
"next": "13.2.4",
|
||||
"next-auth": "^4.22.0",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^1.5.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"nodemailer-sendgrid": "^1.0.3",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"placeholder-loading": "^0.6.0",
|
||||
"react": "18.2.0",
|
||||
@ -37,30 +46,20 @@
|
||||
"react-pdf": "^6.2.2",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-tooltip": "^5.7.2",
|
||||
"sass": "^1.57.1",
|
||||
"short-uuid": "^4.2.2",
|
||||
"string-to-color": "^2.2.2"
|
||||
"string-to-color": "^2.2.2",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/filesystem": "^0.0.32",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/nodemailer-sendgrid": "^1.0.0",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-pdf": "^6.2.0",
|
||||
"@types/react-resizable": "^3.0.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"postcss": "^8.4.19",
|
||||
"sass": "^1.57.1",
|
||||
"stripe-cli": "^0.1.0",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"typescript": "4.8.4"
|
||||
"tailwindcss": "^3.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { NextPage } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
|
||||
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
|
||||
import "../../../node_modules/react-resizable/css/styles.css";
|
||||
import "../styles/tailwind.css";
|
||||
@ -21,15 +20,13 @@ type AppPropsWithLayout = AppProps & {
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, initialSubscription, ...pageProps },
|
||||
pageProps: { session, ...pageProps },
|
||||
}: AppPropsWithLayout) {
|
||||
const getLayout = Component.getLayout || ((page: any) => page);
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<SubscriptionProvider initialSubscription={initialSubscription}>
|
||||
<Toaster position="top-center" />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SubscriptionProvider>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
import Script from "next/script";
|
||||
|
||||
export default function Document(props) {
|
||||
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
|
||||
<Head>
|
||||
@ -63,7 +63,6 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
data: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
@ -87,11 +86,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
where: {
|
||||
documentId: document.id,
|
||||
type: { in: [FieldType.DATE, FieldType.TEXT] },
|
||||
recipientId: { in: signedRecipients.map((r) => r.id) },
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Insert fields other than signatures
|
||||
@ -103,7 +98,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}).format(field.Recipient?.signedAt ?? new Date())
|
||||
}).format(new Date())
|
||||
: field.customText || "",
|
||||
field.positionX,
|
||||
field.positionY,
|
||||
@ -162,11 +157,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
signedField.Signature.typedSignature,
|
||||
signedField.positionX,
|
||||
signedField.positionY,
|
||||
signedField.page,
|
||||
// useHandwritingFont only for typed signatures
|
||||
signedField.type === FieldType.SIGNATURE,
|
||||
// fontSize only for name field
|
||||
signedField.type === FieldType.NAME ? 30 : undefined
|
||||
signedField.page
|
||||
);
|
||||
} else {
|
||||
documentWithInserts = document.document;
|
||||
|
||||
@ -4,7 +4,6 @@ import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import formidable from "formidable";
|
||||
import { isSubscribedServer } from "@documenso/lib/stripe";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@ -16,17 +15,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const form = formidable();
|
||||
|
||||
const user = await getUserFromToken(req, res);
|
||||
if (!user) {
|
||||
return res.status(401).end();
|
||||
};
|
||||
|
||||
const isSubscribed = await isSubscribedServer(req);
|
||||
|
||||
if (!isSubscribed) {
|
||||
throw new Error("User is not subscribed.");
|
||||
}
|
||||
|
||||
|
||||
if (!user) return;
|
||||
form.parse(req, async (err, fields, files) => {
|
||||
if (err) throw err;
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { checkoutSessionHandler as default } from '@documenso/lib/stripe/handlers/checkout-session'
|
||||
@ -1 +0,0 @@
|
||||
export { portalSessionHandler as default } from "@documenso/lib/stripe/handlers/portal-session";
|
||||
@ -1 +0,0 @@
|
||||
export { getSubscriptionHandler as default } from '@documenso/lib/stripe/handlers/get-subscription'
|
||||
@ -1,5 +0,0 @@
|
||||
export const config = {
|
||||
api: { bodyParser: false },
|
||||
};
|
||||
|
||||
export { webhookHandler as default } from "@documenso/lib/stripe/handlers/webhook";
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChangeEvent, ReactElement } from "react";
|
||||
import { ReactElement } from "react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
@ -20,15 +20,12 @@ import {
|
||||
} from "@prisma/client";
|
||||
import { truncate } from "fs";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
type FormValues = {
|
||||
document: File;
|
||||
};
|
||||
|
||||
const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
const stats = [
|
||||
{
|
||||
name: "Draft",
|
||||
@ -65,27 +62,26 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
|
||||
{stats.map((item) => (
|
||||
<Link href={item.link} key={item.name}>
|
||||
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
|
||||
<dt className="truncate text-sm font-medium text-gray-700 ">
|
||||
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow sm:py-5 md:p-6">
|
||||
<dt className="truncate text-sm font-medium text-gray-500 ">
|
||||
<item.icon
|
||||
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
|
||||
className="text-neon mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
|
||||
aria-hidden="true"></item.icon>
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
{getStat(item.name, props)}
|
||||
</dd>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="mt-12">
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: ChangeEvent) => {
|
||||
onChange={(event: any) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
hidden
|
||||
@ -93,15 +89,11 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (hasSubscription) {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
aria-disabled={!hasSubscription}
|
||||
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 aria-disabled:opacity-50 aria-disabled:pointer-events-none">
|
||||
|
||||
className="hover:border-neon relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 00 20 25"
|
||||
@ -112,8 +104,7 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
|
||||
<span id="add_document" className="text-neon mt-2 block text-sm font-medium">
|
||||
Add a new PDF document.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -20,22 +20,14 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
const [documents, setDocuments]: any[] = useState([]);
|
||||
const [filteredDocuments, setFilteredDocuments] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
type statusFilterType = {
|
||||
label: string;
|
||||
value: DocumentStatus | "ALL";
|
||||
};
|
||||
|
||||
const statusFilters: statusFilterType[] = [
|
||||
const statusFilters = [
|
||||
{ label: "All", value: "ALL" },
|
||||
{ label: "Draft", value: "DRAFT" },
|
||||
{ label: "Waiting for others", value: "PENDING" },
|
||||
@ -91,20 +83,6 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
return filteredDocuments;
|
||||
}
|
||||
|
||||
function handleStatusFilterChange(status: statusFilterType) {
|
||||
router.replace(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: { filter: status.value },
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
shallow: true, // Perform a shallow update, without reloading the page
|
||||
}
|
||||
);
|
||||
setSelectedStatusFilter(status);
|
||||
}
|
||||
|
||||
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
|
||||
if (lastXDays < 0) return true;
|
||||
|
||||
@ -137,7 +115,6 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<Button
|
||||
icon={DocumentPlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}>
|
||||
@ -145,26 +122,26 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
|
||||
<div className="pt-5 block w-fit">
|
||||
<div className="mt-3 mb-12">
|
||||
<div className="float-right ml-3 mt-7 block w-fit">
|
||||
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||
</div>
|
||||
<SelectBox
|
||||
className="block w-1/4"
|
||||
className="float-right block w-1/4"
|
||||
label="Created"
|
||||
options={createdFilter}
|
||||
value={selectedCreatedFilter}
|
||||
onChange={setSelectedCreatedFilter}
|
||||
/>
|
||||
<SelectBox
|
||||
className="block w-1/4"
|
||||
className="float-right ml-3 block w-1/4"
|
||||
label="Status"
|
||||
options={statusFilters}
|
||||
value={selectedStatusFilter}
|
||||
onChange={handleStatusFilterChange}
|
||||
onChange={setSelectedStatusFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
||||
<div className="mt-20 max-w-[1100px]" hidden={!loading}>
|
||||
<div className="ph-item">
|
||||
<div className="ph-col-12">
|
||||
<div className="ph-picture"></div>
|
||||
@ -181,7 +158,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
|
||||
<div className="mt-28 flex flex-col" hidden={!documents.length || loading}>
|
||||
<div
|
||||
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
|
||||
hidden={!documents.length || loading}>
|
||||
@ -224,13 +201,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{document.title || "#" + document.id}
|
||||
</td>
|
||||
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{document.Recipient.map((item: any) => (
|
||||
<div key={item.id}>
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
) : (
|
||||
@ -240,8 +217,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
|
||||
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<EnvelopeIcon className="mr-1 inline h-5"></EnvelopeIcon>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
@ -253,9 +230,9 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
|
||||
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
@ -264,7 +241,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
)}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
|
||||
{item.email}
|
||||
</span>
|
||||
|
||||
@ -4,7 +4,6 @@ import { useRouter } from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import { Breadcrumb, Button } from "@documenso/ui";
|
||||
import PDFEditor from "../../../components/editor/pdf-editor";
|
||||
import Layout from "../../../components/layout";
|
||||
@ -15,7 +14,6 @@ import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
|
||||
@ -4,7 +4,7 @@ import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
|
||||
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
|
||||
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import {
|
||||
@ -21,16 +21,12 @@ import {
|
||||
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
export type FormValues = {
|
||||
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
|
||||
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus'>>;
|
||||
};
|
||||
|
||||
type FormSigner = FormValues["signers"][number];
|
||||
|
||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
@ -68,7 +64,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
});
|
||||
const formValues = useWatch({ control, name: "signers" });
|
||||
const cancelButtonRef = useRef(null);
|
||||
const hasEmailError = (formValue: FormSigner): boolean => {
|
||||
const hasEmailError = (formValue: any): boolean => {
|
||||
const index = formValues.findIndex((e) => e.id === formValue.id);
|
||||
return !!errors?.signers?.[index]?.email;
|
||||
};
|
||||
@ -118,7 +114,6 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
: setOpen(true);
|
||||
}}
|
||||
disabled={
|
||||
!hasSubscription ||
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
@ -146,7 +141,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
trigger();
|
||||
}}>
|
||||
<ul role="list" className="divide-y divide-gray-200">
|
||||
{fields.map((item, index) => (
|
||||
{fields.map((item: any, index: number) => (
|
||||
<li
|
||||
key={index}
|
||||
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
|
||||
@ -267,46 +262,38 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<div className="mr-1 flex">
|
||||
<Tooltip label="Resend">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
color="secondary"
|
||||
className="my-auto mr-4 h-9"
|
||||
onClick={() => {
|
||||
if (confirm("Resend this signing request?")) {
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (confirm("Resend this signing request?")) {
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete">
|
||||
<IconButton
|
||||
icon={TrashIcon}
|
||||
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (confirm("Delete this signing request?")) {
|
||||
const removedItem = { ...fields }[index];
|
||||
remove(index);
|
||||
deleteRecipient(item)?.catch((err) => {
|
||||
append(removedItem);
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</Tooltip>
|
||||
}}>
|
||||
Resend
|
||||
</IconButton>
|
||||
<IconButton
|
||||
icon={TrashIcon}
|
||||
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||
onClick={() => {
|
||||
const removedItem = { ...fields }[index];
|
||||
remove(index);
|
||||
deleteRecipient(item)?.catch((err) => {
|
||||
append(removedItem);
|
||||
});
|
||||
}}
|
||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -73,7 +73,7 @@ export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/documents/${recipient.Document.id}/signed?token=${recipientToken}`,
|
||||
destination: `/documents/${recipient.Document.id}/signed`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Login from "../components/login";
|
||||
|
||||
export default function LoginPage(props: any) {
|
||||
@ -14,21 +13,11 @@ export default function LoginPage(props: any) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/login",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const ALLOW_SIGNUP = process.env.NEXT_PUBLIC_ALLOW_SIGNUP === "true";
|
||||
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||
|
||||
return {
|
||||
props: {
|
||||
ALLOW_SIGNUP,
|
||||
ALLOW_SIGNUP: ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { default } from ".";
|
||||
@ -1,6 +1,5 @@
|
||||
import { NextPageContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Signup from "../components/signup";
|
||||
|
||||
export default function SignupPage(props: { source: string }) {
|
||||
@ -15,7 +14,7 @@ export default function SignupPage(props: { source: string }) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SIGNUP !== "true")
|
||||
if (process.env.ALLOW_SIGNUP !== "true")
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
@ -23,16 +22,6 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
};
|
||||
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/signup",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const signupSource: string = context.query["source"];
|
||||
return {
|
||||
props: {
|
||||
|
||||
24
apps/web/process-env.d.ts
vendored
24
apps/web/process-env.d.ts
vendored
@ -1,24 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
DATABASE_URL: string;
|
||||
NEXT_PUBLIC_WEBAPP_URL: string;
|
||||
NEXTAUTH_SECRET: string;
|
||||
NEXTAUTH_URL: string;
|
||||
|
||||
SENDGRID_API_KEY?: string;
|
||||
SMTP_MAIL_HOST?: string;
|
||||
SMTP_MAIL_PORT?: string;
|
||||
SMTP_MAIL_USER?: string;
|
||||
SMTP_MAIL_PASSWORD?: string;
|
||||
|
||||
MAIL_FROM: string;
|
||||
|
||||
STRIPE_API_KEY?: string;
|
||||
STRIPE_WEBHOOK_SECRET?: string;
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
|
||||
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
|
||||
}
|
||||
}
|
||||
@ -16,48 +16,9 @@ module.exports = {
|
||||
qwigley: ["Qwigley", "serif"],
|
||||
},
|
||||
colors: {
|
||||
neon: {
|
||||
DEFAULT: "#37F095",
|
||||
50: "#E2FDF0",
|
||||
100: "#CFFBE5",
|
||||
200: "#A9F9D1",
|
||||
300: "#83F6BD",
|
||||
400: "#5DF3A9",
|
||||
500: "#37F095",
|
||||
600: "#11DE79",
|
||||
700: "#0DAA5D",
|
||||
800: "#097640",
|
||||
900: "#054224",
|
||||
950: "#032816",
|
||||
},
|
||||
"neon-dark": {
|
||||
DEFAULT: "#2CC077",
|
||||
50: "#B5EED2",
|
||||
100: "#A5EAC8",
|
||||
200: "#84E3B4",
|
||||
300: "#62DBA0",
|
||||
400: "#41D48B",
|
||||
500: "#2CC077",
|
||||
600: "#22925B",
|
||||
700: "#17653E",
|
||||
800: "#0D3722",
|
||||
900: "#020906",
|
||||
950: "#000000",
|
||||
},
|
||||
brown: {
|
||||
DEFAULT: "#353434",
|
||||
50: "#918F8F",
|
||||
100: "#878585",
|
||||
200: "#737171",
|
||||
300: "#5E5C5C",
|
||||
400: "#4A4848",
|
||||
500: "#353434",
|
||||
600: "#191818",
|
||||
700: "#000000",
|
||||
800: "#000000",
|
||||
900: "#000000",
|
||||
950: "#000000",
|
||||
},
|
||||
neon: "#37f095",
|
||||
"neon-dark": "#2CC077",
|
||||
brown: "#353434",
|
||||
},
|
||||
borderRadius: {
|
||||
"4xl": "2rem",
|
||||
|
||||
@ -21,6 +21,6 @@
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
, "../../packages/lib/process-env.d.ts" ],
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
name: documenso
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
container_name: database
|
||||
environment:
|
||||
- POSTGRES_USER=documenso
|
||||
- POSTGRES_PASSWORD=password
|
||||
@ -12,7 +10,6 @@ services:
|
||||
|
||||
inbucket:
|
||||
image: inbucket/inbucket
|
||||
container_name: mailserver
|
||||
ports:
|
||||
- 9000:9000
|
||||
- 2500:2500
|
||||
|
||||
@ -33,7 +33,7 @@ services:
|
||||
- SMTP_MAIL_USER=username
|
||||
- SMTP_MAIL_PASSWORD=password
|
||||
- MAIL_FROM=admin@example.com
|
||||
- NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
- ALLOW_SIGNUP=true
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
|
||||
1
documenso
Submodule
1
documenso
Submodule
Submodule documenso added at 0dcab27e65
7862
package-lock.json
generated
7862
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@ -11,7 +11,6 @@
|
||||
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
|
||||
"docker:compose-up": "npm run docker:compose -- up -d",
|
||||
"docker:compose-down": "npm run docker:compose -- down",
|
||||
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
|
||||
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
|
||||
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
|
||||
},
|
||||
@ -27,31 +26,33 @@
|
||||
"@documenso/prisma": "*",
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"avatar-from-initials": "^1.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"next": "13.2.4",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-signature-canvas": "^1.0.6"
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"typescript": "4.8.4"
|
||||
"prettier-plugin-tailwindcss": "^0.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
The Documenso Commercial License (the “Commercial License”)
|
||||
Copyright (c) 2023 Documenso, Inc
|
||||
|
||||
With regard to the Documenso Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, an agreement governing
|
||||
the use of the Software, as mutually agreed by you and Documenso, Inc ("Documenso"),
|
||||
and otherwise have a valid Documenso Enterprise Edition subscription ("Commercial Subscription").
|
||||
Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software.
|
||||
You agree that Documenso and/or its licensors (as applicable) retain all right, title and interest in
|
||||
and to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||
and testing purposes, without requiring a subscription. You agree that Documenso and/or
|
||||
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
This Commercial License applies only to the part of this Software that is not distributed under
|
||||
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
|
||||
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
|
||||
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
|
||||
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
|
||||
be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Documenso Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
@ -1,15 +0,0 @@
|
||||
<div align="center"style="padding: 12px">
|
||||
<a href="https://github.com/documenso/documenso.com">
|
||||
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
|
||||
</a>
|
||||
|
||||
<a href="https://dub.sh/documenso-enterprise">Contact Us</a>
|
||||
</div>
|
||||
|
||||
# Enterprise Edition
|
||||
|
||||
Welcome to the Enterprise Edition ("/ee") of Documenso.com.
|
||||
|
||||
The [/ee](https://github.com/documenso/documenso/tree/main/packages/features/ee) subfolder is the home of all the **Enterprise Edition** features from our [hosted](https://documenso.com/pricing) plan. To use this code in production you need and valid Enterprise License.
|
||||
|
||||
> IMPORTANT: This subfolder is licensed differently than the rest of our [main repo](https://github.com/documenso/documenso). [Contact us](https://dub.sh/documenso-enterprise) to learn more.
|
||||
@ -1,10 +1,9 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import router from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const uploadDocument = async (event: ChangeEvent) => {
|
||||
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
|
||||
export const uploadDocument = async (event: any) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const body = new FormData();
|
||||
const document = event.target.files[0];
|
||||
const fileName: string = event.target.files[0].name;
|
||||
@ -13,31 +12,25 @@ export const uploadDocument = async (event: ChangeEvent) => {
|
||||
toast.error("Non-PDF documents are not supported yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
body.append("document", document || "");
|
||||
|
||||
await toast.promise(
|
||||
fetch("/api/documents", {
|
||||
method: "POST",
|
||||
body,
|
||||
}).then((response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Could not upload document");
|
||||
const response: any = await toast
|
||||
.promise(
|
||||
fetch("/api/documents", {
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
{
|
||||
loading: "Uploading document...",
|
||||
success: `${fileName} uploaded successfully.`,
|
||||
error: "Could not upload document :/",
|
||||
}
|
||||
|
||||
)
|
||||
.then((response: Response) => {
|
||||
response.json().then((createdDocumentIdFromBody) => {
|
||||
router.push(
|
||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
|
||||
);
|
||||
});
|
||||
}),
|
||||
{
|
||||
loading: "Uploading document...",
|
||||
success: `${fileName} uploaded successfully.`,
|
||||
error: "Could not upload document :/",
|
||||
}
|
||||
).catch((_err) => {
|
||||
// Do nothing
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1 @@
|
||||
export const NEXT_PUBLIC_WEBAPP_URL =
|
||||
process.env.IS_PULL_REQUEST === "true"
|
||||
? process.env.RENDER_EXTERNAL_URL
|
||||
: process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
export const NEXT_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
|
||||
@ -11,8 +11,8 @@ export const signingRequestTemplate = (
|
||||
user: any
|
||||
) => {
|
||||
const customContent = `
|
||||
<p style="margin: 30px 0px; text-align: center">
|
||||
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
||||
<p style="margin: 30px;">
|
||||
<a href="${ctaLink}" style="background-color: #37f095; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
||||
${ctaLabel}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@ -4,10 +4,6 @@
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"micro": "^10.0.1",
|
||||
"stripe": "^12.4.0"
|
||||
"bcryptjs": "^2.4.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
packages/lib/process-env.d.ts
vendored
24
packages/lib/process-env.d.ts
vendored
@ -1,24 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
DATABASE_URL: string;
|
||||
NEXT_PUBLIC_WEBAPP_URL: string;
|
||||
NEXTAUTH_SECRET: string;
|
||||
NEXTAUTH_URL: string;
|
||||
|
||||
SENDGRID_API_KEY?: string;
|
||||
SMTP_MAIL_HOST?: string;
|
||||
SMTP_MAIL_PORT?: string;
|
||||
SMTP_MAIL_USER?: string;
|
||||
SMTP_MAIL_PASSWORD?: string;
|
||||
|
||||
MAIL_FROM: string;
|
||||
|
||||
STRIPE_API_KEY?: string;
|
||||
STRIPE_WEBHOOK_SECRET?: string;
|
||||
STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
|
||||
STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
|
||||
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
|
||||
apiVersion: "2022-11-15",
|
||||
typescript: true,
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
export const STRIPE_PLANS = [
|
||||
{
|
||||
name: "Community Plan",
|
||||
prices: {
|
||||
monthly: {
|
||||
price: 30,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
|
||||
},
|
||||
yearly: {
|
||||
price: 300,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,23 +0,0 @@
|
||||
import { CheckoutSessionRequest, CheckoutSessionResponse } from "../handlers/checkout-session"
|
||||
|
||||
export type FetchCheckoutSessionOptions = CheckoutSessionRequest['body']
|
||||
|
||||
export const fetchCheckoutSession = async ({
|
||||
id,
|
||||
priceId
|
||||
}: FetchCheckoutSessionOptions) => {
|
||||
const response = await fetch('/api/stripe/checkout-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
priceId
|
||||
})
|
||||
});
|
||||
|
||||
const json: CheckoutSessionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { GetSubscriptionResponse } from "../handlers/get-subscription";
|
||||
|
||||
export const fetchSubscription = async () => {
|
||||
const response = await fetch("/api/stripe/subscription", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json: GetSubscriptionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { PortalSessionRequest, PortalSessionResponse } from "../handlers/portal-session";
|
||||
|
||||
export type FetchPortalSessionOptions = PortalSessionRequest["body"];
|
||||
|
||||
export const fetchPortalSession = async ({ id }: FetchPortalSessionOptions) => {
|
||||
const response = await fetch("/api/stripe/portal-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
}),
|
||||
});
|
||||
|
||||
const json: PortalSessionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext, NextApiRequest } from "next";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export const isSubscriptionsEnabled = () => {
|
||||
return process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true";
|
||||
};
|
||||
|
||||
export const isSubscribedServer = async (
|
||||
req: NextApiRequest | GetServerSidePropsContext["req"]
|
||||
) => {
|
||||
const { default: prisma } = await import("@documenso/prisma");
|
||||
|
||||
if (!isSubscriptionsEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: {
|
||||
User: {
|
||||
email: token.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return subscription !== null && subscription.status !== SubscriptionStatus.INACTIVE;
|
||||
};
|
||||
@ -1,92 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { stripe } from "../client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export type CheckoutSessionRequest = {
|
||||
body: {
|
||||
id?: string;
|
||||
priceId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CheckoutSessionResponse =
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: token.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No user found",
|
||||
});
|
||||
}
|
||||
|
||||
const { id, priceId } = req.body;
|
||||
|
||||
if (typeof priceId !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No id or priceId found in request",
|
||||
});
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: id,
|
||||
customer_email: user.email,
|
||||
client_reference_id: String(user.id),
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?canceled=true`,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
url: session.url,
|
||||
});
|
||||
};
|
||||
@ -1,63 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Subscription } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export type GetSubscriptionRequest = never;
|
||||
|
||||
export type GetSubscriptionResponse =
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
subscription: Subscription;
|
||||
};
|
||||
|
||||
export const getSubscriptionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: {
|
||||
User: {
|
||||
email: token.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No subscription found",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
subscription,
|
||||
});
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stripe } from "../client";
|
||||
|
||||
|
||||
export type PortalSessionRequest = {
|
||||
body: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PortalSessionResponse =
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const portalSessionHandler = async (req: NextApiRequest, res: NextApiResponse<PortalSessionResponse>) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.body;
|
||||
|
||||
if (typeof id !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No id found in request",
|
||||
});
|
||||
}
|
||||
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: id,
|
||||
return_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
url: session.url,
|
||||
});
|
||||
};
|
||||
@ -1,164 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { stripe } from "../client";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { buffer } from "micro";
|
||||
import Stripe from "stripe";
|
||||
|
||||
const log = (...args: any[]) => console.log("[stripe]", ...args);
|
||||
|
||||
export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
const sig =
|
||||
typeof req.headers["stripe-signature"] === "string" ? req.headers["stripe-signature"] : "";
|
||||
|
||||
if (!sig) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No signature found in request",
|
||||
});
|
||||
}
|
||||
|
||||
log("constructing body...")
|
||||
const body = await buffer(req);
|
||||
log("constructed body")
|
||||
|
||||
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
log("event-type:", event.type);
|
||||
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
|
||||
|
||||
const customerId =
|
||||
typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id;
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
create: {
|
||||
customerId,
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
userId: Number(session.client_reference_id as string),
|
||||
},
|
||||
update: {
|
||||
customerId,
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === "invoice.payment_succeeded") {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const customerId =
|
||||
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
|
||||
|
||||
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
data: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planId: subscription.id,
|
||||
priceId: subscription.items.data[0].price.id,
|
||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === "invoice.payment_failed") {
|
||||
const failedInvoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const customerId = failedInvoice.customer as string;
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
data: {
|
||||
status: SubscriptionStatus.PAST_DUE,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === "customer.subscription.updated") {
|
||||
const updatedSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
const customerId = updatedSubscription.customer as string;
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
data: {
|
||||
status: SubscriptionStatus.ACTIVE,
|
||||
planId: updatedSubscription.id,
|
||||
priceId: updatedSubscription.items.data[0].price.id,
|
||||
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === "customer.subscription.deleted") {
|
||||
const deletedSubscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
const customerId = deletedSubscription.customer as string;
|
||||
|
||||
await prisma.subscription.update({
|
||||
where: {
|
||||
customerId,
|
||||
},
|
||||
data: {
|
||||
status: SubscriptionStatus.INACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: "Webhook received",
|
||||
});
|
||||
}
|
||||
|
||||
log("Unhandled webhook event", event.type);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Unhandled webhook event",
|
||||
});
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
export * from './data/plans'
|
||||
export * from './fetchers/checkout-session'
|
||||
export * from './fetchers/get-subscription'
|
||||
export * from './fetchers/portal-session'
|
||||
export * from './guards/subscriptions'
|
||||
export * from './providers/subscription-provider'
|
||||
@ -1,89 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { fetchSubscription } from "../fetchers/get-subscription";
|
||||
import { Subscription, SubscriptionStatus } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
|
||||
export type SubscriptionContextValue = {
|
||||
subscription: Subscription | null;
|
||||
hasSubscription: boolean;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const SubscriptionContext = createContext<SubscriptionContextValue>({
|
||||
subscription: null,
|
||||
hasSubscription: false,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export const useSubscription = () => {
|
||||
const context = useContext(SubscriptionContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useSubscription must be used within a SubscriptionProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export interface SubscriptionProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialSubscription?: Subscription;
|
||||
}
|
||||
|
||||
export const SubscriptionProvider = ({
|
||||
children,
|
||||
initialSubscription,
|
||||
}: SubscriptionProviderProps) => {
|
||||
const session = useSession();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(
|
||||
initialSubscription || null
|
||||
);
|
||||
|
||||
const hasSubscription = useMemo(() => {
|
||||
console.log({
|
||||
"process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS": process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS,
|
||||
enabled: process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true",
|
||||
"subscription.status": subscription?.status,
|
||||
"subscription.periodEnd": subscription?.periodEnd,
|
||||
});
|
||||
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
|
||||
return (
|
||||
subscription?.status === SubscriptionStatus.ACTIVE &&
|
||||
!!subscription?.periodEnd &&
|
||||
new Date(subscription.periodEnd) > new Date()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [subscription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true" && session.data) {
|
||||
setIsLoading(true);
|
||||
fetchSubscription().then((res) => {
|
||||
if (res.success) {
|
||||
setSubscription(res.subscription);
|
||||
} else {
|
||||
setSubscription(null);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [session.data]);
|
||||
|
||||
return (
|
||||
<SubscriptionContext.Provider
|
||||
value={{
|
||||
subscription,
|
||||
hasSubscription,
|
||||
isLoading,
|
||||
}}>
|
||||
{children}
|
||||
</SubscriptionContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -8,8 +8,7 @@ export async function insertTextInPDF(
|
||||
positionX: number,
|
||||
positionY: number,
|
||||
page: number = 0,
|
||||
useHandwritingFont = true,
|
||||
fontSize = 15
|
||||
useHandwritingFont = true
|
||||
): Promise<string> {
|
||||
const fontBytes = fs.readFileSync("public/fonts/Qwigley-Regular.ttf");
|
||||
|
||||
@ -22,7 +21,7 @@ export async function insertTextInPDF(
|
||||
const pages = pdfDoc.getPages();
|
||||
const pdfPage = pages[page];
|
||||
|
||||
const textSize = useHandwritingFont ? 50 : fontSize;
|
||||
const textSize = useHandwritingFont ? 50 : 15;
|
||||
const textWidth = font.widthOfTextAtSize(text, textSize);
|
||||
const textHeight = font.heightAtSize(textSize);
|
||||
const fieldSize = { width: 192, height: 64 };
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "FieldType" ADD VALUE 'NAME';
|
||||
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Recipient" ADD COLUMN "signedAt" TIMESTAMP(3);
|
||||
@ -1,26 +0,0 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriptionStatus" AS ENUM ('ACTIVE', 'INACTIVE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscription" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"status" "SubscriptionStatus" NOT NULL DEFAULT 'INACTIVE',
|
||||
"planId" TEXT,
|
||||
"priceId" TEXT,
|
||||
"customerId" TEXT,
|
||||
"periodEnd" TIMESTAMP(3),
|
||||
"userId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscription_userId_idx" ON "Subscription"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_planId_customerId_key" ON "Subscription"("planId", "customerId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[customerId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "Subscription_planId_customerId_key";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscription_customerId_key" ON "Subscription"("customerId");
|
||||
@ -1,2 +0,0 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "SubscriptionStatus" ADD VALUE 'PAST_DUE';
|
||||
@ -3,20 +3,20 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"db-studio": "prisma studio",
|
||||
"db-seed": "prisma db seed"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.8.1",
|
||||
"prisma": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.8.1",
|
||||
"prisma": "^4.8.1"
|
||||
},
|
||||
"scripts": {
|
||||
"db-studio": "prisma studio",
|
||||
"db-seed": "prisma db seed"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node --transpile-only ./seed.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,30 +23,6 @@ model User {
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
Document Document[]
|
||||
Subscription Subscription[]
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id Int @id @default(autoincrement())
|
||||
status SubscriptionStatus @default(INACTIVE)
|
||||
planId String?
|
||||
priceId String?
|
||||
customerId String?
|
||||
periodEnd DateTime?
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([customerId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Account {
|
||||
@ -116,7 +92,6 @@ model Recipient {
|
||||
name String @default("") @db.VarChar(255)
|
||||
token String
|
||||
expired DateTime?
|
||||
signedAt DateTime?
|
||||
readStatus ReadStatus @default(NOT_OPENED)
|
||||
signingStatus SigningStatus @default(NOT_SIGNED)
|
||||
sendStatus SendStatus @default(NOT_SENT)
|
||||
@ -126,7 +101,6 @@ model Recipient {
|
||||
}
|
||||
|
||||
enum FieldType {
|
||||
NAME
|
||||
SIGNATURE
|
||||
FREE_SIGNATURE
|
||||
DATE
|
||||
|
||||
@ -6,8 +6,8 @@ export function Button(props: any) {
|
||||
const isLink = typeof props.href !== "undefined" && !props.disabled;
|
||||
const { color = "primary", icon, disabled, onClick } = props;
|
||||
const baseStyles =
|
||||
"inline-flex gap-x-2 items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300 duration-200";
|
||||
const primaryStyles = "text-gray-900 bg-neon hover:bg-neon-dark";
|
||||
"inline-flex items-center justify-center min-w-[80px] rounded-md border border-transparent px-4 py-2 text-sm font-medium shadow-sm disabled:bg-gray-300";
|
||||
const primaryStyles = "text-white bg-neon hover:bg-neon-dark";
|
||||
const secondaryStyles = "border-gray-300 bg-white text-gray-700 hover:bg-gray-50";
|
||||
|
||||
return isLink ? (
|
||||
@ -21,7 +21,7 @@ export function Button(props: any) {
|
||||
)}
|
||||
hidden={props.hidden}>
|
||||
{props.icon ? (
|
||||
<props.icon className="inline-block h-5 text-inherit" aria-hidden="true"></props.icon>
|
||||
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
@ -40,7 +40,7 @@ export function Button(props: any) {
|
||||
disabled={props.disabled || props.loading}
|
||||
hidden={props.hidden}>
|
||||
{props.icon ? (
|
||||
<props.icon className="inline h-5 text-inherit" aria-hidden="true"></props.icon>
|
||||
<props.icon className="mr-1 inline h-5 text-inherit" aria-hidden="true"></props.icon>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { classNames } from "@documenso/lib";
|
||||
|
||||
export function Tooltip(props: any) {
|
||||
let timeout: NodeJS.Timeout;
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
const showTip = () => {
|
||||
timeout = setTimeout(() => {
|
||||
setActive(true);
|
||||
}, props.delay || 40);
|
||||
};
|
||||
|
||||
const hideTip = () => {
|
||||
clearInterval(timeout);
|
||||
setActive(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative" onPointerEnter={showTip} onPointerLeave={hideTip}>
|
||||
{props.children}
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute left-1/4 -translate-x-1/2 transform px-4 transition-all delay-50 duration-120",
|
||||
active && "bottom-9 opacity-100",
|
||||
!active && "pointer-events-none bottom-6 opacity-0"
|
||||
)}>
|
||||
<span className="text-neon-800 bg-neon-200 inline-block rounded py-1 px-2 text-xs">
|
||||
{props.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export { Tooltip } from "./Tooltip";
|
||||
@ -2,4 +2,3 @@ export { Button, IconButton } from "./components/button/index";
|
||||
export { SelectBox } from "./components/selectBox/index";
|
||||
export { Breadcrumb } from "./components/breadcrumb/index";
|
||||
export { Dialog } from "./components/dialog/index";
|
||||
export { Tooltip } from "./components/tooltip/index";
|
||||
|
||||
Reference in New Issue
Block a user