Skip to content

Commit e6e19d2

Browse files
authored
fix(clerk-js): Hide billing tab when appropriate (#6696)
1 parent 375ad8d commit e6e19d2

File tree

17 files changed

+245
-22
lines changed

17 files changed

+245
-22
lines changed

.changeset/grumpy-bags-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': patch
3+
---
4+
5+
Disable billing hooks when the feature is turned off.

.changeset/mighty-lions-cut.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Hide billing tab when no paid plans exist, the user does not have a current or past subscription.

packages/clerk-js/src/ui/components/Checkout/__tests__/Checkout.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('Checkout', () => {
2727
it('displays spinner when checkout is initializing', async () => {
2828
const { wrapper, fixtures } = await createFixtures(f => {
2929
f.withUser({ email_addresses: ['[email protected]'] });
30+
f.withBilling();
3031
});
3132

3233
// Mock billing to prevent actual API calls and stay in loading state
@@ -98,6 +99,7 @@ describe('Checkout', () => {
9899
it('handles checkout initialization errors gracefully', async () => {
99100
const { wrapper, fixtures } = await createFixtures(f => {
100101
f.withUser({ email_addresses: ['[email protected]'] });
102+
f.withBilling();
101103
});
102104

103105
// Mock billing to reject with a Clerk-like error shape
@@ -129,6 +131,7 @@ describe('Checkout', () => {
129131
it('displays proper loading state during checkout initialization', async () => {
130132
const { wrapper, fixtures } = await createFixtures(f => {
131133
f.withUser({ email_addresses: ['[email protected]'] });
134+
f.withBilling();
132135
});
133136

134137
// Mock billing to stay in loading state
@@ -161,6 +164,7 @@ describe('Checkout', () => {
161164
it('maintains accessibility attributes correctly', async () => {
162165
const { wrapper, fixtures } = await createFixtures(f => {
163166
f.withUser({ email_addresses: ['[email protected]'] });
167+
f.withBilling();
164168
});
165169

166170
fixtures.clerk.billing.startCheckout.mockImplementation(() => new Promise(() => {}));
@@ -201,6 +205,7 @@ describe('Checkout', () => {
201205
it('renders without crashing when all required props are provided', async () => {
202206
const { wrapper, fixtures } = await createFixtures(f => {
203207
f.withUser({ email_addresses: ['[email protected]'] });
208+
f.withBilling();
204209
});
205210

206211
// Mock billing to prevent actual API calls
@@ -228,6 +233,7 @@ describe('Checkout', () => {
228233
it('renders without errors for monthly period', async () => {
229234
const { wrapper, fixtures } = await createFixtures(f => {
230235
f.withUser({ email_addresses: ['[email protected]'] });
236+
f.withBilling();
231237
});
232238
fixtures.clerk.billing.startCheckout.mockImplementation(() => new Promise(() => {}));
233239
const { baseElement } = render(
@@ -272,6 +278,7 @@ describe('Checkout', () => {
272278
it('renders with correct CSS classes and structure', async () => {
273279
const { wrapper, fixtures } = await createFixtures(f => {
274280
f.withUser({ email_addresses: ['[email protected]'] });
281+
f.withBilling();
275282
});
276283

277284
fixtures.clerk.billing.startCheckout.mockImplementation(() => new Promise(() => {}));
@@ -305,6 +312,7 @@ describe('Checkout', () => {
305312
it('renders free trial details during confirmation stage', async () => {
306313
const { wrapper, fixtures } = await createFixtures(f => {
307314
f.withUser({ email_addresses: ['[email protected]'] });
315+
f.withBilling();
308316
});
309317

310318
const freeTrialEndsAt = new Date('2025-08-19');
@@ -388,6 +396,7 @@ describe('Checkout', () => {
388396
it('renders trial success details in completed stage', async () => {
389397
const { wrapper, fixtures } = await createFixtures(f => {
390398
f.withUser({ email_addresses: ['[email protected]'] });
399+
f.withBilling();
391400
});
392401

393402
const freeTrialEndsAt = new Date('2025-08-19');
@@ -469,6 +478,7 @@ describe('Checkout', () => {
469478
it('renders existing payment sources during checkout confirmation', async () => {
470479
const { wrapper, fixtures } = await createFixtures(f => {
471480
f.withUser({ email_addresses: ['[email protected]'] });
481+
f.withBilling();
472482
});
473483

474484
fixtures.clerk.user?.getPaymentSources.mockResolvedValue({

packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const OrganizationPaymentAttemptPage = lazy(() =>
3838
);
3939

4040
export const OrganizationProfileRoutes = () => {
41-
const { pages, isMembersPageRoot, isGeneralPageRoot, isBillingPageRoot, isApiKeysPageRoot } =
41+
const { pages, isMembersPageRoot, isGeneralPageRoot, isBillingPageRoot, isApiKeysPageRoot, shouldShowBilling } =
4242
useOrganizationProfileContext();
4343
const { apiKeysSettings, commerceSettings } = useEnvironment();
4444

@@ -83,7 +83,7 @@ export const OrganizationProfileRoutes = () => {
8383
</Route>
8484
</Switch>
8585
</Route>
86-
{commerceSettings.billing.organization.enabled ? (
86+
{commerceSettings.billing.organization.enabled && shouldShowBilling ? (
8787
<Protect
8888
condition={has =>
8989
has({ permission: 'org:sys_billing:read' }) || has({ permission: 'org:sys_billing:manage' })

packages/clerk-js/src/ui/components/OrganizationProfile/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { NavbarMenuButtonRow } from '@/ui/elements/Navbar';
77
import { ProfileCard } from '@/ui/elements/ProfileCard';
88

99
import { ORGANIZATION_PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
10-
import { OrganizationProfileContext, withCoreUserGuard } from '../../contexts';
10+
import { OrganizationProfileContext, SubscriberTypeContext, withCoreUserGuard } from '../../contexts';
1111
import { Flow, localizationKeys } from '../../customizables';
1212
import { Route, Switch } from '../../router';
1313
import type { OrganizationProfileCtx } from '../../types';
@@ -26,7 +26,9 @@ const _OrganizationProfile = (_: OrganizationProfileProps) => {
2626
<Flow.Part>
2727
<Switch>
2828
<Route>
29-
<AuthenticatedRoutes />
29+
<SubscriberTypeContext.Provider value='organization'>
30+
<AuthenticatedRoutes />
31+
</SubscriberTypeContext.Provider>
3032
</Route>
3133
</Switch>
3234
</Flow.Part>

packages/clerk-js/src/ui/components/PricingTable/__tests__/PricingTable.test.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('PricingTable - trial info', () => {
4444

4545
it('shows footer notice with trial end date when active subscription is in free trial', async () => {
4646
const { wrapper, fixtures, props } = await createFixtures(f => {
47+
f.withBilling();
4748
f.withUser({ email_addresses: ['[email protected]'] });
4849
});
4950

@@ -99,6 +100,7 @@ describe('PricingTable - trial info', () => {
99100
it('shows CTA "Start N-day free trial" when eligible and plan has trial', async () => {
100101
const { wrapper, fixtures, props } = await createFixtures(f => {
101102
f.withUser({ email_addresses: ['[email protected]'] });
103+
f.withBilling();
102104
});
103105

104106
// Provide empty props to the PricingTable context
@@ -130,7 +132,9 @@ describe('PricingTable - trial info', () => {
130132
});
131133

132134
it('shows CTA "Start N-day free trial" when user is signed out and plan has trial', async () => {
133-
const { wrapper, fixtures, props } = await createFixtures();
135+
const { wrapper, fixtures, props } = await createFixtures(f => {
136+
f.withBilling();
137+
});
134138

135139
// Provide empty props to the PricingTable context
136140
props.setProps({});
@@ -149,7 +153,9 @@ describe('PricingTable - trial info', () => {
149153
});
150154

151155
it('shows CTA "Subscribe" when user is signed out and plan has no trial', async () => {
152-
const { wrapper, fixtures, props } = await createFixtures();
156+
const { wrapper, fixtures, props } = await createFixtures(f => {
157+
f.withBilling();
158+
});
153159

154160
const nonTrialPlan = {
155161
...trialPlan,
@@ -178,6 +184,7 @@ describe('PricingTable - trial info', () => {
178184
it('shows footer notice with "starts at" when subscription is upcoming and not a free trial', async () => {
179185
const { wrapper, fixtures, props } = await createFixtures(f => {
180186
f.withUser({ email_addresses: ['[email protected]'] });
187+
f.withBilling();
181188
});
182189

183190
// Provide empty props to the PricingTable context
@@ -301,6 +308,7 @@ describe('PricingTable - plans visibility', () => {
301308
it('shows plans when user is signed in and has a subscription', async () => {
302309
const { wrapper, fixtures, props } = await createFixtures(f => {
303310
f.withUser({ email_addresses: ['[email protected]'] });
311+
f.withBilling();
304312
});
305313

306314
// Provide empty props to the PricingTable context
@@ -347,7 +355,9 @@ describe('PricingTable - plans visibility', () => {
347355
});
348356

349357
it('shows plans when user is signed out', async () => {
350-
const { wrapper, fixtures, props } = await createFixtures();
358+
const { wrapper, fixtures, props } = await createFixtures(f => {
359+
f.withBilling();
360+
});
351361

352362
// Provide empty props to the PricingTable context
353363
props.setProps({});
@@ -367,6 +377,7 @@ describe('PricingTable - plans visibility', () => {
367377
it('shows no plans when user is signed in but subscription is null', async () => {
368378
const { wrapper, fixtures, props } = await createFixtures(f => {
369379
f.withUser({ email_addresses: ['[email protected]'] });
380+
f.withBilling();
370381
});
371382

372383
// Provide empty props to the PricingTable context
@@ -387,6 +398,7 @@ describe('PricingTable - plans visibility', () => {
387398
it('shows no plans when user is signed in but subscription is undefined', async () => {
388399
const { wrapper, fixtures, props } = await createFixtures(f => {
389400
f.withUser({ email_addresses: ['[email protected]'] });
401+
f.withBilling();
390402
});
391403

392404
// Provide empty props to the PricingTable context
@@ -407,6 +419,7 @@ describe('PricingTable - plans visibility', () => {
407419
it('prevents flicker by not showing plans while subscription is loading', async () => {
408420
const { wrapper, fixtures, props } = await createFixtures(f => {
409421
f.withUser({ email_addresses: ['[email protected]'] });
422+
f.withBilling();
410423
});
411424

412425
// Provide empty props to the PricingTable context

packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('SubscriptionDetails', () => {
1010
it('Displays spinner when init loading', async () => {
1111
const { wrapper } = await createFixtures(f => {
1212
f.withUser({ email_addresses: ['[email protected]'] });
13+
f.withBilling();
1314
});
1415

1516
const { baseElement } = render(
@@ -31,6 +32,7 @@ describe('SubscriptionDetails', () => {
3132
it('single active monthly subscription', async () => {
3233
const { wrapper, fixtures } = await createFixtures(f => {
3334
f.withUser({ email_addresses: ['[email protected]'] });
35+
f.withBilling();
3436
});
3537

3638
fixtures.clerk.billing.getSubscription.mockResolvedValue({
@@ -134,6 +136,7 @@ describe('SubscriptionDetails', () => {
134136
it('single active annual subscription', async () => {
135137
const { wrapper, fixtures } = await createFixtures(f => {
136138
f.withUser({ email_addresses: ['[email protected]'] });
139+
f.withBilling();
137140
});
138141

139142
fixtures.clerk.billing.getSubscription.mockResolvedValue({
@@ -237,6 +240,7 @@ describe('SubscriptionDetails', () => {
237240
it('active free subscription', async () => {
238241
const { wrapper, fixtures } = await createFixtures(f => {
239242
f.withUser({ email_addresses: ['[email protected]'] });
243+
f.withBilling();
240244
});
241245

242246
fixtures.clerk.billing.getSubscription.mockResolvedValue({
@@ -319,6 +323,7 @@ describe('SubscriptionDetails', () => {
319323
it('one active annual and one upcoming monthly subscription', async () => {
320324
const { wrapper, fixtures } = await createFixtures(f => {
321325
f.withUser({ email_addresses: ['[email protected]'] });
326+
f.withBilling();
322327
});
323328

324329
const planAnnual = {
@@ -478,6 +483,7 @@ describe('SubscriptionDetails', () => {
478483
it('one active and one upcoming FREE subscription', async () => {
479484
const { wrapper, fixtures } = await createFixtures(f => {
480485
f.withUser({ email_addresses: ['[email protected]'] });
486+
f.withBilling();
481487
});
482488

483489
const planMonthly = {
@@ -614,6 +620,7 @@ describe('SubscriptionDetails', () => {
614620
it('allows cancelling a subscription of a monthly plan', async () => {
615621
const { wrapper, fixtures } = await createFixtures(f => {
616622
f.withUser({ email_addresses: ['[email protected]'] });
623+
f.withBilling();
617624
});
618625

619626
const cancelSubscriptionMock = jest.fn().mockResolvedValue({});
@@ -718,6 +725,7 @@ describe('SubscriptionDetails', () => {
718725
it('calls resubscribe when the user clicks Resubscribe for a canceled subscription', async () => {
719726
const { wrapper, fixtures } = await createFixtures(f => {
720727
f.withUser({ email_addresses: ['[email protected]'] });
728+
f.withBilling();
721729
});
722730

723731
const plan = {
@@ -820,6 +828,7 @@ describe('SubscriptionDetails', () => {
820828
it('calls switchToMonthly when the user clicks Switch to monthly for an annual subscription', async () => {
821829
const { wrapper, fixtures } = await createFixtures(f => {
822830
f.withUser({ email_addresses: ['[email protected]'] });
831+
f.withBilling();
823832
});
824833

825834
const plan = {
@@ -925,6 +934,7 @@ describe('SubscriptionDetails', () => {
925934
it('past due subscription shows correct status and disables actions', async () => {
926935
const { wrapper, fixtures } = await createFixtures(f => {
927936
f.withUser({ email_addresses: ['[email protected]'] });
937+
f.withBilling();
928938
});
929939

930940
const plan = {
@@ -1017,6 +1027,7 @@ describe('SubscriptionDetails', () => {
10171027
it('active free trial subscription shows correct labels and behavior', async () => {
10181028
const { wrapper, fixtures } = await createFixtures(f => {
10191029
f.withUser({ email_addresses: ['[email protected]'] });
1030+
f.withBilling();
10201031
});
10211032

10221033
fixtures.clerk.billing.getSubscription.mockResolvedValue({
@@ -1125,6 +1136,7 @@ describe('SubscriptionDetails', () => {
11251136
it('allows cancelling a free trial with specific dialog text', async () => {
11261137
const { wrapper, fixtures } = await createFixtures(f => {
11271138
f.withUser({ email_addresses: ['[email protected]'] });
1139+
f.withBilling();
11281140
});
11291141

11301142
const cancelSubscriptionMock = jest.fn().mockResolvedValue({});

packages/clerk-js/src/ui/components/Subscriptions/__tests__/SubscriptionsList.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('SubscriptionsList', () => {
1919
it('displays free trial badge when subscription is in free trial', async () => {
2020
const { wrapper, fixtures } = await createFixtures(f => {
2121
f.withUser({ email_addresses: ['[email protected]'] });
22+
f.withBilling();
2223
});
2324

2425
const freeTrialSubscription = {
@@ -82,6 +83,7 @@ describe('SubscriptionsList', () => {
8283
it('on past due, no badge, but past due date is shown', async () => {
8384
const { wrapper, fixtures } = await createFixtures(f => {
8485
f.withUser({ email_addresses: ['[email protected]'] });
86+
f.withBilling();
8587
});
8688

8789
const pastDueSubscription = {
@@ -146,6 +148,7 @@ describe('SubscriptionsList', () => {
146148
it('does not display active badge when subscription is active and it is a single item', async () => {
147149
const { wrapper, fixtures } = await createFixtures(f => {
148150
f.withUser({ email_addresses: ['[email protected]'] });
151+
f.withBilling();
149152
});
150153

151154
const activeSubscription = {
@@ -209,6 +212,7 @@ describe('SubscriptionsList', () => {
209212
it('renders upcomming badge when current subscription is canceled but active', async () => {
210213
const { wrapper, fixtures } = await createFixtures(f => {
211214
f.withUser({ email_addresses: ['[email protected]'] });
215+
f.withBilling();
212216
});
213217

214218
const upcomingSubscription = {

packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const PaymentAttemptPage = lazy(() =>
3838
);
3939

4040
export const UserProfileRoutes = () => {
41-
const { pages } = useUserProfileContext();
41+
const { pages, shouldShowBilling } = useUserProfileContext();
4242
const { apiKeysSettings, commerceSettings } = useEnvironment();
4343

4444
const isAccountPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.ACCOUNT;
@@ -80,7 +80,7 @@ export const UserProfileRoutes = () => {
8080
</Route>
8181
</Switch>
8282
</Route>
83-
{commerceSettings.billing.user.enabled ? (
83+
{commerceSettings.billing.user.enabled && shouldShowBilling ? (
8484
<Route path={isBillingPageRoot ? undefined : 'billing'}>
8585
<Switch>
8686
<Route index>

0 commit comments

Comments
 (0)