Skip to content

feat(ui): add UserButton controller - #9185

Merged
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller
Aug 27, 2026
Merged

feat(ui): add UserButton controller#9185
alexcarpenter merged 71 commits into
mainfrom
carp/account-button-controller

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 16, 2026

Copy link
Copy Markdown
Member

Description

The PR #9191 was folded into this one for a final review. See that PR description for additional details.

Stacked on #9184. Connects the UserButton view to live Clerk data.

useUserButtonController() returns a 'loading' | 'hidden' | 'ready' union. When it is ready it carries the view's data contract and the callback behind every row. user-button.tsx is the connected container that owns the popover.

Where the data comes from:

  • activeSession from useUser() and useSession(). The name follows Clerk's own order: first and last name, then username, then the identifier.
  • activeOrganization from useOrganization(), where null is the personal workspace.
  • memberships, suggestions, and invitations from useOrganizationList(), paged as the list scrolls.
  • hasOrganizations from the user resource, so it can answer before those lists load.
  • additionalSessions from the client, without the active one.
  • The membership request count only on the active organization row, and only with org:sys_memberships:manage.

What the rows do:

  • Picking a workspace or an account calls setActive. Signing out calls signOut. Invitations and suggestions accept in place and revalidate the list.
  • Manage account, manage organization, create organization, and invite members open Clerk's own modals, portalled out of the popover so they outlive it. Passing a URL routes to a page of your own instead, and that is the whole opt-in: userProfileUrl, organizationProfileUrl, createOrganizationUrl. afterSelectOrganizationUrl and afterSelectPersonalUrl say where picking a workspace lands.
  • Create organization is withheld from a user without the permission to create one. The personal row is withheld where an organization is required, and hidePersonal withholds it by request.
  • customMenuItems reach the menu through the container, and a custom action closes the popover behind whatever it opens.

The button renders nothing until Clerk answers. While it loads, a signed-out visitor and a session still resolving are indistinguishable, so anything rendered then is a button promised to people who are never going to get one. <ClerkLoading> is where an app that knows its own nav puts a placeholder.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview Aug 27, 2026 3:12pm
swingset Ready Ready Preview Aug 27, 2026 3:12pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3f61f68

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added useUserButtonController with Clerk-backed user, session, organization, invitation, and suggestion state. Added navigation, modal, switching, sign-out, authorization, pagination, and acceptance handlers. Added the client-side UserButton component with pending-state and action guards. Added controller and component tests, plus changeset metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 76060

The PR connects UserButton actions to live account and organization data; the only remaining concern is limited interaction-test coverage for pending actions and closing after selection. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: adding the UserButton controller. It is concise and directly related to the changeset.
Description check ✅ Passed The description directly explains the UserButton controller, connected container, supported data sources, actions, states, and behavior implemented by the changeset.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 4 files. (1 skipped: 1 unsupported.)


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

@github-actions github-actions Bot added the ui label Jul 16, 2026
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from 8786841 to ba22f92 Compare July 16, 2026 21:54
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from ba22f92 to c9bdf23 Compare July 30, 2026 18:38
@alexcarpenter alexcarpenter changed the title feat(ui): add AccountButton controller feat(ui): add UserButton controller Jul 30, 2026
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from c9bdf23 to 93aa6f2 Compare July 31, 2026 13:32
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from 93aa6f2 to 4643795 Compare August 3, 2026 15:08
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from 4643795 to 4467f3d Compare August 3, 2026 15:38
@alexcarpenter
alexcarpenter force-pushed the carp/account-button-controller branch from 4467f3d to fbd5c2f Compare August 3, 2026 17:09
alexcarpenter and others added 29 commits August 27, 2026 17:09
The connected test drives the real controller against a mocked Clerk, which is what
makes it worth having and also what makes it slow. Cases that only ever asserted what
the popover renders now sit in the view test, leaving the connected one to prove the
layers compose. Also covers `hidePersonal` reaching the popover through the container.
UserButtonProps picked only modePriority off the root, so the connected component was hard-wired to the combined surface and the orgs/user modes were reachable only by composing UserButtonView directly.
Presses a custom row on the connected UserButton and checks the app's
callback runs and the popover closes behind it.
An instance with organizations turned off has none to lead with or list,
so the button is the account's whatever `mode` asked for — `orgs` would
otherwise render an empty shell of a switcher. clerk-js withholds its own
OrganizationSwitcher at the mount boundary, which an app importing this
one never crosses, so the gate lives in the component.
The popover's open state and the one action in flight are the same flow, so
they now live in one machine instead of two useStates. Re-entry, clearing
busy, and closing on success stop being hand-written in the container: RUN is
simply unhandled while busy, and busy is only reachable from open.

Dismissing the popover mid-action now abandons the result rather than letting
it land in a surface that is already gone.
Follows the view's rename of `'orgs'` to `'organization'`.

Also corrects the integration suite's opening comment, which attributed
close-on-success to the container and had the navigation case backwards.
Carries the app's own pages and links into the profile the UserButton opens,
through useCustomPages and the built-in page list it orders them against.
Moves useCustomPages and useUserProfilePages out of the shared mosaic hooks folder into user-button.pages, and routes the custom page order through the same applyOrder rule the menu uses, which also stops a custom page named after a built-in from being sent twice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants