diff --git a/.changeset/fuzzy-hotels-decide.md b/.changeset/fuzzy-hotels-decide.md new file mode 100644 index 00000000000..26e45da6afa --- /dev/null +++ b/.changeset/fuzzy-hotels-decide.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fix "You must belong to an organization" screen showing when user has existing memberships, invitations or suggestions diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index 6942ae08806..c0ac1acc82a 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -33,6 +33,7 @@ type ChooseOrganizationScreenProps = { export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) => { const card = useCardState(); + const { user } = useUser(); const { ref, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; @@ -50,7 +51,13 @@ export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) = sx={t => ({ padding: `${t.space.$none} ${t.space.$8}` })} > - + ({ margin: `${t.space.$none} ${t.space.$8}` })}>{card.error} diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index b0604a57582..f6daf9e1dcd 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -3,7 +3,10 @@ import { describe, expect, it } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; import { render } from '@/test/utils'; -import { createFakeUserOrganizationMembership } from '@/ui/components/OrganizationSwitcher/__tests__/test-utils'; +import { + createFakeUserOrganizationMembership, + createFakeUserOrganizationSuggestion, +} from '@/ui/components/OrganizationSwitcher/__tests__/test-utils'; import { TaskChooseOrganization } from '..'; @@ -240,4 +243,75 @@ describe('TaskChooseOrganization', () => { expect(queryByLabelText(/Slug/i)).toBeInTheDocument(); }); }); + + describe('when users are not allowed to create organizations', () => { + it('does not display create organization screen', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withUser({ + create_organization_enabled: false, + tasks: [{ key: 'choose-organization' }], + }); + }); + + const { findByText, queryByText } = render(, { wrapper }); + + expect(await findByText(/you must belong to an organization/i)).toBeInTheDocument(); + expect(await findByText(/contact your organization admin for an invitation/i)).toBeInTheDocument(); + expect(queryByText(/create new organization/i)).not.toBeInTheDocument(); + }); + + it('with existing memberships or suggestions, displays create organization screen', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withForceOrganizationSelection(); + f.withUser({ + create_organization_enabled: false, + tasks: [{ key: 'choose-organization' }], + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Existing Org', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationSuggestion({ + id: '2', + emailAddress: 'two@clerk.com', + publicOrganizationData: { + name: 'OrgTwoSuggestion', + }, + }), + ], + total_count: 1, + }), + ); + + const { findByText, queryByText } = render(, { wrapper }); + + expect(await findByText('Join an existing organization')).toBeInTheDocument(); + expect(await queryByText('Create new organization')).not.toBeInTheDocument(); + expect(await findByText('Existing Org')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx index c42c6f6ef91..dc3375e0d0b 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/index.tsx @@ -1,5 +1,5 @@ import { useClerk, useSession, useUser } from '@clerk/shared/react'; -import { type ComponentType, useState } from 'react'; +import { useState } from 'react'; import { useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts'; import { descriptors, Flex, Flow, localizationKeys, Spinner } from '@/ui/customizables'; @@ -14,10 +14,16 @@ import { ChooseOrganizationScreen } from './ChooseOrganizationScreen'; import { CreateOrganizationScreen } from './CreateOrganizationScreen'; const TaskChooseOrganizationInternal = () => { + const { user } = useUser(); const { userMemberships, userSuggestions, userInvitations } = useOrganizationListInView(); const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading; const hasExistingResources = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count); + const isOrganizationCreationDisabled = !isLoading && !user?.createOrganizationEnabled && !hasExistingResources; + + if (isOrganizationCreationDisabled) { + return ; + } return ( @@ -113,18 +119,6 @@ const TaskChooseOrganizationFlows = withCardStateProvider((props: TaskChooseOrga return setCurrentFlow('create')} />; }); -export const withOrganizationCreationEnabledGuard = (Component: ComponentType) => { - return (props: T) => { - const { user } = useUser(); - - if (!user?.createOrganizationEnabled) { - return ; - } - - return ; - }; -}; - function OrganizationCreationDisabledScreen() { return ( @@ -149,8 +143,5 @@ function OrganizationCreationDisabledScreen() { } export const TaskChooseOrganization = withCoreSessionSwitchGuard( - withTaskGuard( - withCardStateProvider(withOrganizationCreationEnabledGuard(TaskChooseOrganizationInternal)), - 'choose-organization', - ), + withTaskGuard(withCardStateProvider(TaskChooseOrganizationInternal), 'choose-organization'), ); diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index d2890bbdbb1..8e9c64c7b33 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -862,6 +862,7 @@ export const enUS: LocalizationResource = { action__invitationAccept: 'Join', action__suggestionsAccept: 'Request to join', subtitle: 'Join an existing organization or create a new one', + subtitle__createOrganizationDisabled: 'Join an existing organization', suggestionsAcceptedLabel: 'Pending approval', title: 'Choose an organization', }, diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 9e789d9c8fa..806826143d8 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1304,6 +1304,7 @@ export type __internal_LocalizationResource = { chooseOrganization: { title: LocalizationValue; subtitle: LocalizationValue; + subtitle__createOrganizationDisabled: LocalizationValue; suggestionsAcceptedLabel: LocalizationValue; action__suggestionsAccept: LocalizationValue; action__createOrganization: LocalizationValue;