Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-bags-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Disable billing hooks when the feature is turned off.
5 changes: 5 additions & 0 deletions .changeset/mighty-lions-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Hide billing tab when no paid plans exist, the user does not have a current or past subscription.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('Checkout', () => {
it('displays spinner when checkout is initializing', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
f.withBilling();
});

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

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

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

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

// Mock billing to prevent actual API calls
Expand Down Expand Up @@ -228,6 +233,7 @@ describe('Checkout', () => {
it('renders without errors for monthly period', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
f.withBilling();
});
fixtures.clerk.billing.startCheckout.mockImplementation(() => new Promise(() => {}));
const { baseElement } = render(
Expand Down Expand Up @@ -272,6 +278,7 @@ describe('Checkout', () => {
it('renders with correct CSS classes and structure', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
f.withBilling();
});

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

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

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

fixtures.clerk.user?.getPaymentSources.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const OrganizationPaymentAttemptPage = lazy(() =>
);

export const OrganizationProfileRoutes = () => {
const { pages, isMembersPageRoot, isGeneralPageRoot, isBillingPageRoot, isApiKeysPageRoot } =
const { pages, isMembersPageRoot, isGeneralPageRoot, isBillingPageRoot, isApiKeysPageRoot, shouldShowBilling } =
useOrganizationProfileContext();
const { apiKeysSettings, commerceSettings } = useEnvironment();

Expand Down Expand Up @@ -83,7 +83,7 @@ export const OrganizationProfileRoutes = () => {
</Route>
</Switch>
</Route>
{commerceSettings.billing.organization.enabled ? (
{commerceSettings.billing.organization.enabled && shouldShowBilling ? (
<Protect
condition={has =>
has({ permission: 'org:sys_billing:read' }) || has({ permission: 'org:sys_billing:manage' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { NavbarMenuButtonRow } from '@/ui/elements/Navbar';
import { ProfileCard } from '@/ui/elements/ProfileCard';

import { ORGANIZATION_PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
import { OrganizationProfileContext, withCoreUserGuard } from '../../contexts';
import { OrganizationProfileContext, SubscriberTypeContext, withCoreUserGuard } from '../../contexts';
import { Flow, localizationKeys } from '../../customizables';
import { Route, Switch } from '../../router';
import type { OrganizationProfileCtx } from '../../types';
Expand All @@ -26,7 +26,9 @@ const _OrganizationProfile = (_: OrganizationProfileProps) => {
<Flow.Part>
<Switch>
<Route>
<AuthenticatedRoutes />
<SubscriberTypeContext.Provider value='organization'>
<AuthenticatedRoutes />
</SubscriberTypeContext.Provider>
</Route>
</Switch>
</Flow.Part>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('PricingTable - trial info', () => {

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

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

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

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

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

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

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

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

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

it('shows plans when user is signed out', async () => {
const { wrapper, fixtures, props } = await createFixtures();
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withBilling();
});

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

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

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

// Provide empty props to the PricingTable context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('SubscriptionDetails', () => {
it('Displays spinner when init loading', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
f.withBilling();
});

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

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

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

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

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

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

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

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

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

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

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

const cancelSubscriptionMock = jest.fn().mockResolvedValue({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('SubscriptionsList', () => {
it('displays free trial badge when subscription is in free trial', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['[email protected]'] });
f.withBilling();
});

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

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

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

const upcomingSubscription = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const PaymentAttemptPage = lazy(() =>
);

export const UserProfileRoutes = () => {
const { pages } = useUserProfileContext();
const { pages, shouldShowBilling } = useUserProfileContext();
const { apiKeysSettings, commerceSettings } = useEnvironment();

const isAccountPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.ACCOUNT;
Expand Down Expand Up @@ -80,7 +80,7 @@ export const UserProfileRoutes = () => {
</Route>
</Switch>
</Route>
{commerceSettings.billing.user.enabled ? (
{commerceSettings.billing.user.enabled && shouldShowBilling ? (
<Route path={isBillingPageRoot ? undefined : 'billing'}>
<Switch>
<Route index>
Expand Down
Loading
Loading