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'} ); }