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 (
);
}
function CadenceOption({
value,
label,
perSeatCents,
billedNote,
saveBadge,
selected,
}: {
value: BillingCadence;
label: string;
perSeatCents: number;
billedNote: string;
saveBadge?: string;
selected: boolean;
}) {
return (
{label}
{saveBadge &&
{saveBadge} }
{billedNote}
{formatCents(perSeatCents)}
per seat / mo
);
}
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"}
/>
{createCheckout.isPending ? "Opening Stripe..." : "Upgrade to Pro"}
>
);
}
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 && (
<>
>
)}
{createPortal.isPending
? "Opening Stripe..."
: "Manage subscription"}
>
);
}
/**
* 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 ;
}