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>
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import { fetchClaudeOrgs, fetchClaudeUsage } from "@/lib/api/claudeApi";
|
|
import {
|
|
loadClaudeSessionKey,
|
|
saveClaudeSessionKey,
|
|
clearClaudeSessionKey,
|
|
} from "@/lib/storage";
|
|
import type { ClaudeUsageResponse } from "@/types/claude";
|
|
|
|
type Status = "idle" | "loading" | "success" | "error";
|
|
|
|
async function doFetch(
|
|
sessionKey: string,
|
|
set: {
|
|
sessionKey: (v: string | null) => void;
|
|
usage: (v: ClaudeUsageResponse | null) => void;
|
|
status: (v: Status) => void;
|
|
error: (v: string | null) => void;
|
|
}
|
|
) {
|
|
set.status("loading");
|
|
set.error(null);
|
|
try {
|
|
const orgs = await fetchClaudeOrgs(sessionKey);
|
|
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
|
|
const orgUuid = orgs[0].uuid;
|
|
const data = await fetchClaudeUsage(sessionKey, orgUuid);
|
|
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 clearClaudeSessionKey();
|
|
set.sessionKey(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useClaudeUsage() {
|
|
const [sessionKey, setSessionKey] = useState<string | null>(null);
|
|
const [pendingKey, setPendingKey] = useState("");
|
|
const [usage, setUsage] = useState<ClaudeUsageResponse | null>(null);
|
|
const [status, setStatus] = useState<Status>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const setters = { sessionKey: setSessionKey, usage: setUsage, status: setStatus, error: setError };
|
|
|
|
useEffect(() => {
|
|
loadClaudeSessionKey().then((stored) => {
|
|
if (stored) setSessionKey(stored);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (sessionKey) void doFetch(sessionKey, setters);
|
|
}, [sessionKey]);
|
|
|
|
const saveKey = useCallback(async () => {
|
|
const trimmed = pendingKey.trim();
|
|
if (!trimmed.startsWith("sk-ant-")) {
|
|
setError("Session key must start with sk-ant-");
|
|
return;
|
|
}
|
|
await saveClaudeSessionKey(trimmed);
|
|
setSessionKey(trimmed);
|
|
setPendingKey("");
|
|
setError(null);
|
|
}, [pendingKey]);
|
|
|
|
const refresh = useCallback(async () => {
|
|
if (!sessionKey) return;
|
|
await doFetch(sessionKey, setters);
|
|
}, [sessionKey]);
|
|
|
|
// Re-reads the session key from storage and fetches fresh data. Call from
|
|
// useFocusEffect so the dashboard stays current when the key is saved from
|
|
// another tab. If the string value is unchanged, React bails out on setState
|
|
// so we trigger the fetch directly.
|
|
const reload = useCallback(async () => {
|
|
const stored = await loadClaudeSessionKey();
|
|
if (!stored) {
|
|
setSessionKey(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
return;
|
|
}
|
|
// If already set to the same string, setState bails out and [sessionKey]
|
|
// effect won't fire, so call the fetch directly.
|
|
setSessionKey(stored);
|
|
void doFetch(stored, setters);
|
|
}, []);
|
|
|
|
const clearKey = useCallback(async () => {
|
|
await clearClaudeSessionKey();
|
|
setSessionKey(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
setError(null);
|
|
}, []);
|
|
|
|
return {
|
|
sessionKey,
|
|
pendingKey,
|
|
setPendingKey,
|
|
usage,
|
|
status,
|
|
error,
|
|
saveKey,
|
|
refresh,
|
|
reload,
|
|
clearKey,
|
|
};
|
|
}
|