From 304875ee2a084ed928423645c4677c0ca72ce9f9 Mon Sep 17 00:00:00 2001 From: andthezhang Date: Sun, 17 Mar 2024 15:35:11 -0700 Subject: [PATCH] Stripe --- .env | 8 +- bun.lockb | Bin 257316 -> 258411 bytes kirimase.config.json | 13 +- package.json | 5 +- src/app/(app)/account/PlanSettings.tsx | 73 ++++++++++++ .../account/billing/ManageSubscription.tsx | 67 +++++++++++ .../(app)/account/billing/SuccessToast.tsx | 18 +++ src/app/(app)/account/billing/page.tsx | 112 ++++++++++++++++++ src/app/(app)/account/page.tsx | 4 + .../api/billing/manage-subscription/route.ts | 51 ++++++++ src/app/api/webhooks/stripe/route.ts | 98 +++++++++++++++ src/config/subscriptions.ts | 35 ++++++ src/lib/db/schema/subscriptions.ts | 25 ++++ src/lib/env.mjs | 12 +- src/lib/stripe/index.ts | 6 + src/lib/stripe/subscription.ts | 61 ++++++++++ src/lib/utils.ts | 6 + 17 files changed, 584 insertions(+), 10 deletions(-) create mode 100644 src/app/(app)/account/PlanSettings.tsx create mode 100644 src/app/(app)/account/billing/ManageSubscription.tsx create mode 100644 src/app/(app)/account/billing/SuccessToast.tsx create mode 100644 src/app/(app)/account/billing/page.tsx create mode 100644 src/app/api/billing/manage-subscription/route.ts create mode 100644 src/app/api/webhooks/stripe/route.ts create mode 100644 src/config/subscriptions.ts create mode 100644 src/lib/db/schema/subscriptions.ts create mode 100644 src/lib/stripe/index.ts create mode 100644 src/lib/stripe/subscription.ts diff --git a/.env b/.env index 498ab17..f734baa 100644 --- a/.env +++ b/.env @@ -1 +1,7 @@ -DATABASE_URL=postgres://postgres:postgres@localhost:5432/{DB_NAME} \ No newline at end of file +DATABASE_URL=postgres://postgres:postgres@localhost:5432/{DB_NAME} +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= +NEXT_PUBLIC_STRIPE_PRO_PRICE_ID= +NEXT_PUBLIC_STRIPE_MAX_PRICE_ID= +NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID= \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index bc0fcb0b6dd39f543f3cc992968d5eea27d48ed3..f8c077a011e94c6a0523e009d337dc58ef43dd19 100755 GIT binary patch delta 48413 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�)(m=7zO$ldhnR~5~i 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 It9M!Z-$#}1 + + {subscriptionPlan.isSubscribed ? ( +

+ ${subscriptionPlan.price ? subscriptionPlan.price / 100 : 0} / month +

