Files
codexbar-mobile/hooks/useClaudeUsage.ts
T
space 2c31be11c4
CodexBar Mobile Build / build-android (push) Successful in 21m9s
CodexBar Mobile Build / release (push) Successful in 11s
by codex: feat. General Improvements
2026-06-25 14:27:58 +02:00

246 lines
6.8 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import {
fetchClaudeOrgs,
fetchClaudeUsage,
parseClaudeUsageResponse,
} from "@/lib/api/claudeApi";
import {
isClaudeSessionKeyValid,
validateClaudeSessionKey,
} from "@/lib/claudeCredentials";
import { RETRY_DELAY_MS } from "@/lib/constants";
import {
clearClaudeLastActiveOrg,
clearClaudeSessionKey,
clearClaudeUsageCache,
loadClaudeLastActiveOrg,
loadClaudeSessionKey,
loadClaudeUsageCache,
saveClaudeLastActiveOrg,
saveClaudeSessionKey,
saveClaudeUsageCache,
} from "@/lib/storage";
import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude";
type Status = "idle" | "loading" | "success" | "error";
function shouldRetry(error: unknown): boolean {
const message = error instanceof Error ? error.message : "";
return ![
"TOKEN_EXPIRED",
"NO_ORGS_FOUND",
"INVALID_CLAUDE_RESPONSE",
"INVALID_CLAUDE_ORGS_RESPONSE",
].includes(message);
}
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function requestClaudeUsage(auth: ClaudeAuth): Promise<ClaudeUsageResponse> {
let orgUuid = auth.lastActiveOrg?.trim();
if (!orgUuid) {
const orgs = await fetchClaudeOrgs(auth);
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
orgUuid = orgs[0].uuid;
await saveClaudeLastActiveOrg(orgUuid);
}
return fetchClaudeUsage(auth, orgUuid);
}
export function useClaudeUsage() {
const [auth, setAuth] = useState<ClaudeAuth | null>(null);
const [pendingKey, setPendingKey] = useState("");
const [pendingOrg, setPendingOrg] = useState("");
const [usage, setUsage] = useState<ClaudeUsageResponse | null>(null);
const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
const [status, setStatus] = useState<Status>("idle");
const [error, setError] = useState<string | null>(null);
const mounted = useRef(true);
const inFlight = useRef<{
key: string;
promise: Promise<void>;
} | null>(null);
const requestVersion = useRef(0);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const fetchUsage = useCallback((credentials: ClaudeAuth): Promise<void> => {
const sanitized = {
sessionKey: credentials.sessionKey.trim(),
lastActiveOrg: credentials.lastActiveOrg?.trim() || undefined,
};
const requestKey = `${sanitized.lastActiveOrg ?? ""}:${sanitized.sessionKey}`;
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: ClaudeUsageResponse;
try {
data = await requestClaudeUsage(sanitized);
} catch (firstError) {
if (!shouldRetry(firstError)) throw firstError;
await wait(RETRY_DELAY_MS);
if (version !== requestVersion.current) return;
data = await requestClaudeUsage(sanitized);
}
if (version !== requestVersion.current) return;
const fetchedAt = Date.now();
await saveClaudeUsageCache({ 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 clearClaudeSessionKey();
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(() => {
let active = true;
void Promise.all([
loadClaudeSessionKey(),
loadClaudeLastActiveOrg(),
loadClaudeUsageCache(),
]).then(async ([key, org, cache]) => {
if (!active) return;
const storedAuth = key
? { sessionKey: key.trim(), lastActiveOrg: org?.trim() || undefined }
: null;
if (storedAuth && cache) {
try {
if (!Number.isFinite(cache.lastFetchedAt)) {
throw new Error("INVALID_CACHE_TIMESTAMP");
}
const cachedUsage = parseClaudeUsageResponse(cache.data);
setUsage(cachedUsage);
setLastFetchedAt(cache.lastFetchedAt);
setStatus("success");
} catch {
await clearClaudeUsageCache();
}
}
setAuth(storedAuth);
if (storedAuth) await fetchUsage(storedAuth);
});
return () => {
active = false;
};
}, [fetchUsage]);
const saveKey = useCallback(async () => {
const trimmedKey = pendingKey.trim();
const validationError = validateClaudeSessionKey(trimmedKey);
if (validationError) {
setError(validationError);
return;
}
const trimmedOrg = pendingOrg.trim() || undefined;
await saveClaudeSessionKey(trimmedKey);
if (trimmedOrg) await saveClaudeLastActiveOrg(trimmedOrg);
else await clearClaudeLastActiveOrg();
const nextAuth = { sessionKey: trimmedKey, lastActiveOrg: trimmedOrg };
setAuth(nextAuth);
setPendingKey("");
setPendingOrg("");
setError(null);
await fetchUsage(nextAuth);
}, [fetchUsage, pendingKey, pendingOrg]);
const refresh = useCallback(async () => {
if (auth) await fetchUsage(auth);
}, [auth, fetchUsage]);
const reloadCredentials = useCallback(async () => {
const [key, org] = await Promise.all([
loadClaudeSessionKey(),
loadClaudeLastActiveOrg(),
]);
if (!key) {
requestVersion.current += 1;
setAuth(null);
setUsage(null);
setLastFetchedAt(null);
setStatus("idle");
return;
}
const stored = {
sessionKey: key.trim(),
lastActiveOrg: org?.trim() || undefined,
};
setAuth(stored);
await fetchUsage(stored);
}, [fetchUsage]);
const clearKey = useCallback(async () => {
requestVersion.current += 1;
await clearClaudeSessionKey();
setAuth(null);
setUsage(null);
setLastFetchedAt(null);
setStatus("idle");
setError(null);
}, []);
return {
auth,
pendingKey,
setPendingKey,
pendingOrg,
setPendingOrg,
usage,
lastFetchedAt,
status,
error,
keyValidationError: pendingKey
? validateClaudeSessionKey(pendingKey)
: null,
canSaveKey: isClaudeSessionKeyValid(pendingKey),
saveKey,
refresh,
reloadCredentials,
clearKey,
};
}