diff --git a/.changeset/thin-maps-train.md b/.changeset/thin-maps-train.md
new file mode 100644
index 00000000000..98cf1786711
--- /dev/null
+++ b/.changeset/thin-maps-train.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+[Billing Beta] Extend support of `forOrganizations` prop by a few minors.
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 8d1862961e3..10cfbc6eec0 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1151,16 +1151,24 @@ export class Clerk implements ClerkInterface {
}
return;
}
+ // Temporary backward compatibility for legacy prop: `forOrganizations`. Will be removed in the coming minor release.
+ const nextProps = { ...(props as any) } as PricingTableProps & { forOrganizations?: boolean };
+ if (typeof (props as any)?.forOrganizations !== 'undefined') {
+ logger.warnOnce(
+ 'Clerk: [IMPORTANT] prop `forOrganizations` is deprecated and will be removed in the coming minors. Use `for="organization"` instead.',
+ );
+ }
+
void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls =>
controls.mountComponent({
name: 'PricingTable',
appearanceKey: 'pricingTable',
node,
- props,
+ props: nextProps,
}),
);
- this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
+ this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', nextProps));
};
public unmountPricingTable = (node: HTMLDivElement): void => {
diff --git a/packages/clerk-js/src/ui/components/PricingTable/__tests__/PricingTable.test.tsx b/packages/clerk-js/src/ui/components/PricingTable/__tests__/PricingTable.test.tsx
index 7c01dee15ff..cdfcf4efbe3 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/__tests__/PricingTable.test.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/__tests__/PricingTable.test.tsx
@@ -96,6 +96,11 @@ describe('PricingTable - trial info', () => {
await waitFor(() => {
// Trial footer notice uses badge__trialEndsAt localization (short date format)
expect(getByText('Trial ends Jan 15, 2021')).toBeVisible();
+ // Ensure API args: user flow
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'user' }));
+ expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalledWith(
+ expect.objectContaining({ orgId: undefined }),
+ );
});
});
@@ -130,6 +135,11 @@ describe('PricingTable - trial info', () => {
expect(getByRole('heading', { name: 'Pro' })).toBeVisible();
// Button text from Plans.buttonPropsForPlan via freeTrialOr
expect(getByText('Start 14-day free trial')).toBeVisible();
+ // Ensure API args: user flow
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'user' }));
+ expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalledWith(
+ expect.objectContaining({ orgId: undefined }),
+ );
});
});
@@ -151,6 +161,8 @@ describe('PricingTable - trial info', () => {
expect(getByRole('heading', { name: 'Pro' })).toBeVisible();
// Signed out users should see free trial CTA when plan has trial enabled
expect(getByText('Start 14-day free trial')).toBeVisible();
+ // Ensure API args for signed-out flow: getPlans uses user context
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'user' }));
});
});
@@ -474,4 +486,164 @@ describe('PricingTable - plans visibility', () => {
// Assert the plan heading appears after subscription resolves
await findByRole('heading', { name: 'Test Plan' });
});
+
+ it('fetches organization plans and renders when using legacy forOrganizations: true', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withBilling();
+ f.withOrganizations();
+ f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: ['Org1'] });
+ });
+
+ // Set legacy prop via context provider
+ props.setProps({ forOrganizations: true } as any);
+
+ fixtures.clerk.billing.getPlans.mockResolvedValue({ data: [testPlan as any], total_count: 1 });
+ fixtures.clerk.billing.getSubscription.mockResolvedValue({
+ id: 'sub_org_active',
+ status: 'active',
+ activeAt: new Date('2021-01-01'),
+ createdAt: new Date('2021-01-01'),
+ nextPayment: null,
+ pastDueAt: null,
+ updatedAt: null,
+ subscriptionItems: [
+ {
+ id: 'si_active_org',
+ plan: { ...testPlan, forPayerType: 'organization' },
+ createdAt: new Date('2021-01-01'),
+ paymentMethodId: 'src_1',
+ pastDueAt: null,
+ canceledAt: null,
+ periodStart: new Date('2021-01-01'),
+ periodEnd: new Date('2021-01-31'),
+ planPeriod: 'month' as const,
+ status: 'active' as const,
+ isFreeTrial: false,
+ cancel: vi.fn(),
+ pathRoot: '',
+ reload: vi.fn(),
+ },
+ ],
+ pathRoot: '',
+ reload: vi.fn(),
+ });
+
+ const { getByRole } = render(, { wrapper });
+
+ await waitFor(() => {
+ // Ensure plans rendered
+ expect(getByRole('heading', { name: 'Test Plan' })).toBeVisible();
+ // Ensure API args reflect org context
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'organization' }));
+ // Ensure subscription called with active org
+ expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalledWith(expect.objectContaining({ orgId: 'Org1' }));
+ });
+ });
+
+ it('fetches organization plans and renders when using for: organization', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withBilling();
+ f.withOrganizations();
+ f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: ['Org1'] });
+ });
+
+ // Set new prop via context provider
+ props.setProps({ for: 'organization' } as any);
+
+ fixtures.clerk.billing.getPlans.mockResolvedValue({ data: [testPlan as any], total_count: 1 });
+ fixtures.clerk.billing.getSubscription.mockResolvedValue({
+ id: 'sub_org_active',
+ status: 'active',
+ activeAt: new Date('2021-01-01'),
+ createdAt: new Date('2021-01-01'),
+ nextPayment: null,
+ pastDueAt: null,
+ updatedAt: null,
+ subscriptionItems: [
+ {
+ id: 'si_active_org',
+ plan: { ...testPlan, forPayerType: 'organization' },
+ createdAt: new Date('2021-01-01'),
+ paymentMethodId: 'src_1',
+ pastDueAt: null,
+ canceledAt: null,
+ periodStart: new Date('2021-01-01'),
+ periodEnd: new Date('2021-01-31'),
+ planPeriod: 'month' as const,
+ status: 'active' as const,
+ isFreeTrial: false,
+ cancel: vi.fn(),
+ pathRoot: '',
+ reload: vi.fn(),
+ },
+ ],
+ pathRoot: '',
+ reload: vi.fn(),
+ });
+
+ const { getByRole } = render(, { wrapper });
+
+ await waitFor(() => {
+ // Ensure plans rendered
+ expect(getByRole('heading', { name: 'Test Plan' })).toBeVisible();
+ // Ensure getPlans was called with organization filter
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'organization' }));
+ // Ensure subscription called with active org
+ expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalledWith(expect.objectContaining({ orgId: 'Org1' }));
+ });
+ });
+
+ it('fetches user plans and renders when using for: user', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ f.withBilling();
+ });
+
+ // Set new prop via context provider
+ props.setProps({ for: 'user' } as any);
+
+ fixtures.clerk.billing.getPlans.mockResolvedValue({ data: [testPlan as any], total_count: 1 });
+ fixtures.clerk.billing.getSubscription.mockResolvedValue({
+ id: 'sub_active',
+ status: 'active',
+ activeAt: new Date('2021-01-01'),
+ createdAt: new Date('2021-01-01'),
+ nextPayment: null,
+ pastDueAt: null,
+ updatedAt: null,
+ subscriptionItems: [
+ {
+ id: 'si_active',
+ plan: testPlan,
+ createdAt: new Date('2021-01-01'),
+ paymentMethodId: 'src_1',
+ pastDueAt: null,
+ canceledAt: null,
+ periodStart: new Date('2021-01-01'),
+ periodEnd: new Date('2021-01-31'),
+ planPeriod: 'month' as const,
+ status: 'active' as const,
+ isFreeTrial: false,
+ cancel: vi.fn(),
+ pathRoot: '',
+ reload: vi.fn(),
+ },
+ ],
+ pathRoot: '',
+ reload: vi.fn(),
+ });
+
+ const { getByRole } = render(, { wrapper });
+
+ await waitFor(() => {
+ // Should show plans when signed in and has subscription
+ expect(getByRole('heading', { name: 'Test Plan' })).toBeVisible();
+ // Ensure getPlans was called with user filter
+ expect(fixtures.clerk.billing.getPlans).toHaveBeenCalledWith(expect.objectContaining({ for: 'user' }));
+ // Ensure subscription call is for user (no org)
+ expect(fixtures.clerk.billing.getSubscription).toHaveBeenCalledWith(
+ expect.objectContaining({ orgId: undefined }),
+ );
+ });
+ });
});
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index 1e1a147c9be..08fbecdc7f6 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -95,7 +95,12 @@ export function ComponentContextProvider({
);
case 'PricingTable':
return (
-
+
{children}