From 80b52c1e5043c0fa9f3968d4836f449c2b5efe92 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 17 Oct 2025 20:20:33 +0300 Subject: [PATCH 1/2] fix(clerk-js): Backwards compatibility with deprecated prop in PricingTable --- .changeset/thin-maps-train.md | 6 + packages/clerk-js/src/core/clerk.ts | 12 +- .../__tests__/PricingTable.test.tsx | 172 ++++++++++++++++++ .../ui/contexts/ClerkUIComponentsContext.tsx | 7 +- 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 .changeset/thin-maps-train.md diff --git a/.changeset/thin-maps-train.md b/.changeset/thin-maps-train.md new file mode 100644 index 00000000000..afce3b44ee5 --- /dev/null +++ b/.changeset/thin-maps-train.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/nextjs': 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} From 5366e22c612166833ca2d2a1307cf3cfbd5149cf Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 17 Oct 2025 20:22:16 +0300 Subject: [PATCH 2/2] Update changeset for billing beta support Removed patch version for '@clerk/nextjs' and updated support details. --- .changeset/thin-maps-train.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/thin-maps-train.md b/.changeset/thin-maps-train.md index afce3b44ee5..98cf1786711 100644 --- a/.changeset/thin-maps-train.md +++ b/.changeset/thin-maps-train.md @@ -1,6 +1,5 @@ --- '@clerk/clerk-js': patch -'@clerk/nextjs': patch --- [Billing Beta] Extend support of `forOrganizations` prop by a few minors.