+ ) : null} + {subscriptionPlan.stripeCurrentPeriodEnd ? ( +

+ Your plan will{" "} + {!subscriptionPlan.isSubscribed + ? null + : subscriptionPlan.isCanceled + ? "cancel" + : "renew"} + {" on "} + + {subscriptionPlan.stripeCurrentPeriodEnd.toLocaleDateString( + "en-us" + )} + +

+ ) : null} +
+ + + + + + + ); +} diff --git a/src/app/(app)/account/billing/ManageSubscription.tsx b/src/app/(app)/account/billing/ManageSubscription.tsx new file mode 100644 index 0000000..ba66d9d --- /dev/null +++ b/src/app/(app)/account/billing/ManageSubscription.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import React from "react"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface ManageUserSubscriptionButtonProps { + userId: string; + email: string; + isCurrentPlan: boolean; + isSubscribed: boolean; + stripeCustomerId?: string | null; + stripePriceId: string; +} + +export function ManageUserSubscriptionButton({ + userId, + email, + isCurrentPlan, + isSubscribed, + stripeCustomerId, + stripePriceId, +}: ManageUserSubscriptionButtonProps) { + const [isPending, startTransition] = React.useTransition(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + startTransition(async () => { + try { + const res = await fetch("/api/billing/manage-subscription", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email, + userId, + isSubscribed, + isCurrentPlan, + stripeCustomerId, + stripePriceId, + }), + }); + const session: { url: string } = await res.json(); + if (session) { + window.location.href = session.url ?? "/dashboard/billing"; + } + } catch (err) { + console.error((err as Error).message); + toast.error("Something went wrong, please try again later."); + } + }); + }; + + return ( +
+ +
+ ); +} diff --git a/src/app/(app)/account/billing/SuccessToast.tsx b/src/app/(app)/account/billing/SuccessToast.tsx new file mode 100644 index 0000000..a4700c1 --- /dev/null +++ b/src/app/(app)/account/billing/SuccessToast.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { toast } from "sonner"; +import { useSearchParams } from "next/navigation"; +import { useEffect } from "react"; + +export default function SuccessToast() { + const searchParams = useSearchParams(); + + const success = searchParams.get("success") as Boolean | null; + useEffect(() => { + if (success) { + toast.success("Successfully updated subscription."); + } + }, [success]); + + return null; +} diff --git a/src/app/(app)/account/billing/page.tsx b/src/app/(app)/account/billing/page.tsx new file mode 100644 index 0000000..3027fd9 --- /dev/null +++ b/src/app/(app)/account/billing/page.tsx @@ -0,0 +1,112 @@ +import SuccessToast from "./SuccessToast"; +import { ManageUserSubscriptionButton } from "./ManageSubscription"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { storeSubscriptionPlans } from "@/config/subscriptions"; +import { checkAuth, getUserAuth } from "@/lib/auth/utils"; +import { getUserSubscriptionPlan } from "@/lib/stripe/subscription"; +import { CheckCircle2Icon } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +export default async function Billing() { + await checkAuth(); + const { session } = await getUserAuth(); + const subscriptionPlan = await getUserSubscriptionPlan(); + + if (!session) return redirect("/"); + + return ( +
+ + + + +

Billing

+ +

+ Subscription Details +

+

+ {subscriptionPlan.name} +

+

+ {!subscriptionPlan.isSubscribed + ? "You are not subscribed to any plan." + : subscriptionPlan.isCanceled + ? "Your plan will be canceled on " + : "Your plan renews on "} + {subscriptionPlan?.stripeCurrentPeriodEnd + ? subscriptionPlan.stripeCurrentPeriodEnd.toLocaleDateString() + : null} +

+
+
+ {storeSubscriptionPlans.map((plan) => ( + + {plan.name === subscriptionPlan.name ? ( +
+
+ Current Plan +
+
+ ) : null} + + {plan.name} + {plan.description} + + +
+

+ ${plan.price / 100} / month +

+
+
    + {plan.features.map((feature, i) => ( +
  • + + {feature} +
  • + ))} +
+
+ + {session?.user.email ? ( + + ) : ( +
+ + + +
+ )} +
+
+ ))} +
+
+ ); +} diff --git a/src/app/(app)/account/page.tsx b/src/app/(app)/account/page.tsx index 0588287..9a0292c 100644 --- a/src/app/(app)/account/page.tsx +++ b/src/app/(app)/account/page.tsx @@ -1,14 +1,18 @@ import UserSettings from "./UserSettings"; +import PlanSettings from "./PlanSettings"; import { checkAuth, getUserAuth } from "@/lib/auth/utils"; +import { getUserSubscriptionPlan } from "@/lib/stripe/subscription"; export default async function Account() { await checkAuth(); const { session } = await getUserAuth(); + const subscriptionPlan = await getUserSubscriptionPlan(); return (

Account

+
diff --git a/src/app/api/billing/manage-subscription/route.ts b/src/app/api/billing/manage-subscription/route.ts new file mode 100644 index 0000000..399a609 --- /dev/null +++ b/src/app/api/billing/manage-subscription/route.ts @@ -0,0 +1,51 @@ +import { stripe } from "@/lib/stripe/index"; +import { absoluteUrl } from "@/lib/utils"; + +interface ManageStripeSubscriptionActionProps { + isSubscribed: boolean; + stripeCustomerId?: string | null; + isCurrentPlan: boolean; + stripePriceId: string; + email: string; + userId: string; +} + +export async function POST(req: Request) { + const body: ManageStripeSubscriptionActionProps = await req.json(); + const { isSubscribed, stripeCustomerId, userId, stripePriceId, email } = body; + console.log(body); + const billingUrl = absoluteUrl("/account/billing"); + + if (isSubscribed && stripeCustomerId) { + const stripeSession = await stripe.billingPortal.sessions.create({ + customer: stripeCustomerId, + return_url: billingUrl, + }); + + return new Response(JSON.stringify({ url: stripeSession.url }), { + status: 200, + }); + } + + const stripeSession = await stripe.checkout.sessions.create({ + success_url: billingUrl.concat("?success=true"), + cancel_url: billingUrl, + payment_method_types: ["card"], + mode: "subscription", + billing_address_collection: "auto", + customer_email: email, + line_items: [ + { + price: stripePriceId, + quantity: 1, + }, + ], + metadata: { + userId, + }, + }); + + return new Response(JSON.stringify({ url: stripeSession.url }), { + status: 200, + }); +} diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts new file mode 100644 index 0000000..1824a4a --- /dev/null +++ b/src/app/api/webhooks/stripe/route.ts @@ -0,0 +1,98 @@ +import { db } from "@/lib/db/index"; +import { stripe } from "@/lib/stripe/index"; +import { headers } from "next/headers"; +import type Stripe from "stripe"; +import { subscriptions } from "@/lib/db/schema/subscriptions"; +import { eq } from "drizzle-orm"; + +export async function POST(request: Request) { + const body = await request.text(); + const signature = headers().get("Stripe-Signature") ?? ""; + + let event: Stripe.Event; + + try { + event = stripe.webhooks.constructEvent( + body, + signature, + process.env.STRIPE_WEBHOOK_SECRET || "" + ); + console.log(event.type); + } catch (err) { + return new Response( + `Webhook Error: ${err instanceof Error ? err.message : "Unknown Error"}`, + { status: 400 } + ); + } + + const session = event.data.object as Stripe.Checkout.Session; + // console.log("this is the session metadata -> ", session); + + if (!session?.metadata?.userId && session.customer == null) { + console.error("session customer", session.customer); + console.error("no metadata for userid"); + return new Response(null, { + status: 200, + }); + } + + if (event.type === "checkout.session.completed") { + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string + ); + const updatedData = { + stripeSubscriptionId: subscription.id, + stripeCustomerId: subscription.customer as string, + stripePriceId: subscription.items.data[0].price.id, + stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), + }; + + if (session?.metadata?.userId != null) { + const [sub] = await db + .select() + .from(subscriptions) + .where(eq(subscriptions.userId, session.metadata.userId)); + if (sub != undefined) { + await db + .update(subscriptions) + .set(updatedData) + .where(eq(subscriptions.userId, sub.userId!)); + } else { + await db + .insert(subscriptions) + .values({ ...updatedData, userId: session.metadata.userId }); + } + + } else if ( + typeof session.customer === "string" && + session.customer != null + ) { + await db + .update(subscriptions) + .set(updatedData) + .where(eq(subscriptions.stripeCustomerId, session.customer)); + + } + } + + if (event.type === "invoice.payment_succeeded") { + // Retrieve the subscription details from Stripe. + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string + ); + + // Update the price id and set the new period end. + await db + .update(subscriptions) + .set({ + stripePriceId: subscription.items.data[0].price.id, + stripeCurrentPeriodEnd: new Date( + subscription.current_period_end * 1000 + ), + }) + .where(eq(subscriptions.stripeSubscriptionId, subscription.id)); + + } + + return new Response(null, { status: 200 }); +} diff --git a/src/config/subscriptions.ts b/src/config/subscriptions.ts new file mode 100644 index 0000000..aa52a09 --- /dev/null +++ b/src/config/subscriptions.ts @@ -0,0 +1,35 @@ +export interface SubscriptionPlan { + id: string; + name: string; + description: string; + stripePriceId: string; + price: number; + features: Array; +} + +export const storeSubscriptionPlans: SubscriptionPlan[] = [ + { + id: "pro", + name: "Pro", + description: "Pro tier that offers x, y, and z features.", + stripePriceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID ?? "", + price: 1000, + features: ["Feature 1", "Feature 2", "Feature 3"], + }, + { + id: "max", + name: "Max", + description: "Super Pro tier that offers x, y, and z features.", + stripePriceId: process.env.NEXT_PUBLIC_STRIPE_MAX_PRICE_ID ?? "", + price: 3000, + features: ["Feature 1", "Feature 2", "Feature 3"], + }, + { + id: "ultra", + name: "Ultra", + description: "Ultra Pro tier that offers x, y, and z features.", + stripePriceId: process.env.NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID ?? "", + price: 5000, + features: ["Feature 1", "Feature 2", "Feature 3"], + }, +]; diff --git a/src/lib/db/schema/subscriptions.ts b/src/lib/db/schema/subscriptions.ts new file mode 100644 index 0000000..d2a3f55 --- /dev/null +++ b/src/lib/db/schema/subscriptions.ts @@ -0,0 +1,25 @@ +import { + pgTable, + primaryKey, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; + +export const subscriptions = pgTable( + "subscriptions", + { + userId: varchar("user_id", { length: 255 }) + .unique(), + stripeCustomerId: varchar("stripe_customer_id", { length: 255 }).unique(), + stripeSubscriptionId: varchar("stripe_subscription_id", { + length: 255, + }).unique(), + stripePriceId: varchar("stripe_price_id", { length: 255 }), + stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"), + }, + (table) => { + return { + pk: primaryKey(table.userId, table.stripeCustomerId), + }; + } +); diff --git a/src/lib/env.mjs b/src/lib/env.mjs index 6a0a110..2b4f3cd 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -8,9 +8,14 @@ export const env = createEnv({ .default("development"), DATABASE_URL: z.string().min(1), + STRIPE_SECRET_KEY: z.string().min(1), + STRIPE_WEBHOOK_SECRET: z.string().min(1), }, client: { - // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1), + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1), + NEXT_PUBLIC_STRIPE_PRO_PRICE_ID: z.string().min(1), + NEXT_PUBLIC_STRIPE_MAX_PRICE_ID: z.string().min(1), + NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID: z.string().min(1), // NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1), }, // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually // runtimeEnv: { @@ -19,6 +24,9 @@ export const env = createEnv({ // }, // For Next.js >= 13.4.4, you only need to destructure client variables: experimental__runtimeEnv: { - // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, + NEXT_PUBLIC_STRIPE_PRO_PRICE_ID: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID, + NEXT_PUBLIC_STRIPE_MAX_PRICE_ID: process.env.NEXT_PUBLIC_STRIPE_MAX_PRICE_ID, + NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID: process.env.NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID, // NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY, }, }); diff --git a/src/lib/stripe/index.ts b/src/lib/stripe/index.ts new file mode 100644 index 0000000..3e111f2 --- /dev/null +++ b/src/lib/stripe/index.ts @@ -0,0 +1,6 @@ +import Stripe from "stripe"; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", { + apiVersion: "2023-10-16", + typescript: true, +}); diff --git a/src/lib/stripe/subscription.ts b/src/lib/stripe/subscription.ts new file mode 100644 index 0000000..14fcdd3 --- /dev/null +++ b/src/lib/stripe/subscription.ts @@ -0,0 +1,61 @@ +import { storeSubscriptionPlans } from "@/config/subscriptions"; +import { db } from "@/lib/db/index"; +import { subscriptions } from "@/lib/db/schema/subscriptions"; +import { eq } from "drizzle-orm"; +import { stripe } from "@/lib/stripe/index"; +import { getUserAuth } from "@/lib/auth/utils"; + +export async function getUserSubscriptionPlan() { + const { session } = await getUserAuth(); + + if (!session || !session.user) { + throw new Error("User not found."); + } + + const [ subscription ] = await db + .select() + .from(subscriptions) + .where(eq(subscriptions.userId, session.user.id)); + + if (!subscription) + return { + id: undefined, + name: undefined, + description: undefined, + stripePriceId: undefined, + price: undefined, + stripeSubscriptionId: null, + stripeCurrentPeriodEnd: null, + stripeCustomerId: null, + isSubscribed: false, + isCanceled: false, + }; + + const isSubscribed = + subscription.stripePriceId && + subscription.stripeCurrentPeriodEnd && + subscription.stripeCurrentPeriodEnd.getTime() + 86_400_000 > Date.now(); + + const plan = isSubscribed + ? storeSubscriptionPlans.find( + (plan) => plan.stripePriceId === subscription.stripePriceId + ) + : null; + + let isCanceled = false; + if (isSubscribed && subscription.stripeSubscriptionId) { + const stripePlan = await stripe.subscriptions.retrieve( + subscription.stripeSubscriptionId + ); + isCanceled = stripePlan.cancel_at_period_end; + } + + return { + ...plan, + stripeSubscriptionId: subscription.stripeSubscriptionId, + stripeCurrentPeriodEnd: subscription.stripeCurrentPeriodEnd, + stripeCustomerId: subscription.stripeCustomerId, + isSubscribed, + isCanceled, + }; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 199deca..ef66810 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -7,3 +7,9 @@ export function cn(...inputs: ClassValue[]) { } export const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789"); + +export function absoluteUrl(path: string) { + return `${ + process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000" + }${path}`; +} \ No newline at end of file