diff --git a/.changeset/shy-seahorses-begin.md b/.changeset/shy-seahorses-begin.md new file mode 100644 index 0000000000..1961e77037 --- /dev/null +++ b/.changeset/shy-seahorses-begin.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Replace role based check with permission based checks inside the OrganizationSwitcher component. diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx index 728ba9371c..a00b57ce77 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx @@ -2,7 +2,7 @@ import type { OrganizationResource } from '@clerk/types'; import React from 'react'; import { runIfFunctionOrReturn } from '../../../utils'; -import { NotificationCountBadge } from '../../common'; +import { NotificationCountBadge, withGate } from '../../common'; import { useCoreClerk, useCoreOrganization, @@ -175,15 +175,20 @@ export const OrganizationSwitcherPopover = React.forwardRef { - const { membership } = useCoreOrganization(); - const { organizationSettings } = useEnvironment(); - const isAdmin = membership?.role === 'admin'; - const allowRequests = organizationSettings?.domains?.enabled && isAdmin; +const NotificationCountBadgeManageButton = withGate( + () => { + const { organizationSettings } = useEnvironment(); - const { membershipRequests } = useCoreOrganization({ - membershipRequests: allowRequests || undefined, - }); + const isDomainsEnabled = organizationSettings?.domains?.enabled; - return ; -}; + const { membershipRequests } = useCoreOrganization({ + membershipRequests: isDomainsEnabled || undefined, + }); + + return ; + }, + { + // if the user is not able to accept a request we should not notify them + permission: 'org:memberships:manage', + }, +); diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index 8575795b24..c5b430cd18 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -1,6 +1,6 @@ import { forwardRef } from 'react'; -import { NotificationCountBadge } from '../../common'; +import { NotificationCountBadge, withGate } from '../../common'; import { useCoreOrganization, useCoreOrganizationList, @@ -72,28 +72,32 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer( ); }), ); -const NotificationCountBadgeSwitcherTrigger = () => { - /** - * Prefetch user invitations and suggestions - */ - const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams); - const { membership } = useCoreOrganization(); - const { organizationSettings } = useEnvironment(); - const isAdmin = membership?.role === 'admin'; - const allowRequests = organizationSettings?.domains?.enabled && isAdmin; - const { membershipRequests } = useCoreOrganization({ - membershipRequests: allowRequests || undefined, - }); +const NotificationCountBadgeSwitcherTrigger = withGate( + () => { + /** + * Prefetch user invitations and suggestions + */ + const { userInvitations, userSuggestions } = useCoreOrganizationList(organizationListParams); + const { organizationSettings } = useEnvironment(); + const isDomainsEnabled = organizationSettings?.domains?.enabled; + const { membershipRequests } = useCoreOrganization({ + membershipRequests: isDomainsEnabled || undefined, + }); - const notificationCount = - (userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0); + const notificationCount = + (userInvitations.count || 0) + (userSuggestions.count || 0) + (membershipRequests?.count || 0); - return ( - ({ - marginLeft: `${t.space.$2}`, - })} - notificationCount={notificationCount} - /> - ); -}; + return ( + ({ + marginLeft: `${t.space.$2}`, + })} + notificationCount={notificationCount} + /> + ); + }, + { + // if the user is not able to accept a request we should not notify them + permission: 'org:memberships:manage', + }, +); diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx index b1bf8f1a4a..51e4a8cba9 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx @@ -10,30 +10,33 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher'); describe('OrganizationSwitcher', () => { it('renders component', async () => { - const { wrapper } = await createFixtures(f => { + const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'] }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); const { queryByRole } = await act(() => render(, { wrapper })); expect(queryByRole('button')).toBeDefined(); }); describe('Personal Workspace', () => { it('shows the personal workspace when enabled', async () => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'] }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: false }); const { getByText } = await act(() => render(, { wrapper })); expect(getByText('Personal account')).toBeDefined(); }); it('does not show the personal workspace when disabled', async () => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'] }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { queryByText, getByRole, userEvent, getByText } = render(, { wrapper }); await userEvent.click(getByRole('button')); @@ -63,6 +66,8 @@ describe('OrganizationSwitcher', () => { }), ); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(true); + await runFakeTimers(async () => { const { getByText } = render(, { wrapper }); @@ -103,6 +108,8 @@ describe('OrganizationSwitcher', () => { }), ); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(true); + await runFakeTimers(async () => { const { getByText } = render(, { wrapper }); @@ -115,10 +122,11 @@ describe('OrganizationSwitcher', () => { describe('OrganizationSwitcherPopover', () => { it('opens the organization switcher popover when clicked', async () => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'], create_organization_enabled: true }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { getByText, getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button')); @@ -126,10 +134,11 @@ describe('OrganizationSwitcher', () => { }); it('lists all organizations the user belongs to', async () => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: ['Org1', 'Org2'] }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: false }); const { getAllByText, getByText, getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button')); @@ -143,13 +152,14 @@ describe('OrganizationSwitcher', () => { ['Member', 'basic_member'], ['Guest', 'guest_member'], ])('shows the text "%s" for the %s role in the active organization', async (text, role) => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'], organization_memberships: [{ name: 'Org1', role: role as MembershipRole }], }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { getAllByText, getByText, getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button')); @@ -165,6 +175,7 @@ describe('OrganizationSwitcher', () => { organization_memberships: [{ name: 'Org1', role: 'basic_member' }], }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button')); @@ -181,6 +192,7 @@ describe('OrganizationSwitcher', () => { create_organization_enabled: true, }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button', { name: 'Open organization switcher' })); @@ -189,7 +201,7 @@ describe('OrganizationSwitcher', () => { }); it('does not display create organization button if permissions not present', async () => { - const { wrapper, props } = await createFixtures(f => { + const { wrapper, props, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'], @@ -197,6 +209,7 @@ describe('OrganizationSwitcher', () => { create_organization_enabled: false, }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { queryByRole } = await act(() => render(, { wrapper })); expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument(); @@ -211,6 +224,7 @@ describe('OrganizationSwitcher', () => { create_organization_enabled: false, }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce( Promise.resolve({ data: [ @@ -254,6 +268,7 @@ describe('OrganizationSwitcher', () => { create_organization_enabled: false, }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce( Promise.resolve({ data: [ @@ -303,6 +318,7 @@ describe('OrganizationSwitcher', () => { }); }); fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve()); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); props.setProps({ hidePersonal: true }); const { getByRole, getByText, userEvent } = render(, { wrapper }); @@ -330,6 +346,7 @@ describe('OrganizationSwitcher', () => { }); }); + fixtures.clerk.session?.isAuthorized.mockResolvedValue(false); fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve()); const { getByRole, getByText, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button'));