From 9c088eb60ba3d94835eb2ca64b904c01e6f78ef7 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 13 Aug 2026 10:57:28 -0400 Subject: [PATCH] feat(shared): add an enabled param to the organization hooks useOrganization and useOrganizationList attempt to turn organizations on for the instance whenever they are read, which opens a dev-only prompt on an instance that has them disabled. The attempt now follows an enabled param, so a surface that reads organizations only when the instance already has them can opt out. Defaults to true. --- .changeset/org-hooks-enabled-param.md | 5 ++ .../useAttemptToEnableOrganizations.spec.tsx | 84 +++++++++++++++++++ .../hooks/useAttemptToEnableOrganizations.ts | 9 +- .../src/react/hooks/useOrganization.tsx | 12 ++- .../src/react/hooks/useOrganizationList.tsx | 13 ++- 5 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 .changeset/org-hooks-enabled-param.md create mode 100644 packages/shared/src/react/hooks/__tests__/useAttemptToEnableOrganizations.spec.tsx diff --git a/.changeset/org-hooks-enabled-param.md b/.changeset/org-hooks-enabled-param.md new file mode 100644 index 00000000000..98d5ded3a41 --- /dev/null +++ b/.changeset/org-hooks-enabled-param.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': minor +--- + +Add an `enabled` param to `useOrganization()` and `useOrganizationList()`. On a development instance with organizations disabled, reading either hook opens a prompt offering to turn them on. Pass `enabled: false` from a surface that reads organizations only when the instance already has them, and an instance that does not use organizations is never asked about them. Defaults to `true`, so existing callers are unaffected. diff --git a/packages/shared/src/react/hooks/__tests__/useAttemptToEnableOrganizations.spec.tsx b/packages/shared/src/react/hooks/__tests__/useAttemptToEnableOrganizations.spec.tsx new file mode 100644 index 00000000000..00ab30ef0a7 --- /dev/null +++ b/packages/shared/src/react/hooks/__tests__/useAttemptToEnableOrganizations.spec.tsx @@ -0,0 +1,84 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useAttemptToEnableOrganizations } from '../useAttemptToEnableOrganizations'; +import { useOrganization } from '../useOrganization'; +import { useOrganizationList } from '../useOrganizationList'; +import { createMockClerk, createMockQueryClient } from './mocks/clerk'; +import { wrapper } from './wrapper'; + +// Hoisted so the `../../contexts` factory below can reach it: that module is pulled in while the two +// hooks are imported, which is before a plain module-level const would have been assigned. +const mockState = vi.hoisted(() => ({ attemptSpy: vi.fn(), clerk: undefined as any })); +const attemptSpy = mockState.attemptSpy; + +vi.mock('../../contexts', () => ({ + useAssertWrappedByClerkProvider: () => {}, + useClerkInstanceContext: () => mockState.clerk, + useInitialStateContext: () => undefined, +})); + +mockState.clerk = createMockClerk({ + queryClient: createMockQueryClient(), + __internal_attemptToEnableEnvironmentSetting: attemptSpy, +}); + +vi.mock('../base/useUserBase', () => ({ useUserBase: () => ({ id: 'user_1' }) })); +vi.mock('../base/useOrganizationBase', () => ({ useOrganizationBase: () => null })); +vi.mock('../base/useSessionBase', () => ({ useSessionBase: () => null })); + +describe('useAttemptToEnableOrganizations', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('attempts once by default', () => { + const { rerender } = renderHook(() => useAttemptToEnableOrganizations('useOrganizationList'), { wrapper }); + rerender(); + + expect(attemptSpy).toHaveBeenCalledTimes(1); + expect(attemptSpy).toHaveBeenCalledWith({ for: 'organizations', caller: 'useOrganizationList' }); + }); + + // An app that reads the hook without wanting organizations would otherwise be shown the dev-only + // prompt to turn them on, which is an answer to a question it never asked. + it('attempts nothing when disabled', () => { + renderHook(() => useAttemptToEnableOrganizations('useOrganizationList', false), { wrapper }); + + expect(attemptSpy).not.toHaveBeenCalled(); + }); + + it('attempts once the caller opts back in', () => { + const { rerender } = renderHook(({ enabled }) => useAttemptToEnableOrganizations('useOrganization', enabled), { + wrapper, + initialProps: { enabled: false }, + }); + expect(attemptSpy).not.toHaveBeenCalled(); + + rerender({ enabled: true }); + expect(attemptSpy).toHaveBeenCalledTimes(1); + expect(attemptSpy).toHaveBeenCalledWith({ for: 'organizations', caller: 'useOrganization' }); + }); +}); + +// The two public hooks are the only callers, so this is the contract an app actually holds. +describe('the enabled param on the organization hooks', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('attempts by default', () => { + renderHook(() => useOrganizationList(), { wrapper }); + expect(attemptSpy).toHaveBeenCalledWith({ for: 'organizations', caller: 'useOrganizationList' }); + + renderHook(() => useOrganization(), { wrapper }); + expect(attemptSpy).toHaveBeenCalledWith({ for: 'organizations', caller: 'useOrganization' }); + }); + + it('attempts nothing when either hook is disabled', () => { + renderHook(() => useOrganizationList({ enabled: false }), { wrapper }); + renderHook(() => useOrganization({ enabled: false }), { wrapper }); + + expect(attemptSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/react/hooks/useAttemptToEnableOrganizations.ts b/packages/shared/src/react/hooks/useAttemptToEnableOrganizations.ts index 68a178b90dd..d4161a8e475 100644 --- a/packages/shared/src/react/hooks/useAttemptToEnableOrganizations.ts +++ b/packages/shared/src/react/hooks/useAttemptToEnableOrganizations.ts @@ -7,13 +7,16 @@ import { useClerk } from './useClerk'; * * @internal */ -export function useAttemptToEnableOrganizations(caller: 'useOrganization' | 'useOrganizationList') { +export function useAttemptToEnableOrganizations( + caller: 'useOrganization' | 'useOrganizationList', + enabled: boolean = true, +) { const clerk = useClerk(); const hasAttempted = useRef(false); useEffect(() => { // Guard to not run this effect twice on Clerk resource update - if (hasAttempted.current) { + if (!enabled || hasAttempted.current) { return; } @@ -23,5 +26,5 @@ export function useAttemptToEnableOrganizations(caller: 'useOrganization' | 'use for: 'organizations', caller, }); - }, [clerk, caller]); + }, [clerk, caller, enabled]); } diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index 619230df9b2..e0577ed7ce8 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -61,6 +61,15 @@ export type UseOrganizationParams = { * */ invitations?: true | PaginatedHookConfig; + /** + * Whether the hook may turn organizations on for the instance. On a development instance that has + * them disabled, reading this hook opens a prompt offering to enable them. Set to `false` in a + * surface that reads organizations only when the instance already has them, so an instance that + * does not use organizations is never asked about them. + * + * @default true + */ + enabled?: boolean; }; /** @@ -275,10 +284,11 @@ export function useOrganization(params?: T): Us membershipRequests: membershipRequestsListParams, memberships: membersListParams, invitations: invitationsListParams, + enabled, } = params || {}; useAssertWrappedByClerkProvider('useOrganization'); - useAttemptToEnableOrganizations('useOrganization'); + useAttemptToEnableOrganizations('useOrganization', enabled); const organization = useOrganizationBase(); const session = useSessionBase(); diff --git a/packages/shared/src/react/hooks/useOrganizationList.tsx b/packages/shared/src/react/hooks/useOrganizationList.tsx index e50fab1e88e..8f3a9b411bd 100644 --- a/packages/shared/src/react/hooks/useOrganizationList.tsx +++ b/packages/shared/src/react/hooks/useOrganizationList.tsx @@ -51,6 +51,15 @@ export type UseOrganizationListParams = { * */ userSuggestions?: true | PaginatedHookConfig; + /** + * Whether the hook may turn organizations on for the instance. On a development instance that has + * them disabled, reading this hook opens a prompt offering to enable them. Set to `false` in a + * surface that reads organizations only when the instance already has them, so an instance that + * does not use organizations is never asked about them. + * + * @default true + */ + enabled?: boolean; }; const undefinedPaginatedResource = { @@ -250,10 +259,10 @@ export type UseOrganizationListReturn = * ``` */ export function useOrganizationList(params?: T): UseOrganizationListReturn { - const { userMemberships, userInvitations, userSuggestions } = params || {}; + const { userMemberships, userInvitations, userSuggestions, enabled } = params || {}; useAssertWrappedByClerkProvider('useOrganizationList'); - useAttemptToEnableOrganizations('useOrganizationList'); + useAttemptToEnableOrganizations('useOrganizationList', enabled); const userMembershipsSafeValues = useWithSafeValues(userMemberships, { initialPage: 1,