Skip to content

fix(nav): the side menu stops contradicting its own auth state (chat#1912 row 2) - #1914

Merged
sweetmantech merged 2 commits into
mainfrom
fix/side-menu-auth-state
Jul 30, 2026
Merged

fix(nav): the side menu stops contradicting its own auth state (chat#1912 row 2)#1914
sweetmantech merged 2 commits into
mainfrom
fix/side-menu-auth-state

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Closes row 2 of chat#1912.

Why

Reproduced on prod 2026-07-30 at 390px, signed out: the opened side menu shows a Sign In button at the top and, at the bottom of the same panel, the user-chip slot rendering a permanently loading skeleton (accessible name Loading user profile, disabled). Still both present 12 seconds later — a first-time visitor is told to sign in and simultaneously shown a profile chip loading forever for an account that does not exist.

Root cause, UserProfileButton.tsx:

if (!userData) return <UserProfileButtonSkeleton />;

userData is absent in two very different situations — "authenticated, account still fetching" and "nobody is signed in" — and both rendered the skeleton. For the signed-out case nothing ever arrives to resolve it.

Originally reported by a referred first-time visitor, relayed by a live outreach customer on 2026-07-29 ("I put someone else into your site and they had issues with the side menu").

What changed

  • New lib/auth/getUserProfileState.ts — a pure loading | signed-out | ready decision, gated on Privy's authenticated flag rather than on the presence of account data.
  • UserProfileButton renders nothing when signed out, the skeleton only while genuinely loading, the chip when ready.

Gating on authenticated rather than !!userData also fixes the logout path: the chip disappears the moment the session goes, instead of lingering until the account query is evicted. There is a test for that.

Scope note

The issue also recorded a signed-in variant of this bug (Sign In alongside the authenticated avatar chip, plus a Privy already logged in warning). That state did not reproduce across normal load, Slow 3G + 4x CPU throttling, and force-opening the menu on first paint — details in the issue. This PR fixes the variant that does reproduce. The original sighting is un-reproduced, not disproven, so row 2 should be re-checked against the preview before it is closed.

Tests

TDD red→green, two files:

  • lib/auth/__tests__/getUserProfileState.test.ts — 5 cases (RED: module not found), including the stale-data-after-logout guard.
  • components/Sidebar/__tests__/UserProfileButton.test.tsx — 4 cases. The signed-out case was RED against the real defect (expected <button …> to be null), the other three green throughout, proving the loading and ready paths are unchanged.

Full suite 291 passed / 77 files, tsc --noEmit clean.

Verification

Preview verification against the issue's Works when script (390px signed-out, the 15s re-check, signed-in, console) will be posted as a PR comment once the preview builds.

🤖 Generated with Claude Code


Summary by cubic

Fixes the side menu so signed-out users no longer see a stuck profile skeleton next to Sign In, the profile chip disappears immediately on logout, and the account slot now offers Sign in. Wallet-only users (e.g. mini-app) keep their profile chip. Addresses chat#1912 row 2.

  • Bug Fixes
    • Updated lib/auth/getUserProfileState to decide loading | signed-out | ready using @privy-io/react-auth readiness/auth plus wallet presence, not userData. Treats wallet-only sessions as signed in and marks logout as signed-out immediately.
    • Updated UserProfileButton to render SignInSlotButton when signed out, show a skeleton only while loading, and keep the chip for wallet-only users.
    • Added tests for the helper and component covering signed-out, loading, ready, logout, wallet-only, and click-to-login.

Written for commit f140e5d. Summary will update on new commits.

Review in cubic

…1912 row 2)

UserProfileButton rendered UserProfileButtonSkeleton whenever userData was
absent, without distinguishing "authenticated, account still fetching" from
"nobody is signed in". A signed-out visitor therefore got a Sign In button at
the top of the menu and a profile skeleton pinned at the bottom of the same
panel, permanently, because no account would ever arrive to resolve it.

Extracts the decision into getUserProfileState (loading | signed-out | ready),
gated on Privy's authenticated flag rather than on the presence of account
data, so a torn-down session reads as signed-out immediately instead of
lingering on stale data. The signed-out slot now renders nothing.

Reported by a referred first-time visitor via a live customer, 2026-07-29.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 30, 2026 5:56pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6930ceaf-fdb2-46c8-81df-ac785cf7a93e

📥 Commits

Reviewing files that changed from the base of the PR and between 9fcd2aa and f140e5d.

⛔ Files ignored due to path filters (2)
  • components/Sidebar/__tests__/UserProfileButton.test.tsx is excluded by !**/*.test.* and included by components/**
  • lib/auth/__tests__/getUserProfileState.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (3)
  • components/Sidebar/SignInSlotButton.tsx
  • components/Sidebar/UserProfileButton.tsx
  • lib/auth/getUserProfileState.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6579c1bca9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (!userData) return <UserProfileButtonSkeleton />;
const profileState = getUserProfileState({
isPrivyReady: ready,
isAuthenticated: authenticated,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat wallet-only users as signed out

In the Farcaster mini-app path, useAutoLogin deliberately skips Privy login when isMiniApp, while useUser still bootstraps account data from wagmiAddress; those prepared wallet-only users can therefore have userData with Privy authenticated === false. Passing Privy's flag as the only auth gate makes profileState become signed-out, so the loaded profile button and menu are permanently suppressed for that context. Please include the wallet/account-prepared state in this gate instead of treating every !authenticated account as stale.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Confidence score: 3/5

  • In lib/auth/getUserProfileState.ts, stale userData can be treated as valid when auth identity changes outside the local signOut flow, so the sidebar may briefly show the previous user’s profile after a new Privy session authenticates; this is a concrete cross-user UI data leak/regression risk—clear or revalidate cached profile state on identity/session changes before rendering.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/auth/getUserProfileState.ts">

<violation number="1" location="lib/auth/getUserProfileState.ts:25">
P2: After an auth identity changes outside the local `signOut` path, the sidebar can render the previous user's profile as soon as the new Privy session becomes authenticated. The state helper treats any lingering `userData` as ready, while `useUser` does not clear or associate that data with the current Privy identity before starting the new account fetch. Requiring the loaded account to match the current identity, or clearing it on identity changes, would keep stale profile data out of the chip.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}: UserProfileStateInput): UserProfileState {
if (!isPrivyReady) return "loading";
if (!isAuthenticated) return "signed-out";
return hasUserData ? "ready" : "loading";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: After an auth identity changes outside the local signOut path, the sidebar can render the previous user's profile as soon as the new Privy session becomes authenticated. The state helper treats any lingering userData as ready, while useUser does not clear or associate that data with the current Privy identity before starting the new account fetch. Requiring the loaded account to match the current identity, or clearing it on identity changes, would keep stale profile data out of the chip.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/auth/getUserProfileState.ts, line 25:

<comment>After an auth identity changes outside the local `signOut` path, the sidebar can render the previous user's profile as soon as the new Privy session becomes authenticated. The state helper treats any lingering `userData` as ready, while `useUser` does not clear or associate that data with the current Privy identity before starting the new account fetch. Requiring the loaded account to match the current identity, or clearing it on identity changes, would keep stale profile data out of the chip.</comment>

<file context>
@@ -0,0 +1,26 @@
+}: UserProfileStateInput): UserProfileState {
+  if (!isPrivyReady) return "loading";
+  if (!isAuthenticated) return "signed-out";
+  return hasUserData ? "ready" : "loading";
+}
</file context>

Comment thread components/Sidebar/UserProfileButton.tsx Outdated
// A signed-out visitor has no profile to load, so the slot stays empty
// rather than showing a skeleton that never resolves next to a Sign In
// button (chat#1912 row 2).
if (profileState === "signed-out") return null;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning null, what do you think of returning a signin button to reinforce that as an important first step for using the chat app?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f140e5d1 — the slot now renders a Sign in button (SignInSlotButton, avatar-shaped so it sits where the account chip will), wired to Privy login. Two tests cover it: the signed-out slot renders sign-in and never a skeleton, and clicking it opens the login prompt.

One thing worth your call, since it changes what the panel looks like signed out. Both menus already show a Sign In affordance at the top when signed out:

  • desktop MenuNewChatButton renders the label Sign In when there is no email
  • mobile SideMenu → the top button renders Sign In when there is no address

So a signed-out panel now has two sign-in affordances, top and bottom.

I do not think that reintroduces the bug this PR fixes: the original defect was a contradiction (a Sign In button above a signed-in user's chip). Two buttons that both say "sign in" to a signed-out visitor are consistent, just redundant, and redundancy is arguably the point when it is the first step you want reinforced.

If you would rather have exactly one, the cleaner split is to let the account slot own sign-in and stop the top button doubling as it (it would read New Chat always, and still opens login because isPrepared() calls login() when there is no address, so behaviour would not change). I did not make that call unilaterally since it edits copy outside this PR's scope. Say the word and it is a two-line change.

…ers keep theirs

Review (@sweetmantech): the signed-out slot returned null; it now renders a
Sign in button, so the account slot points at the first step rather than
going blank.

Review (codex P2 / cubic P2): gating on Privy's authenticated flag alone was
a regression for Farcaster mini-app visitors. useAutoLogin skips Privy login
when isMiniApp while useUser still bootstraps the account from the wagmi
address, so those users have userData with authenticated false and would have
lost their profile chip entirely. getUserProfileState now takes hasWallet and
only reads signed-out when neither a Privy session nor a wallet is present.

Also drops a pre-existing unused secondaryName in the same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Confidence score: 3/5

  • In components/Sidebar/UserProfileButton.tsx, the signed-out behavior appears to contradict the PR description (<SignInSlotButton> is rendered instead of nothing), which creates concrete UX/regression risk if downstream expectations or acceptance criteria were based on a hidden state; align the implementation and PR description (or update both product requirements and tests) before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="components/Sidebar/UserProfileButton.tsx">

<violation number="1" location="components/Sidebar/UserProfileButton.tsx:32">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The PR description claims UserProfileButton 'renders nothing when signed out,' but the implementation returns a `<SignInSlotButton>` — a visible 'Sign in' button with an icon. This contradicts the documented behavior. Consider updating the PR description or commit message to accurately reflect that the signed-out slot now renders a sign-in CTA rather than staying empty.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


// A signed-out visitor has no profile to load, so the slot offers the way in
// rather than a skeleton that can never resolve (chat#1912 row 2).
if (profileState === "signed-out") return <SignInSlotButton onClick={login} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: Flag AI Slop and Fabricated Changes

The PR description claims UserProfileButton 'renders nothing when signed out,' but the implementation returns a <SignInSlotButton> — a visible 'Sign in' button with an icon. This contradicts the documented behavior. Consider updating the PR description or commit message to accurately reflect that the signed-out slot now renders a sign-in CTA rather than staying empty.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Sidebar/UserProfileButton.tsx, line 32:

<comment>The PR description claims UserProfileButton 'renders nothing when signed out,' but the implementation returns a `<SignInSlotButton>` — a visible 'Sign in' button with an icon. This contradicts the documented behavior. Consider updating the PR description or commit message to accurately reflect that the signed-out slot now renders a sign-in CTA rather than staying empty.</comment>

<file context>
@@ -14,21 +15,21 @@ import { ChevronDown } from "lucide-react";
-  if (profileState === "signed-out") return null;
+  // A signed-out visitor has no profile to load, so the slot offers the way in
+  // rather than a skeleton that can never resolve (chat#1912 row 2).
+  if (profileState === "signed-out") return <SignInSlotButton onClick={login} />;
   if (profileState === "loading") return <UserProfileButtonSkeleton />;
 
</file context>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — 2026-07-30

Preview https://chat-opvxaf8pg-recoup.vercel.app, confirmed built from head f140e5d1 (deployment resolved by sha, not by branch). Signed in as sweetman@recoupable.com.

Results

# Check Documented / before Actual on preview
1 Signed out, 390px, menu open "Sign In" at the top and a grey profile skeleton pinned at the bottom (Loading user profile, disabled) top affordance plus a "Sign in" button in the account slot; no skeleton
2 It stays fixed — re-check after 15s both still present, permanently button list byte-identical after 15s; stillLoadingSkeleton: false
3 Signed in, 390px n/a account chip sweetman@recoupable.c… renders; hasAnySignIn: false, hasSkeleton: false
4 Console in the signed-in session Attempted to log in, but user is already logged in no errors or warnings at all
5 Signed in, 1440px n/a hasUserMenu: true, hasSignIn: false, hasSkeleton: false
6 Signed out, 1440px skeleton hasSkeleton: false, sign-in slot present
7 Account slot CTA n/a opens the Privy prompt in place, completing login through it lands signed in

Assertions were made programmatically against the accessible names, not read off the screenshots.

Screenshots

Signed out, 390px — the state a referred first-time visitor hit. The skeleton is gone and the slot offers the way in:

signed out 390

Signed in, 390px — chip renders, no Sign In affordance anywhere in the panel:

signed in 390

Signed out, 1440px:

signed out 1440

One thing for the author to decide

The first screenshot shows it plainly: a signed-out panel now carries two sign-in affordances, Sign In at the top and Sign in in the account slot. That is the direct consequence of the requested change, and I do not think it reintroduces this PR's bug — the original defect was a contradiction (Sign In above a signed-in user's chip), whereas two controls that agree are redundant, not contradictory. Reasoning and a two-line alternative are in this thread.

Review findings

Validated against the code before acting, not taken on trust.

Finding Verdict Action
@sweetmantech — return a sign-in button instead of null Valid new SignInSlotButton in the account slot, wired to Privy login; 2 tests
codex P2 / cubic P2 — wallet-only users treated as signed out Valid, and a regression I introduced see below
cubic P2 — stale userData can render the previous user's profile after an identity change Pre-existing, not from this PR the old code was if (!userData) return skeleton, so lingering data rendered the same way before. Genuinely worth fixing, but it needs useUser to associate userData with the current Privy identity, which is a bigger change than this PR's scope. Not addressed here

The regression, in full: gating on Privy's authenticated flag alone would have hidden the profile chip entirely for Farcaster mini-app visitors. useAutoLogin deliberately skips Privy login when isMiniApp, while useUser still derives address from wagmiAddress and bootstraps the account from it — so those users hold a real userData with authenticated === false. getUserProfileState now takes hasWallet and only reads signed-out when neither a Privy session nor a wallet is present. Two tests cover the wallet-only ready and loading paths.

Also removed a pre-existing unused secondaryName in the same file so it lints clean.

Suite 294 passed / 78 files, tsc --noEmit clean, eslint clean on the touched files.

Note unrelated to this PR

The signed-in screenshot shows "Recoupable Pro: Active" on sweetman@recoupable.com, a brand-new account that lands on /setup with an empty roster. That matches the finding recorded under Findings outside this issue's scope in chat#1912 — new accounts appear to be seeded on the Pro allotment. Not caused by and not addressed in this PR.

@sweetmantech
sweetmantech merged commit 1bec1fd into main Jul 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant