feat: add guards and subscription ui

This commit is contained in:
Mythie
2023-05-06 16:08:21 +10:00
parent 7eaa00b836
commit 6934e573d5
13 changed files with 379 additions and 47 deletions

View File

@ -0,0 +1,72 @@
import { useMemo, 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, hasSubscription, isLoading } = useSubscription();
const [isAnnual, setIsAnnual] = useState(true);
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",
"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-neon-600 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">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Corrupti voluptates delectus
doloremque hic vel!
</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>
);
};

View File

@ -4,9 +4,18 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { updateUser } from "@documenso/features"; import { updateUser } from "@documenso/features";
import { getUser } from "@documenso/lib/api"; import { getUser } from "@documenso/lib/api";
import {
STRIPE_PLANS,
fetchCheckoutSession,
fetchPortalSession,
isSubscriptionsEnabled,
useSubscription,
} from "@documenso/lib/stripe";
import { SubscriptionStatus } from '@prisma/client'
import { Button } from "@documenso/ui"; import { Button } from "@documenso/ui";
import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { BillingPlans } from "./billing-plans";
const subNavigation = [ const subNavigation = [
{ {
@ -35,6 +44,7 @@ function classNames(...classes: any) {
export default function Setttings() { export default function Setttings() {
const session = useSession(); const session = useSession();
const { subscription, hasSubscription } = useSubscription();
const [user, setUser] = useState({ const [user, setUser] = useState({
email: "", email: "",
name: "", name: "",
@ -179,24 +189,63 @@ export default function Setttings() {
</div> </div>
<div <div
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation?.[2]?.name} hidden={
subNavigation.find((e) => e.current)?.name !== subNavigation?.[2]?.name
}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"> className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* Billing section */} {/* Billing section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div> <div>
<h2 className="text-lg font-medium leading-6 text-gray-900">Billing</h2> <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"> <p className="mt-1 text-sm text-gray-500">
Your subscription is currently <strong>{user?.subscription ? 'Active' : 'Inactive'}</strong>. Your subscription is currently{" "}
<strong>{subscription?.status && subscription?.status !== SubscriptionStatus.INACTIVE ? "Active" : "Inactive"}</strong>.
</p> </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>
)}
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
We use Stripe to process payments. Your card details are never stored on our Lorem ipsum dolor sit amet consectetur adipisicing elit. Est consectetur magnam
servers. illo aperiam expedita porro eos eum nam sapiente.
</p> </p>
<div className="mt-8"> <div className="mt-8">
<Button onClick={() => {}}>Manage my subscription</Button> <div className="grid grid-cols-1 lg:grid-cols-2">
<BillingPlans />
</div> </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>

View File

@ -1,6 +1,7 @@
import { ReactElement, ReactNode } from "react"; import { ReactElement, ReactNode } from "react";
import { NextPage } from "next"; import { NextPage } from "next";
import type { AppProps } from "next/app"; 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/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css"; import "../../../node_modules/react-resizable/css/styles.css";
import "../styles/tailwind.css"; import "../styles/tailwind.css";
@ -20,13 +21,15 @@ type AppPropsWithLayout = AppProps & {
export default function App({ export default function App({
Component, Component,
pageProps: { session, ...pageProps }, pageProps: { session, initialSubscription, ...pageProps },
}: AppPropsWithLayout) { }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page: any) => page); const getLayout = Component.getLayout || ((page: any) => page);
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<Toaster position="top-center"></Toaster> <SubscriptionProvider initialSubscription={initialSubscription}>
<Toaster position="top-center" />
{getLayout(<Component {...pageProps} />)} {getLayout(<Component {...pageProps} />)}
</SubscriptionProvider>
</SessionProvider> </SessionProvider>
); );
} }

View File

@ -1,9 +1,6 @@
import { Head, Html, Main, NextScript } from "next/document"; 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 ( return (
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en"> <Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
<Head> <Head>

View File

@ -4,6 +4,7 @@ import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { getUserFromToken } from "@documenso/lib/server"; import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma"; import prisma from "@documenso/prisma";
import formidable from "formidable"; import formidable from "formidable";
import { isSubscribedServer } from "@documenso/lib/stripe";
export const config = { export const config = {
api: { api: {
@ -15,7 +16,17 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const form = formidable(); const form = formidable();
const user = await getUserFromToken(req, res); const user = await getUserFromToken(req, res);
if (!user) return; if (!user) {
return res.status(401).end();
};
const isSubscribed = await isSubscribedServer(req);
if (!isSubscribed) {
throw new Error("User is not subscribed.");
}
form.parse(req, async (err, fields, files) => { form.parse(req, async (err, fields, files) => {
if (err) throw err; if (err) throw err;

View File

@ -20,12 +20,15 @@ import {
} from "@prisma/client"; } from "@prisma/client";
import { truncate } from "fs"; import { truncate } from "fs";
import { Tooltip as ReactTooltip } from "react-tooltip"; import { Tooltip as ReactTooltip } from "react-tooltip";
import { useSubscription } from "@documenso/lib/stripe";
type FormValues = { type FormValues = {
document: File; document: File;
}; };
const DashboardPage: NextPageWithLayout = (props: any) => { const DashboardPage: NextPageWithLayout = (props: any) => {
const { hasSubscription } = useSubscription();
const stats = [ const stats = [
{ {
name: "Draft", name: "Draft",
@ -90,9 +93,12 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
</div> </div>
<div <div
onClick={() => { onClick={() => {
if (hasSubscription) {
document?.getElementById("fileUploadHelper")?.click(); document?.getElementById("fileUploadHelper")?.click();
}
}} }}
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={!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">
<svg <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 group-hover:text-gray-700 duration-200"

View File

@ -20,9 +20,11 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { DocumentStatus } from "@prisma/client"; import { DocumentStatus } from "@prisma/client";
import { Tooltip as ReactTooltip } from "react-tooltip"; import { Tooltip as ReactTooltip } from "react-tooltip";
import { useSubscription } from "@documenso/lib/stripe";
const DocumentsPage: NextPageWithLayout = (props: any) => { const DocumentsPage: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
const { hasSubscription } = useSubscription();
const [documents, setDocuments]: any[] = useState([]); const [documents, setDocuments]: any[] = useState([]);
const [filteredDocuments, setFilteredDocuments] = useState([]); const [filteredDocuments, setFilteredDocuments] = useState([]);
@ -135,6 +137,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<Button <Button
icon={DocumentPlusIcon} icon={DocumentPlusIcon}
disabled={!hasSubscription}
onClick={() => { onClick={() => {
document?.getElementById("fileUploadHelper")?.click(); document?.getElementById("fileUploadHelper")?.click();
}}> }}>

View File

@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib"; import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server"; import { getUserFromToken } from "@documenso/lib/server";
import { useSubscription } from "@documenso/lib/stripe";
import { Breadcrumb, Button } from "@documenso/ui"; import { Breadcrumb, Button } from "@documenso/ui";
import PDFEditor from "../../../components/editor/pdf-editor"; import PDFEditor from "../../../components/editor/pdf-editor";
import Layout from "../../../components/layout"; import Layout from "../../../components/layout";
@ -14,6 +15,7 @@ import { Document as PrismaDocument } from "@prisma/client";
const DocumentsDetailPage: NextPageWithLayout = (props: any) => { const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
const { hasSubscription } = useSubscription();
return ( return (
<div className="mt-4"> <div className="mt-4">

View File

@ -21,6 +21,7 @@ import {
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client"; import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form"; import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useSubscription } from "@documenso/lib/stripe";
export type FormValues = { export type FormValues = {
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>; signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
@ -29,6 +30,7 @@ export type FormValues = {
type FormSigner = FormValues["signers"][number]; type FormSigner = FormValues["signers"][number];
const RecipientsPage: NextPageWithLayout = (props: any) => { const RecipientsPage: NextPageWithLayout = (props: any) => {
const { hasSubscription } = useSubscription();
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso"; const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
const breadcrumbItems = [ const breadcrumbItems = [
{ {
@ -116,6 +118,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
: setOpen(true); : setOpen(true);
}} }}
disabled={ disabled={
!hasSubscription ||
(formValues.length || 0) === 0 || (formValues.length || 0) === 0 ||
!formValues.some( !formValues.some(
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT" (r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"

192
package-lock.json generated
View File

@ -2023,6 +2023,14 @@
"devOptional": true, "devOptional": true,
"peer": true "peer": true
}, },
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -2296,6 +2304,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@ -2485,6 +2501,14 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/detective": { "node_modules/detective": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz",
@ -3954,6 +3978,21 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "dev": true
}, },
"node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/http-signature": { "node_modules/http-signature": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -3972,7 +4011,6 @@
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
}, },
@ -4033,8 +4071,7 @@
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"node_modules/inquirer": { "node_modules/inquirer": {
"version": "6.5.2", "version": "6.5.2",
@ -4995,6 +5032,27 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/micro": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
"integrity": "sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==",
"dependencies": {
"arg": "4.1.0",
"content-type": "1.0.4",
"raw-body": "2.4.1"
},
"bin": {
"micro": "dist/src/bin/micro.js"
},
"engines": {
"node": ">= 16.0.0"
}
},
"node_modules/micro/node_modules/arg": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@ -6203,6 +6261,20 @@
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
}, },
"node_modules/raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.3",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -6713,6 +6785,11 @@
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
}, },
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -6873,6 +6950,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@ -7436,6 +7521,14 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/tough-cookie": { "node_modules/tough-cookie": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -7666,6 +7759,14 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
@ -8039,7 +8140,10 @@
"name": "@documenso/lib", "name": "@documenso/lib",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@documenso/prisma": "*",
"@prisma/client": "^4.8.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"micro": "^10.0.1",
"stripe": "^12.4.0" "stripe": "^12.4.0"
} }
}, },
@ -8348,7 +8452,10 @@
"@documenso/lib": { "@documenso/lib": {
"version": "file:packages/lib", "version": "file:packages/lib",
"requires": { "requires": {
"@documenso/prisma": "*",
"@prisma/client": "^4.8.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"micro": "^10.0.1",
"stripe": "^12.4.0" "stripe": "^12.4.0"
} }
}, },
@ -9616,6 +9723,11 @@
"devOptional": true, "devOptional": true,
"peer": true "peer": true
}, },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"call-bind": { "call-bind": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
@ -9825,6 +9937,11 @@
"xdg-basedir": "^3.0.0" "xdg-basedir": "^3.0.0"
} }
}, },
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": { "cookie": {
"version": "0.5.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@ -9967,6 +10084,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
}, },
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="
},
"detective": { "detective": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz",
@ -11113,6 +11235,18 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "dev": true
}, },
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"http-signature": { "http-signature": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@ -11127,7 +11261,6 @@
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": { "requires": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
} }
@ -11173,8 +11306,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"inquirer": { "inquirer": {
"version": "6.5.2", "version": "6.5.2",
@ -11910,6 +12042,23 @@
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true "dev": true
}, },
"micro": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz",
"integrity": "sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==",
"requires": {
"arg": "4.1.0",
"content-type": "1.0.4",
"raw-body": "2.4.1"
},
"dependencies": {
"arg": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
"integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
}
}
},
"micromatch": { "micromatch": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
@ -12689,6 +12838,17 @@
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
}, },
"raw-body": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz",
"integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.3",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"react": { "react": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -13038,6 +13198,11 @@
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
}, },
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"shebang-command": { "shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -13166,6 +13331,11 @@
"tweetnacl": "~0.14.0" "tweetnacl": "~0.14.0"
} }
}, },
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
},
"stop-iteration-iterator": { "stop-iteration-iterator": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
@ -13582,6 +13752,11 @@
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
}, },
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"tough-cookie": { "tough-cookie": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -13753,6 +13928,11 @@
"crypto-random-string": "^1.0.0" "crypto-random-string": "^1.0.0"
} }
}, },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"update-browserslist-db": { "update-browserslist-db": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",

View File

@ -11,6 +11,7 @@
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml", "docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
"docker:compose-up": "npm run docker:compose -- up -d", "docker:compose-up": "npm run docker:compose -- up -d",
"docker:compose-down": "npm run docker:compose -- down", "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", "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" "d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
}, },

