From 664b131c303b59fccf2368663a5d14e143959922 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 23 Jul 2026 01:13:22 -0500 Subject: [PATCH] fix(desktop): billing page always fetches fresh state (team-account desync) The x-nous-credits-* headers are best-effort and can drift out of sync, notably in team/org accounts where another member's spend moves the shared balance without touching this client's headers. The billing endpoint is the source of truth, so the page no longer trusts a cache: staleTime 0 + refetchOnMount 'always' force a fresh fetch on every open and focus (still polling 30s while mounted). The credits.* invalidation nudge still pulls a crossing in immediately. --- .../app/settings/billing/use-billing-state.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/app/settings/billing/use-billing-state.ts b/apps/desktop/src/app/settings/billing/use-billing-state.ts index 7611a055508cd..606495d27e752 100644 --- a/apps/desktop/src/app/settings/billing/use-billing-state.ts +++ b/apps/desktop/src/app/settings/billing/use-billing-state.ts @@ -11,15 +11,22 @@ export const EMPTY_BILLING_VALUE = '—' export const FALLBACK_PORTAL_BILLING_URL = 'https://portal.nousresearch.com/billing' export const FALLBACK_PORTAL_URL = 'https://portal.nousresearch.com' -// Billing polls on its own while the page is mounted (react-query only ticks an -// active observer), so the view stays live without a manual refresh control — -// matching every other data view in the app. It pauses when the window is -// backgrounded (refetchIntervalInBackground defaults to false). +// The billing endpoint is the authoritative source of truth for balance / cap / +// plan — the inference `x-nous-credits-*` headers are best-effort and can drift +// out of sync (notably in team/org accounts where another member's spend moves +// the shared balance without ever touching THIS client's headers). So the page +// never trusts a cache: `staleTime: 0` + `refetchOnMount: 'always'` force a +// fresh fetch every time it opens or regains focus, and it keeps polling every +// 30s while mounted (react-query only ticks an active observer; it pauses when +// the window is backgrounded — refetchIntervalInBackground defaults to false). +// A `credits.*` notice crossing additionally invalidates ['billing','state'] to +// pull the change in immediately rather than waiting for the next poll tick. const BILLING_QUERY_OPTIONS = { refetchInterval: 30_000, + refetchOnMount: 'always', refetchOnWindowFocus: true, retry: false, - staleTime: 30_000 + staleTime: 0 } as const export interface BillingSummaryItemView {