diff --git a/frontend/apps/artcraft-website/index.html b/frontend/apps/artcraft-website/index.html index df3be4cbd5..70e0dff208 100644 --- a/frontend/apps/artcraft-website/index.html +++ b/frontend/apps/artcraft-website/index.html @@ -1,19 +1,30 @@ - + - + ArtCraft. AI Video and Images. Fast and Open Desktop App. - - + + @@ -23,10 +34,9 @@ -
diff --git a/frontend/apps/artcraft-website/src/app/app.tsx b/frontend/apps/artcraft-website/src/app/app.tsx index 2811118451..dafdede0d9 100644 --- a/frontend/apps/artcraft-website/src/app/app.tsx +++ b/frontend/apps/artcraft-website/src/app/app.tsx @@ -1,4 +1,4 @@ -import { Route, Routes } from "react-router-dom"; +import { Route, Routes, Navigate } from "react-router-dom"; import Download from "../pages/download"; import Media from "../pages/media"; import PressKit from "../pages/press-kit"; @@ -15,6 +15,7 @@ import Login from "../pages/login"; import Signup from "../pages/signup"; import ForgotPassword from "../pages/forgot-password"; import Welcome from "../pages/welcome"; +import { CheckoutSuccess, CheckoutCancel } from "../pages/checkout"; export function App() { return ( @@ -38,6 +39,17 @@ export function App() { } /> } /> } /> + } /> + } /> + {/* Redirects for underscore-based URLs (legacy Stripe config) */} + } + /> + } + /> ); diff --git a/frontend/apps/artcraft-website/src/components/index.ts b/frontend/apps/artcraft-website/src/components/index.ts index c152451d62..16ad97296b 100644 --- a/frontend/apps/artcraft-website/src/components/index.ts +++ b/frontend/apps/artcraft-website/src/components/index.ts @@ -1,3 +1,4 @@ export * from "./navbar"; export * from "./footer"; export * from "./download-button"; +export * from "./pricing-table"; diff --git a/frontend/apps/artcraft-website/src/components/pricing-table/index.ts b/frontend/apps/artcraft-website/src/components/pricing-table/index.ts new file mode 100644 index 0000000000..3dfb9818d7 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/pricing-table/index.ts @@ -0,0 +1 @@ +export { default as PricingTable } from "./pricing-table"; diff --git a/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx new file mode 100644 index 0000000000..8550c1a360 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx @@ -0,0 +1,412 @@ +import { faCheck } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { + SUBSCRIPTION_PLANS, + SubscriptionPlanDetails, +} from "@storyteller/subscription"; +import { twMerge } from "tailwind-merge"; +import { useState, useEffect } from "react"; +import { TabSelector } from "@storyteller/ui-tab-selector"; +import { + UsersApi, + BillingApi, + UserInfo, + StorytellerApiHostStore, +} from "@storyteller/api"; +import { useNavigate } from "react-router-dom"; + +const billingTabs = [ + { id: "yearly", label: "Yearly" }, + { id: "monthly", label: "Monthly" }, +]; + +// Mapping from our plan slugs to API plan slugs +const PLAN_SLUG_MAP: Record = { + artcraft_basic: "artcraft_basic", + artcraft_pro: "artcraft_pro", + artcraft_max: "artcraft_max", +}; + +interface PricingTableProps { + includeFree?: boolean; + showHeader?: boolean; + title?: string; + subtitle?: string; + className?: string; + compact?: boolean; +} + +const PricingTable = ({ + includeFree = false, + showHeader = true, + title = "Choose Your Plan", + subtitle = "Support open-source development. Your subscription keeps ArtCraft free and open for everyone.", + className = "", + compact = false, +}: PricingTableProps) => { + const navigate = useNavigate(); + const [billingType, setBillingType] = useState("yearly"); + const isYearly = billingType === "yearly"; + + // User and subscription state + const [user, setUser] = useState(undefined); + const [activePlanSlug, setActivePlanSlug] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [processingPlan, setProcessingPlan] = useState(null); + + // Fetch user session and subscriptions on mount + useEffect(() => { + const fetchUserData = async () => { + try { + const usersApi = new UsersApi(); + const sessionResponse = await usersApi.GetSession(); + + if ( + sessionResponse.success && + sessionResponse.data?.loggedIn && + sessionResponse.data?.user + ) { + setUser(sessionResponse.data.user); + + // Fetch active subscriptions + const billingApi = new BillingApi(); + const subscriptionsResponse = + await billingApi.ListActiveSubscriptions(); + + if ( + subscriptionsResponse.success && + subscriptionsResponse.data?.active_subscriptions + ) { + // Find ArtCraft subscription + const artcraftSub = + subscriptionsResponse.data.active_subscriptions.find( + (sub) => sub.namespace === "artcraft", + ); + if (artcraftSub) { + setActivePlanSlug(artcraftSub.product_slug); + } + } + } + } catch (error) { + console.error("Error fetching user data:", error); + } finally { + setIsLoading(false); + } + }; + + fetchUserData(); + }, []); + + const plans = includeFree + ? SUBSCRIPTION_PLANS + : SUBSCRIPTION_PLANS.filter((plan) => plan.slug !== "free"); + + const getColorSchemeClasses = ( + colorScheme: SubscriptionPlanDetails["colorScheme"], + ) => { + const baseClasses = + "relative rounded-3xl p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md"; + + switch (colorScheme) { + case "dark": + return twMerge( + baseClasses, + "bg-[#1C1C20] border-white/10 hover:border-white/20", + ); + case "green": + return twMerge( + baseClasses, + "bg-gradient-to-b from-[#002D23]/80 via-[#006B54]/50 to-[#00D28B]/10 border-[#00a873]/50", + "hover:border-[#00a873] hover:shadow-[0_0_30px_rgba(0,210,139,0.2)]", + ); + case "purple": + return twMerge( + baseClasses, + "bg-gradient-to-b from-[#2D004D]/80 via-[#6400A8]/50 to-[#C03FFF]/10 border-[#9D4CFF]/50", + "hover:border-[#9D4CFF] hover:shadow-[0_0_30px_rgba(192,63,255,0.2)]", + ); + case "orange": + return twMerge( + baseClasses, + "bg-gradient-to-b from-[#332100]/80 via-[#B35C00]/50 to-[#FFB347]/10 border-[#FF8C00]/50", + "hover:border-[#FF8C00] hover:shadow-[0_0_30px_rgba(255,179,71,0.2)]", + ); + default: + return twMerge( + baseClasses, + "bg-white/5 border-white/10 hover:border-white/20", + ); + } + }; + + const isCurrentPlan = (planSlug: string) => { + return planSlug === activePlanSlug; + }; + + const getButtonText = (planSlug: string) => { + if (planSlug === "free") return "Get Started"; + + if (isCurrentPlan(planSlug)) { + return "Current Plan"; + } + + // User has an active paid subscription - show "Switch Plan" + if (activePlanSlug && activePlanSlug !== "free") { + return "Switch Plan"; + } + + // No subscription - show "Select Plan" + return "Select Plan"; + }; + + const handlePlanClick = async (planSlug: string) => { + // Free plan - redirect to download + if (planSlug === "free") { + navigate("/download"); + return; + } + + // If current plan, do nothing + if (isCurrentPlan(planSlug)) { + return; + } + + // Not logged in - redirect to signup + if (!user) { + navigate("/signup"); + return; + } + + // Set loading state for this plan + setProcessingPlan(planSlug); + + try { + const cadence = isYearly ? "yearly" : "monthly"; + const apiPlanSlug = PLAN_SLUG_MAP[planSlug] || planSlug; + + // Determine if we need checkout (new subscription) or portal (switch plan) + const hasActiveSub = activePlanSlug && activePlanSlug !== "free"; + + const endpoint = hasActiveSub + ? "/v1/stripe_artcraft/portal/switch_plan" + : "/v1/stripe_artcraft/checkout/subscription"; + + const apiHost = + StorytellerApiHostStore.getInstance().getApiSchemeAndHost(); + const response = await fetch(`${apiHost}${endpoint}`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + plan: apiPlanSlug, + cadence: cadence, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Redirect to Stripe + const redirectUrl = + data.stripe_checkout_redirect_url || data.stripe_portal_url; + if (redirectUrl) { + window.location.href = redirectUrl; + } else { + console.error("No redirect URL returned from API"); + } + } catch (error) { + console.error("Error initiating checkout:", error); + } finally { + setProcessingPlan(null); + } + }; + + const formatPrice = (plan: SubscriptionPlanDetails) => { + if (plan.monthlyPrice === 0) { + return { current: 0, original: null }; + } + + if (isYearly) { + const val = Math.round(plan.yearlyPrice / 12); + const original = plan.originalYearlyPrice + ? Math.round(plan.originalYearlyPrice / 12) + : null; + return { current: val, original }; + } else { + const val = plan.originalMonthlyPrice || plan.monthlyPrice; + return { current: val, original: null }; + } + }; + + // Determine grid columns based on number of plans + const gridCols = + plans.length === 3 + ? "grid-cols-1 md:grid-cols-3" + : "grid-cols-1 md:grid-cols-2 lg:grid-cols-4"; + + return ( +
+ {showHeader && ( +
+

+ {title} +

+

+ {subtitle} +

+
+ )} + + {/* Billing Toggle */} +
+ + + -20% + +
+ +
+ {plans.map((plan) => { + const isPopular = plan.slug === "artcraft_pro"; + const isCurrent = isCurrentPlan(plan.slug); + const { current: price, original: originalPrice } = formatPrice(plan); + const isProcessing = processingPlan === plan.slug; + + return ( +
+ {isPopular && !isCurrent && ( +
+ MOST POPULAR +
+ )} + + {isCurrent && ( +
+ CURRENT PLAN +
+ )} + +

+ {plan.name} +

+
+ {originalPrice !== null && ( + + ${originalPrice} + + )} + ${price} + /month +
+
+ {plan.monthlyPrice === 0 + ? "Free forever" + : isYearly + ? "Billed yearly" + : "Billed monthly"} +
+ + + +
    + {plan.features.map((feature, idx) => ( + + ))} +
+
+ ); + })} +
+
+ ); +}; + +const Feature = ({ + text, + highlighted = false, +}: { + text: string; + highlighted?: boolean; +}) => ( +
  • +
    + +
    + + {text} + +
  • +); + +export default PricingTable; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx b/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx new file mode 100644 index 0000000000..d68b2ee601 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/cancel.tsx @@ -0,0 +1,98 @@ +import { faXmarkCircle, faArrowLeft } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { Link } from "react-router-dom"; +import Seo from "../../components/seo"; + +const CheckoutCancel = () => { + return ( +
    + + + {/* Background gradient - subtle/neutral */} +
    +
    +
    + +
    + {/* Cancel Card */} +
    +
    + {/* Cancel Icon */} +
    +
    + +
    +
    + + {/* Header */} +

    + Checkout Cancelled +

    +

    + No worries! Your checkout was cancelled and no payment was made. + You can try again whenever you're ready. +

    + + {/* Info Box */} +
    +

    + + Changed your mind? + {" "} + No problem. You can return to the pricing page to complete your + purchase at any time. +

    +
    + + {/* CTA Buttons */} +
    + + +
    +
    + + {/* Footer Links */} +
    + + FAQ + + • + + Get Help + +
    +
    +
    +
    + ); +}; + +export default CheckoutCancel; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/index.ts b/frontend/apps/artcraft-website/src/pages/checkout/index.ts new file mode 100644 index 0000000000..6cfdec89f8 --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/index.ts @@ -0,0 +1,2 @@ +export { default as CheckoutSuccess } from "./success"; +export { default as CheckoutCancel } from "./cancel"; diff --git a/frontend/apps/artcraft-website/src/pages/checkout/success.tsx b/frontend/apps/artcraft-website/src/pages/checkout/success.tsx new file mode 100644 index 0000000000..8da500f28c --- /dev/null +++ b/frontend/apps/artcraft-website/src/pages/checkout/success.tsx @@ -0,0 +1,149 @@ +import { + faCheckCircle, + faDownload, + faDesktop, + faRocket, + faArrowLeft, +} from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Button } from "@storyteller/ui-button"; +import { Link } from "react-router-dom"; +import { isMobile, isMacOs } from "react-device-detect"; +import { DOWNLOAD_LINKS } from "../../config/downloads"; +import Seo from "../../components/seo"; + +const CheckoutSuccess = () => { + const downloadUrl = isMacOs ? DOWNLOAD_LINKS.MACOS : DOWNLOAD_LINKS.WINDOWS; + + return ( +
    + + + {/* Background gradient - celebratory green/purple */} +
    +
    +
    + +
    + {/* Success Card */} +
    +
    + {/* Success Icon */} +
    +
    + +
    +
    + + {/* Header */} +

    + Thank You for Your Support! +

    +

    + Your subscription is now active. Thank you for helping keep + ArtCraft open-source and free for everyone. You're now ready to + create amazing AI-generated art! +

    + + {/* Next Steps */} +
    +
    +
    + +
    +

    Next Steps

    +
    + +
    + {[ + "Download ArtCraft if you haven't already", + "Log in with your account", + "Your subscription is automatically activated", + "Start creating with all premium features!", + ].map((step, idx) => ( +
    +
    + {idx + 1} +
    + {step} +
    + ))} +
    +
    + + {/* CTA Buttons */} + {!isMobile ? ( +
    + + +
    + ) : ( +
    +
    + +
    + ArtCraft is a desktop application.
    + Please head to your computer to download and start creating. +
    + )} + + {/* Download Links */} + {!isMobile && ( + + )} +
    + + {/* Footer Link */} +
    + + View Plan Details + +
    +
    +
    +
    + ); +}; + +export default CheckoutSuccess; diff --git a/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx index 8d1b9ab2f2..2832a98519 100644 --- a/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx +++ b/frontend/apps/artcraft-website/src/pages/pricing/pricing.tsx @@ -1,100 +1,8 @@ -import { faCheck } from "@fortawesome/pro-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Button } from "@storyteller/ui-button"; import Footer from "../../components/footer"; -import { - SUBSCRIPTION_PLANS, - SubscriptionPlanDetails, -} from "@storyteller/subscription"; -import { twMerge } from "tailwind-merge"; import Seo from "../../components/seo"; -import { useState } from "react"; -import { TabSelector } from "@storyteller/ui-tab-selector"; - -const billingTabs = [ - { id: "yearly", label: "Yearly" }, - { id: "monthly", label: "Monthly" }, -]; +import { PricingTable } from "../../components/pricing-table"; const Pricing = () => { - const [billingType, setBillingType] = useState("yearly"); - const isYearly = billingType === "yearly"; - - const getColorSchemeClasses = ( - colorScheme: SubscriptionPlanDetails["colorScheme"], - isCurrent: boolean, - ) => { - const baseClasses = - "relative rounded-3xl p-8 border flex flex-col transition-all duration-300 backdrop-blur-md"; - - switch (colorScheme) { - case "dark": - return twMerge( - baseClasses, - "bg-[#1C1C20] border-white/10 hover:border-white/20", - ); - case "green": - // Basic - return twMerge( - baseClasses, - "bg-gradient-to-b from-[#002D23]/80 via-[#006B54]/50 to-[#00D28B]/10 border-[#00a873]/50", - "hover:border-[#00a873] hover:shadow-[0_0_30px_rgba(0,210,139,0.2)]", - ); - case "purple": - // Pro - return twMerge( - baseClasses, - "bg-gradient-to-b from-[#2D004D]/80 via-[#6400A8]/50 to-[#C03FFF]/10 border-[#9D4CFF]/50", - "hover:border-[#9D4CFF] hover:shadow-[0_0_30px_rgba(192,63,255,0.2)]", - ); - case "orange": - // Max - return twMerge( - baseClasses, - "bg-gradient-to-b from-[#332100]/80 via-[#B35C00]/50 to-[#FFB347]/10 border-[#FF8C00]/50", - "hover:border-[#FF8C00] hover:shadow-[0_0_30px_rgba(255,179,71,0.2)]", - ); - default: - return twMerge( - baseClasses, - "bg-white/5 border-white/10 hover:border-white/20", - ); - } - }; - - const getButtonText = (planSlug: string) => { - // Since this is the website, all buttons should probably lead to download or signup/login - // If we want to mirror the app, we can't really do "Switch Plan" because we don't know the logged in user's state easily here (unless we fetch it). - // For now, let's keep it simple: "Get Started" or "Start Free Trial" - if (planSlug === "free") return "Get Started"; - return "Select Plan"; - }; - - const getButtonHref = (planSlug: string) => { - if (planSlug === "free") return "/download"; - return "/signup"; - }; - - const formatPrice = (plan: SubscriptionPlanDetails) => { - if (plan.monthlyPrice === 0) { - return { current: 0, original: null }; - } - - if (isYearly) { - // Annual price divided by 12 - const val = Math.round(plan.yearlyPrice / 12); - // Check if there's an original price to show savings - const original = plan.originalYearlyPrice - ? Math.round(plan.originalYearlyPrice / 12) - : null; - return { current: val, original }; - } else { - // Monthly price - const val = plan.originalMonthlyPrice || plan.monthlyPrice; - return { current: val, original: null }; - } - }; - return (
    {
    -
    -

    - Simple, Transparent Pricing -

    -

    - Start for free and scale as you grow. No hidden fees. -

    - - {/* Billing Toggle */} -
    - - - -20% - -
    -
    - -
    - {SUBSCRIPTION_PLANS.map((plan) => { - const isPopular = plan.slug === "artcraft_pro"; - const { current: price, original: originalPrice } = - formatPrice(plan); - - return ( -
    - {isPopular && ( -
    - MOST POPULAR -
    - )} - -

    - {plan.name} -

    -
    - {originalPrice !== null && ( - - ${originalPrice} - - )} - ${price} - /month -
    -
    - {plan.monthlyPrice === 0 - ? "Free forever" - : isYearly - ? "Billed yearly" - : "Billed monthly"} -
    - - - -
      - {plan.features.map((feature, idx) => ( - - ))} -
    -
    - ); - })} -
    +