diff --git a/js/mobile/src/features/network-settings/BillingSection.tsx b/js/mobile/src/features/network-settings/BillingSection.tsx
new file mode 100644
index 0000000..8d8dbb1
--- /dev/null
+++ b/js/mobile/src/features/network-settings/BillingSection.tsx
@@ -0,0 +1,281 @@
+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'}
+
+
+
+ );
+}
diff --git a/js/mobile/src/features/network-settings/NetworkSettingsScreen.tsx b/js/mobile/src/features/network-settings/NetworkSettingsScreen.tsx
index 0331a67..9ae3632 100644
--- a/js/mobile/src/features/network-settings/NetworkSettingsScreen.tsx
+++ b/js/mobile/src/features/network-settings/NetworkSettingsScreen.tsx
@@ -15,6 +15,7 @@ import { Avatar } from '@/components/Avatar';
import { toUserMessage } from '@/lib/errors';
import type { RootStackScreenProps } from '@/navigation/types';
import { AddMembersSheet } from './AddMembersSheet';
+import { BillingSection } from './BillingSection';
export function NetworkSettingsScreen({
route,
@@ -157,6 +158,10 @@ export function NetworkSettingsScreen({
)}
) : null}
+
+
+
+
{isAdmin ? (
diff --git a/js/mobile/src/hooks/use-billing.ts b/js/mobile/src/hooks/use-billing.ts
new file mode 100644
index 0000000..2163efa
--- /dev/null
+++ b/js/mobile/src/hooks/use-billing.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { apiClient } from '@/api/client';
+import type { BillingCadence } from '@/api/types';
+
+/** Plan + quota summary. Member-accessible (sourced from `/usage`). */
+export function useNetworkUsage(networkId: string) {
+ return useQuery({
+ queryKey: ['network-usage', networkId],
+ queryFn: () => apiClient.getNetworkUsage(networkId),
+ });
+}
+
+/** Full billing status. Admin-gated (`/billing`). */
+export function useNetworkBilling(networkId: string, enabled: boolean) {
+ return useQuery({
+ queryKey: ['network-billing', networkId],
+ queryFn: () => apiClient.getNetworkBilling(networkId),
+ enabled,
+ });
+}
+
+export function useCreateCheckoutSession(networkId: string) {
+ return useMutation({
+ mutationFn: (cadence: BillingCadence) =>
+ apiClient.createCheckoutSession(networkId, cadence),
+ });
+}
+
+export function useCreatePortalSession(networkId: string) {
+ return useMutation({
+ mutationFn: () => apiClient.createPortalSession(networkId),
+ });
+}