Skip to content

Commit 80e6477

Browse files
feat(clerk-js): Add ability to set active organization by slug (#3825)
Co-authored-by: Izaak Lauer <8404559+izaaklauer@users.noreply.github.com>
1 parent aa06f3b commit 80e6477

File tree

7 files changed

+92
-9
lines changed

7 files changed

+92
-9
lines changed

.changeset/wicked-seahorses-juggle.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+
Introduce ability to set an active organization by slug

packages/clerk-js/src/core/__tests__/clerk.test.ts

+44-6
Original file line numberDiff line numberDiff line change
@@ -335,18 +335,56 @@ describe('Clerk singleton', () => {
335335
return Promise.resolve();
336336
});
337337

338-
await sut.setActive({ organization: { id: 'org-id' } as Organization, beforeEmit: beforeEmitMock });
338+
await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
339339

340340
await waitFor(() => {
341341
expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
342342
expect(mockSession.touch).toHaveBeenCalled();
343343
expect(mockSession.getToken).toHaveBeenCalled();
344-
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org-id');
344+
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
345345
expect(beforeEmitMock).toBeCalledWith(mockSession);
346346
expect(sut.session).toMatchObject(mockSession);
347347
});
348348
});
349349

350+
it('sets active organization by slug', async () => {
351+
const mockSession2 = {
352+
id: '1',
353+
status: 'active',
354+
user: {
355+
organizationMemberships: [
356+
{
357+
id: 'orgmem_id',
358+
organization: {
359+
id: 'org_id',
360+
slug: 'some-org-slug',
361+
},
362+
},
363+
],
364+
},
365+
touch: jest.fn(),
366+
getToken: jest.fn(),
367+
};
368+
mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession2] }));
369+
const sut = new Clerk(productionPublishableKey);
370+
await sut.load();
371+
372+
mockSession2.touch.mockImplementationOnce(() => {
373+
sut.session = mockSession2 as any;
374+
return Promise.resolve();
375+
});
376+
mockSession2.getToken.mockImplementation(() => 'mocked-token');
377+
378+
await sut.setActive({ organization: 'some-org-slug' });
379+
380+
await waitFor(() => {
381+
expect(mockSession2.touch).toHaveBeenCalled();
382+
expect(mockSession2.getToken).toHaveBeenCalled();
383+
expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
384+
expect(sut.session).toMatchObject(mockSession2);
385+
});
386+
});
387+
350388
mockNativeRuntime(() => {
351389
it('calls session.touch in a non-standard browser', async () => {
352390
mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] }));
@@ -365,11 +403,11 @@ describe('Clerk singleton', () => {
365403
return Promise.resolve();
366404
});
367405

368-
await sut.setActive({ organization: { id: 'org-id' } as Organization, beforeEmit: beforeEmitMock });
406+
await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
369407

370408
expect(executionOrder).toEqual(['session.touch', 'before emit']);
371409
expect(mockSession.touch).toHaveBeenCalled();
372-
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org-id');
410+
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
373411
expect(mockSession.getToken).toBeCalled();
374412
expect(beforeEmitMock).toBeCalledWith(mockSession);
375413
expect(sut.session).toMatchObject(mockSession);
@@ -1892,12 +1930,12 @@ describe('Clerk singleton', () => {
18921930
BaseResource._fetch = jest.fn().mockResolvedValue({});
18931931
const sut = new Clerk(developmentPublishableKey);
18941932

1895-
await sut.getOrganization('some-org-id');
1933+
await sut.getOrganization('org_id');
18961934

18971935
// @ts-expect-error - Mocking a protected method
18981936
expect(BaseResource._fetch).toHaveBeenCalledWith({
18991937
method: 'GET',
1900-
path: '/organizations/some-org-id',
1938+
path: '/organizations/org_id',
19011939
});
19021940
});
19031941
});

packages/clerk-js/src/core/clerk.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
inBrowser,
7676
isDevAccountPortalOrigin,
7777
isError,
78+
isOrganizationId,
7879
isRedirectForFAPIInitiatedFlow,
7980
noOrganizationExists,
8081
noUserExists,
@@ -707,9 +708,18 @@ export class Clerk implements ClerkInterface {
707708
// However, if the `organization` parameter is not given (i.e. `undefined`), we want
708709
// to keep the organization id that the session had.
709710
const shouldSwitchOrganization = organization !== undefined;
711+
710712
if (newSession && shouldSwitchOrganization) {
711-
const organizationId = typeof organization === 'string' ? organization : organization?.id;
712-
newSession.lastActiveOrganizationId = organizationId || null;
713+
const organizationIdOrSlug = typeof organization === 'string' ? organization : organization?.id;
714+
715+
if (isOrganizationId(organizationIdOrSlug)) {
716+
newSession.lastActiveOrganizationId = organizationIdOrSlug || null;
717+
} else {
718+
const matchingOrganization = newSession.user.organizationMemberships.find(
719+
mem => mem.organization.slug === organizationIdOrSlug,
720+
);
721+
newSession.lastActiveOrganizationId = matchingOrganization?.organization.id || null;
722+
}
713723
}
714724

715725
// If this.session exists, then signOut was triggered by the current tab
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { isOrganizationId } from '../organization';
2+
3+
describe('isOrganizationId(string)', () => {
4+
it('should return true for strings starting with `org_`', () => {
5+
expect(isOrganizationId('org_123')).toBe(true);
6+
expect(isOrganizationId('org_abc')).toBe(true);
7+
});
8+
9+
it('should return false for strings not starting with `org_`', () => {
10+
expect(isOrganizationId('user_123')).toBe(false);
11+
expect(isOrganizationId('123org_')).toBe(false);
12+
expect(isOrganizationId('ORG_123')).toBe(false);
13+
});
14+
15+
it('should handle falsy values', () => {
16+
expect(isOrganizationId(undefined)).toBe(false);
17+
expect(isOrganizationId(null)).toBe(false);
18+
expect(isOrganizationId('')).toBe(false);
19+
});
20+
});

packages/clerk-js/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from './queryStateParams';
2525
export * from './normalizeRoutingOptions';
2626
export * from './image';
2727
export * from './completeSignUpFlow';
28+
export * from './organization';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Checks and assumes a string is an organization ID if it starts with 'org_', specifically for
3+
* disambiguating with slugs. `_` is a disallowed character in slug names, so slugs cannot
4+
* start with `org_`.
5+
*/
6+
export function isOrganizationId(id: string | null | undefined): boolean {
7+
return typeof id === 'string' && id.startsWith('org_');
8+
}

packages/types/src/clerk.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ export type SetActiveParams = {
682682
session?: ActiveSessionResource | string | null;
683683

684684
/**
685-
* The organization resource or organization id (string version) to be set as active in the current session.
685+
* The organization resource or organization ID/slug (string version) to be set as active in the current session.
686686
* If `null`, the currently active organization is removed as active.
687687
*/
688688
organization?: OrganizationResource | string | null;

0 commit comments

Comments
 (0)