fix(console-ui): restore sidebar nav — stop Privy bridge starving App Router transitions#460
fix(console-ui): restore sidebar nav — stop Privy bridge starving App Router transitions#460Gajesh2007 wants to merge 1 commit into
Conversation
… Router transitions The Privy auth bridge reported auth state up to the app via an effect keyed on usePrivy()'s login/logout/getAccessToken. The Privy SDK hands back fresh identities for those methods on most renders, so the effect re-fired and called onAuthChange (a setState in PrivyClientProvider) on essentially every render. The rendered output never changed, so it produced no DOM mutations and was invisible — but it perpetually re-scheduled default-priority work and starved React's lower-priority navigation transitions. Under the Next 16 App Router this made <Link> / router.push() silently no-op: the sidebar rendered and was hittable, but clicking did nothing and the URL never changed (no pushState, no RSC fetch, no error). It only reproduced in production because Privy is gated on NEXT_PUBLIC_PRIVY_APP_ID (absent in local dev), so #457's dev-only verification could not surface it; the #457 hydration fix was correct but orthogonal. Fix: keep a live ref to the Privy handle and expose stable login/logout/ getAccessToken via useCallback([]), and key the bridge effect on a stable userId, so onAuthChange fires only on real auth changes. Navigation transitions are no longer starved and SPA nav works again. Adds a regression test that fails on the old bridge (onAuthChange fired per-render) and passes on the fix. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
These two files fall entirely outside current threat model Trust boundaries touched
Neither file appears in any Threat relevance
New attack surface not covered by an existing threat
Recommendation: Add SEC-* findings resolvedNone — this PR does not appear to address any open findings. 🔐 Threat model: |
ethenotethan
left a comment
There was a problem hiding this comment.
Automated Code Review — Layr-Labs/d-inference#
Verdict: REQUEST_CHANGES
Security — ✅ No issues found
Performance — 1 finding(s) (1 blocking)
- 🟡 [MEDIUM]
console-ui/src/components/providers/PrivyRealProvider.tsx:16— useRef assignment on every render creates unnecessary work- Suggestion: Move privyRef.current = privy assignment inside useEffect or use a different pattern to avoid executing on every render
Type_diligence — 1 finding(s)
- 🔵 [INFO]
console-ui/__tests__/privy-auth-bridge.test.tsx:68— Unsafe cast to { id?: string } without type assertion check- Suggestion: Use type assertion with check: const userId = user && typeof user === 'object' && 'id' in user ? (user as { id?: string }).id : null
Additive_complexity — 2 finding(s)
- 🔵 [INFO]
console-ui/__tests__/privy-auth-bridge.test.tsx:21— Hoisted counter object adds unnecessary indirection for simple test state- Suggestion: Use a simple module-level variable:
let usePrivyCalls = 0;instead of{ usePrivyCalls: { n: 0 } }
- Suggestion: Use a simple module-level variable:
- 🔵 [INFO]
console-ui/src/components/providers/PrivyRealProvider.tsx:33— Type assertion with object destructuring is more complex than needed- Suggestion: Use direct property access:
const userId = user?.id ?? null;instead of casting to{ id?: string } | null
- Suggestion: Use direct property access:
4 finding(s) total, 1 blocking. Verdict: REQUEST_CHANGES.
🤖 Automated review by Centaur · DAR-186
| const privy = usePrivy(); | ||
| const { ready, authenticated, user, login, logout } = privy; | ||
| const { ready, authenticated, user } = privy; | ||
|
|
There was a problem hiding this comment.
🟡 [MEDIUM] ⚡ useRef assignment on every render creates unnecessary work
💡 Suggestion: Move privyRef.current = privy assignment inside useEffect or use a different pattern to avoid executing on every render
📊 Score: 2×4 = 8 · Category: Repeated work
|
|
||
| const state = onAuthChange.mock.calls[0][0]; | ||
| expect(state.ready).toBe(true); | ||
| expect(state.authenticated).toBe(false); |
There was a problem hiding this comment.
🔵 [INFO] 🏷️ Unsafe cast to { id?: string } without type assertion check
💡 Suggestion: Use type assertion with check: const userId = user && typeof user === 'object' && 'id' in user ? (user as { id?: string }).id : null
📊 Score: 2×3 = 6 · Category: unsafe_cast
| // the effect on a stable `userId`, so `onAuthChange` fires only on real auth | ||
| // changes. | ||
|
|
||
| const { usePrivyCalls } = vi.hoisted(() => ({ usePrivyCalls: { n: 0 } })); |
There was a problem hiding this comment.
🔵 [INFO] 🧩 Hoisted counter object adds unnecessary indirection for simple test state
💡 Suggestion: Use a simple module-level variable: let usePrivyCalls = 0; instead of { usePrivyCalls: { n: 0 } }
📊 Score: 2×3 = 6 · Category: over-abstraction
| const logout = useCallback(() => privyRef.current.logout(), []); | ||
|
|
||
| // Identify the user by a stable primitive so an unstable `user` object | ||
| // identity from Privy doesn't re-fire this effect every render. |
There was a problem hiding this comment.
🔵 [INFO] 🧩 Type assertion with object destructuring is more complex than needed
💡 Suggestion: Use direct property access: const userId = user?.id ?? null; instead of casting to { id?: string } | null
📊 Score: 2×2 = 4 · Category: over-abstraction
|
Found 1 test failure on Blacksmith runners: Failure
|

