import { useState } from 'react';
import { Linking, Pressable, Text, View } from 'react-native';
import { ExternalLink } from 'lucide-react-native';
import { toast } from 'sonner-native';
import type { BillingCadence, BillingStatus } from '@/api/types';
import {
useCreateCheckoutSession,
useCreatePortalSession,
useNetworkBilling,
useNetworkUsage,
} from '@/hooks/use-billing';
import { useIsNetworkAdmin } from '@/hooks/use-networks';
import { toUserMessage } from '@/lib/errors';
import { cn } from '@/lib/utils';
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 InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
{label}
{value}
);
}
/**
* Plan + usage summary for every member, plus admin-only upgrade/manage
* controls. Mirrors desktop's BillingSection — `/usage` powers the
* everyone-visible summary; `/billing` (admin-gated) drives the controls.
* Stripe checkout/portal URLs are opened in the system browser.
*/
export function BillingSection({ networkId }: { networkId: string }) {
const isAdmin = useIsNetworkAdmin(networkId);
const { data: usage } = useNetworkUsage(networkId);
const isPro = usage?.plan === 'pro';
return (
Plan & billing
{isPro ? 'Llink Pro' : 'Llink Free'}
}
/>
{!isPro && usage?.limit != null ? (
{usage.used} / {usage.limit}
}
/>
) : null}
{isAdmin ? : null}
);
}
function AdminBillingControls({ networkId }: { networkId: string }) {
const {
data: billing,
isLoading,
error,
} = useNetworkBilling(networkId, true);
if (isLoading || !billing) {
return (
{error ? `Couldn’t load billing: ${toUserMessage(error)}` : 'Loading…'}
);
}
return billing.plan === 'pro' ? (
) : (
);
}
function FreeBilling({
networkId,
billing,
}: {
networkId: string;
billing: BillingStatus;
}) {
const createCheckout = useCreateCheckoutSession(networkId);
const [cadence, setCadence] = useState('annual');
const annualPerSeatMonthlyCents = Math.round(billing.price_annual_cents / 12);
const savingsPct = Math.round(
(1 - annualPerSeatMonthlyCents / billing.price_monthly_cents) * 100,
);
const handleUpgrade = async () => {
try {
const { url } = await createCheckout.mutateAsync(cadence);
await Linking.openURL(url);
} catch (err) {
toast.error(toUserMessage(err));
}
};
return (
0 ? `Save ${savingsPct}%` : undefined}
selected={cadence === 'annual'}
onPress={() => setCadence('annual')}
/>
setCadence('monthly')}
/>
{createCheckout.isPending ? 'Opening Stripe…' : 'Upgrade to Pro'}
);
}
function CadenceOption({
label,
note,
perSeatCents,
badge,
selected,
onPress,
}: {
label: string;
note: string;
perSeatCents: number;
badge?: string;
selected: boolean;
onPress: () => void;
}) {
return (
{label}
{badge ? (
{badge}
) : null}
{note}
{formatCents(perSeatCents)}
per seat / mo
);
}
function ProBilling({
networkId,
billing,
}: {
networkId: string;
billing: BillingStatus;
}) {
const createPortal = useCreatePortalSession(networkId);
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;
const handleManage = async () => {
try {
const { url } = await createPortal.mutateAsync();
await Linking.openURL(url);
} catch (err) {
toast.error(toUserMessage(err));
}
};
return (
{billing.cancel_at_period_end && renewal ? (
Your subscription downgrades to Free on {renewal}.
) : null}
{billing.plan_status === 'past_due' ? (
Your last payment failed. Update your payment method to keep Pro
active.
) : null}
{`${cadenceLabel} · ${formatCents(perSeatCents)} / seat / mo`}
}
/>
{billing.seats}}
/>
{renewal ? (
{renewal}}
/>
) : null}
{createPortal.isPending ? 'Opening Stripe…' : 'Manage subscription'}
);
}