125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import { useState, useCallback, useEffect } from "react";
|
|
import { fetchClaudeOrgs, fetchClaudeUsage } from "@/lib/api/claudeApi";
|
|
import {
|
|
loadClaudeSessionKey,
|
|
saveClaudeSessionKey,
|
|
clearClaudeSessionKey,
|
|
loadClaudeLastActiveOrg,
|
|
saveClaudeLastActiveOrg,
|
|
clearClaudeLastActiveOrg,
|
|
} from "@/lib/storage";
|
|
import type { ClaudeAuth, ClaudeUsageResponse } from "@/types/claude";
|
|
|
|
type Status = "idle" | "loading" | "success" | "error";
|
|
|
|
async function doFetch(
|
|
auth: ClaudeAuth,
|
|
set: {
|
|
auth: (v: ClaudeAuth | null) => void;
|
|
usage: (v: ClaudeUsageResponse | null) => void;
|
|
status: (v: Status) => void;
|
|
error: (v: string | null) => void;
|
|
}
|
|
) {
|
|
set.status("loading");
|
|
set.error(null);
|
|
try {
|
|
let orgUuid = auth.lastActiveOrg;
|
|
if (!orgUuid) {
|
|
const orgs = await fetchClaudeOrgs(auth);
|
|
if (!orgs.length) throw new Error("NO_ORGS_FOUND");
|
|
orgUuid = orgs[0].uuid;
|
|
}
|
|
const data = await fetchClaudeUsage(auth, 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.auth(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 [status, setStatus] = useState<Status>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const setters = { auth: setAuth, usage: setUsage, status: setStatus, error: setError };
|
|
|
|
useEffect(() => {
|
|
Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]).then(
|
|
([key, org]) => {
|
|
if (key) setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
|
}
|
|
);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (auth) void doFetch(auth, setters);
|
|
}, [auth]);
|
|
|
|
const saveKey = useCallback(async () => {
|
|
const trimmed = pendingKey.trim();
|
|
if (!trimmed.startsWith("sk-ant-")) {
|
|
setError("Session key must start with sk-ant-");
|
|
return;
|
|
}
|
|
const orgTrimmed = pendingOrg.trim() || undefined;
|
|
await saveClaudeSessionKey(trimmed);
|
|
if (orgTrimmed) await saveClaudeLastActiveOrg(orgTrimmed);
|
|
else await clearClaudeLastActiveOrg();
|
|
setAuth({ sessionKey: trimmed, lastActiveOrg: orgTrimmed });
|
|
setPendingKey("");
|
|
setPendingOrg("");
|
|
setError(null);
|
|
}, [pendingKey, pendingOrg]);
|
|
|
|
const refresh = useCallback(async () => {
|
|
if (!auth) return;
|
|
await doFetch(auth, setters);
|
|
}, [auth]);
|
|
|
|
const reload = useCallback(async () => {
|
|
const [key, org] = await Promise.all([loadClaudeSessionKey(), loadClaudeLastActiveOrg()]);
|
|
if (!key) {
|
|
setAuth(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
return;
|
|
}
|
|
// Creating a new object always triggers the [auth] useEffect even if values are identical
|
|
setAuth({ sessionKey: key, lastActiveOrg: org ?? undefined });
|
|
}, []);
|
|
|
|
const clearKey = useCallback(async () => {
|
|
await clearClaudeSessionKey();
|
|
setAuth(null);
|
|
setUsage(null);
|
|
setStatus("idle");
|
|
setError(null);
|
|
}, []);
|
|
|
|
return {
|
|
auth,
|
|
pendingKey,
|
|
setPendingKey,
|
|
pendingOrg,
|
|
setPendingOrg,
|
|
usage,
|
|
status,
|
|
error,
|
|
saveKey,
|
|
refresh,
|
|
reload,
|
|
clearKey,
|
|
};
|
|
}
|