Skip to content

feat(shared,ui): Render plan badge in OrganizationSwitcher#9149

Open
dstaley wants to merge 5 commits into
mainfrom
ds.feat/billing-plan-name
Open

feat(shared,ui): Render plan badge in OrganizationSwitcher#9149
dstaley wants to merge 5 commits into
mainfrom
ds.feat/billing-plan-name

Conversation

@dstaley

@dstaley dstaley commented Jul 13, 2026

Copy link
Copy Markdown
Member

Description

This PR adds a new prop to the OrganizationSwitcher component, showPlanName, that renders a badge when the current active organization is subscribed to a Clerk Billing Plan. The badge has an animated loading state (since it's resolved separately from the Organization information). Free plans use the primary color scheme, whereas paid plans use the secondary. This gives paid plans more visual weight.

Dark Mode (Paid) Light Mode (Paid)
CleanShot 2026-07-13 at 14 48 35@2x CleanShot 2026-07-13 at 14 49 09@2x
Dark Mode (Free) Light Mode (Free)
CleanShot 2026-07-13 at 14 51 21@2x CleanShot 2026-07-13 at 14 50 57@2x
Dark Mode (Loading) Light Mode (Loading)
CleanShot 2026-07-13 at 14 57 30@2x CleanShot 2026-07-13 at 14 49 51@2x

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:

Summary by CodeRabbit

  • New Features
    • Added showPlanName option to display the active organization’s Billing plan name in the Organization Switcher.
    • Shows a loading badge while plan details are being retrieved.
    • Added appearance customization for the plan badge shown in the Organization Switcher trigger.
  • Style
    • Introduced a subtle pulse animation for loading feedback.
  • Bug Fixes
    • Plan badge display now correctly follows billing configuration and subscription status.
  • Tests
    • Added coverage to verify the badge is shown/hidden based on billing settings.

@dstaley dstaley requested a review from alexcarpenter July 13, 2026 19:58
@vercel

vercel Bot commented Jul 13, 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, Comment Jul 13, 2026 9:56pm
swingset Ready Ready Preview, Comment Jul 13, 2026 9:56pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6d49e41

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

This PR includes changesets to release 23 packages
Name Type
@clerk/shared Minor
@clerk/ui Minor
@clerk/astro Patch
@clerk/backend Patch
@clerk/chrome-extension Patch
@clerk/clerk-js Patch
@clerk/electron Patch
@clerk/expo-passkeys Patch
@clerk/expo Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue Patch
@clerk/swingset Patch

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

@github-actions github-actions Bot added the ui label Jul 13, 2026
)}
</AcceptedInvitationsProvider>
</Flow.Root>
<SubscriberTypeContext.Provider value='organization'>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the only thing changed here is wrapping the existing Flow.Root in the SubscriberTypeContext.Provider

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 226a8209-678a-4bf4-974c-d297f3111980

📥 Commits

Reviewing files that changed from the base of the PR and between f8f1071 and 6d49e41.

📒 Files selected for processing (1)
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx

📝 Walkthrough

Walkthrough

OrganizationSwitcher can optionally display the active organization’s Billing plan name. The change adds subscription-aware badge rendering, loading styling, appearance customization, organization subscriber context, tests, and release metadata.

Changes

Organization plan badge

Layer / File(s) Summary
Plan display contract and subscriber context
packages/shared/src/types/clerk.ts, packages/ui/src/components/OrganizationSwitcher/index.tsx
Adds the optional showPlanName prop and provides organization subscriber context to the switcher subtree.
Subscription badge rendering and styling
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx, packages/ui/src/elements/OrganizationPreview.tsx, packages/ui/src/elements/Badge.tsx, packages/ui/src/styledSystem/animations.ts, packages/ui/src/customizables/elementDescriptors.ts, packages/ui/src/internal/appearance.ts
Renders loading or subscription plan badges, separates the badge from the organization name layout, adds pulse animation support, and exposes badge appearance customization.
Behavior tests and release metadata
packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx, .changeset/hungry-lands-fold.md
Tests enabled and disabled organization billing behavior and records minor package releases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OrganizationSwitcherTrigger
  participant useSubscription
  participant Billing
  participant OrganizationPreview
  participant Badge
  OrganizationSwitcherTrigger->>useSubscription: Read active organization subscription
  useSubscription->>Billing: Fetch subscription
  Billing-->>useSubscription: Return subscription items
  useSubscription-->>OrganizationSwitcherTrigger: Loading or plan item state
  OrganizationSwitcherTrigger->>OrganizationPreview: Pass plan badge
  OrganizationPreview->>Badge: Render loading or plan badge
Loading

Poem

A bunny hops through plans so bright,
A badge appears in silver light.
It pulses softly while things load,
Then names the plan along the road.
“Pro Plan!” says the rabbit, “off we go!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: rendering a plan badge in OrganizationSwitcher across shared and UI packages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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


