Skip to content

Comments

fix: a user cannot click outside of the org selection modal#5031

Open
mcstepp wants to merge 6 commits intomainfrom
ENG-2460-fix-workspace-picker
Open

fix: a user cannot click outside of the org selection modal#5031
mcstepp wants to merge 6 commits intomainfrom
ENG-2460-fix-workspace-picker

Conversation

@mcstepp
Copy link
Collaborator

@mcstepp mcstepp commented Feb 13, 2026

What does this PR do?

Fixes #4935

Fix: Prevent org selector modal from closing when clicking outside the modal and improve auth error handling
Problem: Users could close the org selector modal during authentication, leading to:

  • Stale pending session tokens causing "user does not belong to org" errors
  • Confusing error messages when switching between different Google accounts
  • Auth state mismatches when users closed the modal and signed in with a different account

Changes

Org Selector Modal

  • Made modal uncloseable from the outside by setting preventOutsideClose={true}
  • Users must now select an org to proceed, or close the modal to restart, or navigate back
  • Removed unnecessary clientReady state that caused React hydration warnings

Error Handling

  • Clear pending session cookie on org selection errors to prevent stale token issues
  • Improved error message: "Your session has expired. Please sign in again." (consistent across all auth flows)
  • Map WorkOS "does not belong to" errors to session expiration for better UX
  • Use Next.js router instead of window.location.href to prevent TRPC errors during redirects

Session Management

  • Pending session cookie is now deleted on both success and error in completeOrgSelection
  • Session expiration check in sign-in page uses consistent error messaging
  • Auto-redirect to sign-in after 1.5s when session expires

Side Quests

  • Updated ESLint to not error in IDEs for unused vars prefixed with _ (biome linter accepts this, and the build works, so this was more cosmetic annoyance for false positive than a hard failure)
Screenshot 2026-02-13 at 3 42 34 PM Screenshot 2026-02-13 at 3 07 12 PM

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

Prerequisites: A user with multiple workspaces

  • Modal cannot be closed by clicking outside or pressing ESC
  • X button is hidden
  • Switching Google accounts mid-flow shows proper error message
  • Session expiration redirects to sign-in with clear message
  • Successful org selection works as expected

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@linear
Copy link

linear bot commented Feb 13, 2026

@vercel
Copy link

vercel bot commented Feb 13, 2026

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

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Feb 17, 2026 3:30pm
engineering Ready Ready Preview, Comment Feb 17, 2026 3:30pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Make the org-selection modal always open, remove external close handlers, centralize pending-session cleanup with a new clearPendingAuth, tighten orgs URL param parsing, map WorkOS "does not belong to" errors to the standardized pending-session expired message, and add a stricter unused-vars lint rule.

Changes

Cohort / File(s) Summary
Org selector UI & flow
web/apps/dashboard/app/auth/sign-in/org-selector.tsx
Removed onClose prop and hydration effects; dialog is always open; outside-close calls clearPendingAuth and navigates via router.push; simplified submission and error flows.
Sign-in page integration
web/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx
Added deleteCookie, AuthErrorCode, and errorMessages imports; use standardized pending-session expired message; removed onClose usage and its handler; delete last-org cookie on auto-select failure.
Auth actions / pending-session cleanup
web/apps/dashboard/app/auth/actions.ts
Imported UNKEY_LAST_ORG_COOKIE; added exported clearPendingAuth(): Promise<void> to delete PENDING_SESSION_COOKIE and UNKEY_LAST_ORG_COOKIE.
Sign-in hook / URL param validation
web/apps/dashboard/app/auth/hooks/useSignIn.ts
Validate orgs URL param is an array; clear orgs on parse failure or absence; suppress JSON parse errors; clear existing error before email sign-in; reduced effect dependencies.
WorkOS server logic
web/apps/dashboard/lib/auth/workos.ts
Removed fragile WorkOSError shape; added defensive guards when inspecting errors; map errors containing "does not belong to" to PENDING_SESSION_EXPIRED (via errorMessages) instead of surfacing membership errors.
Auth error messages
web/apps/dashboard/lib/auth/types.ts
Updated errorMessages[AuthErrorCode.PENDING_SESSION_EXPIRED] text to: "Your session has expired. Please sign in again."
Lint rules
web/apps/dashboard/eslint.config.mjs
Added no-unused-vars rule treating unused variables as errors while ignoring identifiers starting with _ for args, vars, and caught errors.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Browser Client
    participant Router as Next.js Router
    participant Server as App Server
    participant Cookie as Cookie Store

    User->>Client: Open sign-in page with pending session
    Client->>Client: Render OrgSelector (always open)
    User->>Client: Selects organization
    Client->>Server: POST completeOrgSelection(orgId)
    alt Success
        Server-->>Client: success
        Client->>Cookie: delete PENDING_SESSION_COOKIE, set/delete last-org
        Client->>Router: router.push(dashboard)
        Router-->>User: redirect to dashboard
    else "does not belong to" error -> mapped to PENDING_SESSION_EXPIRED
        Server-->>Client: mapped PENDING_SESSION_EXPIRED error
        Client->>Cookie: clearPendingAuth (delete PENDING_SESSION_COOKIE & last-org)
        Client->>Router: router.push('/auth/sign-in' + error)
        Router-->>User: redirect to sign-in with error message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: preventing the org selection modal from being closed via outside clicks, which is the primary issue addressed in the PR.
