import { catalogTiers, designSlugFromName } from '@shared/pricing'; import { IconAtom as Atom, IconCheck as Check, IconHeartHandshake as Handshake, IconHexagon as Hexagon, type TablerIcon as IconComponent, IconX as X, } from '@tabler/icons-react'; import { motion } from 'framer-motion'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; import { useDocsVisibility } from '@/hooks/api'; import { ApiError, apiJson } from '@/lib/api'; import { cn } from '@/lib/utils'; import logoIcon from '../assets/logo-icon.svg'; import { FresnelEdge } from './effects/FresnelEdge'; import { Button } from './ui/button'; interface Tier { id: string; name: string; tagline: string; /** Null for the contact-us tier, which has no self-serve price to display. */ price: number | null; originalPrice: number | null; unit: string; cta: string; ctaHref: string; /** * When set, the CTA posts to /license-checkout for this tier and redirects * straight into the provider's hosted checkout instead of navigating. */ checkoutTier?: string; ctaVariant: 'outline' | 'exciting' | 'default'; popular: boolean; icon: IconComponent; accent: { icon: string; bg: string; shadow: string; glow: string; priceGradient: string | null; check: string; }; features: { text: string; included: boolean }[]; delay: number; } export function PricingSection() { const { t } = useTranslation(); const { userDocsEnabled } = useDocsVisibility(); // The free tier's CTA sends people to the getting-started guide. With user // docs turned off there is no guide to send them to, so the button falls // back to signup rather than pointing at a route that now 404s. const graphiteCtaHref = userDocsEnabled ? '/docs/getting-started' : '/signup'; const tiers: Tier[] = [ { id: 'graphite', name: 'Graphite', tagline: t('landing.pricing.graphite.tagline'), price: 0, originalPrice: null, unit: t('landing.pricing.units.forever'), cta: t('landing.pricing.graphite.cta'), ctaHref: graphiteCtaHref, ctaVariant: 'default', popular: false, icon: Hexagon, accent: { icon: 'text-muted-foreground', bg: 'bg-muted/30 dark:bg-background/50', shadow: '', glow: 'oklch(0.7 0.02 250 / 0.06)', priceGradient: null, check: 'text-muted-foreground', }, features: [ { text: t('landing.pricing.graphite.features.unlimitedProjects'), included: true }, { text: t('landing.pricing.graphite.features.localDev'), included: true }, { text: t('landing.pricing.graphite.features.production'), included: true }, { text: t('landing.pricing.graphite.features.backups'), included: true }, { text: t('landing.pricing.graphite.features.cicd'), included: true }, { text: t('landing.pricing.graphite.features.addOns'), included: true }, { text: t('landing.pricing.graphite.features.supabase'), included: true }, { text: t('landing.pricing.graphite.features.fairSource'), included: true }, { text: t('landing.pricing.graphite.features.community'), included: true }, ], delay: 0.1, }, { id: 'fullerene', name: 'Fullerene', tagline: t('landing.pricing.fullerene.tagline'), price: 149, originalPrice: 299, unit: t('landing.pricing.units.oneTime'), cta: t('landing.pricing.fullerene.cta'), ctaHref: '/#pricing', checkoutTier: 'fullerene', ctaVariant: 'exciting', popular: true, icon: Atom, accent: { icon: 'text-primary', bg: 'bg-primary/[0.03] dark:bg-primary/[0.05]', shadow: 'shadow-lg shadow-primary/10', glow: 'oklch(0.82 0.14 192 / 0.08)', priceGradient: 'from-primary to-primary/70', check: 'text-primary', }, features: [ { text: t('landing.pricing.fullerene.features.graphite'), included: true }, { text: t('landing.pricing.fullerene.features.deployModes'), included: true }, { text: t('landing.pricing.fullerene.features.replication'), included: true }, { text: t('landing.pricing.fullerene.features.healthChecks'), included: true }, { text: t('landing.pricing.fullerene.features.cicd'), included: true }, { text: t('landing.pricing.fullerene.features.monitoring'), included: true }, { text: t('landing.pricing.fullerene.features.unlimited'), included: true }, { text: t('landing.pricing.fullerene.features.support'), included: true }, ], delay: 0.2, }, { id: 'agency', name: 'Agency', tagline: t('landing.pricing.agency.tagline'), price: null, originalPrice: null, unit: t('landing.pricing.units.contact'), cta: t('landing.pricing.agency.cta'), ctaHref: '/contact', ctaVariant: 'default', popular: false, icon: Handshake, accent: { icon: 'text-secondary-accent', bg: 'bg-secondary-accent/[0.03] dark:bg-secondary-accent/[0.05]', shadow: 'shadow-lg shadow-secondary-accent/10', glow: 'oklch(0.65 0.26 350 / 0.06)', priceGradient: null, check: 'text-secondary-accent', }, features: [ { text: t('landing.pricing.agency.features.fullerene'), included: true }, { text: t('landing.pricing.agency.features.exception'), included: true }, { text: t('landing.pricing.agency.features.clients'), included: true }, { text: t('landing.pricing.agency.features.whiteLabel'), included: true }, { text: t('landing.pricing.agency.features.customTerms'), included: true }, { text: t('landing.pricing.agency.features.priority'), included: true }, ], delay: 0.3, }, ]; // When the operator has activated products via `vibecarbon configure`, those // drive WHICH cards show, their order (price-sorted), and their price — // matched to each tier's bespoke design above. The hardcoded `tiers` are the // fallback when no catalog is configured. Curated visuals, taglines, discounts // and feature copy stay local (they aren't in the provider catalog). const tiersById = new Map(tiers.map((tier) => [tier.id, tier])); const agencyTier = tiersById.get('agency'); const displayTiers: Tier[] = catalogTiers.length ? (() => { const mapped = catalogTiers.map((ct, i) => { const slug = designSlugFromName(ct.name); const design = tiersById.get(slug); const price = Math.round(ct.amount / 100); const delay = 0.1 * (i + 1); if (design) { // Agency is a contact-only tier — never let a catalog product's // price override its `price: null` contact semantics (e.g. a $0 // "Agency" placeholder product must still read "Contact us"). return slug === 'agency' ? { ...design, delay } : { ...design, price, delay }; } // Unrecognized product — default (graphite) styling, content from the catalog. return { ...tiers[0], id: ct.priceId, name: ct.name, tagline: ct.description ?? tiers[0].tagline, price, originalPrice: null, popular: false, features: ct.features.length ? ct.features.map((text) => ({ text, included: true })) : tiers[0].features, delay, }; }); // Agency has no purchasable Stripe product, so it never shows up in the // catalog on its own — always append the static contact-us card unless // the operator activated a product literally named "Agency". if (agencyTier && !mapped.some((tier) => tier.id === 'agency')) { return [...mapped, { ...agencyTier, delay: 0.1 * (mapped.length + 1) }]; } return mapped; })() : tiers; return (
{/* Header */}

{t('landing.pricing.headline1')}{' '} {t('landing.pricing.headlineSketch')}{' '} {t('landing.pricing.headline2')}{' '} {t('landing.pricing.headlineScale')}

{t('landing.pricing.subheading')}

{/* Background glow */}
{/* Pricing Cards */}
{displayTiers.map((tier) => ( ))}
); } function TierCard({ tier }: { tier: Tier }) { const { t } = useTranslation(); const Icon = tier.icon; const [redirecting, setRedirecting] = useState(false); const [checkoutError, setCheckoutError] = useState(null); // One click from pricing to payment: create the checkout session and hand // off to the provider's hosted page (which collects the buyer's email). async function startCheckout() { setCheckoutError(null); setRedirecting(true); try { const data = await apiJson<{ url: string }>( '/api/v1/billing/license-checkout', { method: 'POST', body: { tier: tier.checkoutTier } }, t('landing.pricing.checkoutError') ); window.location.href = data.url; } catch (err) { setCheckoutError(err instanceof ApiError ? err.message : t('landing.pricing.checkoutError')); setRedirecting(false); } } return ( {/* Badge — rendered outside FresnelEdge so it paints above the edge highlight line */} {tier.popular && (
{/* Animated gradient border */} {/* Inner dark fill */} {/* Shimmer highlight */} {t('landing.pricing.popular')}
)}
{/* Mouse-tracking glow */}
{/* Icon + Tier name */}