if (isLoading) {
// 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter
return <LoadingBadge sx={{ width: '5ch' }} />;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the screenshots in the PR make this look larger than it actually is. This is what it looks like when not zoomed in.

Image

return (
<Badge
elementDescriptor={descriptors.organizationSwitcherTriggerBadge}
elementId={descriptors.organizationSwitcherTriggerBadge.setId(slug)}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

added this so you can specifically target certain plans to customize the badge

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9149

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9149

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9149

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9149

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9149

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9149

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9149

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9149

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9149

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9149

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9149

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9149

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9149

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9149

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9149

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9149

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9149

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9149

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9149

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9149

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9149

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9149

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9149

commit: 6d49e41

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-13T21:58:17.736Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 2
🔴 Breaking changes 0
🟡 Non-breaking changes 2
🟢 Additions 0

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/shared

Current version: 4.25.2
Recommended bump: MINOR → 4.26.0

Subpath ./types

🟡 Non-breaking Changes (1)

Modified: OrganizationSwitcherProps
// ... 8 unchanged lines elided ...
    skipInvitationScreen?: boolean;
    appearance?: ClerkAppearanceTheme;
    organizationProfileProps?: Pick<OrganizationProfileProps, 'appearance' | 'customPages'>;
+   showPlanName?: boolean;
  };

Static analyzer: Breaking change in type alias OrganizationSwitcherProps: Type changed: import("@clerk/shared").~CreateOrganizationMode&import("@clerk/shared").~OrganizationProfileMode&{defaultOpen?:boolean;…import("@clerk/shared").~CreateOrganizationMode&import("@clerk/shared").~OrganizationProfileMode&{defaultOpen?:boolean;…

🤖 AI review (reclassified as non-breaking) (95%): The only difference is the addition of a new optional property showPlanName?: boolean to OrganizationSwitcherProps. OrganizationSwitcherProps is used as an input parameter type (mountOrganizationSwitcher parameter), and adding an optional input property is non-breaking — existing callers don't need to pass it, and their existing object literals remain valid.


@clerk/ui

Current version: 1.25.2
Recommended bump: MINOR → 1.26.0

Subpath ./internal

🟡 Non-breaking Changes (1)

Modified: ElementsConfig
// ... 170 unchanged lines elided ...
    userButtonPopoverFooterPagesLink: WithOptions<'terms' | 'privacy'>;
    organizationSwitcherTrigger: WithOptions<'personal' | 'organization', 'open'>;
    organizationSwitcherTriggerIcon: WithOptions<never, 'open'>;
+   organizationSwitcherTriggerBadge: WithOptions<string>;
    organizationSwitcherPopoverRootBox: WithOptions;
    organizationSwitcherPopoverCard: WithOptions;
    organizationSwitcherPopoverMain: WithOptions;
// ... 379 unchanged lines elided ...

Static analyzer: Breaking change in type alias ElementsConfig: Type changed: {button:import("@clerk/ui").~WithOptions<string>;input:import("@clerk/ui").~WithOptions;checkbox:import("@clerk/ui").~W…{button:import("@clerk/ui").~WithOptions<string>;input:import("@clerk/ui").~WithOptions;checkbox:import("@clerk/ui").~W…

🤖 AI review (reclassified as non-breaking) (72%): The before and after snippets show the elided section grew by one line (475 → 476 lines), indicating a new property was added to ElementsConfig; ElementsConfig is used only as an output/mapped-type key source (type Elements = { [k in keyof ElementsConfig]: ... }[keyof ElementsConfig]), so consumers never construct a value of this type directly, and adding a new key is non-breaking.


Report generated by Break Check

Last ran on 6d49e41.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx (1)

90-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the free-plan (primary) and loading-state paths.

Both new tests only cover a paid plan (isDefault: falsesecondary colorScheme). Per the PR objective, free plans should render with the primary color scheme, and there's also an animated loading state (LoadingBadge) rendered while the subscription is being resolved — neither is exercised here. As per path instructions, **/*.{test,spec}.{ts,tsx,js,jsx} requires that "Unit tests are required for all new functionality" and to "Verify proper error handling and edge cases."

Consider adding:

  • A test with isDefault: true asserting the primary colorScheme is applied.
  • A test asserting the pulsing LoadingBadge renders while getSubscription is pending.

Do you want me to draft these additional test cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx`
around lines 90 - 166, Add coverage in OrganizationSwitcher tests for both
missing paths: configure the mocked subscription plan with isDefault: true and
assert the rendered plan badge uses the primary color scheme, then keep
getSubscription pending and assert the animated LoadingBadge appears during
resolution. Reuse the existing fixtures and showPlanName setup while preserving
the paid-plan and billing-disabled tests.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`:
- Line 66: Update OrganizationSwitcherTrigger’s badge rendering and the
corresponding PlanBadge usage around the noted lines so PlanBadge is mounted
only when showPlanName is enabled. Ensure useSubscription() is not invoked on
the default showPlanName={false} path, while preserving the existing badge
behavior when showPlanName is true.
- Around line 106-108: Update the LoadingBadge render in
OrganizationSwitcherTrigger to apply the same organizationSwitcherTriggerBadge
descriptor and corresponding elementId used by the normal plan badge, while
preserving its loading width and existing behavior.

---

Nitpick comments:
In
`@packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx`:
- Around line 90-166: Add coverage in OrganizationSwitcher tests for both
missing paths: configure the mocked subscription plan with isDefault: true and
assert the rendered plan badge uses the primary color scheme, then keep
getSubscription pending and assert the animated LoadingBadge appears during
resolution. Reuse the existing fixtures and showPlanName setup while preserving
the paid-plan and billing-disabled tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: beb9aa54-c754-49fd-a67d-36e1d6dce633

📥 Commits

Reviewing files that changed from the base of the PR and between ea75149 and 393cb2b.

📒 Files selected for processing (9)
  • .changeset/hungry-lands-fold.md
  • packages/shared/src/types/clerk.ts
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx
  • packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
  • packages/ui/src/components/OrganizationSwitcher/index.tsx
  • packages/ui/src/customizables/elementDescriptors.ts
  • packages/ui/src/elements/Badge.tsx
  • packages/ui/src/internal/appearance.ts
  • packages/ui/src/styledSystem/animations.ts

gap={3}
size='xs'
organization={organization}
badge={<PlanBadge />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Avoid fetching subscriptions when showPlanName is disabled.

OrganizationSwitcher always mounts <PlanBadge />, and PlanBadge calls useSubscription() before checking showPlanName. With the organization subscriber context and billing permissions, this makes the default showPlanName={false} path eligible to fetch billing data unnecessarily. Gate mounting at the caller or split the hook into a child rendered only when the prop is enabled.

Suggested gating
-    const { hidePersonal } = useOrganizationSwitcherContext();
+    const { hidePersonal, showPlanName } = useOrganizationSwitcherContext();

-            badge={<PlanBadge />}
+            badge={showPlanName ? <PlanBadge /> : undefined}

Also applies to: 98-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`
at line 66, Update OrganizationSwitcherTrigger’s badge rendering and the
corresponding PlanBadge usage around the noted lines so PlanBadge is mounted
only when showPlanName is enabled. Ensure useSubscription() is not invoked on
the default showPlanName={false} path, while preserving the existing badge
behavior when showPlanName is true.

Comment on lines +106 to +108
if (isLoading) {
// 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter
return <LoadingBadge sx={{ width: '5ch' }} />;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Apply the OrganizationSwitcher badge descriptor to the loading state.

The normal plan badge uses organizationSwitcherTriggerBadge, but LoadingBadge is rendered without an elementDescriptor or elementId. Appearance overrides therefore cannot target the loading placeholder. As per path instructions, themable elements under packages/ui/src must apply element descriptors.

Suggested fix
-    return <LoadingBadge sx={{ width: '5ch' }} />;
+    return (
+      <LoadingBadge
+        elementDescriptor={descriptors.organizationSwitcherTriggerBadge}
+        elementId={descriptors.organizationSwitcherTriggerBadge.setId('loading')}
+        sx={{ width: '5ch' }}
+      />
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isLoading) {
// 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter
return <LoadingBadge sx={{ width: '5ch' }} />;
if (isLoading) {
// 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter
return (
<LoadingBadge
elementDescriptor={descriptors.organizationSwitcherTriggerBadge}
elementId={descriptors.organizationSwitcherTriggerBadge.setId('loading')}
sx={{ width: '5ch' }}
/>
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`
around lines 106 - 108, Update the LoadingBadge render in
OrganizationSwitcherTrigger to apply the same organizationSwitcherTriggerBadge
descriptor and corresponding elementId used by the normal plan badge, while
preserving its loading width and existing behavior.

Source: Path instructions

@alexcarpenter alexcarpenter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you add an example showing how this wraps?

colorScheme={isDefault ? 'primary' : 'secondary'}
sx={{ whiteSpace: 'nowrap' }}
>
<Text

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we need the extra text usage in the badge vs supplying the sizing to the badge component?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

nah, we use <Text> in Badge in a few places, but here we don't really need it. Removed!

@dstaley

dstaley commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

@alexcarpenter played around a bit and this is where I landed

with a long organization name:
CleanShot 2026-07-13 at 16 26 33@2x

with a long plan name:
CleanShot 2026-07-13 at 16 25 57@2x

with long organization and long plan name:
CleanShot 2026-07-13 at 16 24 43@2x

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.

3 participants