From 3ac40829c00ec3f38fe1d7cbf9d7b69e9f7fc4b2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:33:34 -0300 Subject: [PATCH] feat(quota): show OAuth token expiry on provider cards (small, blue, informative) (#3178) Show OAuth token expiry on provider cards (small, blue, informative). Integrated into release/v3.8.10. --- .../ProviderLimits/parts/QuotaCardHeader.tsx | 32 +++++++++++- src/i18n/messages/en.json | 2 + src/i18n/messages/pt-BR.json | 2 + tests/unit/quota-token-expiry-display.test.ts | 49 +++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 tests/unit/quota-token-expiry-display.test.ts diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx index 855c2b8579..2b3cf96acb 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx @@ -4,7 +4,8 @@ import { useTranslations } from "next-intl"; import Badge from "@/shared/components/Badge"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { pickDisplayValue } from "@/shared/utils/maskEmail"; -import { STATUS_EMOJI, type CardStatus } from "../utils"; +import { STATUS_EMOJI, formatCountdown, type CardStatus } from "../utils"; +import { translateUsageOrFallback } from "../i18nFallback"; interface Props { connection: any; @@ -41,6 +42,27 @@ export default function QuotaCardHeader({ connection.provider ); + // OAuth token expiry — informative only. Shown small/blue for connections that + // expose a concrete token expiry (e.g. Codex), so an operator can see at a + // glance when the access token rotates. Hidden for API-key / no-expiry connections. + const tokenExpiryIso = + connection.authType === "oauth" + ? connection.tokenExpiresAt || connection.expiresAt || null + : null; + const tokenExpiryMs = tokenExpiryIso ? new Date(tokenExpiryIso).getTime() : NaN; + const hasTokenExpiry = Number.isFinite(tokenExpiryMs); + const tokenCountdown = hasTokenExpiry ? formatCountdown(tokenExpiryIso) : null; + const tokenExpiryLabel = !hasTokenExpiry + ? null + : tokenCountdown + ? translateUsageOrFallback(t, "tokenExpiresIn", `Token expires in ${tokenCountdown}`, { + time: tokenCountdown, + }) + : translateUsageOrFallback(t, "tokenExpired", "Token expired"); + const tokenExpiryTitle = hasTokenExpiry + ? new Date(tokenExpiryMs).toLocaleString() + : undefined; + return (
@@ -85,6 +107,14 @@ export default function QuotaCardHeader({ {accountName} + {tokenExpiryLabel && ( + + {tokenExpiryLabel} + + )}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 767ab319f7..5bcc85bd2a 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -6289,6 +6289,8 @@ "notApplicable": "N/A", "rawPlanWithValue": "Raw plan: {plan}", "noPlanFromProvider": "No plan from provider", + "tokenExpiresIn": "Token expires in {time}", + "tokenExpired": "Token expired", "noQuotaData": "No quota data", "cardExpand": "Expand", "cardCollapse": "Collapse", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 948f2f2d39..f7566516da 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -7611,6 +7611,8 @@ "notApplicable": "N/D", "rawPlanWithValue": "Plano bruto: {plan}", "noPlanFromProvider": "Sem plano do provedor", + "tokenExpiresIn": "Token expira em {time}", + "tokenExpired": "Token expirado", "noQuotaData": "Sem dados de cota", "cardExpand": "Expandir", "cardCollapse": "Recolher", diff --git a/tests/unit/quota-token-expiry-display.test.ts b/tests/unit/quota-token-expiry-display.test.ts new file mode 100644 index 0000000000..32c4616634 --- /dev/null +++ b/tests/unit/quota-token-expiry-display.test.ts @@ -0,0 +1,49 @@ +/** + * The quota card shows an informative (small, blue) OAuth token-expiry line for + * connections that expose a concrete token expiry (e.g. Codex), and an expired + * marker otherwise. API-key / no-expiry connections show nothing. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const headerPath = path.join( + repoRoot, + "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardHeader.tsx" +); +const source = fs.readFileSync(headerPath, "utf8"); + +test("QuotaCardHeader derives token expiry only for OAuth connections with a known expiry", () => { + assert.match(source, /connection\.authType === "oauth"/, "must gate on oauth auth type"); + assert.match( + source, + /connection\.tokenExpiresAt \|\| connection\.expiresAt/, + "must read tokenExpiresAt with expiresAt fallback" + ); + assert.match(source, /formatCountdown\(/, "must format the countdown"); +}); + +test("QuotaCardHeader renders the expiry as a small blue (sky) informative line", () => { + assert.match(source, /text-\[10px\]/, "expiry line must be small (10px)"); + assert.match(source, /text-sky-500/, "active expiry must be blue (sky-500)"); + assert.match(source, /tokenExpiresIn/, "must use the tokenExpiresIn i18n key"); + assert.match(source, /tokenExpired/, "must use the tokenExpired i18n key"); +}); + +test("token expiry i18n keys exist in en and pt-BR", () => { + for (const locale of ["en", "pt-BR"]) { + const msgs = JSON.parse( + fs.readFileSync(path.join(repoRoot, `src/i18n/messages/${locale}.json`), "utf8") + ); + assert.ok(msgs.usage?.tokenExpiresIn, `${locale}: usage.tokenExpiresIn must exist`); + assert.ok(msgs.usage?.tokenExpired, `${locale}: usage.tokenExpired must exist`); + assert.match( + msgs.usage.tokenExpiresIn, + /\{time\}/, + `${locale}: tokenExpiresIn must interpolate {time}` + ); + } +});