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"; 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 }) => window.electronLink.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 }) => window.electronLink.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 ; }