feat(codex-oauth): show per-account usage in Auth Center (#4887)

* feat(codex-oauth): show per-account usage in Auth Center

Each ChatGPT (Codex OAuth) account under Settings → 认证 now displays its
own subscription usage — reset countdowns and per-window progress bars —
directly in the account list, instead of usage only being visible on the
active provider card.

- Add useCodexOauthQuotaByAccountId(accountId) and refactor
  useCodexOauthQuota to delegate to it (shared query key → cache reuse)
- Add CodexOauthAccountQuota, a thin per-account wrapper that reuses the
  existing SubscriptionQuotaView expanded layout (same look and 5-state
  handling as provider cards), with a light spinner on first load
- Render it under each account row in CodexOAuthSection; fetch once when
  the Auth Center opens, manual refresh available (no polling)

Copilot is intentionally left out — same as before, this is Codex-only.

* refactor(codex-oauth): stable async loading placeholder for account usage

The account header (login + badges + actions) already renders independently
of the usage query — the quota is fetched async via Tauri invoke + React
Query, so the account never waits on it. Make that visually obvious and
jump-free: while the usage loads, show a spinner inside a placeholder shaped
like the final quota card (same rounded-xl / border / bg-card), so the card
morphs smoothly into the data instead of popping in from an empty gap.

* fix(codex-oauth): scope account quota to auth center

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
SaladDay
2026-08-03 09:47:49 -04:00
committed by GitHub
parent 0e604b75bd
commit 492245dcb9
5 changed files with 194 additions and 43 deletions
@@ -0,0 +1,74 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CodexOAuthSection } from "@/components/providers/forms/CodexOAuthSection";
import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel";
const mocks = vi.hoisted(() => ({
useCodexOauth: vi.fn(),
renderAccountQuota: vi.fn(),
}));
vi.mock("@/components/providers/forms/hooks/useCodexOauth", () => ({
useCodexOauth: mocks.useCodexOauth,
}));
vi.mock("@/components/CodexOauthAccountQuota", () => ({
default: ({ accountId }: { accountId: string }) => {
mocks.renderAccountQuota(accountId);
return <div data-testid="account-quota">{accountId}</div>;
},
}));
vi.mock("@/components/providers/forms/CopilotAuthSection", () => ({
CopilotAuthSection: () => <div />,
}));
vi.mock("@/components/providers/forms/XaiOAuthSection", () => ({
XaiOAuthSection: () => <div />,
}));
describe("CodexOAuthSection", () => {
beforeEach(() => {
mocks.useCodexOauth.mockReturnValue({
accounts: [
{
id: "account-1",
provider: "codex_oauth",
login: "user@example.com",
avatar_url: null,
authenticated_at: 0,
is_default: true,
github_domain: "",
},
],
defaultAccountId: "account-1",
hasAnyAccount: true,
pollingState: "idle",
deviceCode: null,
error: null,
isPolling: false,
isAddingAccount: false,
isRemovingAccount: false,
isSettingDefaultAccount: false,
addAccount: vi.fn(),
removeAccount: vi.fn(),
setDefaultAccount: vi.fn(),
cancelAuth: vi.fn(),
logout: vi.fn(),
});
});
it("does not render account quota by default", () => {
render(<CodexOAuthSection />);
expect(mocks.renderAccountQuota).not.toHaveBeenCalled();
expect(screen.queryByTestId("account-quota")).not.toBeInTheDocument();
});
it("renders account quota in Auth Center", () => {
render(<AuthCenterPanel />);
expect(mocks.renderAccountQuota).toHaveBeenCalledWith("account-1");
expect(screen.getByTestId("account-quota")).toHaveTextContent("account-1");
});
});