Skip to content

Commit d37d44a

Browse files
authoredOct 31, 2023
feat(clerk-js): Use org:sys_domains:read for improved fine grain control of the UI (#1988)
Backport of #1896
1 parent 3ba3f38 commit d37d44a

19 files changed

+344
-208
lines changed
 

‎.changeset/curly-news-push.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/types': patch
4+
---
5+
6+
Shows list of domains if member has the `org:sys_domain:read` permission.

‎packages/clerk-js/src/core/resources/Session.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import type {
1515
import { unixEpochToDate } from '../../utils/date';
1616
import { eventBus, events } from '../events';
1717
import { SessionTokenCache } from '../tokenCache';
18-
import { PublicUserData } from './internal';
19-
import { BaseResource, Token, User } from './internal';
18+
import { BaseResource, PublicUserData, Token, User } from './internal';
2019

2120
export class Session extends BaseResource implements SessionResource {
2221
pathRoot = '/client/sessions';
@@ -71,7 +70,7 @@ export class Session extends BaseResource implements SessionResource {
7170
};
7271

7372
// TODO: Fix this eslint error
74-
// eslint-disable-next-line @typescript-eslint/require-await
73+
7574
getToken: GetToken = async (options?: GetTokenOptions): Promise<string | null> => {
7675
return runWithExponentialBackOff(() => this._getToken(options), {
7776
shouldRetry: (error: unknown, currentIteration: number) => !is4xxError(error) && currentIteration < 4,
@@ -82,7 +81,7 @@ export class Session extends BaseResource implements SessionResource {
8281
* @experimental The method is experimental and subject to change in future releases.
8382
*/
8483
isAuthorized: IsAuthorized = async params => {
85-
return new Promise(resolve => {
84+
return new Promise((resolve, reject) => {
8685
// if there is no active organization user can not be authorized
8786
if (!this.lastActiveOrganizationId || !this.user) {
8887
return resolve(false);
@@ -106,7 +105,22 @@ export class Session extends BaseResource implements SessionResource {
106105
if (params.role) {
107106
return resolve(activeOrganizationRole === params.role);
108107
}
109-
return resolve(false);
108+
109+
if (params.any) {
110+
return resolve(
111+
!!params.any.find(permObj => {
112+
if (permObj.permission) {
113+
return activeOrganizationPermissions.includes(permObj.permission);
114+
}
115+
if (permObj.role) {
116+
return activeOrganizationRole === permObj.role;
117+
}
118+
return false;
119+
}),
120+
);
121+
}
122+
123+
return reject();
110124
});
111125
};
112126

‎packages/clerk-js/src/core/test/fixtures.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
export const mockJwt =
1414
'eyJhbGciOiJSUzI1NiIsImtpZCI6Imluc18yR0lvUWhiVXB5MGhYN0IyY1ZrdVRNaW5Yb0QiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL2FjY291bnRzLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsImV4cCI6MTY2NjY0ODMxMCwiaWF0IjoxNjY2NjQ4MjUwLCJpc3MiOiJodHRwczovL2NsZXJrLmluc3BpcmVkLnB1bWEtNzQubGNsLmRldiIsIm5iZiI6MTY2NjY0ODI0MCwic2lkIjoic2Vzc18yR2JEQjRlbk5kQ2E1dlMxenBDM1h6Zzl0SzkiLCJzdWIiOiJ1c2VyXzJHSXBYT0VwVnlKdzUxcmtabjlLbW5jNlN4ciJ9.n1Usc-DLDftqA0Xb-_2w8IGs4yjCmwc5RngwbSRvwevuZOIuRoeHmE2sgCdEvjfJEa7ewL6EVGVcM557TWPW--g_J1XQPwBy8tXfz7-S73CEuyRFiR97L2AHRdvRtvGtwR-o6l8aHaFxtlmfWbQXfg4kFJz2UGe9afmh3U9-f_4JOZ5fa3mI98UMy1-bo20vjXeWQ9aGrqaxHQxjnzzC-1Kpi5LdPvhQ16H0dPB8MHRTSM5TAuLKTpPV7wqixmbtcc2-0k6b9FKYZNqRVTaIyV-lifZloBvdzlfOF8nW1VVH_fx-iW5Q3hovHFcJIULHEC1kcAYTubbxzpgeVQepGg';
1515

16-
type OrgParams = Partial<OrganizationJSON> & { role?: MembershipRole; permissions?: OrganizationPermission[] };
16+
export type OrgParams = Partial<OrganizationJSON> & { role?: MembershipRole; permissions?: OrganizationPermission[] };
1717

1818
type WithUserParams = Omit<
1919
Partial<UserJSON>,

‎packages/clerk-js/src/ui/common/Gate.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import type { IsAuthorized, OrganizationPermission } from '@clerk/types';
1+
import type { IsAuthorized } from '@clerk/types';
22
import type { ComponentType, PropsWithChildren, ReactNode } from 'react';
33
import React, { useEffect } from 'react';
44

55
import { useCoreSession } from '../contexts';
66
import { useFetch } from '../hooks';
77
import { useRouter } from '../router';
88

9-
type GateParams = Omit<Parameters<IsAuthorized>[0], 'permission'> & { permission: OrganizationPermission };
9+
type GateParams = Parameters<IsAuthorized>[0];
1010
type GateProps = PropsWithChildren<
1111
GateParams & {
1212
fallback?: ReactNode;

‎packages/clerk-js/src/ui/components/OrganizationProfile/DomainList.tsx

+71-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import type { GetDomainsParams, OrganizationEnrollmentMode } from '@clerk/types';
2-
import type { OrganizationDomainVerificationStatus } from '@clerk/types';
1+
import type {
2+
GetDomainsParams,
3+
OrganizationDomainResource,
4+
OrganizationDomainVerificationStatus,
5+
OrganizationEnrollmentMode,
6+
} from '@clerk/types';
37
import React, { useMemo } from 'react';
48

5-
import { withGate } from '../../common';
9+
import { stripOrigin, toURL, trimLeadingSlash } from '../../../utils';
10+
import { useGate, withGate } from '../../common';
611
import { useCoreOrganization } from '../../contexts';
12+
import type { LocalizationKey } from '../../customizables';
713
import { Box, Col, localizationKeys, Spinner } from '../../customizables';
814
import { ArrowBlockButton, BlockWithTrailingComponent, ThreeDotsMenu } from '../../elements';
915
import { useInView } from '../../hooks';
@@ -17,10 +23,61 @@ type DomainListProps = GetDomainsParams & {
1723
* Enables internal links to navigate to the correct page
1824
* based on when this component is used
1925
*/
20-
redirectSubPath: string;
26+
redirectSubPath: 'organization-settings/domain' | 'domain';
2127
fallback?: React.ReactNode;
2228
};
2329

30+
const useDomainList = () => {
31+
const { isAuthorizedUser: canDeleteDomain } = useGate({ permission: 'org:sys_domains:delete' });
32+
const { isAuthorizedUser: canVerifyDomain } = useGate({ permission: 'org:sys_domains:manage' });
33+
34+
return {
35+
showDotMenu: canDeleteDomain || canVerifyDomain,
36+
canVerifyDomain,
37+
canDeleteDomain,
38+
};
39+
};
40+
41+
const buildDomainListRelativeURL = (parentPath: string, domainId: string, mode?: 'verify' | 'remove') =>
42+
trimLeadingSlash(stripOrigin(toURL(`${parentPath}/${domainId}/${mode || ''}`)));
43+
44+
const useMenuActions = (
45+
parentPath: string,
46+
domainId: string,
47+
): { label: LocalizationKey; onClick: () => Promise<unknown>; isDestructive?: boolean }[] => {
48+
const { canDeleteDomain, canVerifyDomain } = useDomainList();
49+
const { navigate } = useRouter();
50+
51+
const menuActions = [];
52+
53+
if (canVerifyDomain) {
54+
menuActions.push({
55+
label: localizationKeys('organizationProfile.profilePage.domainSection.unverifiedDomain_menuAction__verify'),
56+
onClick: () => navigate(buildDomainListRelativeURL(parentPath, domainId, 'verify')),
57+
});
58+
}
59+
60+
if (canDeleteDomain) {
61+
menuActions.push({
62+
label: localizationKeys('organizationProfile.profilePage.domainSection.unverifiedDomain_menuAction__remove'),
63+
isDestructive: true,
64+
onClick: () => navigate(buildDomainListRelativeURL(parentPath, domainId, 'remove')),
65+
});
66+
}
67+
68+
return menuActions;
69+
};
70+
71+
const DomainListDotMenu = ({
72+
redirectSubPath,
73+
domainId,
74+
}: Pick<DomainListProps, 'redirectSubPath'> & {
75+
domainId: OrganizationDomainResource['id'];
76+
}) => {
77+
const actions = useMenuActions(redirectSubPath, domainId);
78+
return <ThreeDotsMenu actions={actions} />;
79+
};
80+
2481
export const DomainList = withGate(
2582
(props: DomainListProps) => {
2683
const { verificationStatus, enrollmentMode, redirectSubPath, fallback, ...rest } = props;
@@ -31,6 +88,7 @@ export const DomainList = withGate(
3188
},
3289
});
3390

91+
const { showDotMenu } = useDomainList();
3492
const { ref } = useInView({
3593
threshold: 0,
3694
onChange: inView => {
@@ -69,7 +127,7 @@ export const DomainList = withGate(
69127
<Col>
70128
{domainList.length === 0 && !domains?.isLoading && fallback}
71129
{domainList.map(d => {
72-
if (!(d.verification && d.verification.status === 'verified')) {
130+
if (!(d.verification && d.verification.status === 'verified') || !showDotMenu) {
73131
return (
74132
<BlockWithTrailingComponent
75133
key={d.id}
@@ -82,23 +140,12 @@ export const DomainList = withGate(
82140
})}
83141
badge={<EnrollmentBadge organizationDomain={d} />}
84142
trailingComponent={
85-
<ThreeDotsMenu
86-
actions={[
87-
{
88-
label: localizationKeys(
89-
'organizationProfile.profilePage.domainSection.unverifiedDomain_menuAction__verify',
90-
),
91-
onClick: () => navigate(`${redirectSubPath}${d.id}/verify`),
92-
},
93-
{
94-
label: localizationKeys(
95-
'organizationProfile.profilePage.domainSection.unverifiedDomain_menuAction__remove',
96-
),
97-
isDestructive: true,
98-
onClick: () => navigate(`${redirectSubPath}${d.id}/remove`),
99-
},
100-
]}
101-
/>
143+
showDotMenu ? (
144+
<DomainListDotMenu
145+
redirectSubPath={redirectSubPath}
146+
domainId={d.id}
147+
/>
148+
) : undefined
102149
}
103150
>
104151
{d.name}
@@ -116,7 +163,7 @@ export const DomainList = withGate(
116163
padding: `${t.space.$3} ${t.space.$4}`,
117164
minHeight: t.sizes.$10,
118165
})}
119-
onClick={() => navigate(`${redirectSubPath}${d.id}`)}
166+
onClick={() => navigate(buildDomainListRelativeURL(redirectSubPath, d.id))}
120167
>
121168
{d.name}
122169
</ArrowBlockButton>
@@ -154,6 +201,6 @@ export const DomainList = withGate(
154201
);
155202
},
156203
{
157-
permission: 'org:sys_domains:manage',
204+
permission: 'org:sys_domains:read',
158205
},
159206
);

‎packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabInvitations.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const OrganizationMembersTabInvitations = () => {
5858
onClick={() => navigate('organization-settings/domain')}
5959
/>
6060
}
61-
redirectSubPath={'organization-settings/domain/'}
61+
redirectSubPath={'organization-settings/domain'}
6262
verificationStatus={'verified'}
6363
enrollmentMode={'automatic_invitation'}
6464
/>

‎packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationMembersTabRequests.tsx

+45-37
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BlockButton } from '../../common';
2-
import { useOrganizationProfileContext } from '../../contexts';
1+
import { BlockButton, Gate } from '../../common';
2+
import { useEnvironment, useOrganizationProfileContext } from '../../contexts';
33
import { Col, Flex, localizationKeys } from '../../customizables';
44
import { Header } from '../../elements';
55
import { useRouter } from '../../router';
@@ -8,10 +8,13 @@ import { MembershipWidget } from './MembershipWidget';
88
import { RequestToJoinList } from './RequestToJoinList';
99

1010
export const OrganizationMembersTabRequests = () => {
11+
const { organizationSettings } = useEnvironment();
1112
const { navigate } = useRouter();
1213
//@ts-expect-error
1314
const { __unstable_manageBillingUrl } = useOrganizationProfileContext();
1415

16+
const isDomainsEnabled = organizationSettings?.domains?.enabled;
17+
1518
return (
1619
<Col
1720
gap={8}
@@ -20,42 +23,47 @@ export const OrganizationMembersTabRequests = () => {
2023
}}
2124
>
2225
{__unstable_manageBillingUrl && <MembershipWidget />}
23-
<Col
24-
gap={2}
25-
sx={{
26-
width: '100%',
27-
}}
28-
>
29-
<Header.Root>
30-
<Header.Title
31-
localizationKey={localizationKeys(
32-
'organizationProfile.membersPage.requestsTab.autoSuggestions.headerTitle',
33-
)}
34-
textVariant='largeMedium'
35-
/>
36-
<Header.Subtitle
37-
localizationKey={localizationKeys(
38-
'organizationProfile.membersPage.requestsTab.autoSuggestions.headerSubtitle',
39-
)}
40-
variant='regularRegular'
41-
/>
42-
</Header.Root>
43-
<DomainList
44-
fallback={
45-
<BlockButton
46-
colorScheme='primary'
47-
textLocalizationKey={localizationKeys(
48-
'organizationProfile.membersPage.requestsTab.autoSuggestions.primaryButton',
49-
)}
50-
id='manageVerifiedDomains'
51-
onClick={() => navigate('organization-settings/domain')}
26+
27+
{isDomainsEnabled && (
28+
<Gate permission='org:sys_domains:manage'>
29+
<Col
30+
gap={2}
31+
sx={{
32+
width: '100%',
33+
}}
34+
>
35+
<Header.Root>
36+
<Header.Title
37+
localizationKey={localizationKeys(
38+
'organizationProfile.membersPage.requestsTab.autoSuggestions.headerTitle',
39+
)}
40+
textVariant='largeMedium'
41+
/>
42+
<Header.Subtitle
43+
localizationKey={localizationKeys(
44+
'organizationProfile.membersPage.requestsTab.autoSuggestions.headerSubtitle',
45+
)}
46+
variant='regularRegular'
47+
/>
48+
</Header.Root>
49+
<DomainList
50+
fallback={
51+
<BlockButton
52+
colorScheme='primary'
53+
textLocalizationKey={localizationKeys(
54+
'organizationProfile.membersPage.requestsTab.autoSuggestions.primaryButton',
55+
)}
56+
id='manageVerifiedDomains'
57+
onClick={() => navigate('organization-settings/domain')}
58+
/>
59+
}
60+
redirectSubPath={'organization-settings/domain'}
61+
verificationStatus={'verified'}
62+
enrollmentMode={'automatic_suggestion'}
5263
/>
53-
}
54-
redirectSubPath={'organization-settings/domain/'}
55-
verificationStatus={'verified'}
56-
enrollmentMode={'automatic_suggestion'}
57-
/>
58-
</Col>
64+
</Col>
65+
</Gate>
66+
)}
5967

6068
<Flex
6169
direction='col'

‎packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const OrganizationProfileRoutes = (props: PropsOfComponent<typeof Profile
7777
</Route>
7878
<Route path=':id'>
7979
<Gate
80-
permission={'org:sys_domains:manage'}
80+
any={[{ permission: 'org:sys_domains:manage' }, { permission: 'org:sys_domains:delete' }]}
8181
redirectTo='../../'
8282
>
8383
<VerifiedDomainPage />

‎packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationSettings.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const OrganizationSettings = () => {
2626
<Header.Subtitle localizationKey={localizationKeys('organizationProfile.start.headerSubtitle__settings')} />
2727
</Header.Root>
2828
<OrganizationProfileSection />
29-
<Gate permission='org:sys_domains:manage'>
29+
<Gate permission='org:sys_domains:read'>
3030
<OrganizationDomainsSection />
3131
</Gate>
3232
<OrganizationDangerSection />
@@ -85,7 +85,7 @@ const OrganizationDomainsSection = () => {
8585
subtitle={localizationKeys('organizationProfile.profilePage.domainSection.subtitle')}
8686
id='organizationDomains'
8787
>
88-
<DomainList redirectSubPath={'domain/'} />
88+
<DomainList redirectSubPath={'domain'} />
8989

9090
<AddBlockButton
9191
textLocalizationKey={localizationKeys('organizationProfile.profilePage.domainSection.primaryButton')}

‎packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx

+63-55
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from '@clerk/types';
22

3-
import { CalloutWithAction } from '../../common';
3+
import { CalloutWithAction, useGate } from '../../common';
44
import { useCoreOrganization, useEnvironment } from '../../contexts';
55
import type { LocalizationKey } from '../../customizables';
66
import { Col, Flex, localizationKeys, Spinner, Text } from '../../customizables';
@@ -54,13 +54,16 @@ const useCalloutLabel = (
5454
export const VerifiedDomainPage = withCardStateProvider(() => {
5555
const card = useCardState();
5656
const { organizationSettings } = useEnvironment();
57-
const { organization } = useCoreOrganization();
58-
const { domains } = useCoreOrganization({
57+
58+
const { organization, domains } = useCoreOrganization({
5959
domains: {
6060
infinite: true,
6161
},
6262
});
6363

64+
const { isAuthorizedUser: canManageDomain } = useGate({ permission: 'org:sys_domains:manage' });
65+
const { isAuthorizedUser: canDeleteDomain } = useGate({ permission: 'org:sys_domains:delete' });
66+
6467
const { navigateToFlowStart } = useNavigateToFlowStart();
6568
const { params, navigate, queryParams } = useRouter();
6669
const mode = (queryParams.mode || 'edit') as 'select' | 'edit';
@@ -200,69 +203,74 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
200203
<Col gap={6}>
201204
<Tabs>
202205
<TabsList>
203-
<Tab
204-
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.start.headerTitle__enrollment')}
205-
/>
206-
{allowsEdit && (
206+
{canManageDomain && (
207+
<Tab
208+
localizationKey={localizationKeys(
209+
'organizationProfile.verifiedDomainPage.start.headerTitle__enrollment',
210+
)}
211+
/>
212+
)}
213+
{allowsEdit && canDeleteDomain && (
207214
<Tab
208215
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.start.headerTitle__danger')}
209216
/>
210217
)}
211218
</TabsList>
212219
<TabPanels>
213-
<TabPanel
214-
sx={{ width: '100%' }}
215-
direction={'col'}
216-
gap={4}
217-
>
218-
{calloutLabel.length > 0 && (
219-
<CalloutWithAction icon={InformationCircle}>
220-
{calloutLabel.map((label, index) => (
221-
<Text
222-
key={index}
223-
as={'span'}
224-
sx={[
225-
t => ({
226-
lineHeight: t.lineHeights.$short,
227-
color: 'inherit',
228-
display: 'block',
229-
}),
230-
]}
231-
localizationKey={label}
232-
/>
233-
))}
234-
</CalloutWithAction>
235-
)}
236-
<Header.Root>
237-
<Header.Subtitle
238-
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.subtitle')}
239-
variant='regularRegular'
240-
/>
241-
</Header.Root>
242-
<Form.Root
243-
onSubmit={updateEnrollmentMode}
244-
gap={6}
220+
{canManageDomain && (
221+
<TabPanel
222+
sx={{ width: '100%' }}
223+
direction={'col'}
224+
gap={4}
245225
>
246-
<Form.ControlRow elementId={enrollmentMode.id}>
247-
<Form.Control {...enrollmentMode.props} />
248-
</Form.ControlRow>
249-
250-
{allowsEdit && (
251-
<Form.ControlRow elementId={deletePending.id}>
252-
<Form.Control {...deletePending.props} />
253-
</Form.ControlRow>
226+
{calloutLabel.length > 0 && (
227+
<CalloutWithAction icon={InformationCircle}>
228+
{calloutLabel.map((label, index) => (
229+
<Text
230+
key={index}
231+
as={'span'}
232+
sx={[
233+
t => ({
234+
lineHeight: t.lineHeights.$short,
235+
color: 'inherit',
236+
display: 'block',
237+
}),
238+
]}
239+
localizationKey={label}
240+
/>
241+
))}
242+
</CalloutWithAction>
254243
)}
244+
<Header.Root>
245+
<Header.Subtitle
246+
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.subtitle')}
247+
variant='regularRegular'
248+
/>
249+
</Header.Root>
250+
<Form.Root
251+
onSubmit={updateEnrollmentMode}
252+
gap={6}
253+
>
254+
<Form.ControlRow elementId={enrollmentMode.id}>
255+
<Form.Control {...enrollmentMode.props} />
256+
</Form.ControlRow>
255257

256-
<FormButtons
257-
localizationKey={localizationKeys(
258-
'organizationProfile.verifiedDomainPage.enrollmentTab.formButton__save',
258+
{allowsEdit && (
259+
<Form.ControlRow elementId={deletePending.id}>
260+
<Form.Control {...deletePending.props} />
261+
</Form.ControlRow>
259262
)}
260-
isDisabled={domainStatus.isLoading || !domain || !isFormDirty}
261-
/>
262-
</Form.Root>
263-
</TabPanel>
264263

265-
{allowsEdit && (
264+
<FormButtons
265+
localizationKey={localizationKeys(
266+
'organizationProfile.verifiedDomainPage.enrollmentTab.formButton__save',
267+
)}
268+
isDisabled={domainStatus.isLoading || !domain || !isFormDirty}
269+
/>
270+
</Form.Root>
271+
</TabPanel>
272+
)}
273+
{allowsEdit && canDeleteDomain && (
266274
<TabPanel
267275
direction={'col'}
268276
gap={4}

‎packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx

+5-26
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ const { createFixtures } = bindCreateFixtures('OrganizationProfile');
1212

1313
describe('OrganizationMembers', () => {
1414
it('renders the Organization Members page', async () => {
15-
const { wrapper, fixtures } = await createFixtures(f => {
15+
const { wrapper } = await createFixtures(f => {
1616
f.withOrganizations();
1717
f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: ['Org1'] });
1818
});
1919

20-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
21-
2220
const { getByText, getByRole } = render(<OrganizationMembers />, { wrapper });
2321

2422
await waitFor(() => {
@@ -34,14 +32,12 @@ describe('OrganizationMembers', () => {
3432
});
3533

3634
it('shows requests if domains is turned on', async () => {
37-
const { wrapper, fixtures } = await createFixtures(f => {
35+
const { wrapper } = await createFixtures(f => {
3836
f.withOrganizations();
3937
f.withOrganizationDomains();
4038
f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: ['Org1'] });
4139
});
4240

43-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
44-
4541
const { getByRole } = render(<OrganizationMembers />, { wrapper });
4642

4743
await waitFor(() => {
@@ -50,13 +46,11 @@ describe('OrganizationMembers', () => {
5046
});
5147

5248
it('shows an invite button inside invitations tab if the current user is an admin', async () => {
53-
const { wrapper, fixtures } = await createFixtures(f => {
49+
const { wrapper } = await createFixtures(f => {
5450
f.withOrganizations();
5551
f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: [{ name: 'Org1', role: 'admin' }] });
5652
});
5753

58-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
59-
6054
const { getByRole, getByText } = render(<OrganizationMembers />, { wrapper });
6155

6256
await waitFor(async () => {
@@ -67,16 +61,14 @@ describe('OrganizationMembers', () => {
6761
});
6862

6963
it('does not show invitations and requests if user is not an admin', async () => {
70-
const { wrapper, fixtures } = await createFixtures(f => {
64+
const { wrapper } = await createFixtures(f => {
7165
f.withOrganizations();
7266
f.withUser({
7367
email_addresses: ['test@clerk.com'],
74-
organization_memberships: [{ name: 'Org1' }],
68+
organization_memberships: [{ name: 'Org1', permissions: [] }],
7569
});
7670
});
7771

78-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
79-
8072
const { queryByRole } = render(<OrganizationMembers />, { wrapper });
8173

8274
await waitFor(() => {
@@ -92,8 +84,6 @@ describe('OrganizationMembers', () => {
9284
f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: [{ name: 'Org1', role: 'admin' }] });
9385
});
9486

95-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
96-
9787
const { getByRole } = render(<OrganizationMembers />, { wrapper });
9888

9989
await waitFor(async () => {
@@ -155,8 +145,6 @@ describe('OrganizationMembers', () => {
155145
}),
156146
);
157147

158-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
159-
160148
const { queryByText, queryAllByRole } = render(<OrganizationMembers />, { wrapper });
161149

162150
await waitFor(() => {
@@ -203,8 +191,6 @@ describe('OrganizationMembers', () => {
203191
});
204192
});
205193

206-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
207-
208194
fixtures.clerk.organization?.getMemberships.mockReturnValueOnce(
209195
Promise.resolve({ data: membersList, total_count: 0 }),
210196
);
@@ -240,8 +226,6 @@ describe('OrganizationMembers', () => {
240226
}),
241227
);
242228

243-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
244-
245229
await runFakeTimers(async () => {
246230
const { getByText } = render(<OrganizationMembers />, { wrapper });
247231
await waitFor(() => {
@@ -277,8 +261,6 @@ describe('OrganizationMembers', () => {
277261
});
278262
});
279263

280-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
281-
282264
fixtures.clerk.organization?.getInvitations.mockReturnValue(
283265
Promise.resolve({
284266
data: invitationList,
@@ -327,8 +309,6 @@ describe('OrganizationMembers', () => {
327309
});
328310
});
329311

330-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
331-
332312
fixtures.clerk.organization?.getDomains.mockReturnValue(
333313
Promise.resolve({
334314
data: [],
@@ -373,7 +353,6 @@ describe('OrganizationMembers', () => {
373353
}),
374354
);
375355

376-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
377356
const { findByText } = render(<OrganizationMembers />, { wrapper });
378357
await waitFor(() => expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled());
379358
expect(await findByText('You')).toBeInTheDocument();

‎packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationSettings.test.tsx

+42-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { OrganizationDomainResource, OrganizationMembershipResource } from '@clerk/types';
22
import { describe, it } from '@jest/globals';
3-
import { render, waitFor } from '@testing-library/react';
43
import userEvent from '@testing-library/user-event';
54

5+
import { act, render, waitFor } from '../../../../testUtils';
66
import { bindCreateFixtures } from '../../../utils/test/createFixtures';
77
import { OrganizationSettings } from '../OrganizationSettings';
88
import { createFakeDomain, createFakeMember } from './utils';
@@ -28,7 +28,7 @@ describe('OrganizationSettings', () => {
2828
total_count: 1,
2929
}),
3030
);
31-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
31+
3232
const { getByText } = render(<OrganizationSettings />, { wrapper });
3333
await waitFor(() => {
3434
expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();
@@ -54,7 +54,6 @@ describe('OrganizationSettings', () => {
5454
total_count: 1,
5555
}),
5656
);
57-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
5857
const { getByText } = render(<OrganizationSettings />, { wrapper });
5958
await waitFor(() => {
6059
expect(getByText('Settings')).toBeDefined();
@@ -75,7 +74,6 @@ describe('OrganizationSettings', () => {
7574
});
7675

7776
fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(adminsList));
78-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
7977
const { getByText } = render(<OrganizationSettings />, { wrapper });
8078
await waitFor(() => {
8179
expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();
@@ -85,18 +83,54 @@ describe('OrganizationSettings', () => {
8583
});
8684
});
8785

86+
it('hides domains when `read` permission is missing', async () => {
87+
const { wrapper, fixtures } = await createFixtures(f => {
88+
f.withOrganizations();
89+
f.withOrganizationDomains();
90+
f.withUser({
91+
email_addresses: ['test@clerk.dev'],
92+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_memberships:read'] }],
93+
});
94+
});
95+
const { queryByText } = await act(() => render(<OrganizationSettings />, { wrapper }));
96+
await new Promise(r => setTimeout(r, 100));
97+
expect(queryByText('Verified domains')).not.toBeInTheDocument();
98+
expect(fixtures.clerk.organization?.getDomains).not.toBeCalled();
99+
});
100+
101+
it('shows domains when `read` permission exists', async () => {
102+
const { wrapper, fixtures } = await createFixtures(f => {
103+
f.withOrganizations();
104+
f.withOrganizationDomains();
105+
f.withUser({
106+
email_addresses: ['test@clerk.dev'],
107+
organization_memberships: [{ name: 'Org1', permissions: ['org:sys_domains:read'] }],
108+
});
109+
});
110+
fixtures.clerk.organization?.getDomains.mockReturnValue(
111+
Promise.resolve({
112+
data: [],
113+
total_count: 0,
114+
}),
115+
);
116+
const { queryByText } = await act(() => render(<OrganizationSettings />, { wrapper }));
117+
118+
await new Promise(r => setTimeout(r, 100));
119+
expect(queryByText('Verified domains')).toBeInTheDocument();
120+
expect(fixtures.clerk.organization?.getDomains).toBeCalled();
121+
});
122+
88123
describe('Danger section', () => {
89124
it('always displays danger section and the leave organization button', async () => {
90-
const { wrapper, fixtures } = await createFixtures(f => {
125+
const { wrapper } = await createFixtures(f => {
91126
f.withOrganizations();
92127
f.withUser({
93128
email_addresses: ['test@clerk.com'],
94129
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
95130
});
96131
});
97132

98-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
99-
const { getByText, queryByRole } = render(<OrganizationSettings />, { wrapper });
133+
const { getByText, queryByRole } = await act(() => render(<OrganizationSettings />, { wrapper }));
100134
await waitFor(() => {
101135
expect(getByText('Danger')).toBeDefined();
102136
expect(getByText(/leave organization/i).closest('button')).toBeInTheDocument();
@@ -105,15 +139,14 @@ describe('OrganizationSettings', () => {
105139
});
106140

107141
it('enabled leave organization button with delete organization button', async () => {
108-
const { wrapper, fixtures } = await createFixtures(f => {
142+
const { wrapper } = await createFixtures(f => {
109143
f.withOrganizations();
110144
f.withUser({
111145
email_addresses: ['test@clerk.com'],
112146
organization_memberships: [{ name: 'Org1', admin_delete_enabled: true }],
113147
});
114148
});
115149

116-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
117150
const { getByText } = render(<OrganizationSettings />, { wrapper });
118151
await waitFor(() => {
119152
expect(getByText('Danger')).toBeDefined();
@@ -144,7 +177,6 @@ describe('OrganizationSettings', () => {
144177
});
145178
});
146179

147-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
148180
fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(adminsList));
149181
const { getByText, getByRole } = render(<OrganizationSettings />, { wrapper });
150182
await waitFor(() => {
@@ -173,7 +205,6 @@ describe('OrganizationSettings', () => {
173205
total_count: 0,
174206
}),
175207
);
176-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
177208
const { getByText } = render(<OrganizationSettings />, { wrapper });
178209
await waitFor(async () => {
179210
await userEvent.click(getByText('Org1', { exact: false }));
@@ -193,7 +224,6 @@ describe('OrganizationSettings', () => {
193224
});
194225

195226
fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(adminsList));
196-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
197227
const { findByText } = render(<OrganizationSettings />, { wrapper });
198228
await waitFor(async () => {
199229
// expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled();

‎packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx

+19-25
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,30 @@ const { createFixtures } = bindCreateFixtures('OrganizationSwitcher');
1010

1111
describe('OrganizationSwitcher', () => {
1212
it('renders component', async () => {
13-
const { wrapper, fixtures } = await createFixtures(f => {
13+
const { wrapper } = await createFixtures(f => {
1414
f.withOrganizations();
1515
f.withUser({ email_addresses: ['test@clerk.com'] });
1616
});
17-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
1817
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
1918
expect(queryByRole('button')).toBeDefined();
2019
});
2120

2221
describe('Personal Workspace', () => {
2322
it('shows the personal workspace when enabled', async () => {
24-
const { wrapper, props, fixtures } = await createFixtures(f => {
23+
const { wrapper, props } = await createFixtures(f => {
2524
f.withOrganizations();
2625
f.withUser({ email_addresses: ['test@clerk.com'] });
2726
});
28-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
2927
props.setProps({ hidePersonal: false });
3028
const { getByText } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
3129
expect(getByText('Personal account')).toBeDefined();
3230
});
3331

3432
it('does not show the personal workspace when disabled', async () => {
35-
const { wrapper, props, fixtures } = await createFixtures(f => {
33+
const { wrapper, props } = await createFixtures(f => {
3634
f.withOrganizations();
3735
f.withUser({ email_addresses: ['test@clerk.com'] });
3836
});
39-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
4037
props.setProps({ hidePersonal: true });
4138
const { queryByText, getByRole, userEvent, getByText } = render(<OrganizationSwitcher />, { wrapper });
4239
await userEvent.click(getByRole('button'));
@@ -49,7 +46,10 @@ describe('OrganizationSwitcher', () => {
4946
it('shows the counter for pending suggestions and invitations', async () => {
5047
const { wrapper, fixtures } = await createFixtures(f => {
5148
f.withOrganizations();
52-
f.withUser({ email_addresses: ['test@clerk.com'] });
49+
f.withUser({
50+
email_addresses: ['test@clerk.com'],
51+
organization_memberships: [{ name: 'Org1', id: '1', permissions: ['org:sys_memberships:manage'] }],
52+
});
5353
});
5454

5555
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
@@ -66,8 +66,6 @@ describe('OrganizationSwitcher', () => {
6666
}),
6767
);
6868

69-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
70-
7169
await runFakeTimers(async () => {
7270
const { getByText } = render(<OrganizationSwitcher />, { wrapper });
7371

@@ -108,8 +106,6 @@ describe('OrganizationSwitcher', () => {
108106
}),
109107
);
110108

111-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(true);
112-
113109
await runFakeTimers(async () => {
114110
const { getByText } = render(<OrganizationSwitcher />, { wrapper });
115111

@@ -122,23 +118,23 @@ describe('OrganizationSwitcher', () => {
122118

123119
describe('OrganizationSwitcherPopover', () => {
124120
it('opens the organization switcher popover when clicked', async () => {
125-
const { wrapper, props, fixtures } = await createFixtures(f => {
121+
const { wrapper, props } = await createFixtures(f => {
126122
f.withOrganizations();
127123
f.withUser({ email_addresses: ['test@clerk.com'], create_organization_enabled: true });
128124
});
129-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
125+
130126
props.setProps({ hidePersonal: true });
131127
const { getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
132128
await userEvent.click(getByRole('button'));
133129
expect(getByText('Create Organization')).toBeDefined();
134130
});
135131

136132
it('lists all organizations the user belongs to', async () => {
137-
const { wrapper, props, fixtures } = await createFixtures(f => {
133+
const { wrapper, props } = await createFixtures(f => {
138134
f.withOrganizations();
139135
f.withUser({ email_addresses: ['test@clerk.com'], organization_memberships: ['Org1', 'Org2'] });
140136
});
141-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
137+
142138
props.setProps({ hidePersonal: false });
143139
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
144140
await userEvent.click(getByRole('button'));
@@ -152,14 +148,14 @@ describe('OrganizationSwitcher', () => {
152148
['Member', 'basic_member'],
153149
['Guest', 'guest_member'],
154150
])('shows the text "%s" for the %s role in the active organization', async (text, role) => {
155-
const { wrapper, props, fixtures } = await createFixtures(f => {
151+
const { wrapper, props } = await createFixtures(f => {
156152
f.withOrganizations();
157153
f.withUser({
158154
email_addresses: ['test@clerk.com'],
159155
organization_memberships: [{ name: 'Org1', role: role as MembershipRole }],
160156
});
161157
});
162-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
158+
163159
props.setProps({ hidePersonal: true });
164160
const { getAllByText, getByText, getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
165161
await userEvent.click(getByRole('button'));
@@ -175,7 +171,7 @@ describe('OrganizationSwitcher', () => {
175171
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
176172
});
177173
});
178-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
174+
179175
props.setProps({ hidePersonal: true });
180176
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
181177
await userEvent.click(getByRole('button'));
@@ -192,7 +188,7 @@ describe('OrganizationSwitcher', () => {
192188
create_organization_enabled: true,
193189
});
194190
});
195-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
191+
196192
props.setProps({ hidePersonal: true });
197193
const { getByRole, userEvent } = render(<OrganizationSwitcher />, { wrapper });
198194
await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
@@ -201,15 +197,15 @@ describe('OrganizationSwitcher', () => {
201197
});
202198

203199
it('does not display create organization button if permissions not present', async () => {
204-
const { wrapper, props, fixtures } = await createFixtures(f => {
200+
const { wrapper, props } = await createFixtures(f => {
205201
f.withOrganizations();
206202
f.withUser({
207203
email_addresses: ['test@clerk.com'],
208204
organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
209205
create_organization_enabled: false,
210206
});
211207
});
212-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
208+
213209
props.setProps({ hidePersonal: true });
214210
const { queryByRole } = await act(() => render(<OrganizationSwitcher />, { wrapper }));
215211
expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument();
@@ -224,7 +220,7 @@ describe('OrganizationSwitcher', () => {
224220
create_organization_enabled: false,
225221
});
226222
});
227-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
223+
228224
fixtures.clerk.user?.getOrganizationInvitations.mockReturnValueOnce(
229225
Promise.resolve({
230226
data: [
@@ -268,7 +264,7 @@ describe('OrganizationSwitcher', () => {
268264
create_organization_enabled: false,
269265
});
270266
});
271-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
267+
272268
fixtures.clerk.user?.getOrganizationSuggestions.mockReturnValueOnce(
273269
Promise.resolve({
274270
data: [
@@ -318,7 +314,6 @@ describe('OrganizationSwitcher', () => {
318314
});
319315
});
320316
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
321-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
322317

323318
props.setProps({ hidePersonal: true });
324319
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
@@ -346,7 +341,6 @@ describe('OrganizationSwitcher', () => {
346341
});
347342
});
348343

349-
fixtures.clerk.session?.isAuthorized.mockResolvedValue(false);
350344
fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve());
351345
const { getByRole, getByText, userEvent } = render(<OrganizationSwitcher />, { wrapper });
352346
await userEvent.click(getByRole('button'));

‎packages/clerk-js/src/ui/hooks/useFetch.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const useFetch = <T>(
3535
requestStatus.setError();
3636
setData(null);
3737
});
38-
}, []);
38+
}, [JSON.stringify(params)]);
3939

4040
return {
4141
status: requestStatus,

‎packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import type {
66
ExternalAccountJSON,
77
OAuthProvider,
88
OrganizationEnrollmentMode,
9-
OrganizationJSON,
109
PhoneNumberJSON,
10+
PublicUserDataJSON,
1111
SamlAccountJSON,
1212
SessionJSON,
1313
SignInJSON,
1414
SignUpJSON,
1515
UserJSON,
1616
UserSettingsJSON,
1717
} from '@clerk/types';
18-
import type { MembershipRole, PublicUserDataJSON } from '@clerk/types';
1918

19+
import type { OrgParams } from '../../../core/test/fixtures';
2020
import { createUser, getOrganizationId } from '../../../core/test/fixtures';
2121
import { createUserFixture } from './fixtures';
2222

@@ -38,8 +38,6 @@ export const createClientFixtureHelpers = (baseClient: ClientJSON) => {
3838
};
3939

4040
const createUserFixtureHelpers = (baseClient: ClientJSON) => {
41-
type OrgParams = Partial<OrganizationJSON> & { role?: MembershipRole };
42-
4341
type WithUserParams = Omit<
4442
Partial<UserJSON>,
4543
'email_addresses' | 'phone_numbers' | 'external_accounts' | 'saml_accounts' | 'organization_memberships'

‎packages/clerk-js/src/ui/utils/test/mockHelpers.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,27 @@ const mockProp = <T>(obj: T, k: keyof T) => {
2020
}
2121
};
2222

23-
const mockMethodsOf = (obj: any) => {
24-
Object.keys(obj).forEach(k => mockProp(obj, k));
23+
const mockMethodsOf = <T extends Record<string, any> | null = any>(obj: T, options?: { exclude: (keyof T)[] }) => {
24+
if (!obj) {
25+
return;
26+
}
27+
Object.keys(obj)
28+
.filter(key => !options?.exclude.includes(key as keyof T))
29+
.forEach(k => mockProp(obj, k));
2530
};
2631

2732
export const mockClerkMethods = (clerk: LoadedClerk): DeepJestMocked<LoadedClerk> => {
2833
mockMethodsOf(clerk);
2934
mockMethodsOf(clerk.client.signIn);
3035
mockMethodsOf(clerk.client.signUp);
3136
clerk.client.sessions.forEach(session => {
32-
mockMethodsOf(session);
37+
mockMethodsOf(session, {
38+
exclude: ['isAuthorized'],
39+
});
3340
mockMethodsOf(session.user);
34-
session.user?.emailAddresses.forEach(mockMethodsOf);
35-
session.user?.phoneNumbers.forEach(mockMethodsOf);
36-
session.user?.externalAccounts.forEach(mockMethodsOf);
41+
session.user?.emailAddresses.forEach(m => mockMethodsOf(m));
42+
session.user?.phoneNumbers.forEach(m => mockMethodsOf(m));
43+
session.user?.externalAccounts.forEach(m => mockMethodsOf(m));
3744
session.user?.organizationMemberships.forEach(m => {
3845
mockMethodsOf(m);
3946
mockMethodsOf(m.organization);

‎packages/clerk-js/src/utils/__tests__/url.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isValidUrl,
1616
mergeFragmentIntoUrl,
1717
requiresUserInput,
18+
trimLeadingSlash,
1819
trimTrailingSlash,
1920
} from '../url';
2021

@@ -239,6 +240,15 @@ describe('trimTrailingSlash(string)', () => {
239240
});
240241
});
241242

243+
describe('trimLeadingSlash(string)', () => {
244+
it('trims all the leading slashes', () => {
245+
expect(trimLeadingSlash('')).toBe('');
246+
expect(trimLeadingSlash('/foo')).toBe('foo');
247+
expect(trimLeadingSlash('/foo/')).toBe('foo/');
248+
expect(trimLeadingSlash('//foo//bar///')).toBe('foo//bar///');
249+
});
250+
});
251+
242252
describe('appendQueryParams(base,url)', () => {
243253
it('returns the same url if no params provided', () => {
244254
const base = new URL('https://dashboard.clerk.com');

‎packages/clerk-js/src/utils/url.ts

+12
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ export const trimTrailingSlash = (path: string): string => {
211211
return (path || '').replace(/\/+$/, '');
212212
};
213213

214+
/**
215+
* trimLeadingSlash(path: string): string
216+
*
217+
* Strips the leading slashes from a string
218+
*
219+
* @returns {string} Returns the string without leading slashes
220+
* @param path
221+
*/
222+
export const trimLeadingSlash = (path: string): string => {
223+
return (path || '').replace(/^\/+/, '');
224+
};
225+
214226
export const stripSameOrigin = (url: URL, baseUrl: URL): string => {
215227
const sameOrigin = baseUrl.origin === url.origin;
216228
return sameOrigin ? stripOrigin(url) : `${url}`;

‎packages/types/src/session.ts

+29-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ import type { UserResource } from './user';
66

77
export type IsAuthorized = (isAuthorizedParams: IsAuthorizedParams) => Promise<IsAuthorizedReturnValues>;
88

9-
interface IsAuthorizedParams {
10-
// Adding (string & {}) allows for getting eslint autocomplete but also accepts any string
11-
// eslint-disable-next-line
12-
permission?: OrganizationPermission | (string & {});
13-
role?: string;
14-
}
9+
type IsAuthorizedParams =
10+
| {
11+
any: (
12+
| {
13+
role: string;
14+
permission?: never;
15+
}
16+
| {
17+
role?: never;
18+
// Adding (string & {}) allows for getting eslint autocomplete but also accepts any string
19+
// eslint-disable-next-line
20+
permission: OrganizationPermission | (string & {});
21+
}
22+
)[];
23+
role?: never;
24+
permission?: never;
25+
}
26+
| {
27+
any?: never;
28+
role: string;
29+
permission?: never;
30+
}
31+
| {
32+
any?: never;
33+
role?: never;
34+
// Adding (string & {}) allows for getting eslint autocomplete but also accepts any string
35+
// eslint-disable-next-line
36+
permission: OrganizationPermission | (string & {});
37+
};
1538

1639
type IsAuthorizedReturnValues = boolean;
1740

0 commit comments

Comments
 (0)
Please sign in to comment.