d5b0f9c833
- Onboarding flow (welcome -> Codex setup -> Claude setup -> done) with dark-themed screens and step indicators; completion stored in SecureStore - Smart root redirect checks onboarding state on launch - Dashboard tab shows both services at a glance via ServiceStatusCard; useFocusEffect + reload() keeps it fresh when credentials change in tabs - Settings tab manages credentials and exposes re-run setup wizard - Polished Codex and Claude detail screens with service headers and empty states - 4-tab layout (Home, Codex, Claude, Settings) with dark-mode tab bar - Both hooks gain reload() for cross-tab credential refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import { fetchCodexUsage } from "@/lib/api/codexApi";
|
|
import { loadCodexAuth, saveCodexAuth, clearCodexAuth } from "@/lib/storage";
|
|
import { pickAndReadCodexAuth } from "@/lib/fileReader";
|
|
import type { CodexUsageResponse, CodexAuth } from "@/types/codex";
|
|
|
|
type Status = "idle" | "loading" | "success" | "error";
|
|
|
|
async function doFetch(
|
|
auth: CodexAuth,
|
|
set: {
|
|
auth: (v: CodexAuth | null) => void;
|
|
usage: (v: CodexUsageResponse | null) => void;
|
|
status: (v: Status) => void;
|
|
error: (v: string | null) => void;
|
|
}
|
|
) {
|
|
set.status("loading");
|
|
set.error(null);
|
|
try {
|
|
const data = await fetchCodexUsage(auth);
|
|
set.usage(data);
|
|
set.status("success");
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
|
set.error(msg);
|
|
set.status("error");
|
|
if (msg === "TOKEN_EXPIRED") {
|
|
await clearCodexAuth();
|
|
set.auth(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useCodexUsage() {
|
|
const [auth, setAuth] = useState<CodexAuth | null>(null);
|
|
const [usage, setUsage] = useState<CodexUsageResponse | null>(null);
|
|
const [status, setStatus] = useState<Status>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError };
|
|
|
|
useEffect(() => {
|
|
loadCodexAuth().then((stored) => {
|
|
if (stored) setAuth(stored);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (auth) void doFetch(auth, setters);
|
|
}, [auth]);
|
|
|
|
const importAuthFile = useCallback(async () => {
|
|
try {
|
|
const parsed = await pickAndReadCodexAuth();
|
|
await saveCodexAuth(parsed);
|
|
setAuth(parsed);
|
|
setError(null);
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : "UNKNOWN_ERROR";
|
|
if (msg !== "PICKER_CANCELLED") setError(msg);
|
|
}
|
|
}, []);
|
|
|
|
const refresh = useCallback(async () => {
|
|
if (!auth) return;
|
|
await doFetch(auth, setters);
|
|
}, [auth]);
|
|
|
|
// Re-reads auth from storage and fetches fresh data. Call from useFocusEffect
|
|
// so the dashboard stays current when credentials are saved from another tab.
|
|
const reload = useCallback(async () => {
|
|
const stored = await loadCodexAuth();
|
|
if (!stored) {
|
|
setAuth(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
return;
|
|
}
|
|
// Always pass a fresh object so the [auth] effect fires even if the content
|
|
// is identical (JSON.parse returns a new reference each time, which is fine).
|
|
setAuth(stored);
|
|
}, []);
|
|
|
|
const clearAuth = useCallback(async () => {
|
|
await clearCodexAuth();
|
|
setAuth(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
setError(null);
|
|
}, []);
|
|
|
|
return { auth, usage, status, error, importAuthFile, refresh, reload, clearAuth };
|
|
}
|