From 6934e573d56f18eae0b1406a7ef108e33a54c291 Mon Sep 17 00:00:00 2001 From: Mythie Date: Sat, 6 May 2023 16:08:21 +1000 Subject: [PATCH] feat: add guards and subscription ui --- apps/web/components/billing-plans.tsx | 72 +++++++ apps/web/components/settings.tsx | 63 +++++- apps/web/pages/_app.tsx | 9 +- .../pages/{_document.jsx => _document.tsx} | 5 +- apps/web/pages/api/documents/index.ts | 13 +- apps/web/pages/dashboard.tsx | 10 +- apps/web/pages/documents.tsx | 3 + apps/web/pages/documents/[id]/index.tsx | 2 + apps/web/pages/documents/[id]/recipients.tsx | 3 + package-lock.json | 192 +++++++++++++++++- package.json | 1 + packages/features/uploadDocument.ts | 32 +-- packages/lib/stripe/data/plans.ts | 21 +- 13 files changed, 379 insertions(+), 47 deletions(-) create mode 100644 apps/web/components/billing-plans.tsx rename apps/web/pages/{_document.jsx => _document.tsx} (72%) diff --git a/apps/web/components/billing-plans.tsx b/apps/web/components/billing-plans.tsx new file mode 100644 index 000000000..ec3c23ef4 --- /dev/null +++ b/apps/web/components/billing-plans.tsx @@ -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 ( +
+ {!subscription && + STRIPE_PLANS.map((plan) => ( +
+

{plan.name}

+ +
+ + + + + Annual billing{" "} + (Save $60) + + +
+ +

+ ${(isAnnual ? plan.prices.yearly.price : plan.prices.monthly.price).toFixed(2)}{" "} + {isAnnual ? "/yr" : "/mo"} +

+ +

+ Lorem ipsum dolor sit amet consectetur, adipisicing elit. Corrupti voluptates delectus + doloremque hic vel! +

+ +
+ +
+
+ ))} +
+ ); +}; diff --git a/apps/web/components/settings.tsx b/apps/web/components/settings.tsx index 5556d3198..95f0c6292 100644 --- a/apps/web/components/settings.tsx +++ b/apps/web/components/settings.tsx @@ -4,9 +4,18 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { updateUser } from "@documenso/features"; 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 { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { useSession } from "next-auth/react"; +import { BillingPlans } from "./billing-plans"; const subNavigation = [ { @@ -35,6 +44,7 @@ function classNames(...classes: any) { export default function Setttings() { const session = useSession(); + const { subscription, hasSubscription } = useSubscription(); const [user, setUser] = useState({ email: "", name: "", @@ -179,24 +189,63 @@ export default function Setttings() { diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index f8f86b960..cd5541cbb 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -1,6 +1,7 @@ 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"; @@ -20,13 +21,15 @@ type AppPropsWithLayout = AppProps & { export default function App({ Component, - pageProps: { session, ...pageProps }, + pageProps: { session, initialSubscription, ...pageProps }, }: AppPropsWithLayout) { const getLayout = Component.getLayout || ((page: any) => page); return ( - - {getLayout()} + + + {getLayout()} + ); } diff --git a/apps/web/pages/_document.jsx b/apps/web/pages/_document.tsx similarity index 72% rename from apps/web/pages/_document.jsx rename to apps/web/pages/_document.tsx index 0058cd7bb..2c6c7d5d8 100644 --- a/apps/web/pages/_document.jsx +++ b/apps/web/pages/_document.tsx @@ -1,9 +1,6 @@ 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 ( diff --git a/apps/web/pages/api/documents/index.ts b/apps/web/pages/api/documents/index.ts index 2b5d96b32..92958ba5c 100644 --- a/apps/web/pages/api/documents/index.ts +++ b/apps/web/pages/api/documents/index.ts @@ -4,6 +4,7 @@ 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: { @@ -15,7 +16,17 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) { const form = formidable(); 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) => { if (err) throw err; diff --git a/apps/web/pages/dashboard.tsx b/apps/web/pages/dashboard.tsx index 5d3d93d94..30905ab36 100644 --- a/apps/web/pages/dashboard.tsx +++ b/apps/web/pages/dashboard.tsx @@ -20,12 +20,15 @@ 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", @@ -90,9 +93,12 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
{ - document?.getElementById("fileUploadHelper")?.click(); + if (hasSubscription) { + 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"> { const router = useRouter(); + const { hasSubscription } = useSubscription(); const [documents, setDocuments]: any[] = useState([]); const [filteredDocuments, setFilteredDocuments] = useState([]); @@ -135,6 +137,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {