by codex: feat. General Improvements
CodexBar Mobile Build / build-android (push) Successful in 21m9s
CodexBar Mobile Build / release (push) Successful in 11s

This commit is contained in:
2026-06-25 14:27:58 +02:00
parent 9dd17cf565
commit 2c31be11c4
33 changed files with 1217 additions and 296 deletions
+143 -48
View File
@@ -1,54 +1,138 @@
import { useState, useCallback, useEffect } from "react";
import { fetchCodexUsage } from "@/lib/api/codexApi";
import { loadCodexAuth, saveCodexAuth, clearCodexAuth } from "@/lib/storage";
import { useCallback, useEffect, useRef, useState } from "react";
import {
fetchCodexUsage,
parseCodexUsageResponse,
} from "@/lib/api/codexApi";
import { RETRY_DELAY_MS } from "@/lib/constants";
import { pickAndReadCodexAuth } from "@/lib/fileReader";
import type { CodexUsageResponse, CodexAuth } from "@/types/codex";
import {
clearCodexAuth,
clearCodexUsageCache,
loadCodexAuth,
loadCodexUsageCache,
saveCodexAuth,
saveCodexUsageCache,
} from "@/lib/storage";
import type { CodexAuth, CodexUsageResponse } 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);
}
}
function shouldRetry(error: unknown): boolean {
const message = error instanceof Error ? error.message : "";
return (
message !== "TOKEN_EXPIRED" &&
message !== "INVALID_CODEX_RESPONSE"
);
}
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function useCodexUsage() {
const [auth, setAuth] = useState<CodexAuth | null>(null);
const [usage, setUsage] = useState<CodexUsageResponse | null>(null);
const [lastFetchedAt, setLastFetchedAt] = useState<number | 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 };
const mounted = useRef(true);
const inFlight = useRef<{
key: string;
promise: Promise<void>;
} | null>(null);
const requestVersion = useRef(0);
useEffect(() => {
loadCodexAuth().then((stored) => {
if (stored) setAuth(stored);
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const fetchUsage = useCallback((credentials: CodexAuth): Promise<void> => {
const requestKey = `${credentials.accountId ?? ""}:${credentials.accessToken}`;
if (inFlight.current?.key === requestKey) return inFlight.current.promise;
const version = ++requestVersion.current;
const promise = (async () => {
if (mounted.current) {
setStatus("loading");
setError(null);
}
try {
let data: CodexUsageResponse;
try {
data = await fetchCodexUsage(credentials);
} catch (firstError) {
if (!shouldRetry(firstError)) throw firstError;
await wait(RETRY_DELAY_MS);
if (version !== requestVersion.current) return;
data = await fetchCodexUsage(credentials);
}
if (version !== requestVersion.current) return;
const fetchedAt = Date.now();
await saveCodexUsageCache({ data, lastFetchedAt: fetchedAt });
if (mounted.current && version === requestVersion.current) {
setUsage(data);
setLastFetchedAt(fetchedAt);
setStatus("success");
}
} catch (caught: unknown) {
if (version !== requestVersion.current) return;
const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR";
if (message === "TOKEN_EXPIRED") {
await clearCodexAuth();
if (mounted.current) {
setAuth(null);
setUsage(null);
setLastFetchedAt(null);
}
}
if (mounted.current) {
setError(message);
setStatus("error");
}
}
})().finally(() => {
if (inFlight.current?.promise === promise) inFlight.current = null;
});
inFlight.current = { key: requestKey, promise };
return promise;
}, []);
useEffect(() => {
if (auth) void doFetch(auth, setters);
}, [auth]);
let active = true;
void Promise.all([loadCodexAuth(), loadCodexUsageCache()]).then(
async ([storedAuth, cache]) => {
if (!active) return;
if (storedAuth && cache) {
try {
if (!Number.isFinite(cache.lastFetchedAt)) {
throw new Error("INVALID_CACHE_TIMESTAMP");
}
const cachedUsage = parseCodexUsageResponse(cache.data);
setUsage(cachedUsage);
setLastFetchedAt(cache.lastFetchedAt);
setStatus("success");
} catch {
await clearCodexUsageCache();
}
}
setAuth(storedAuth);
if (storedAuth) await fetchUsage(storedAuth);
}
);
return () => {
active = false;
};
}, [fetchUsage]);
const importAuthFile = useCallback(async () => {
try {
@@ -56,39 +140,50 @@ export function useCodexUsage() {
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);
await fetchUsage(parsed);
} catch (caught: unknown) {
const message = caught instanceof Error ? caught.message : "UNKNOWN_ERROR";
if (message !== "PICKER_CANCELLED") setError(message);
}
}, []);
}, [fetchUsage]);
const refresh = useCallback(async () => {
if (!auth) return;
await doFetch(auth, setters);
}, [auth]);
if (auth) await fetchUsage(auth);
}, [auth, fetchUsage]);
// 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 reloadCredentials = useCallback(async () => {
const stored = await loadCodexAuth();
if (!stored) {
requestVersion.current += 1;
setAuth(null);
setUsage(null);
setLastFetchedAt(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);
}, []);
await fetchUsage(stored);
}, [fetchUsage]);
const clearAuth = useCallback(async () => {
requestVersion.current += 1;
await clearCodexAuth();
setAuth(null);
setUsage(null);
setLastFetchedAt(null);
setStatus("idle");
setError(null);
}, []);
return { auth, usage, status, error, importAuthFile, refresh, reload, clearAuth };
return {
auth,
usage,
lastFetchedAt,
status,
error,
importAuthFile,
refresh,
reloadCredentials,
clearAuth,
};
}