From 13b17db8b56f7d87d5b72391e994b7c083435fcc Mon Sep 17 00:00:00 2001 From: Michael Tikhonovsky Date: Mon, 28 Oct 2024 18:26:20 -0400 Subject: [PATCH 1/2] added one time purchase packs --- src/app/api/webhooks/stripe/route.ts | 237 +++++++++++++++++---------- src/app/pricing/page.tsx | 196 +++++++++++++++------- src/config/stripe.ts | 24 +++ src/server/api/routers/users.ts | 34 +++- 4 files changed, 339 insertions(+), 152 deletions(-) diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts index ed11daa..db3088c 100644 --- a/src/app/api/webhooks/stripe/route.ts +++ b/src/app/api/webhooks/stripe/route.ts @@ -4,6 +4,7 @@ import { brainrotusers } from "@/server/db/schemas/users/schema"; import { eq } from "drizzle-orm"; import { headers } from "next/headers"; import type Stripe from "stripe"; +import { CREDIT_AMOUNTS } from "@/config/stripe"; export const dynamic = "force-dynamic"; @@ -13,13 +14,14 @@ 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("Webhook event constructed successfully"); + console.log("Webhook event constructed successfully", event.type); } catch (err) { console.error("Error constructing webhook event:", err); return new Response( @@ -29,110 +31,163 @@ export async function POST(request: Request) { } const session = event.data.object as Stripe.Checkout.Session; - if (!session?.metadata?.userId) { - console.log("Missing userId in session metadata"); - return new Response(null, { status: 200 }); - } - - if (event.type === "checkout.session.completed") { - console.log("Handling checkout.session.completed event"); - const subscription = await stripe.subscriptions.retrieve( - session.subscription as string, - ); - - const user = await db.query.brainrotusers.findFirst({ - where: eq(brainrotusers.id, parseInt(session.metadata.userId)), - }); - if (!user) { - return new Response(null, { status: 400 }); + try { + // Early return if no userId in metadata + if (!session?.metadata?.userId) { + console.log("Missing userId in session metadata"); + return new Response(null, { status: 200 }); } - console.log("Updating user subscription details in the database"); - await db - .update(brainrotusers) - .set({ - stripeSubscriptionId: subscription.id, - stripeCustomerId: subscription.customer as string, - stripePriceId: subscription.items.data[0]?.price.id, - stripeCurrentPeriodEnd: new Date( - subscription.current_period_end * 1000, - ), - subscribed: true, - credits: user.credits + 250, - }) - .where(eq(brainrotusers.id, user.id)); - - console.log("User subscription details updated successfully"); - } + // Handle successful checkouts + if (event.type === "checkout.session.completed") { + console.log("Handling checkout.session.completed event"); + + // One-time payment (credit packs) + if (session.mode === "payment" && session.metadata?.creditPacks) { + const creditPacks = parseInt(session.metadata.creditPacks); + const creditsToAdd = creditPacks * CREDIT_AMOUNTS.PACK_SIZE; + + const user = await db.query.brainrotusers.findFirst({ + where: eq(brainrotusers.id, parseInt(session.metadata.userId)), + }); + + if (!user) { + console.error("User not found for credit pack purchase"); + return new Response(null, { status: 400 }); + } + + await db + .update(brainrotusers) + .set({ + credits: user.credits + creditsToAdd, + }) + .where(eq(brainrotusers.id, user.id)); + + console.log(`Added ${creditsToAdd} credits to user ${user.id}`); + } + // Subscription payment + else if (session.mode === "subscription") { + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string, + ); + + const user = await db.query.brainrotusers.findFirst({ + where: eq(brainrotusers.id, parseInt(session.metadata.userId)), + }); + + if (!user) { + console.error("User not found for subscription"); + return new Response(null, { status: 400 }); + } + + console.log("Updating user subscription details in the database"); + await db + .update(brainrotusers) + .set({ + stripeSubscriptionId: subscription.id, + stripeCustomerId: subscription.customer as string, + stripePriceId: subscription.items.data[0]?.price.id, + stripeCurrentPeriodEnd: new Date( + subscription.current_period_end * 1000, + ), + subscribed: true, + credits: user.credits + 250, // Monthly credits for subscription + }) + .where(eq(brainrotusers.id, user.id)); + + console.log("User subscription details updated successfully"); + } + } - if (event.type === "invoice.payment_succeeded") { - console.log("Handling invoice.payment_succeeded event"); - // Retrieve the subscription details from Stripe. - const subscription = await stripe.subscriptions.retrieve( - session.subscription as string, - ); + // Handle successful subscription invoice payments + if (event.type === "invoice.payment_succeeded") { + console.log("Handling invoice.payment_succeeded event"); - console.log("Updating user subscription details in the database"); - await db - .update(brainrotusers) - .set({ - stripePriceId: subscription.items.data[0]?.price.id, - stripeCurrentPeriodEnd: new Date( - subscription.current_period_end * 1000, - ), - subscribed: true, - }) - .where(eq(brainrotusers.stripeSubscriptionId, subscription.id)); + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string, + ); - console.log("User subscription details updated successfully"); - } + const user = await db.query.brainrotusers.findFirst({ + where: eq(brainrotusers.stripeSubscriptionId, subscription.id), + }); + + if (user) { + await db + .update(brainrotusers) + .set({ + stripePriceId: subscription.items.data[0]?.price.id, + stripeCurrentPeriodEnd: new Date( + subscription.current_period_end * 1000, + ), + subscribed: true, + credits: user.credits + 250, // Refresh monthly credits + }) + .where(eq(brainrotusers.id, user.id)); + + console.log("User subscription renewed successfully"); + } + } - if (event.type === "customer.subscription.deleted") { - const subscription = await stripe.subscriptions.retrieve( - session.subscription as string, - ); - await db - .update(brainrotusers) - .set({ - stripePriceId: null, - stripeCurrentPeriodEnd: null, - stripeCustomerId: null, - stripeSubscriptionId: null, - subscribed: false, - }) - .where( - eq( - brainrotusers.id, - parseInt(subscription.metadata.userId ?? "0") ?? 0, - ), + // Handle subscription cancellations + if (event.type === "customer.subscription.deleted") { + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string, ); - console.log("User subscription deleted successfully"); - } - - if (event.type === "customer.subscription.updated") { - const subscription = await stripe.subscriptions.retrieve( - session.subscription as string, - ); - const user = await db.query.brainrotusers.findFirst({ - where: eq( - brainrotusers.id, - parseInt(subscription.metadata.userId ?? "0"), - ), - }); - if (user) { await db .update(brainrotusers) .set({ - credits: user.credits + 250, + stripePriceId: null, + stripeCurrentPeriodEnd: null, + stripeCustomerId: null, + stripeSubscriptionId: null, + subscribed: false, }) - .where(eq(brainrotusers.id, user.id)); + .where( + eq( + brainrotusers.id, + parseInt(subscription.metadata.userId ?? "0") ?? 0, + ), + ); + + console.log("User subscription deleted successfully"); } - console.log("User subscription deleted successfully"); - } + // Handle subscription updates + if (event.type === "customer.subscription.updated") { + const subscription = await stripe.subscriptions.retrieve( + session.subscription as string, + ); - console.log("Webhook handling completed"); - return new Response(null, { status: 200 }); + const user = await db.query.brainrotusers.findFirst({ + where: eq( + brainrotusers.id, + parseInt(subscription.metadata.userId ?? "0"), + ), + }); + + if (user) { + await db + .update(brainrotusers) + .set({ + credits: user.credits + 250, + }) + .where(eq(brainrotusers.id, user.id)); + + console.log("User subscription updated successfully"); + } + } + + console.log("Webhook handling completed successfully"); + return new Response(null, { status: 200 }); + } catch (error) { + console.error("Error processing webhook:", error); + return new Response( + `Webhook Error: ${ + error instanceof Error ? error.message : "Unknown Error" + }`, + { status: 500 }, + ); + } } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index bd0335d..7b9f74e 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,78 +1,154 @@ +"use client"; import { Button } from "@/components/ui/button"; -import ProButton from "../ProButton"; -import { BadgeCheck, Crown } from "lucide-react"; +import { Crown, BadgeCheck, Coins, Plus, Minus, Zap } from "lucide-react"; import Image from "next/image"; +import ProButton from "../ProButton"; +import { useState } from "react"; +import { trpc } from "@/trpc/client"; export default function Page() { - return ( -
-
-
-
-

Brainrot Pro 🧠

-

- $ 15 / mo -

-
+ const [creditPacks, setCreditPacks] = useState(1); + const cost = creditPacks * 5; + const totalCredits = creditPacks * 25; + + const { mutate: createStripeSession } = + trpc.user.createCreditPackSession.useMutation({ + onSuccess: ({ url }) => { + if (url) window.location.href = url; + }, + }); + + const { mutate: createProSubscription } = + trpc.user.createStripeSession.useMutation({ + onSuccess: ({ url }) => { + if (url) window.location.href = url; + }, + }); -
- holy shit + return ( +
+
+ {/* Credit Packs Card */} +
+
-
- "I love brainrot.js like I love elk meat" - Joe Rogan +

+ Credit Packs 💳 +

+
+

$ {cost}

+

one-time

-
- "brainrot.js is the archetypal weapon against neo-Marxist - tyranny." - Jordan Peterson +
+ +
+
+
+
+ +

{totalCredits} credits

+
+
+ + +
+
+

+ Each pack contains 25 credits (~2-3 videos) +

-
-
- -

- 250 credits / month{" "} - (~25 videos) -

-
-
- - My love and admiration -
-
- - all agents, 3+ min video, more (coming soon) + +
+ + {/* Pro Plan Card */} +
+
+
+

+ Brainrot Pro 🧠 +

+
+

$ 15

+

/ mo

+
- {/*
- - Access to all agents + +
+ holy shit +
+
+ "I love brainrot.js like I love elk meat" - Joe Rogan +
+
+ "brainrot.js is the archetypal weapon against neo-Marxist + tyranny." - Jordan Peterson +
+
-
- - Up to 3 minute long videos -
*/} -
- - 60 fps + +
+
+ +

250 credits / month (~25 videos)

+
+
+ + My love and admiration +
+
+ + all agents, 3+ min video, more (coming soon) +
+
+ + 60 fps +
- - - + +
diff --git a/src/config/stripe.ts b/src/config/stripe.ts index b57ff5a..47ba013 100644 --- a/src/config/stripe.ts +++ b/src/config/stripe.ts @@ -22,3 +22,27 @@ export const PLANS = [ }, }, ]; + +export const CREDIT_AMOUNTS = { + PACK_SIZE: 25, + PACK_PRICE: 5, +} as const; + +export const CREDIT_PACK_PRICES = { + test: "price_1QF0zaJ9brh1H24beY3bFkJ2", + production: "price_1QF0zaJ9brh1H24beY3bFkJ2", +} as const; + +export const getPriceId = (type: "creditPack" | "subscription") => { + const isProduction = process.env.NODE_ENV === "production"; + + if (type === "creditPack") { + return isProduction + ? CREDIT_PACK_PRICES.production + : CREDIT_PACK_PRICES.test; + } + + return isProduction + ? PLANS[0]?.price.priceIds.production + : PLANS[0]?.price.priceIds.test; +}; diff --git a/src/server/api/routers/users.ts b/src/server/api/routers/users.ts index e27eae4..1e5ace1 100644 --- a/src/server/api/routers/users.ts +++ b/src/server/api/routers/users.ts @@ -14,7 +14,7 @@ import { z } from "zod"; import OpenAI from "openai"; import { absoluteUrl } from "@/lib/utils"; import { getUserSubscriptionPlan, stripe } from "@/lib/stripe"; -import { PLANS } from "@/config/stripe"; +import { PLANS, getPriceId } from "@/config/stripe"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, @@ -278,6 +278,38 @@ export const userRouter = createTRPCRouter({ }, ), + createCreditPackSession: protectedProcedure + .input(z.object({ creditPacks: z.number().min(1).max(10) })) + .mutation(async ({ ctx, input }) => { + const dbUser = await ctx.db.query.brainrotusers.findFirst({ + where: eq(brainrotusers.id, ctx.user_id), + }); + + if (!dbUser) { + throw new Error("User not found"); + } + + const session = await stripe.checkout.sessions.create({ + success_url: absoluteUrl("/"), + cancel_url: absoluteUrl("/"), + payment_method_types: ["card"], + mode: "payment", + billing_address_collection: "auto", + line_items: [ + { + price: getPriceId("creditPack"), + quantity: input.creditPacks, + }, + ], + metadata: { + userId: ctx.user_id, + creditPacks: input.creditPacks, + }, + }); + + return { url: session.url }; + }), + // Mutation to create a Stripe checkout session for the user createStripeSession: protectedProcedure .input( From 8c0cb19e5b9f9a62aa3443fbedb0e82575b4c4b3 Mon Sep 17 00:00:00 2001 From: Michael Tikhonovsky Date: Tue, 29 Oct 2024 13:02:54 -0400 Subject: [PATCH 2/2] pro button change --- src/app/pricing/page.tsx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index 7b9f74e..e608b68 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -18,13 +18,6 @@ export default function Page() { }, }); - const { mutate: createProSubscription } = - trpc.user.createStripeSession.useMutation({ - onSuccess: ({ url }) => { - if (url) window.location.href = url; - }, - }); - return (
@@ -139,16 +132,17 @@ export default function Page() {
- - + + +