Summary
The console-ui sidebar links (Stats, Providers, Earn, Models, Billing, Settings, …) are visible but unclickable in production: clicking does nothing, the URL never changes. This is not the issue #457 tried to fix (that hydration fix is correct, but orthogonal — the sidebar is rendered, on‑screen, and hit‑testable; no overlay, no
#418).Root cause: the Privy auth bridge (
PrivyRealProvider→PrivyAuthBridge) reports auth state up to the app through an effect keyed onusePrivy()'slogin/logout/getAccessToken. The Privy SDK hands back fresh identities for those methods on most renders, so the effect re-fired and calledonAuthChange(asetStateinPrivyClientProvider) on essentially every render. The rendered output never changed, so it produced no DOM mutations and was invisible, but it perpetually re‑scheduled default‑priority work and starved React's lower‑priority navigation transitions. Under the Next 16 App Router,<Link>/router.push()then silently no‑op (nopushState, no RSC fetch, no error).It only reproduces in production because Privy is gated on
NEXT_PUBLIC_PRIVY_APP_ID(absent in local dev), which is exactly why #457's dev‑only verification couldn't surface it.Fix: keep a live ref to the Privy handle and expose stable
login/logout/getAccessToken(useCallback([])), and key the bridge effect on a stableuserId, soonAuthChangefires only on real auth changes. Navigation transitions are no longer starved and SPA navigation works again.Evidence (production build, headed Chromium, returning‑user localStorage)
history.pushStatepreventDefaultruns/preventDefaultruns/statsBisect that pinned it down: a bare
<PrivyProvider>(noPrivyAuthBridge) navigates fine (SPA_OK); re‑adding the original bridge reproduces the no‑op. Disabling GA, Datadog, the client route cache (staleTimes),prefetch, a Suspense boundary, and upgrading Next (incl.16.3.0-canarywith the #94144 cache fix) all did not fix it — only stabilizing the bridge does.Before / After — behavior
flowchart LR subgraph Before [Before: nav unclickable] A1[Click sidebar Stats] --> B1[next/link onClick<br/>preventDefault] B1 --> C1[startTransition router.push] C1 --> D1{transition starved by<br/>per-render setState loop} D1 --> E1[no pushState / no RSC fetch] E1 --> F1[URL stays / — nothing happens] end subgraph After [After: SPA nav] A2[Click sidebar Stats] --> B2[next/link onClick<br/>preventDefault] B2 --> C2[startTransition router.push] C2 --> D2[transition commits] D2 --> E2[pushState + RSC fetch] E2 --> F2[URL becomes /stats] endBefore / After — code
flowchart TB subgraph Before [Before: PrivyAuthBridge] a1[usePrivy returns fresh<br/>login/logout/getAccessToken each render] a1 --> a2[effect deps include those identities] a2 --> a3[effect re-fires every render] a3 --> a4[onAuthChange -> setState in parent] a4 --> a5[re-render -> usePrivy -> new identities] a5 --> a2 a5 -.default-priority churn.-> a6[React navigation transition never commits] end subgraph After [After: PrivyAuthBridge] b1[privyRef holds live handle] b1 --> b2[login/logout/getAccessToken = useCallback([]) stable] b2 --> b3[effect keyed on ready/authenticated/userId + stable cbs] b3 --> b4[onAuthChange fires once per real auth change] b4 --> b5[no churn -> transitions commit] endTest plan
cd console-ui && npm run build— passesnpx eslint src/— 0 errorsnpx vitest run— only the 3 pre-existingprovider-dashboard-*failures remain (present on mastere8ba9c0f, unrelated to this change); all others pass__tests__/privy-auth-bridge.test.tsx— fails on the old bridge (onAuthChangecalled per render), passes on the fix/statsvia SPA (3/3); cross-page nav/stats → /modelsworks; mobile (390px) overlay nav works and auto-closes; theme toggle + toasts unaffectedconsole.darkbloom.devand re-verify sidebar nav in the live deployMade with Cursor
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.