Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apps/web/app/Layout/StripeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { createContext, type PropsWithChildren, use } from "react";

type StripeContext = { plans: { yearly: string; monthly: string } };
const StripeContext = createContext<StripeContext | undefined>(undefined);

export function StripeContextProvider({
children,
plans,
}: PropsWithChildren & Partial<StripeContext>) {
return (
<StripeContext.Provider value={plans ? { plans } : undefined}>
{children}
</StripeContext.Provider>
);
}

export function useStripeContext() {
const context = use(StripeContext);
if (!context) {
throw new Error(
"useStripeContext must be used within a StripeContextProvider",
);
}
return context;
}
5 changes: 1 addition & 4 deletions apps/web/app/api/webhooks/stripe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
38 changes: 24 additions & 14 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,6 +20,7 @@ import {
ReactQueryProvider,
SessionProvider,
} from "./Layout/providers";
import { StripeContextProvider } from "./Layout/StripeContext";
//@ts-expect-error
import { script } from "./themeScript";

Expand Down Expand Up @@ -111,20 +113,28 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<PostHogProvider bootstrapData={bootstrapData}>
<AuthContextProvider user={userPromise}>
<SessionProvider>
<PublicEnvContext
value={{
webUrl: buildEnv.NEXT_PUBLIC_WEB_URL,
}}
<StripeContextProvider
plans={
serverEnv().VERCEL_ENV === "production"
? STRIPE_PLAN_IDS.production
: STRIPE_PLAN_IDS.development
}
>
<ReactQueryProvider>
<SonnerToaster />
<main className="w-full">{children}</main>
<PosthogIdentify />
<MetaPixel />
<GTag />
<PurchaseTracker />
</ReactQueryProvider>
</PublicEnvContext>
<PublicEnvContext
value={{
webUrl: buildEnv.NEXT_PUBLIC_WEB_URL,
}}
>
<ReactQueryProvider>
<SonnerToaster />
<main className="w-full">{children}</main>
<PosthogIdentify />
<MetaPixel />
<GTag />
<PurchaseTracker />
</ReactQueryProvider>
</PublicEnvContext>
</StripeContextProvider>
</SessionProvider>
</AuthContextProvider>
</PostHogProvider>
Expand Down
74 changes: 38 additions & 36 deletions apps/web/components/UpgradeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
Expand Down Expand Up @@ -260,11 +257,13 @@ export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => {

<Button
variant="blue"
onClick={planCheckout}
onClick={() => planCheckout.mutate()}
className="mt-5 w-full max-w-sm h-14 text-lg"
disabled={proLoading}
disabled={planCheckout.isPending}
>
{proLoading ? "Loading..." : "Upgrade to Cap Pro"}
{planCheckout.isPending
? "Loading..."
: "Upgrade to Cap Pro"}
</Button>
<button
type="button"
Expand Down Expand Up @@ -304,6 +303,9 @@ export const UpgradeModal = ({ open, onOpenChange }: UpgradeModalProps) => {
);
};

export const UpgradeModal =
buildEnv.NEXT_PUBLIC_IS_CAP !== "true" ? () => null : UpgradeModalImpl;

const ProRiveArt = memo(() => {
const { RiveComponent: ProModal } = useRive({
src: "/rive/main.riv",
Expand Down
79 changes: 35 additions & 44 deletions apps/web/components/pages/HomePage/Pricing/ProCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Button, Switch } from "@cap/ui";
import { getProPlanId } from "@cap/utils";
import {
faCloud,
faCreditCard,
Expand All @@ -11,17 +10,18 @@ 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";

export const ProCard = () => {
const stripeCtx = useStripeContext();
const [users, setUsers] = useState(1);
const [isAnnually, setIsAnnually] = useState(true);
const [proLoading, setProLoading] = useState(false);
const [guestLoading, setGuestLoading] = useState(false);
const proArtRef = useRef<ProArtRef>(null);

const CAP_PRO_ANNUAL_PRICE_PER_USER = homepageCopy.pricing.pro.pricing.annual;
Expand All @@ -40,10 +40,8 @@ export const ProCard = () => {
const incrementUsers = () => setUsers((prev) => prev + 1);
const decrementUsers = () => setUsers((prev) => (prev > 1 ? prev - 1 : 1));

const guestCheckout = async (planId: string) => {
setGuestLoading(true);

try {
const guestCheckout = useMutation({
mutationFn: async (planId: string) => {
const response = await fetch(`/api/settings/billing/guest-checkout`, {
method: "POST",
headers: {
Expand All @@ -58,46 +56,39 @@ export const ProCard = () => {
} else {
toast.error("Failed to create checkout session");
}
} catch (error) {
},
onError: () => {
toast.error("An error occurred. Please try again.");
} finally {
setGuestLoading(false);
}
};

const planCheckout = async (planId?: string) => {
setProLoading(true);
},
});

if (!planId) {
planId = getProPlanId(isAnnually ? "yearly" : "monthly");
}
const planCheckout = useMutation({
mutationFn: async () => {
const planId = stripeCtx.plans[isAnnually ? "yearly" : "monthly"];

const response = await fetch(`/api/settings/billing/subscribe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ priceId: planId, quantity: users }),
});
const data = await response.json();

if (data.auth === false) {
// User not authenticated, do guest checkout
setProLoading(false);
await guestCheckout(planId);
return;
}
const response = await fetch(`/api/settings/billing/subscribe`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ priceId: planId, quantity: users }),
});
const data = await response.json();

if (data.subscription === true) {
toast.success("You are already on the Cap Pro plan");
}
if (data.auth === false) {
await guestCheckout.mutateAsync(planId);
return;
}

if (data.url) {
window.location.href = data.url;
}
if (data.subscription === true) {
toast.success("You are already on the Cap Pro plan");
}

setProLoading(false);
};
if (data.url) {
window.location.href = data.url;
}
},
});

return (
<div
Expand Down Expand Up @@ -306,12 +297,12 @@ export const ProCard = () => {
<Button
variant="blue"
size="lg"
onClick={() => planCheckout()}
disabled={proLoading || guestLoading}
onClick={() => planCheckout.mutate()}
disabled={planCheckout.isPending || guestCheckout.isPending}
className="w-full font-medium"
aria-label="Purchase Cap Pro License"
>
{proLoading || guestLoading
{planCheckout.isPending || guestCheckout.isPending
? "Loading..."
: homepageCopy.pricing.pro.cta}
</Button>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/components/pages/_components/ComparePlans.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions packages/env/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading
Loading