diff --git a/apps/web/app/Layout/StripeContext.tsx b/apps/web/app/Layout/StripeContext.tsx new file mode 100644 index 0000000000..869f092ee8 --- /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 StripeContextProvider", + ); + } + 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..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 } from "@cap/env"; +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,6 +20,7 @@ import { ReactQueryProvider, SessionProvider, } from "./Layout/providers"; +import { StripeContextProvider } from "./Layout/StripeContext"; //@ts-expect-error import { script } from "./themeScript"; @@ -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..74c5c6a18d 100644 --- a/apps/web/components/UpgradeModal.tsx +++ b/apps/web/components/UpgradeModal.tsx @@ -2,9 +2,9 @@ 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, @@ -23,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; @@ -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..2d39931d44 100644 --- a/apps/web/components/pages/_components/ComparePlans.tsx +++ b/apps/web/components/pages/_components/ComparePlans.tsx @@ -1,13 +1,14 @@ "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"; 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, @@ -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(), {