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 (
);
}
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 }) => 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'}
/>
{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 }) => 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 && (
<>
>
)}
{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 ;
}