diff --git a/.env.example b/.env.example index c66837b90..f9ddba801 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,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 2b4738c00..36b7f475c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,4 +22,4 @@ "latex", "plaintext" ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index c70655905..cf5b78ba1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ +> We are launching TOMORROW on Product Hunt soon! Sign up to support the launch: +>
Product Hunt
+

- Documenso Logo + Documenso Logo

Open Source Signing Infrastructure

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() { + + + +