Description check ✅ Passed The description is comprehensive and well-structured, addressing all key sections: problem statement, changes made, testing instructions, and checklist items completed.
Linked Issues check ✅ Passed The PR successfully addresses all requirements from linked issue #4935: prevents org selector modal from closing via outside clicks (preventOutsideClose={true}), clears pending session cookies on errors, improves error messaging consistency, and maps WorkOS errors appropriately.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #4935. The ESLint configuration update for unused variables is a minor side improvement but remains within scope as mentioned in the PR description.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ENG-2460-fix-workspace-picker

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
web/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx (1)

93-98: Remove the empty if-block — it has no effect.

This conditional block checks result.code === AuthErrorCode.PENDING_SESSION_EXPIRED but executes no code. The comment explains the intent, but the block itself is a no-op since setError(result.message) on line 90 already handles displaying the error regardless of the error code.

Consider removing the empty block entirely, or if you want to preserve the documentation, convert it to a comment without the conditional:

🔧 Suggested fix
             setIsLoading(false);
             setIsAutoSelecting(false);
-
-            // If session expired, the pending auth was already cleared
-            // Just show the error and let user see org selector
-            if (result.code === AuthErrorCode.PENDING_SESSION_EXPIRED) {
-              // Session expired error already shown, user will see sign-in form
-            }
+            // Note: If session expired (PENDING_SESSION_EXPIRED), the pending auth
+            // was already cleared server-side. Error is shown above via setError.
             return;

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/apps/dashboard/lib/auth/workos.ts`:
- Around line 862-871: Remove the unsafe type assertion "error as WorkOSError"
and change the catch parameter to be explicitly typed as "unknown"; then perform
structural narrowing before accessing message (e.g., check typeof error ===
'object' && error !== null && 'message' in error && typeof (error as
any).message === 'string') and use that string to test for "does not belong to"
so you avoid asserting WorkOSError. Apply the same change to the other
occurrence (the similar pattern around the symbol at line ~635) so all
WorkOSError handling uses explicit unknown catch typing and runtime checks
instead of type assertions.

Copy link
Member

@perkinsjr perkinsjr left a comment

Choose a reason for hiding this comment

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

No modal hijacking.

If I want to escape because I clicked the wrong user, I am now stuck till it timesout.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/apps/dashboard/app/auth/sign-in/org-selector.tsx`:
- Around line 36-40: The handleClose callback currently awaits
clearPendingAuth() without handling rejections; wrap the await in a try/catch
inside handleClose (the function defined as handleClose) so any error from
clearPendingAuth() is caught and logged (e.g., console.error or a logger) and
then always call router.push("/auth/sign-in") in a finally or after the catch so
the redirect occurs even if cookie/cleanup fails.
🧹 Nitpick comments (1)
web/apps/dashboard/app/auth/sign-in/org-selector.tsx (1)

57-57: isOpen is always true; consider simplifying.

Since isOpen is initialized to true and never modified, you could use a constant instead of useState. This makes the intent clearer that the dialog cannot be closed programmatically.

♻️ Proposed simplification
-const [isOpen] = useState(true);
+const isOpen = true;

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.


// If session expired, the pending auth was already cleared
// Just show the error and let user see org selector
if (result.code === AuthErrorCode.PENDING_SESSION_EXPIRED) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this seems pointless, why are we doing an if check if we don't run any conditional code?

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.

Clicking outside workspace picker shows org membership error

3 participants