import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { plansApi, type PublicPlanResponse } from '@/api/plans' import { PageMeta } from '@/components/common/PageMeta' import { MarketingFooter } from '@/components/common/MarketingFooter' import { useAppConfig } from '@/hooks/useAppConfig' import '@/styles/landing.css' /* --------------------------------------------------------------------------- * v1 hardcoded comparison table * * The marketing /pricing page surfaces a small "what's in each plan" table. * Long-term, the source of truth for "plan X has feature Y" should be a * server-side feature-flag mapping (likely keyed off feature_flag.display_name * + plan_features). For v1 we hardcode the well-known features so we can ship * the page without a backend dependency. Replace this block when a server-side * feature mapping endpoint exists. * ------------------------------------------------------------------------- */ type PlanColumn = 'starter' | 'pro' | 'enterprise' const COMPARISON_ROWS: Array<{ feature: string values: Record }> = [ { feature: 'PSA Integration', values: { starter: false, pro: true, enterprise: true } }, { feature: 'KB Accelerator', values: { starter: false, pro: true, enterprise: true } }, { feature: 'AI Builder', values: { starter: true, pro: true, enterprise: true } }, { feature: 'Custom Branding', values: { starter: false, pro: false, enterprise: true } }, { feature: 'Priority Support', values: { starter: false, pro: true, enterprise: true } }, ] function formatPrice(cents: number | null | undefined): string { if (cents == null) return '' const dollars = cents / 100 // Whole dollars (no decimals) for marketing display. return `$${Math.round(dollars).toLocaleString()}` } function PricingNotFound() { return (

Page not found

This page is not available.

Go to login
) } interface PlanCardProps { plan: PublicPlanResponse | null fallback: { plan: string display_name: string description: string } recommended?: boolean hidePrice?: boolean ctaLabel: string ctaHref: string ctaTestId: string } function PlanCard({ plan, fallback, recommended, hidePrice, ctaLabel, ctaHref, ctaTestId }: PlanCardProps) { const displayName = plan?.display_name ?? fallback.display_name const description = plan?.description ?? fallback.description const monthlyCents = plan?.monthly_price_cents ?? null return (
{recommended && ( Recommended )}

{displayName}

{description}

{hidePrice ? (
Custom pricing
) : monthlyCents != null ? (
{formatPrice(monthlyCents)} / month
) : (
Contact us
)}
{ctaLabel}
) } export function PricingPage() { const appConfig = useAppConfig() const [plans, setPlans] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Fetch plans on mount when self-serve is enabled. useEffect(() => { if (appConfig.isLoading) return if (!appConfig.self_serve_enabled) return let cancelled = false plansApi .getPublic() .then((data) => { if (cancelled) return setPlans(data) setError(null) }) .catch(() => { if (cancelled) return // Non-fatal: page still renders with fallback descriptions and no // server-driven prices. The CTA still works via /register?plan=... setError('Unable to load live pricing.') }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [appConfig.isLoading, appConfig.self_serve_enabled]) // Self-serve disabled: render a 404-style fallback. Done after hooks so // the React rules-of-hooks invariant holds. if (!appConfig.isLoading && !appConfig.self_serve_enabled) { return ( <> ) } const planByName = (name: string) => plans?.find((p) => p.plan.toLowerCase() === name) ?? null return (
{/* ---- HERO ---- */}

Simple pricing for MSPs of every size

Try Pro free for 14 days. No credit card required.

{/* ---- PLAN CARDS ---- */}
{loading && (
Loading pricing…
)} {error && (
{error}
)} {/* ---- COMPARISON TABLE ---- */}

Compare plans

{COMPARISON_ROWS.map((row) => ( ))}
Feature Starter Pro Enterprise
{row.feature} {row.values.starter ? '✓' : '—'} {row.values.pro ? '✓' : '—'} {row.values.enterprise ? '✓' : '—'}
{/* ---- TESTIMONIAL SLOT (placeholder) ---- */}
"Pilot testimonials coming soon."
ResolutionFlow pilot, 2026
{/* ---- TRUST STRIP ---- */}
Built on Stripe + AWS · Encrypted in transit and at rest
) } export default PricingPage