From c261b38fb5e7901e25f6e67807219e96509deec0 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Oct 2025 19:18:50 +0800 Subject: [PATCH 1/4] unify stripe envs + properly differentiate stripe plans --- apps/web/app/Layout/AuthContext.tsx | 4 + apps/web/app/Layout/StripeContext.tsx | 27 +++++++ apps/web/app/api/webhooks/stripe/route.ts | 5 +- apps/web/app/layout.tsx | 38 +++++---- apps/web/components/UpgradeModal.tsx | 74 ++++++++--------- .../pages/HomePage/Pricing/ProCard.tsx | 79 ++++++++----------- .../pages/_components/ComparePlans.tsx | 6 +- packages/env/server.ts | 6 +- packages/utils/src/constants/plans.ts | 11 +-- packages/utils/src/lib/stripe/stripe.ts | 5 +- 10 files changed, 138 insertions(+), 117 deletions(-) create mode 100644 apps/web/app/Layout/StripeContext.tsx diff --git a/apps/web/app/Layout/AuthContext.tsx b/apps/web/app/Layout/AuthContext.tsx index efbad3b04f..4ce3b6e41b 100644 --- a/apps/web/app/Layout/AuthContext.tsx +++ b/apps/web/app/Layout/AuthContext.tsx @@ -13,6 +13,10 @@ export function AuthContextProvider({ }: { children: React.ReactNode; user: ReturnType; + stripePlans?: { + yearly: string; + monthly: string; + }; }) { return ( {children} diff --git a/apps/web/app/Layout/StripeContext.tsx b/apps/web/app/Layout/StripeContext.tsx new file mode 100644 index 0000000000..a6ea578070 --- /dev/null +++ b/apps/web/app/Layout/StripeContext.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { createContext, type PropsWithChildren, use } from "react"; + +type StripeContext = { plans: { yearly: string; monthly: string } }; +const StripeContext = createContext(undefined); + +export function StripeContextProvider({ + children, + plans, +}: PropsWithChildren & Partial) { + return ( + + {children} + + ); +} + +export function useStripeContext() { + const context = use(StripeContext); + if (!context) { + throw new Error( + "useStripeContext must be used within a StripeContextProvideriteContextProvider", + ); + } + return context; +} diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index eca18c5b8a..63e8887ba6 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -115,10 +115,7 @@ export const POST = async (req: Request) => { console.log("Webhook received"); const buf = await req.text(); const sig = req.headers.get("Stripe-Signature") as string; - const webhookSecret = - serverEnv().VERCEL_ENV === "production" - ? serverEnv().STRIPE_WEBHOOK_SECRET_LIVE - : serverEnv().STRIPE_WEBHOOK_SECRET_TEST; + const webhookSecret = serverEnv().STRIPE_WEBHOOK_SECRET; let event: Stripe.Event; try { diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 2f10466c7f..e089ac7d2e 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,6 @@ import "@/app/globals.css"; import { getCurrentUser } from "@cap/database/auth/session"; -import { buildEnv } from "@cap/env"; +import { buildEnv, serverEnv } from "@cap/env"; import { Analytics as DubAnalytics } from "@dub/analytics/react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import type { Metadata } from "next"; @@ -21,6 +21,8 @@ import { } from "./Layout/providers"; //@ts-expect-error import { script } from "./themeScript"; +import { STRIPE_PLAN_IDS } from "@cap/utils"; +import { StripeContextProvider } from "./Layout/StripeContext"; const defaultFont = localFont({ src: [ @@ -111,20 +113,28 @@ export default async function RootLayout({ children }: PropsWithChildren) { - - - -
{children}
- - - - -
-
+ + + +
{children}
+ + + + +
+
+
diff --git a/apps/web/components/UpgradeModal.tsx b/apps/web/components/UpgradeModal.tsx index a5b2e9bf44..b750e20ae7 100644 --- a/apps/web/components/UpgradeModal.tsx +++ b/apps/web/components/UpgradeModal.tsx @@ -1,10 +1,11 @@ "use client"; +import { useStripeContext } from "@/app/Layout/StripeContext"; import { buildEnv } from "@cap/env"; import { Button, Dialog, DialogContent, Switch } from "@cap/ui"; -import { getProPlanId } from "@cap/utils"; import NumberFlow from "@number-flow/react"; import { Fit, Layout, useRive } from "@rive-app/react-canvas"; +import { useMutation } from "@tanstack/react-query"; import { AnimatePresence, motion } from "framer-motion"; import { BarChart3, @@ -56,10 +57,8 @@ const modalVariants = { }, }; -export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => { - if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true") return; - - const [proLoading, setProLoading] = useState(false); +const UpgradeModalImpl = ({ open, onOpenChange }: UpgradeModalProps) => { + const stripeCtx = useStripeContext(); const [isAnnual, setIsAnnual] = useState(true); const [proQuantity, setProQuantity] = useState(1); const { push } = useRouter(); @@ -132,38 +131,36 @@ export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => { }, ]; - const planCheckout = async () => { - setProLoading(true); - - const planId = getProPlanId(isAnnual ? "yearly" : "monthly"); - - const response = await fetch(`/api/settings/billing/subscribe`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ priceId: planId, quantity: proQuantity }), - }); - const data = await response.json(); + const planCheckout = useMutation({ + mutationFn: async () => { + const planId = stripeCtx.plans[isAnnual ? "yearly" : "monthly"]; - if (data.auth === false) { - localStorage.setItem("pendingPriceId", planId); - localStorage.setItem("pendingQuantity", proQuantity.toString()); - push(`/login?next=/dashboard`); - return; - } + const response = await fetch(`/api/settings/billing/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ priceId: planId, quantity: proQuantity }), + }); + const data = await response.json(); - if (data.subscription === true) { - toast.success("You are already on the Cap Pro plan"); - onOpenChange(false); - } + if (data.auth === false) { + localStorage.setItem("pendingPriceId", planId); + localStorage.setItem("pendingQuantity", proQuantity.toString()); + push(`/login?next=/dashboard`); + return; + } - if (data.url) { - window.location.href = data.url; - } + if (data.subscription === true) { + toast.success("You are already on the Cap Pro plan"); + onOpenChange(false); + } - setProLoading(false); - }; + if (data.url) { + window.location.href = data.url; + } + }, + }); return ( @@ -260,11 +257,13 @@ export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => { diff --git a/apps/web/components/pages/_components/ComparePlans.tsx b/apps/web/components/pages/_components/ComparePlans.tsx index 1cfc5a4e1c..e20ee50a8a 100644 --- a/apps/web/components/pages/_components/ComparePlans.tsx +++ b/apps/web/components/pages/_components/ComparePlans.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@cap/ui"; -import { getProPlanId, userIsPro } from "@cap/utils"; +import { userIsPro } from "@cap/utils"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { clsx } from "clsx"; @@ -13,6 +13,7 @@ import { type CommercialArtRef, } from "../HomePage/Pricing/CommercialArt"; import { ProArt, type ProArtRef } from "../HomePage/Pricing/ProArt"; +import { useStripeContext } from "@/app/Layout/StripeContext"; const COLUMN_WIDTH = "min-w-[200px]"; @@ -93,6 +94,7 @@ export const ComparePlans = () => { const [proLoading, setProLoading] = useState(false); const [guestLoading, setGuestLoading] = useState(false); const [commercialLoading, setCommercialLoading] = useState(false); + const stripeCtx = useStripeContext(); // Check if user is already pro or any loading state is active const isDisabled = useMemo( @@ -247,7 +249,7 @@ export const ComparePlans = () => { ); const planCheckout = async (planId?: string) => { - const finalPlanId = planId || getProPlanId("yearly"); + const finalPlanId = planId || stripeCtx.plans.yearly; setProLoading(true); try { diff --git a/packages/env/server.ts b/packages/env/server.ts index f580818472..187c548bec 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -37,10 +37,8 @@ function createServerEnv() { RESEND_FROM_DOMAIN: z.string().optional(), DEEPGRAM_API_KEY: z.string().optional(), NEXT_LOOPS_KEY: z.string().optional(), - STRIPE_SECRET_KEY_TEST: z.string().optional(), - STRIPE_SECRET_KEY_LIVE: z.string().optional(), - STRIPE_WEBHOOK_SECRET_LIVE: z.string().optional(), - STRIPE_WEBHOOK_SECRET_TEST: z.string().optional(), + STRIPE_SECRET_KEY: z.string().optional(), + STRIPE_WEBHOOK_SECRET: z.string().optional(), DISCORD_FEEDBACK_WEBHOOK_URL: z.string().optional(), OPENAI_API_KEY: z.string().optional(), GROQ_API_KEY: z.string().optional(), diff --git a/packages/utils/src/constants/plans.ts b/packages/utils/src/constants/plans.ts index bbfb233dec..0f44770079 100644 --- a/packages/utils/src/constants/plans.ts +++ b/packages/utils/src/constants/plans.ts @@ -1,6 +1,6 @@ -import { buildEnv, NODE_ENV } from "@cap/env"; +import { buildEnv } from "@cap/env"; -const planIds = { +export const STRIPE_PLAN_IDS = { development: { yearly: "price_1Q3esrFJxA1XpeSsFwp486RN", monthly: "price_1P9C1DFJxA1XpeSsTwwuddnq", @@ -11,13 +11,6 @@ const planIds = { }, }; -export const getProPlanId = (billingCycle: "yearly" | "monthly") => { - const value = NODE_ENV; - const environment = value === "development" ? "development" : "production"; - - return planIds[environment]?.[billingCycle] || ""; -}; - export const userIsPro = ( user?: { stripeSubscriptionStatus?: string | null; diff --git a/packages/utils/src/lib/stripe/stripe.ts b/packages/utils/src/lib/stripe/stripe.ts index b74376cbd7..eabcc9b346 100644 --- a/packages/utils/src/lib/stripe/stripe.ts +++ b/packages/utils/src/lib/stripe/stripe.ts @@ -1,10 +1,7 @@ import { serverEnv } from "@cap/env"; import Stripe from "stripe"; -const key = () => - serverEnv().STRIPE_SECRET_KEY_TEST ?? - serverEnv().STRIPE_SECRET_KEY_LIVE ?? - ""; +const key = () => serverEnv().STRIPE_SECRET_KEY ?? ""; export const STRIPE_AVAILABLE = () => key() !== ""; export const stripe = () => new Stripe(key(), { From a20aab20791e4dc1633704f4e511f70fec9499a4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Oct 2025 19:20:16 +0800 Subject: [PATCH 2/4] cleanup auth context --- apps/web/app/Layout/AuthContext.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/web/app/Layout/AuthContext.tsx b/apps/web/app/Layout/AuthContext.tsx index 4ce3b6e41b..efbad3b04f 100644 --- a/apps/web/app/Layout/AuthContext.tsx +++ b/apps/web/app/Layout/AuthContext.tsx @@ -13,10 +13,6 @@ export function AuthContextProvider({ }: { children: React.ReactNode; user: ReturnType; - stripePlans?: { - yearly: string; - monthly: string; - }; }) { return ( {children} From e15168d9885a79dc339f778c33899cef2223f117 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Oct 2025 19:23:21 +0800 Subject: [PATCH 3/4] formatting --- apps/web/app/layout.tsx | 4 ++-- apps/web/components/UpgradeModal.tsx | 2 +- apps/web/components/pages/HomePage/Pricing/ProCard.tsx | 4 ++-- apps/web/components/pages/_components/ComparePlans.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index e089ac7d2e..1292e33ee9 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,7 @@ import "@/app/globals.css"; import { getCurrentUser } from "@cap/database/auth/session"; import { buildEnv, serverEnv } from "@cap/env"; +import { STRIPE_PLAN_IDS } from "@cap/utils"; import { Analytics as DubAnalytics } from "@dub/analytics/react"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import type { Metadata } from "next"; @@ -19,10 +20,9 @@ import { ReactQueryProvider, SessionProvider, } from "./Layout/providers"; +import { StripeContextProvider } from "./Layout/StripeContext"; //@ts-expect-error import { script } from "./themeScript"; -import { STRIPE_PLAN_IDS } from "@cap/utils"; -import { StripeContextProvider } from "./Layout/StripeContext"; const defaultFont = localFont({ src: [ diff --git a/apps/web/components/UpgradeModal.tsx b/apps/web/components/UpgradeModal.tsx index b750e20ae7..74c5c6a18d 100644 --- a/apps/web/components/UpgradeModal.tsx +++ b/apps/web/components/UpgradeModal.tsx @@ -1,6 +1,5 @@ "use client"; -import { useStripeContext } from "@/app/Layout/StripeContext"; import { buildEnv } from "@cap/env"; import { Button, Dialog, DialogContent, Switch } from "@cap/ui"; import NumberFlow from "@number-flow/react"; @@ -24,6 +23,7 @@ import { import { useRouter } from "next/navigation"; import { memo, useState } from "react"; import { toast } from "sonner"; +import { useStripeContext } from "@/app/Layout/StripeContext"; interface UpgradeModalProps { open: boolean; diff --git a/apps/web/components/pages/HomePage/Pricing/ProCard.tsx b/apps/web/components/pages/HomePage/Pricing/ProCard.tsx index 0dab4a8017..295f9b6f90 100644 --- a/apps/web/components/pages/HomePage/Pricing/ProCard.tsx +++ b/apps/web/components/pages/HomePage/Pricing/ProCard.tsx @@ -10,13 +10,13 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import NumberFlow from "@number-flow/react"; +import { useMutation } from "@tanstack/react-query"; import clsx from "clsx"; import { useRef, useState } from "react"; import { toast } from "sonner"; +import { useStripeContext } from "@/app/Layout/StripeContext"; import { homepageCopy } from "../../../../data/homepage-copy"; import { ProArt, type ProArtRef } from "./ProArt"; -import { useStripeContext } from "@/app/Layout/StripeContext"; -import { useMutation } from "@tanstack/react-query"; export const ProCard = () => { const stripeCtx = useStripeContext(); diff --git a/apps/web/components/pages/_components/ComparePlans.tsx b/apps/web/components/pages/_components/ComparePlans.tsx index e20ee50a8a..2d39931d44 100644 --- a/apps/web/components/pages/_components/ComparePlans.tsx +++ b/apps/web/components/pages/_components/ComparePlans.tsx @@ -8,12 +8,12 @@ import { clsx } from "clsx"; import { use, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useAuthContext } from "@/app/Layout/AuthContext"; +import { useStripeContext } from "@/app/Layout/StripeContext"; import { CommercialArt, type CommercialArtRef, } from "../HomePage/Pricing/CommercialArt"; import { ProArt, type ProArtRef } from "../HomePage/Pricing/ProArt"; -import { useStripeContext } from "@/app/Layout/StripeContext"; const COLUMN_WIDTH = "min-w-[200px]"; From 70be4894ee4cd196d123a86530afdc9a89cc56b8 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 15 Oct 2025 19:26:52 +0800 Subject: [PATCH 4/4] Update apps/web/app/Layout/StripeContext.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/web/app/Layout/StripeContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/Layout/StripeContext.tsx b/apps/web/app/Layout/StripeContext.tsx index a6ea578070..869f092ee8 100644 --- a/apps/web/app/Layout/StripeContext.tsx +++ b/apps/web/app/Layout/StripeContext.tsx @@ -20,7 +20,7 @@ export function useStripeContext() { const context = use(StripeContext); if (!context) { throw new Error( - "useStripeContext must be used within a StripeContextProvideriteContextProvider", + "useStripeContext must be used within a StripeContextProvider", ); } return context;