Files
codexbar-mobile/app/(tabs)/index.tsx
T
Space-Banane dc7e497388
CodexBar Mobile Build / build-android (push) Successful in 17m26s
CodexBar Mobile Build / release (push) Successful in 12s
Fix Codex auth import and reset credit details
# Conflicts:
#	app/(tabs)/codex.tsx
#	app/(tabs)/index.tsx
#	app/(tabs)/settings.tsx
#	hooks/useCodexUsage.ts
#	lib/api/codexApi.ts
#	package-lock.json
#	package.json
2026-07-04 18:59:56 +02:00

339 lines
13 KiB
TypeScript

import {
ScrollView,
View,
Text,
TouchableOpacity,
RefreshControl,
ActivityIndicator,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useEffect } from "react";
import { MaterialIcons } from "@expo/vector-icons";
import { useCodexUsage } from "@/hooks/useCodexUsage";
import { useClaudeUsage } from "@/hooks/useClaudeUsage";
import { onUsageDataLoaded } from "@/lib/notifications";
import { ProgressBar } from "@/components/ProgressBar";
import { ResetCountdown } from "@/components/ResetCountdown";
import { LastUpdated } from "@/components/LastUpdated";
import { ErrorMessage } from "@/components/ErrorMessage";
import { ScreenErrorBoundary } from "@/components/ScreenErrorBoundary";
import { COLORS, getUsageColor } from "@/lib/constants";
import { windowLabel } from "@/lib/timeUtils";
function UsageRow({
label,
percent,
resetAtSeconds,
resetAtISO,
}: {
label: string;
percent: number;
resetAtSeconds?: number;
resetAtISO?: string;
}) {
const color = getUsageColor(percent);
return (
<View className="mb-4">
<View className="flex-row justify-between items-baseline mb-1.5">
<Text className="text-xs font-medium text-neutral-500 dark:text-neutral-400">
{label}
</Text>
<Text className="text-sm font-bold" style={{ color }}>
{Math.round(percent)}% used
</Text>
</View>
<ProgressBar percent={percent} />
<ResetCountdown resetAtSeconds={resetAtSeconds} resetAtISO={resetAtISO} />
</View>
);
}
function SetupPrompt({ label }: { label: string }) {
return (
<View>
<Text className="text-sm text-neutral-400 dark:text-neutral-500 mb-3">
{label}
</Text>
<TouchableOpacity
onPress={() => router.navigate("/(tabs)/settings")}
className="flex-row items-center self-start gap-x-1.5 px-3 py-2 rounded-xl border border-neutral-200 dark:border-neutral-700"
>
<Text className="text-xs font-semibold text-neutral-600 dark:text-neutral-300">
Set up in Settings
</Text>
<MaterialIcons name="arrow-forward" size={12} color="#737373" />
</TouchableOpacity>
</View>
);
}
function DashboardTabContent() {
const codex = useCodexUsage();
const claude = useClaudeUsage();
// Fire daily digest reschedule + threshold check whenever fresh data arrives
useEffect(() => {
if (codex.usage || claude.usage) {
void onUsageDataLoaded(
claude.usage,
codex.usage
);
}
}, [codex.lastFetchedAt, claude.lastFetchedAt]);
useFocusEffect(
useCallback(() => {
void codex.reloadCredentials();
void claude.reloadCredentials();
}, [codex.reloadCredentials, claude.reloadCredentials])
);
const isRefreshing =
codex.status === "loading" || claude.status === "loading";
const handleRefresh = () => {
void codex.refresh();
void claude.refresh();
};
const connectedCount = [codex.auth, claude.auth].filter(Boolean).length;
return (
<SafeAreaView
className="flex-1 bg-neutral-50 dark:bg-neutral-950"
edges={["top"]}
>
<ScrollView
className="flex-1"
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={COLORS.codex}
/>
}
>
{/* Header */}
<View className="flex-row items-center justify-between pt-6 pb-5">
<View className="flex-row items-center gap-x-3">
<View className="w-10 h-10 rounded-2xl bg-emerald-500 items-center justify-center">
<MaterialIcons name="bar-chart" size={20} color="white" />
</View>
<View>
<Text className="text-xl font-bold text-neutral-900 dark:text-white tracking-tight">
Codexbar
</Text>
<Text className="text-xs text-neutral-400 mt-0.5">
{connectedCount === 0
? "No services connected"
: connectedCount === 2
? "2 services connected"
: "1 of 2 services connected"}
</Text>
</View>
</View>
<TouchableOpacity
onPress={handleRefresh}
className="w-9 h-9 rounded-xl bg-neutral-100 dark:bg-neutral-800 items-center justify-center"
>
<MaterialIcons name="refresh" size={18} color={COLORS.codex} />
</TouchableOpacity>
</View>
{/* Codex card */}
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
<View style={{ height: 3, backgroundColor: COLORS.codex }} />
<View className="p-4">
<View className="flex-row items-center justify-between mb-4">
<View className="flex-row items-center gap-x-2.5">
<View
className="w-8 h-8 rounded-xl items-center justify-center"
style={{ backgroundColor: `${COLORS.codex}18` }}
>
<MaterialIcons name="auto-awesome" size={16} color={COLORS.codex} />
</View>
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
Codex CLI
</Text>
</View>
<View className="flex-row items-center gap-x-2">
{codex.usage?.planType && (
<View className="px-2.5 py-1 rounded-full bg-green-100 dark:bg-green-900/30">
<Text className="text-xs font-semibold text-green-700 dark:text-green-300 capitalize">
{codex.usage.planType}
</Text>
</View>
)}
{codex.auth && codex.status === "error" && (
<View className="w-2 h-2 rounded-full bg-red-500" />
)}
{codex.status === "success" && !codex.usage?.planType && (
<View className="w-2 h-2 rounded-full bg-green-500" />
)}
</View>
</View>
{!codex.auth && (
<SetupPrompt label="Import auth.json to see your Codex rate limits." />
)}
{codex.auth && codex.status === "loading" && !codex.usage && (
<ActivityIndicator
color={COLORS.codex}
style={{ marginVertical: 16 }}
/>
)}
{codex.auth && codex.status === "error" && (
<ErrorMessage message={codex.error} />
)}
{codex.usage && (
<View>
{codex.usage.primary && (
<UsageRow
label={`${windowLabel(codex.usage.primary.windowSeconds)} window`}
percent={codex.usage.primary.usedPercent}
resetAtSeconds={codex.usage.primary.resetsAt}
/>
)}
{codex.usage.secondary && (
<UsageRow
label={`${windowLabel(codex.usage.secondary.windowSeconds)} window`}
percent={codex.usage.secondary.usedPercent}
resetAtSeconds={codex.usage.secondary.resetsAt}
/>
)}
{!codex.usage.credits.unlimited &&
codex.usage.credits.hasCredits && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
<MaterialIcons name="toll" size={14} color="#a3a3a3" />
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
${Number(codex.usage.credits.balance).toFixed(2)}{" "}
credits remaining
</Text>
</View>
)}
{codex.usage.credits.unlimited && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800 flex-row items-center gap-x-2">
<MaterialIcons
name="all-inclusive"
size={14}
color={COLORS.codex}
/>
<Text className="text-xs text-green-600 dark:text-green-400">
Unlimited credits
</Text>
</View>
)}
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
{!!codex.usage.resetCoupons && (
<View className="mb-3">
<View className="flex-row items-center gap-x-2">
<MaterialIcons name="restart-alt" size={14} color={COLORS.codex} />
<Text className="text-xs text-neutral-500 dark:text-neutral-400">
{codex.usage.resetCoupons.availableCount ?? 0} reset credits
available
</Text>
</View>
{!!codex.usage.resetCoupons.nextExpiringCredit?.timeUntilExpiry && (
<Text className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
Next expires in {codex.usage.resetCoupons.nextExpiringCredit.timeUntilExpiry}
</Text>
)}
</View>
)}
<LastUpdated timestamp={codex.lastFetchedAt} />
</View>
</View>
)}
</View>
</View>
{/* Claude card */}
<View className="rounded-2xl bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 mb-4 overflow-hidden">
<View style={{ height: 3, backgroundColor: COLORS.claude }} />
<View className="p-4">
<View className="flex-row items-center justify-between mb-4">
<View className="flex-row items-center gap-x-2.5">
<View
className="w-8 h-8 rounded-xl items-center justify-center"
style={{ backgroundColor: `${COLORS.claude}18` }}
>
<MaterialIcons name="psychology" size={16} color={COLORS.claude} />
</View>
<Text className="text-sm font-semibold text-neutral-900 dark:text-white">
Claude.ai
</Text>
</View>
{claude.auth && claude.status === "error" && (
<View className="w-2 h-2 rounded-full bg-red-500" />
)}
{claude.status === "success" && (
<View className="w-2 h-2 rounded-full bg-green-500" />
)}
</View>
{!claude.auth && (
<SetupPrompt label="Add your session key to see Claude usage windows." />
)}
{claude.auth && claude.status === "loading" && !claude.usage && (
<ActivityIndicator
color={COLORS.claude}
style={{ marginVertical: 16 }}
/>
)}
{claude.auth && claude.status === "error" && (
<ErrorMessage message={claude.error} />
)}
{claude.usage && (
<View>
<UsageRow
label="5-hour window"
percent={claude.usage.five_hour.utilization}
resetAtISO={claude.usage.five_hour.resets_at}
/>
<UsageRow
label="7-day window"
percent={claude.usage.seven_day.utilization}
resetAtISO={claude.usage.seven_day.resets_at}
/>
{(claude.usage.seven_day_sonnet ||
claude.usage.seven_day_opus) && (
<View className="pt-3 border-t border-neutral-100 dark:border-neutral-800">
<Text className="text-xs font-semibold uppercase tracking-widest text-neutral-400 mb-3">
By Model (7-day)
</Text>
{claude.usage.seven_day_sonnet && (
<UsageRow
label="Sonnet"
percent={claude.usage.seven_day_sonnet.utilization}
/>
)}
{claude.usage.seven_day_opus && (
<UsageRow
label="Opus"
percent={claude.usage.seven_day_opus.utilization}
/>
)}
</View>
)}
<View className="mt-3 border-t border-neutral-100 pt-3 dark:border-neutral-800">
<LastUpdated timestamp={claude.lastFetchedAt} />
</View>
</View>
)}
</View>
</View>
</ScrollView>
</SafeAreaView>
);
}
export default function DashboardTab() {
return (
<ScreenErrorBoundary screenName="Dashboard">
<DashboardTabContent />
</ScreenErrorBoundary>
);
}