diff --git a/.changeset/bitter-paths-march.md b/.changeset/bitter-paths-march.md new file mode 100644 index 00000000000..f9a19e8815c --- /dev/null +++ b/.changeset/bitter-paths-march.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': patch +--- + +Fixes a bug where `usePlans()` would display stale data even if the `for` property has changed. diff --git a/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx b/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx index 926b7cb6c3d..17b5d14d4ca 100644 --- a/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx @@ -1,4 +1,5 @@ -import { renderHook, waitFor } from '@testing-library/react'; +import { render, renderHook, screen, waitFor } from '@testing-library/react'; +import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mockUser: any = { id: 'user_1' }; @@ -7,7 +8,10 @@ const mockOrganization: any = { id: 'org_1' }; const getPlansSpy = vi.fn((args: any) => Promise.resolve({ // pageSize maps to limit; default to 10 if missing - data: Array.from({ length: args.limit ?? args.pageSize ?? 10 }, (_, i) => ({ id: `plan_${i + 1}` })), + data: Array.from, Partial>( + { length: args.limit ?? args.pageSize ?? 10 }, + (_, i) => ({ id: `plan_${i + 1}`, forPayerType: args.for }), + ), total_count: 25, }), ); @@ -37,6 +41,8 @@ vi.mock('../../contexts', () => { }; }); +import type { BillingPlanResource } from '@clerk/types'; + import { usePlans } from '../usePlans'; import { wrapper } from './wrapper'; @@ -112,4 +118,69 @@ describe('usePlans', () => { expect(getPlansSpy.mock.calls[0][0]).toStrictEqual({ for: 'organization', pageSize: 3, initialPage: 1 }); expect(result.current.data.length).toBe(3); }); + + it('mounts user and organization hooks together and renders their respective data', async () => { + const DualPlans = () => { + const userPlans = usePlans({ initialPage: 1, pageSize: 2 }); + const orgPlans = usePlans({ initialPage: 1, pageSize: 2, for: 'organization' } as any); + + return ( + <> +
{userPlans.data.length}
+
{orgPlans.data.length}
+ + ); + }; + + render(, { wrapper }); + + await waitFor(() => expect(screen.getByTestId('user-count').textContent).toBe('2')); + await waitFor(() => expect(screen.getByTestId('org-count').textContent).toBe('2')); + + expect(getPlansSpy).toHaveBeenCalledTimes(2); + const calls = getPlansSpy.mock.calls.map(c => c[0]); + expect(calls).toEqual( + expect.arrayContaining([ + { for: 'user', initialPage: 1, pageSize: 2 }, + { for: 'organization', initialPage: 1, pageSize: 2 }, + ]), + ); + + // Ensure orgId does not leak into the fetcher params + for (const call of calls) { + expect(call).not.toHaveProperty('orgId'); + } + }); + + it('conditionally renders hooks based on prop passed to render', async () => { + const UserPlansCount = () => { + const userPlans = usePlans({ initialPage: 1, pageSize: 2 }); + return
{userPlans.data.map(p => p.forPayerType)[0]}
; + }; + + const OrgPlansCount = () => { + const orgPlans = usePlans({ initialPage: 1, pageSize: 2, for: 'organization' } as any); + return
{orgPlans.data.map(p => p.forPayerType)[0]}
; + }; + + const Conditional = ({ showOrg }: { showOrg: boolean }) => (showOrg ? : ); + + const { rerender } = render(, { wrapper }); + + await waitFor(() => expect(screen.getByTestId('user-type').textContent).toBe('user')); + expect(getPlansSpy).toHaveBeenCalledTimes(1); + expect(getPlansSpy.mock.calls[0][0]).toStrictEqual({ for: 'user', initialPage: 1, pageSize: 2 }); + + rerender(); + + await waitFor(() => expect(screen.getByTestId('org-type').textContent).toBe('organization')); + expect(getPlansSpy).toHaveBeenCalledTimes(2); + const calls = getPlansSpy.mock.calls.map(c => c[0]); + expect(calls).toEqual( + expect.arrayContaining([ + { for: 'user', initialPage: 1, pageSize: 2 }, + { for: 'organization', initialPage: 1, pageSize: 2 }, + ]), + ); + }); }); diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index 58b3e1ba1be..b49da476bfd 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -53,9 +53,11 @@ export function createBillingPaginatedHook { const { for: _for, ...paginationParams } = params || ({} as Partial); + const safeFor = _for || 'user'; + useAssertWrappedByClerkProvider(hookName); - const fetchFn = useFetcher(_for || 'user'); + const fetchFn = useFetcher(safeFor); const safeValues = useWithSafeValues(paginationParams, { initialPage: 1, @@ -74,17 +76,18 @@ export function createBillingPaginatedHook