diff --git a/.changeset/rich-actors-cross.md b/.changeset/rich-actors-cross.md
new file mode 100644
index 0000000000..137f4ac986
--- /dev/null
+++ b/.changeset/rich-actors-cross.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Hide members page of if user doesn't have any membership related permissions.
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
index c25477d9db..b8f7a62f16 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembers.tsx
@@ -22,12 +22,13 @@ export const OrganizationMembers = withCardStateProvider(() => {
const { organizationSettings } = useEnvironment();
const card = useCardState();
const { isAuthorizedUser: canManageMemberships } = useGate({ permission: 'org:sys_memberships:manage' });
+ const { isAuthorizedUser: canReadMemberships } = useGate({ permission: 'org:sys_memberships:read' });
const isDomainsEnabled = organizationSettings?.domains?.enabled;
const { membershipRequests } = useCoreOrganization({
membershipRequests: isDomainsEnabled || undefined,
});
- // @ts-expect-error
+ // @ts-expect-error This property is not typed. It is used by our dashboard in order to render a billing widget.
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
if (canManageMemberships === null) {
@@ -55,7 +56,9 @@ export const OrganizationMembers = withCardStateProvider(() => {
-
+ {canReadMemberships && (
+
+ )}
{canManageMemberships && (
{
)}
-
-
- {canManageMemberships && __unstable_manageBillingUrl && }
-
-
-
+ {canReadMemberships && (
+
+
+ {canManageMemberships && __unstable_manageBillingUrl && }
+
+
+
+ )}
{canManageMemberships && (
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
index 7832af2bce..a4aa8e013d 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+import { useGate } from '../../../ui/common';
+import { ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID } from '../../../ui/constants';
import { useCoreOrganization, useOrganizationProfileContext } from '../../contexts';
import { Breadcrumbs, NavBar, NavbarContextProvider, OrganizationPreview } from '../../elements';
import type { PropsOfComponent } from '../../styledSystem';
@@ -10,6 +12,17 @@ export const OrganizationProfileNavbar = (
const { organization } = useCoreOrganization();
const { pages } = useOrganizationProfileContext();
+ const { isAuthorizedUser: allowMembersRoute } = useGate({
+ some: [
+ {
+ permission: 'org:sys_memberships:read',
+ },
+ {
+ permission: 'org:sys_memberships:manage',
+ },
+ ],
+ });
+
if (!organization) {
return null;
}
@@ -24,7 +37,11 @@ export const OrganizationProfileNavbar = (
sx={t => ({ margin: `0 0 ${t.space.$4} ${t.space.$2}` })}
/>
}
- routes={pages.routes}
+ routes={pages.routes.filter(
+ r =>
+ r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS ||
+ (r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS && allowMembersRoute),
+ )}
contentRef={props.contentRef}
/>
{props.children}
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
index 0af1a3cd55..7f2baece00 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
@@ -129,7 +129,12 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent
-
+
+
+
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx
index 85c05bf4df..1163112072 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx
@@ -70,7 +70,7 @@ describe('OrganizationMembers', () => {
f.withOrganizations();
f.withUser({
email_addresses: ['test@clerk.dev'],
- organization_memberships: [{ name: 'Org1', permissions: [] }],
+ organization_memberships: [{ name: 'Org1', permissions: ['org:sys_memberships:read'] }],
});
});
@@ -85,6 +85,26 @@ describe('OrganizationMembers', () => {
});
});
+ it('does not show members tab or navbar route if user is lacking permissions', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.dev'],
+ organization_memberships: [{ name: 'Org1', permissions: [] }],
+ });
+ });
+
+ fixtures.clerk.organization?.getRoles.mockRejectedValue(null);
+
+ const { queryByRole } = render(, { wrapper });
+
+ await waitFor(() => {
+ expect(queryByRole('tab', { name: 'Members' })).not.toBeInTheDocument();
+ expect(queryByRole('tab', { name: 'Invitations' })).not.toBeInTheDocument();
+ expect(queryByRole('tab', { name: 'Requests' })).not.toBeInTheDocument();
+ });
+ });
+
it('navigates to invite screen when user clicks on Invite button', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withOrganizations();
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
index 3b654a948e..05a4aaa2e5 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
@@ -53,4 +53,24 @@ describe('OrganizationProfile', () => {
expect(getByText('Custom1')).toBeDefined();
expect(getByText('ExternalLink')).toBeDefined();
});
+
+ it('removes member nav item if user is lacking permissions', async () => {
+ const { wrapper } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [
+ {
+ name: 'Org1',
+ permissions: [],
+ },
+ ],
+ });
+ });
+
+ const { queryByText } = render(, { wrapper });
+ expect(queryByText('Org1')).toBeInTheDocument();
+ expect(queryByText('Members')).not.toBeInTheDocument();
+ expect(queryByText('Settings')).toBeInTheDocument();
+ });
});