import { useAtomValue } from "@effect/atom-react";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { EnvironmentId } from "@t3tools/contracts";
import { providerClients } from "@t3tools/client-runtime/provider-clients";
import { normalizeProviderAccentColor } from "@t3tools/client-runtime/state/provider-instance-display";
import {
collectLimitAccounts,
collectExternalUsageLinks,
collectLimitNotices,
collectLimitPools,
cursorUsageWindowDetails,
displayLimitWindows,
formatResetsIn,
paceRemainingPercent,
remainingPercent,
type LimitAccount,
type LimitPoolWindow,
} from "@t3tools/shared/usageLimits";
import { Fragment, type ReactNode, useId, useState } from "react";
import { Linking, Pressable, View } from "react-native";
import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScrollView";
import { Defs, Path, Pattern, Rect, Svg } from "react-native-svg";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SymbolView } from "../../components/ui/AppSymbol";
import { AppText as Text } from "../../components/ui/AppText";
import { ProviderIcon } from "../../components/ProviderIcon";
import { SettingsScreen } from "../settings/components/SettingsScreen";
import { environmentPresentations } from "../../state/presentation";
import { ResetCredits } from "./UsageLimitsSection";
import { usageProviderKind, useProviderColors } from "./usageProviders";
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;
function accountName(account: LimitAccount) {
if (account.displayName) return account.displayName;
if (!account.email) return providerClients.get(account.driver)?.label ?? account.driver;
return emailInitials(account.email) || "Account";
}
function emailInitials(email: string) {
const [local = "", domain = ""] = email.split("@");
return `${local[0] ?? ""}${domain[0] ?? ""}`.toUpperCase();
}
function accountHue(email: string) {
let hash = 0;
for (let index = 0; index < email.length; index += 1) {
hash = (hash * 31 + email.charCodeAt(index)) | 0;
}
return Math.abs(hash) % 360;
}
// Same hashed hue as the web account chip, so an account keeps its color across clients.
function AccountChip({ email }: { readonly email: string }) {
const hue = accountHue(email);
return (
{emailInitials(email)}
);
}
// Same gap-and-tick as the Limits rows: two clipped copies of the segment leave a see-through gap.
function PaceGap({
timeLeft,
children,
}: {
readonly timeLeft: number | null;
readonly children: ReactNode;
}) {
const [width, setWidth] = useState(0);
const tickX =
timeLeft === null || width < 6
? null
: Math.min(Math.max((width * timeLeft) / 100, 3), width - 3);
return (
setWidth(event.nativeEvent.layout.width)}
>
{tickX === null
? children
: (
[
[0, tickX - 3],
[tickX + 3, width],
] as const
).map(([start, end]) => (
{children}
))}
{tickX !== null ? (
) : null}
);
}
function AccountSegment({
remaining,
color,
refillPercent,
}: {
readonly remaining: number;
readonly color: string;
readonly refillPercent: number;
}) {
const patternId = useId().replace(/:/g, "");
const [size, setSize] = useState({ width: 0, height: 0 });
return (
{
const { width, height } = event.nativeEvent.layout;
setSize((previous) =>
previous.width === width && previous.height === height ? previous : { width, height },
);
}}
>
{size.width > 0 && size.height > 0 ? (
// Numeric canvas dimensions clear Android SVG geometry cached before a segment resized.
) : null}
);
}
function PoolWindowCard({
pool,
color,
now,
environmentIds,
label,
description,
wide,
}: {
readonly pool: LimitPoolWindow;
readonly color: string;
readonly now: number;
readonly environmentIds: readonly string[] | null;
readonly label?: string;
readonly description?: string;
readonly wide: boolean;
}) {
const navigation = useNavigation();
const [barWidth, setBarWidth] = useState(0);
// Each segment needs room for its account, percent and reset to label itself.
const segmentWidth = barWidth / Math.max(pool.columns.length, 1);
const inlineLabels = wide && segmentWidth >= 96;
const inlineReset = wide && segmentWidth >= 150;
const inlineName = wide && segmentWidth >= 220;
const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
const openAccount = (account: LimitAccount) =>
navigation.navigate("SettingsUsageAccount", {
accountKey: account.key,
windowId: pool.id,
windowKind: pool.kind,
environmentIds,
now,
});
const summary = (
<>
{label ?? pool.label}
{pool.remainingPercent}%
left
{pool.pace ? (
{PACE_LABEL[pool.pace]}
) : null}
{description ? {description} : null}
{nextRefill ? (
↻ +{nextRefill.restoresPercent}%{" "}
{formatResetsIn(nextRefill.member.window, now)
?.replace("resets in ", "in ")
.replace("resets now", "now")}
) : null}
>
);
const bar = (
setBarWidth(event.nativeEvent.layout.width)}
>
{pool.columns.map((member, index) => {
if (!member.window)
return (
);
const { account, window } = member;
const timeLeft = paceRemainingPercent(window, now);
const credits = account.limits.resetCredits?.availableCount ?? 0;
const resetsIn = formatResetsIn(window, now)?.replace("resets in ", "");
return (
openAccount(account)}
className={wide ? "h-9 min-w-0 flex-1 rounded-md" : "h-7 min-w-0 flex-1 rounded-md"}
>
{inlineLabels ? (
{account.email ? : null}
{account.email && !(account.displayName && inlineName) ? null : (
{accountName(account)}
)}
{remainingPercent(window)}%
{inlineReset && (resetsIn || credits) ? (
{resetsIn ? (
<>
{resetsIn}
>
) : null}
{credits ? (
<>
{credits}
>
) : null}
) : null}
) : (
{index + 1}
)}
);
})}
);
const legend = (
{pool.columns.map(({ account, window }, index) => {
if (!window) return null;
const segmentColor = normalizeProviderAccentColor(account.accentColor) ?? color;
const credits = account.limits.resetCredits?.availableCount ?? 0;
const resetsIn = formatResetsIn(window, now);
return (
openAccount(account)}
className="min-h-[44px] flex-row items-center gap-2 active:opacity-60"
>
{index + 1}
{accountName(account)}
{remainingPercent(window)}%
{resetsIn ? (
<>
{resetsIn.replace("resets in ", "")}
>
) : null}
{credits ? (
<>
{resetsIn ? · : null}
{credits}
>
) : null}
);
})}
);
if (wide) {
return (
{summary}
{bar}
{inlineLabels ? null : legend}
);
}
return (
{summary}
{bar}
{legend}
);
}
export function UsageLimitsSection({
now,
failedLabels,
selectedEnvironmentIds,
wide = false,
}: {
readonly now: number;
readonly failedLabels: readonly string[];
readonly selectedEnvironmentIds: ReadonlySet | null;
readonly wide?: boolean;
}) {
const presentations = useAtomValue(environmentPresentations.presentationsWithProvidersAtom);
const selected =
selectedEnvironmentIds === null
? presentations
: new Map([...presentations].filter(([id]) => selectedEnvironmentIds.has(id)));
const pools = collectLimitPools(collectLimitAccounts(selected), now);
const notices = collectLimitNotices(selected);
const externalLinks = collectExternalUsageLinks(selected);
const colors = useProviderColors();
return (
{pools.length === 0 &&
notices.length === 0 &&
failedLabels.length === 0 &&
externalLinks.length === 0 ? (
{selected.size === 0
? "Select an environment to see limits."
: "No provider on the selected environments reports subscription limits."}
) : null}
{pools.map((pool) => {
const windows = displayLimitWindows(pool);
return (
{providerClients.get(pool.driver)?.label ?? pool.driver}
{pool.accounts
.filter((account) => account.limits.windows.length === 0)
.map((account) => (
{account.email ? : null}
{accountName(account)}
Limits unavailable
))}
{windows.map((window) => {
const details =
pool.driver === "cursor" ? cursorUsageWindowDetails(window.id) : undefined;
return (
);
})}
);
})}
{externalLinks.map((link) => (
{link.label}
{link.accounts.join(", ")}
{link.message ? (
{link.message}
) : null}
void Linking.openURL(link.url).catch(() => undefined)}
>
Manage usage
))}
{notices.length > 0 || failedLabels.length > 0 ? (
{notices.map((notice) => (
{notice}
))}
{failedLabels.length > 0 ? (
{failedLabels.join(", ")} could not refresh limits. Showing the last known values.
) : null}
) : null}
);
}
type AccountScreenProps = StaticScreenProps<{
accountKey: string;
windowId: string;
windowKind: LimitPoolWindow["kind"];
environmentIds: readonly string[] | null;
now: number;
}>;
export function UsageLimitAccountScreen({ route }: AccountScreenProps) {
const insets = useSafeAreaInsets();
const presentations = useAtomValue(environmentPresentations.presentationsWithProvidersAtom);
const { accountKey, windowId, windowKind, environmentIds, now } = route.params;
const selectedIds =
environmentIds === null ? null : new Set(environmentIds.map((id) => EnvironmentId.make(id)));
const selected =
selectedIds === null
? presentations
: new Map([...presentations].filter(([id]) => selectedIds.has(id)));
const accounts = collectLimitAccounts(selected);
const account = accounts.find((candidate) => candidate.key === accountKey);
const pool = collectLimitPools(accounts, now)
.find((candidate) => candidate.driver === account?.driver)
?.windows.find((candidate) => candidate.id === windowId && candidate.kind === windowKind);
const window = pool?.members.find((member) => member.account.key === accountKey)?.window;
const evenPace = window ? paceRemainingPercent(window, now) : null;
const reset = pool?.resets.find((candidate) => candidate.member.account.key === accountKey);
const [revealed, setRevealed] = useState(false);
return (
{!account || !window ? (
This account is no longer reporting limits on the selected environments.
) : (
<>
{account.displayName ??
providerClients.get(account.driver)?.label ??
account.driver}
{account.email ? (
setRevealed((value) => !value)}
className="min-h-[44px] justify-center"
>
{revealed ? account.email : "••••••@••••••"}
) : null}
{account.plan ? (
{account.plan}
) : null}
{window.label}
{remainingPercent(window)}% left
{window.resetsAt ? (
Resets{" "}
{new Date(window.resetsAt).toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "short",
})}
) : null}
{evenPace !== null ? (
Even pace: {evenPace}% left by now
) : null}
{reset && reset.restoresPercent > 0 ? (
Restores {reset.restoresPercent}% of the pool
) : null}
{account.environments.length ? "Signed in" : "Source"}
{account.environments.length ? (
account.environments.map((environment) => (
{environment.label}
))
) : (
{account.sourceLabel}
)}
{account.redeem && account.limits.resetCredits ? (
Reset credits
) : null}
>
)}
);
}