diff --git a/.env.example b/.env.example index 7da56a2e3..3577a185b 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,13 @@ 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. -ALLOW_SIGNUP=true +NEXT_PUBLIC_ALLOW_SIGNUP=true +NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 17ceca23e..41c0f1a88 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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"] } diff --git a/apps/web/components/billing-plans.tsx b/apps/web/components/billing-plans.tsx new file mode 100644 index 000000000..419478cfd --- /dev/null +++ b/apps/web/components/billing-plans.tsx @@ -0,0 +1,70 @@ +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 ( +
+ {!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"} +

+ +

+ All you need for easy signing.

Includes everthing we build this year. +

+
+ +
+
+ ))} +
+ ); +}; diff --git a/apps/web/components/billing-warning.tsx b/apps/web/components/billing-warning.tsx new file mode 100644 index 000000000..bf68c44f2 --- /dev/null +++ b/apps/web/components/billing-warning.tsx @@ -0,0 +1,51 @@ +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 && ( +
+
+
+
+ +
+

+ Your subscription is past due.{" "} + + Please update your payment information to avoid any service interruptions. + +

+
+
+
+ )} + + {subscription?.status === SubscriptionStatus.INACTIVE && ( +
+
+
+
+ +
+

+ 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.{" "} + + You can update your payment information here + +

+
+
+
+ )} + + ) +} \ No newline at end of file diff --git a/apps/web/components/layout.tsx b/apps/web/components/layout.tsx index 408472633..06a5bb2de 100644 --- a/apps/web/components/layout.tsx +++ b/apps/web/components/layout.tsx @@ -1,8 +1,13 @@ 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(); @@ -30,11 +35,16 @@ function useRedirectToLoginIfUnauthenticated() { export default function Layout({ children }: any) { useRedirectToLoginIfUnauthenticated(); + const { subscription } = useSubscription(); + return ( <>
- + +
+ +
{children}
diff --git a/apps/web/components/settings.tsx b/apps/web/components/settings.tsx index 32d868804..fb2bf49c4 100644 --- a/apps/web/components/settings.tsx +++ b/apps/web/components/settings.tsx @@ -4,8 +4,11 @@ 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 { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline"; +import { BillingPlans } from "./billing-plans"; +import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline"; +import { SubscriptionStatus } from "@prisma/client"; import { useSession } from "next-auth/react"; const subNavigation = [ @@ -20,20 +23,29 @@ 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) => { @@ -158,6 +170,7 @@ export default function Setttings() { + + + +