import { useState } from 'react'; import { ExternalLink } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Separator } from '@/components/ui/separator'; import { Muted } from '@/components/ui/typography'; import { CopyableEmail } from '@/components/copyable-email'; import { cn } from '@/lib/utils'; import { toUserMessage } from '@/lib/errors'; import { SUPPORT_EMAIL } from '@/lib/constants'; import { useCreateCheckoutSession, useCreatePortalSession, useNetworkBilling, } from '@/hooks/use-billing'; import { useNetworkUsage } from '@/hooks/use-network-usage'; import { useIsNetworkAdmin } from '@/hooks/use-networks'; import type { BillingCadence, BillingStatus } from '@/api/types'; import { platform } from '@/lib/platform'; function formatCents(cents: number): string { if (cents % 100 === 0) return `$${cents / 100}`; return `$${(cents / 100).toFixed(2)}`; } function formatDate(date: Date): string { return date.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric', }); } function PlanStatusBadge({ status }: { status: BillingStatus['plan_status'] }) { if (status === 'past_due') return Past due; if (status === 'canceled') return Canceled; if (status === 'trialing') return Trialing; return null; } function InfoRow({ label, value, }: { label: React.ReactNode; value: React.ReactNode; }) { return (
{label}
{value}
); } function CadenceOption({ value, label, perSeatCents, billedNote, saveBadge, selected, }: { value: BillingCadence; label: string; perSeatCents: number; billedNote: string; saveBadge?: string; selected: boolean; }) { return ( ); } function formatResetLocal(resetAt: Date): string { const time = resetAt.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', }); const now = new Date(); const isSameDay = resetAt.getFullYear() === now.getFullYear() && resetAt.getMonth() === now.getMonth() && resetAt.getDate() === now.getDate(); return `${isSameDay ? 'today' : 'tomorrow'} at ${time}`; } /** * Read-only plan + quota summary, sourced from `/usage` (member-accessible). * The `/billing` endpoint is admin-gated, so we can't use it for the * everyone-visible summary. */ function PlanSummary({ networkId }: { networkId: string }) { const { data: usage } = useNetworkUsage(networkId); if (!usage) return null; const isPro = usage.plan === 'pro'; return ( <> {isPro ? 'Llink Pro' : 'Llink Free'} {isPro ? 'Pro' : 'Free'}
} /> {!isPro && ( <> {usage.used} / {usage.limit} Resets {formatResetLocal(usage.reset_at)} ) : ( ) } /> )} ); } function FreeBilling({ networkId, billing, }: { networkId: string; billing: BillingStatus; }) { const createCheckout = useCreateCheckoutSession(networkId); const [cadence, setCadence] = useState('annual'); const handleUpgrade = () => { createCheckout.mutate(cadence, { onSuccess: ({ url }) => platform.link.openExternal(url), }); }; const annualPerSeatMonthlyCents = Math.round(billing.price_annual_cents / 12); const savingsPct = Math.round( (1 - annualPerSeatMonthlyCents / billing.price_monthly_cents) * 100, ); return ( <> setCadence(v as BillingCadence)} className="gap-0" > 0 ? `Save ${savingsPct}%` : undefined} selected={cadence === 'annual'} />
); } function ProBilling({ networkId, billing, }: { networkId: string; billing: BillingStatus; }) { const createPortal = useCreatePortalSession(networkId); const handleManage = () => { createPortal.mutate(undefined, { onSuccess: ({ url }) => platform.link.openExternal(url), }); }; const cadenceLabel = billing.cadence === 'annual' ? 'Annual' : 'Monthly'; const perSeatCents = billing.cadence === 'annual' ? Math.round(billing.price_annual_cents / 12) : billing.price_monthly_cents; const renewal = billing.current_period_end ? formatDate(billing.current_period_end) : null; return ( <> {billing.cancel_at_period_end && renewal && (
Your subscription is set to downgrade to Free on {renewal}.
)} {billing.plan_status === 'past_due' && (
Your last payment failed. Update your payment method to keep Pro active.
)} {`${cadenceLabel} · ${formatCents(perSeatCents)} / seat / mo`} } /> {renewal && ( <> )}
); } /** * Unified billing section. Shows the plan + usage summary to every member, * and the admin-only management controls (upgrade / portal) below. * * `/billing` is admin-gated, so the management controls are the only part * that depends on it — members rely on `/usage` for the summary. */ export function BillingSection({ networkId }: { networkId: string }) { const isAdmin = useIsNetworkAdmin(networkId); return ( <> {isAdmin && ( <> Billing support Invoices, receipts, or plan changes } value={} /> )} ); } function AdminBillingControls({ networkId }: { networkId: string }) { const { data: billing, isLoading, error } = useNetworkBilling(networkId); if (isLoading || !billing) { return (
Loading billing...
); } if (error) { return (
Couldn't load billing: {toUserMessage(error)}
); } if (billing.plan === 'pro') { return ; } return ; }