'use client'; import { Button } from '@/components/ui/button'; import { apiFetch } from '@/lib/api'; import { Loader2 } from 'lucide-react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { Suspense, useCallback, useEffect, useState } from 'react'; type Plan = 'hobby' | 'pro' | 'team' | 'enterprise'; type Tier = 'pro_monthly' | 'pro_yearly' | 'team_monthly' | 'team_yearly'; interface BillingStatus { plan: Plan; hasCustomer: boolean; hasSubscription: boolean; suspended: boolean; suspendedReason: string | null; } const PLAN_LABEL: Record = { hobby: 'Hobby (free)', pro: 'Pro', team: 'Team', enterprise: 'Enterprise', }; function BillingInner() { const router = useRouter(); const searchParams = useSearchParams(); const justSubscribed = searchParams.get('success') === 'true'; const cancelledCheckout = searchParams.get('cancelled') === 'true'; const autoUpgradeTier = searchParams.get('tier') as Tier | null; const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); const loadStatus = useCallback(() => { apiFetch('/v1/billing/status') .then(setStatus) .catch((e) => { const err = e as { status?: number }; if (err.status === 401) router.push('/login?returnTo=/settings/billing'); else setError((e as Error).message); }); }, [router]); useEffect(() => { loadStatus(); }, [loadStatus]); // Came back from Stripe Checkout? Poll briefly — the webhook usually arrives // within a couple seconds and flips the plan. useEffect(() => { if (!justSubscribed) return; let tries = 0; const id = setInterval(() => { tries += 1; loadStatus(); if (tries >= 6) clearInterval(id); }, 1500); return () => clearInterval(id); }, [justSubscribed, loadStatus]); const startCheckout = useCallback( async (tier: Tier) => { setBusy(tier); setError(null); try { const res = await apiFetch<{ url: string }>('/v1/billing/checkout-session', { method: 'POST', body: JSON.stringify({ tier }), }); window.location.href = res.url; } catch (e) { setBusy(null); const detail = (e as { detail?: { detail?: string; error?: string } }).detail; setError(detail?.detail ?? detail?.error ?? (e as Error).message); } }, [], ); // Auto-fire checkout when the user lands here from the pricing page CTA. useEffect(() => { if (!autoUpgradeTier || !status) return; if (status.hasSubscription) return; // already paying — don't redirect void startCheckout(autoUpgradeTier); }, [autoUpgradeTier, status, startCheckout]); async function openPortal() { setBusy('portal'); setError(null); try { const res = await apiFetch<{ url: string }>('/v1/billing/portal', { method: 'POST', body: '{}', }); window.location.href = res.url; } catch (e) { setBusy(null); const detail = (e as { detail?: { detail?: string; error?: string } }).detail; setError(detail?.detail ?? detail?.error ?? (e as Error).message); } } if (!status && !error) { return (

Loading billing…

); } return (

Billing

Manage your subscription, payment method, and invoices.

{cancelledCheckout && (
Checkout cancelled. No charge made.
)} {justSubscribed && (
Subscription active. Plan will update within a few seconds — refreshing…
)} {status?.suspended && (
Subscription paused — {status.suspendedReason ?? 'payment issue'}. Update your payment method below to restore access. Existing servers keep running.
)} {error && (
{error}
)} {status && (
Current plan
{PLAN_LABEL[status.plan]}
{status.hasSubscription && ( )}
{!status.hasSubscription && (

You're on the free tier. Upgrade below to unlock more servers, faster Claude analysis and higher daily limits.

)}
)} {status && !status.hasSubscription && ( <>

Choose a plan

Annual saves 2 months. VAT calculated automatically based on your billing address. Cancel any time from the billing portal — service continues until end of period.

Need Enterprise (BYOC, SSO, EU-data-residency)?{' '} Contact sales .

)}
← Compare all plans
); } function TierCard({ name, monthly, yearly, features, busy, onSubscribe, monthlyTier, yearlyTier, highlight, }: { name: string; monthly: number; yearly: number; features: string[]; busy: string | null; onSubscribe: (tier: Tier) => void; monthlyTier: Tier; yearlyTier: Tier; highlight?: boolean; }) { return (
{name}
€{monthly} / month
    {features.map((f) => (
  • — {f}
  • ))}
); } export default function BillingPage() { return ( } > ); }