View File

@ -1,7 +1,7 @@
import { ChangeEvent } from "react";
import router from "next/router"; import router from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { ChangeEvent } from "react";
export const uploadDocument = async (event: ChangeEvent) => { export const uploadDocument = async (event: ChangeEvent) => {
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) { if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
@ -16,24 +16,28 @@ export const uploadDocument = async (event: ChangeEvent) => {
body.append("document", document || ""); body.append("document", document || "");
await toast await toast.promise(
.promise(
fetch("/api/documents", { fetch("/api/documents", {
method: "POST", method: "POST",
body, body,
}).then((response: Response) => {
if (!response.ok) {
throw new Error("Could not upload document");
}
response.json().then((createdDocumentIdFromBody) => {
router.push(
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
);
});
}), }),
{ {
loading: "Uploading document...", loading: "Uploading document...",
success: `${fileName} uploaded successfully.`, success: `${fileName} uploaded successfully.`,
error: "Could not upload document :/", error: "Could not upload document :/",
} }
) ).catch((_err) => {
.then((response: Response) => { // Do nothing
response.json().then((createdDocumentIdFromBody) => {
router.push(
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
);
});
}); });
} }
}; };

View File

@ -1,14 +1,15 @@
export const STRIPE_PLANS = [ export const STRIPE_PLANS = [
{ {
name: "Community Plan (Monthly)", name: "Community Plan",
period: "monthly", prices: {
monthly: {
price: 30, price: 30,
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "", priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
}, },
{ yearly: {
name: "Community Plan (Yearly)",
period: "yearly",
price: 300, price: 300,
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "", priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
}, },
},
},
]; ];