From 200c9c2612d7d4bc45891bf7dff0b6f34d649886 Mon Sep 17 00:00:00 2001 From: MarconLP <13001502+MarconLP@users.noreply.github.com> Date: Thu, 20 Apr 2023 19:25:09 +0200 Subject: [PATCH] add paywall --- src/atoms/paywallAtom.ts | 3 + src/components/Paywall.tsx | 167 +++++++++++++++++++++++++++++++ src/env.mjs | 6 +- src/pages/videos.tsx | 6 +- src/server/api/routers/stripe.ts | 79 ++++++++------- 5 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 src/atoms/paywallAtom.ts create mode 100644 src/components/Paywall.tsx diff --git a/src/atoms/paywallAtom.ts b/src/atoms/paywallAtom.ts new file mode 100644 index 0000000..82f2397 --- /dev/null +++ b/src/atoms/paywallAtom.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export default atom(false); diff --git a/src/components/Paywall.tsx b/src/components/Paywall.tsx new file mode 100644 index 0000000..7b7c258 --- /dev/null +++ b/src/components/Paywall.tsx @@ -0,0 +1,167 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; +import { useAtom } from "jotai"; +import paywallAtom from "~/atoms/paywallAtom"; +import { CloseIcon } from "next/dist/client/components/react-dev-overlay/internal/icons/CloseIcon"; +import { api } from "~/utils/api"; +import { useRouter } from "next/router"; +import { CheckIcon } from "@heroicons/react/20/solid"; + +export default function Paywall() { + const { mutateAsync: createCheckoutSession } = + api.stripe.createCheckoutSession.useMutation(); + const router = useRouter(); + const [open, setOpen] = useAtom(paywallAtom); + const [billedAnnually, setBilledAnnually] = useState(false); + + function closeModal() { + setOpen(false); + } + + function openModal() { + setOpen(true); + } + + const handleCheckout = async () => { + const { checkoutUrl } = await createCheckoutSession({ billedAnnually }); + if (checkoutUrl) { + void router.push(checkoutUrl); + } + }; + + return ( + + + +
+ + +
+
+ + + +
+
+
+
+

+ Upgrade your plan +

+

+ Turn your testimonials into your most powerful + marketing tool with Senja. +

+
+

+ No contract. Cancel any time +

+
+
+
+
+ Monthly + + + Annually + + + 20% Off + +
+
+
+
+
+ Pro +
+
+ {billedAnnually ? "$4" : "$5"} + + / mo. + +
+
+ {billedAnnually + ? "billed annually" + : "billed monthly"} +
+
+ +
+
+ {[ + "Unlimited videos", + "Video uploads", + "Custom branding", + ].map((x) => ( +
+
+ +
+
+ {x} +
+
+ ))} +
+
+
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/env.mjs b/src/env.mjs index 3b02feb..b0efa4b 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -30,7 +30,8 @@ const server = z.object({ AWS_BUCKET_NAME: z.string(), STRIPE_SECRET_KEY: z.string(), STRIPE_WEBHOOK_SECRET: z.string(), - STRIPE_PRICE_ID: z.string() + STRIPE_MONTHLY_PRICE_ID: z.string(), + STRIPE_ANNUAL_PRICE_ID: z.string() }); /** @@ -66,7 +67,8 @@ const processEnv = { NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, - STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID + STRIPE_MONTHLY_PRICE_ID: process.env.STRIPE_MONTHLY_PRICE_ID, + STRIPE_ANNUAL_PRICE_ID: process.env.STRIPE_ANNUAL_PRICE_ID }; // Don't touch the part below diff --git a/src/pages/videos.tsx b/src/pages/videos.tsx index 54420cd..d7a6d6b 100644 --- a/src/pages/videos.tsx +++ b/src/pages/videos.tsx @@ -14,10 +14,13 @@ import VideoUploadModal from "~/components/VideoUploadModal"; import { useAtom } from "jotai"; import uploadVideoModalOpen from "~/atoms/uploadVideoModalOpen"; import recordVideoModalOpen from "~/atoms/recordVideoModalOpen"; +import Paywall from "~/components/Paywall"; +import paywallAtom from "~/atoms/paywallAtom"; const VideoList: NextPage = () => { const [, setRecordOpen] = useAtom(uploadVideoModalOpen); const [, setUploadOpen] = useAtom(recordVideoModalOpen); + const [, setPaywallOpen] = useAtom(paywallAtom); const router = useRouter(); const { status } = useSession(); const { data: videos, isLoading } = api.video.getAll.useQuery(); @@ -39,6 +42,7 @@ const VideoList: NextPage = () => {
+ {status === "authenticated" && ( @@ -67,7 +71,7 @@ const VideoList: NextPage = () => { Record a video