Skip to content

fix(shell): harden account menu sign out#365

Merged
Nima-Naderi merged 2 commits into
mainfrom
codex/account-menu-signout
Jun 5, 2026
Merged

fix(shell): harden account menu sign out#365
Nima-Naderi merged 2 commits into
mainfrom
codex/account-menu-signout

Conversation

@Nima-Naderi
Copy link
Copy Markdown
Collaborator

@Nima-Naderi Nima-Naderi commented Jun 5, 2026

Summary

  • replace the Settings footer account control with a left-aligned row that shows the signed-in user's name directly
  • remove the confusing billing status chip from the footer account button
  • route account-menu sign-out through Matrix app-session cleanup before Clerk sign-out, and reuse that safe menu in the top menu bar
  • bound Clerk sign-out failures with a timeout recovery path and log non-OK Matrix app-session cleanup responses

Tests

  • flox activate -- bun run test -- tests/shell/user-button.test.tsx tests/shell/menu-bar-focus.test.tsx tests/shell/settings-panel.test.tsx tests/shell/billing-gate.test.tsx tests/shell/mobile-shell.test.tsx tests/shell/user-button-hydration.test.tsx
  • flox activate -- bun run typecheck
  • flox activate -- npx react-doctor@latest shell --diff --verbose
  • flox activate -- bun run check:patterns

Review/Monitoring

  • CI/Vercel checks are being monitored on PR fix(shell): harden account menu sign out #365.
  • Greptile initial review was 3/5; the two reported sign-out resilience findings were fixed in fix(shell): bound account sign out failures.
  • Waiting for the latest Greptile review to confirm 5/5.

Invariants

  • Source of truth: Clerk remains the account identity source; Matrix platform app-session cookie remains the platform routing/session source for app.matrix-os.com.
  • Lock/transaction scope: no database writes or multi-step server transactions are introduced; client sign-out performs app-session cookie cleanup before invoking Clerk sign-out.
  • Acceptable orphan states: if app-session cleanup fails or times out, the failure is logged and Clerk sign-out still runs; if Clerk sign-out times out, the account menu re-enables sign-out so the user has an in-page retry path.
  • Auth source of truth: Clerk controls user authentication, while /api/auth/app-session clears Matrix's platform session cookie for routed shell access.
  • Deferred scope: this does not change billing entitlement logic, VPS provisioning, or the existing top-bar billing status indicator.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 5, 2026

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

Project Deployment Actions Updated (UTC)
matrix-os-www Ready Ready Preview, Comment Jun 5, 2026 11:34am

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR replaces Clerk's built-in UserButton with a custom UserButton dropdown across all shell surfaces (dock, menubar, settings footer), hardening sign-out by clearing the Matrix app-session cookie before invoking Clerk sign-out, and adding a 10 s timeout guard around the Clerk call.

  • UserButton.tsx is substantially rewritten: the Clerk ClerkUserButton component is removed in favour of a Radix DropdownMenu, a new clearMatrixAppSession DELETE helper with abort-on-timeout, and clerkSignOutWithTimeout using Promise.race to bound the Clerk call.
  • MenuBar.tsx / Settings.tsx are simplified to delegate to the new UserButton with a variant prop, removing the billing status chip from the dock and the "Account" heading from the settings footer.
  • Tests cover the two-phase sign-out sequence, non-OK session-clear responses, and the timeout-re-enable path.

Confidence Score: 5/5

Safe to merge; the sign-out hardening is well-structured with timeout guards on both the Matrix session clear and the Clerk call, and the test suite covers the two-phase flow, non-OK cleanup responses, and the re-enable-on-timeout path.

The Matrix session clear has proper error handling, and the Clerk sign-out is now bounded. The one edge case — a post-timeout Clerk signOut that eventually completes and triggers its own redirect — is a minor UX surprise rather than a correctness or security issue, and it was intentionally left as an acceptable orphan state in the PR description.

shell/src/components/UserButton.tsx — specifically the clerkSignOutWithTimeout post-timeout dangling call path.

Important Files Changed

Filename Overview
shell/src/components/UserButton.tsx Major rewrite adding custom dropdown, two-phase sign-out, and timeout guards; one subtle post-timeout UX edge case worth noting.
shell/src/components/MenuBar.tsx Replaces ClerkUserButton with UserButton variant="menubar"; removes unused ServerIcon import; no issues found.
shell/src/components/Settings.tsx Simplifies SettingsAccountFooter to delegate to AccountButton with variant="settings"; safe change.
tests/shell/user-button.test.tsx New test suite covers the two-phase sign-out, non-OK session-clear responses, and the timeout-re-enable path; well structured.
tests/shell/menu-bar-focus.test.tsx Updated to open the new custom account menu via pointerDown before asserting Switch computer link; correctly updated.
tests/shell/billing-gate.test.tsx Added useUser/useClerk mocks required by the new UserButton; no functional changes.
tests/shell/user-button-hydration.test.tsx Added useUser/useClerk mocks; hydration guard behaviour unchanged.
tests/shell/mobile-shell.test.tsx Mock additions only; no functional changes to the mobile-shell test logic.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant B as UserButton
    participant M as /api/auth/app-session
    participant C as Clerk

    U->>B: Click "Sign out"
    B->>B: setSigningOut(true)
    B->>M: DELETE (AbortController, 10s timeout)
    alt Matrix session cleared OK
        M-->>B: 200 OK
    else Non-OK response
        M-->>B: 4xx/5xx → console.warn + continue
    else Timeout/network error
        M-->>B: AbortError/NetworkError → console.warn + continue
    end
    B->>C: "signOut({ redirectUrl }) via Promise.race (10s timeout)"
    alt Clerk sign-out succeeds
        C-->>B: resolved → Clerk navigates to redirectUrl
    else Clerk sign-out times out
        B->>B: setSigningOut(false) → button re-enabled
        Note over B,C: in-flight signOut() still running (may redirect later)
    else Clerk sign-out fails (non-timeout)
        B->>B: setSigningOut(false)
        B->>U: window.location.replace(redirectUrl)
    end
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
shell/src/components/UserButton.tsx:55-69
**Dangling Clerk `signOut()` call after the timeout**

`Promise.race` stops waiting for the `signOut` promise when the 10 s timeout fires, but the underlying Clerk call is still in-flight. Clerk's `signOut({ redirectUrl })` handles its own navigation internally — if it eventually settles after the button has been re-enabled, it will navigate the user to the sign-in page from a state they perceive as a "retry" window. The test covers the re-enable path but mocks `signOut` as a permanently-pending promise, so the post-timeout settlement scenario is not exercised. There is no way to cancel the Clerk call once started, but wrapping the settled case in a checked flag (`let signedOut = false` set before the race, checked in a `.then()` on the original promise) would allow you to no-op the navigation if the race was already lost to the timeout.

Reviews (2): Last reviewed commit: "fix(shell): bound account sign out failu..." | Re-trigger Greptile

Comment thread shell/src/components/UserButton.tsx
Comment thread shell/src/components/UserButton.tsx
@Nima-Naderi Nima-Naderi merged commit 016a614 into main Jun 5, 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