diff --git a/frontend/apps/artcraft-webapp/src/components/settings-modal/BillingSection.tsx b/frontend/apps/artcraft-webapp/src/components/settings-modal/BillingSection.tsx new file mode 100644 index 0000000000..dc38dbca35 --- /dev/null +++ b/frontend/apps/artcraft-webapp/src/components/settings-modal/BillingSection.tsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@storyteller/ui-button"; +import { LoaderCircleIcon } from "lucide-react"; +import { BillingApi } from "@storyteller/api"; +import { + FREE_PLAN, + SUBSCRIPTION_PLANS_BY_SLUG, +} from "@storyteller/subscription"; +import { toast } from "../toast/toast"; + +interface BillingSectionProps { + /** Closes the settings modal before navigating to the pricing page. */ + onCloseModal: () => void; +} + +export function BillingSection({ onCloseModal }: BillingSectionProps) { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [activePlanSlug, setActivePlanSlug] = useState(null); + const [isOpeningPortal, setIsOpeningPortal] = useState(false); + + useEffect(() => { + let cancelled = false; + const fetchSubscription = async () => { + try { + const response = await new BillingApi().ListActiveSubscriptions(); + if (cancelled) return; + const artcraftSub = response.data?.active_subscriptions.find( + (sub) => sub.namespace === "artcraft", + ); + setActivePlanSlug(artcraftSub?.product_slug ?? null); + } catch (error) { + console.error("Error fetching subscriptions:", error); + } finally { + if (!cancelled) setIsLoading(false); + } + }; + fetchSubscription(); + return () => { + cancelled = true; + }; + }, []); + + const handleManagePlan = async () => { + setIsOpeningPortal(true); + try { + const response = await new BillingApi().GetPortalUrl(); + if (!response.success || !response.data) { + throw new Error( + response.errorMessage || "Failed to open the billing portal", + ); + } + // Keep the button in its loading state until the browser navigates + // away to the Stripe portal. + window.location.href = response.data.stripePortalUrl; + } catch (error) { + console.error("Error opening billing portal:", error); + toast.error("Could not open the billing portal. Please try again."); + setIsOpeningPortal(false); + } + }; + + const handleViewPlans = () => { + onCloseModal(); + navigate("/pricing"); + }; + + if (isLoading) { + return
Loading billing details...
; + } + + const hasPaidPlan = activePlanSlug !== null && activePlanSlug !== "free"; + const planName = activePlanSlug + ? (SUBSCRIPTION_PLANS_BY_SLUG.get(activePlanSlug)?.name ?? activePlanSlug) + : FREE_PLAN.name; + + return ( +
+
+
+

+ Current plan: {planName} +

+

+ {hasPaidPlan + ? "Update your payment method, view invoices, or cancel your subscription in the billing portal." + : "Subscribe to a plan for monthly credits and full access to ArtCraft tools."} +

+
+ {hasPaidPlan ? ( + + ) : ( + + )} +
+ {hasPaidPlan && ( + <> +
+
+
+

Switch plans

+

+ Compare plans and upgrade or downgrade on the pricing page. +

+
+ +
+ + )} +
+ ); +} diff --git a/frontend/apps/artcraft-webapp/src/components/settings-modal/SettingsModal.tsx b/frontend/apps/artcraft-webapp/src/components/settings-modal/SettingsModal.tsx index 440e577512..b5a700d60c 100644 --- a/frontend/apps/artcraft-webapp/src/components/settings-modal/SettingsModal.tsx +++ b/frontend/apps/artcraft-webapp/src/components/settings-modal/SettingsModal.tsx @@ -1,6 +1,12 @@ import { useEffect, useState } from "react"; import { Modal } from "@storyteller/ui-modal"; -import { KeyIcon, KeyboardIcon, SettingsIcon, UserIcon } from "lucide-react"; +import { + CreditCardIcon, + KeyIcon, + KeyboardIcon, + SettingsIcon, + UserIcon, +} from "lucide-react"; import { DynamicIcon } from "@storyteller/icons"; import { Switch } from "@storyteller/ui-switch"; import { KeybindsSettings, useKeybindsStore } from "@storyteller/keybinds"; @@ -11,8 +17,9 @@ import { useLightboxSoundStore } from "../../lib/lightbox-sound-store"; import { useSession } from "../../lib/session"; import { AccountSection } from "./AccountSection"; import { ApiKeySection } from "./ApiKeySection"; +import { BillingSection } from "./BillingSection"; -type Tab = "general" | "keybinds" | "account" | "apiKeys"; +type Tab = "general" | "keybinds" | "account" | "billing" | "apiKeys"; interface SettingsModalProps { isOpen: boolean; @@ -23,6 +30,7 @@ const TABS: { id: Tab; label: string; icon: typeof SettingsIcon }[] = [ { id: "general", label: "General", icon: SettingsIcon }, { id: "keybinds", label: "Keybinds", icon: KeyboardIcon }, { id: "account", label: "Account", icon: UserIcon }, + { id: "billing", label: "Billing", icon: CreditCardIcon }, { id: "apiKeys", label: "API Keys", icon: KeyIcon }, ]; @@ -80,6 +88,7 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) { )} {tab === "account" && } + {tab === "billing" && } {tab === "apiKeys" && } @@ -190,6 +199,30 @@ function AccountPanel() { ); } +function BillingPanel({ onCloseModal }: { onCloseModal: () => void }) { + const { user, authChecked } = useSession(); + + if (!authChecked) { + return ( +
Loading billing details...
+ ); + } + + if (!user) { + return ( +
+ You need to be signed in to manage billing. +
+ ); + } + + return ( +
+ +
+ ); +} + function ApiKeysPanel() { const { user, authChecked } = useSession(); diff --git a/frontend/apps/artcraft-webapp/tsconfig.app.json b/frontend/apps/artcraft-webapp/tsconfig.app.json index 3f059117f1..77340f1f4f 100644 --- a/frontend/apps/artcraft-webapp/tsconfig.app.json +++ b/frontend/apps/artcraft-webapp/tsconfig.app.json @@ -80,10 +80,10 @@ "path": "../../libs/components/gallery-modal/tsconfig.lib.json" }, { - "path": "../../libs/components/keybinds/tsconfig.lib.json" + "path": "../../libs/components/switch/tsconfig.lib.json" }, { - "path": "../../libs/components/switch/tsconfig.lib.json" + "path": "../../libs/state/subscription/tsconfig.lib.json" }, { "path": "../../libs/components/promptbox/tsconfig.lib.json" @@ -127,6 +127,9 @@ { "path": "../../libs/components/button/tsconfig.lib.json" }, + { + "path": "../../libs/icons/tsconfig.lib.json" + }, { "path": "../../libs/components/vfx/tsconfig.lib.json" }, @@ -137,7 +140,7 @@ "path": "../../libs/api/tsconfig.lib.json" }, { - "path": "../../libs/icons/tsconfig.lib.json" + "path": "../../libs/components/keybinds/tsconfig.lib.json" } ] } diff --git a/frontend/apps/artcraft-webapp/tsconfig.json b/frontend/apps/artcraft-webapp/tsconfig.json index 9b2d5e6fed..2ce61abd54 100644 --- a/frontend/apps/artcraft-webapp/tsconfig.json +++ b/frontend/apps/artcraft-webapp/tsconfig.json @@ -45,10 +45,10 @@ "path": "../../libs/components/gallery-modal" }, { - "path": "../../libs/components/keybinds" + "path": "../../libs/components/switch" }, { - "path": "../../libs/components/switch" + "path": "../../libs/state/subscription" }, { "path": "../../libs/components/promptbox" @@ -92,6 +92,9 @@ { "path": "../../libs/components/button" }, + { + "path": "../../libs/icons" + }, { "path": "../../libs/components/vfx" }, @@ -102,7 +105,7 @@ "path": "../../libs/api" }, { - "path": "../../libs/icons" + "path": "../../libs/components/keybinds" }, { "path": "./tsconfig.app.json"