{tier.name}

{/* Tagline */}

{tier.tagline}

{/* Price. Fixed row height (= the text-4xl/lg:text-5xl line height) with bottom alignment: the Contact-us card's smaller text would otherwise produce a shorter line box and pull its pill/CTA/divider up a few px relative to the sibling cards. */}
{tier.originalPrice && ( ${tier.originalPrice} )} {tier.accent.priceGradient ? ( ${tier.price} ) : tier.price === null ? ( {t('landing.pricing.contactUs')} ) : ( {t('common.free')} )}
{/* Price unit pill */}
{tier.unit}
{/* CTA */}
{tier.checkoutTier ? ( <> {checkoutError && (

{checkoutError}

)} ) : ( )}
{/* Divider */}
{/* Features */}
    {tier.features.map((feature) => (
  • {feature.included ? ( ) : ( )} {feature.text}
  • ))}
); } /** * GlowTracker with customizable glow color per tier. */ function GlowTrackerCustom({ color }: { color: string }) { const ref = useRef(null); const [pos, setPos] = useState({ x: 0, y: 0 }); const [visible, setVisible] = useState(false); useEffect(() => { const el = ref.current; const parent = el?.parentElement; if (!parent) return; const handleMove = (e: MouseEvent) => { const rect = parent.getBoundingClientRect(); setPos({ x: e.clientX - rect.left, y: e.clientY - rect.top }); }; const handleEnter = () => setVisible(true); const handleLeave = () => setVisible(false); parent.addEventListener('mousemove', handleMove); parent.addEventListener('mouseenter', handleEnter); parent.addEventListener('mouseleave', handleLeave); return () => { parent.removeEventListener('mousemove', handleMove); parent.removeEventListener('mouseenter', handleEnter); parent.removeEventListener('mouseleave', handleLeave); }; }, []); return (
); }