Skip to content

Commit

Permalink
feat(clerk-js): Add ability to set active organization by slug (#3825)
Browse files Browse the repository at this point in the history
Co-authored-by: Izaak Lauer <[email protected]>
  • Loading branch information
wobsoriano and izaaklauer committed Aug 1, 2024
1 parent 9968994 commit 282ba80
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .changeset/wicked-seahorses-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/clerk-js": patch
"@clerk/types": patch
---

Introduce ability to set an active organization by slug
50 changes: 44 additions & 6 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,18 +335,56 @@ describe('Clerk singleton', () => {
return Promise.resolve();
});

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

await waitFor(() => {
expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
expect(mockSession.touch).toHaveBeenCalled();
expect(mockSession.getToken).toHaveBeenCalled();
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org-id');
expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
expect(beforeEmitMock).toBeCalledWith(mockSession);
expect(sut.session).toMatchObject(mockSession);
});
});

it('sets active organization by slug', async () => {
const mockSession2 = {
id: '1',
status: 'active',
user: {
organizationMemberships: [
{
id: 'orgmem_id',
organization: {
id: 'org_id',
slug: 'some-org-slug',
},
},
],
},
touch: jest.fn(),
getToken: jest.fn(),
};
mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession2] }));
const sut = new Clerk(productionPublishableKey);
await sut.load();

mockSession2.touch.mockImplementationOnce(() => {
sut.session = mockSession2 as any;
return Promise.resolve();
});
mockSession2.getToken.mockImplementation(() => 'mocked-token');

await sut.setActive({ organization: 'some-org-slug' });

await waitFor(() => {
expect(mockSession2.touch).toHaveBeenCalled();
expect(mockSession2.getToken).toHaveBeenCalled();
expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
expect(sut.session).toMatchObject(mockSession2);
});
});

mockNativeRuntime(() => {
it('calls session.touch in a non-standard browser', async () => {
mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] }));
Expand All @@ -365,11 +403,11 @@ describe('Clerk singleton', () => {
return Promise.resolve();
});

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

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

await sut.getOrganization('some-org-id');
await sut.getOrganization('org_id');

// @ts-expect-error - Mocking a protected method
expect(BaseResource._fetch).toHaveBeenCalledWith({
method: 'GET',
path: '/organizations/some-org-id',
path: '/organizations/org_id',
});
});
});
Expand Down
14 changes: 12 additions & 2 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
inBrowser,
isDevAccountPortalOrigin,
isError,
isOrganizationId,
isRedirectForFAPIInitiatedFlow,
noOrganizationExists,
noUserExists,
Expand Down Expand Up @@ -707,9 +708,18 @@ export class Clerk implements ClerkInterface {
// However, if the `organization` parameter is not given (i.e. `undefined`), we want
// to keep the organization id that the session had.
const shouldSwitchOrganization = organization !== undefined;

if (newSession && shouldSwitchOrganization) {
const organizationId = typeof organization === 'string' ? organization : organization?.id;
newSession.lastActiveOrganizationId = organizationId || null;
const organizationIdOrSlug = typeof organization === 'string' ? organization : organization?.id;

if (isOrganizationId(organizationIdOrSlug)) {
newSession.lastActiveOrganizationId = organizationIdOrSlug || null;
} else {
const matchingOrganization = newSession.user.organizationMemberships.find(
mem => mem.organization.slug === organizationIdOrSlug,
);
newSession.lastActiveOrganizationId = matchingOrganization?.organization.id || null;
}
}

// If this.session exists, then signOut was triggered by the current tab
Expand Down
20 changes: 20 additions & 0 deletions packages/clerk-js/src/utils/__tests__/organization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { isOrganizationId } from '../organization';

describe('isOrganizationId(string)', () => {
it('should return true for strings starting with `org_`', () => {
expect(isOrganizationId('org_123')).toBe(true);
expect(isOrganizationId('org_abc')).toBe(true);
});

it('should return false for strings not starting with `org_`', () => {
expect(isOrganizationId('user_123')).toBe(false);
expect(isOrganizationId('123org_')).toBe(false);
expect(isOrganizationId('ORG_123')).toBe(false);
});

it('should handle falsy values', () => {
expect(isOrganizationId(undefined)).toBe(false);
expect(isOrganizationId(null)).toBe(false);
expect(isOrganizationId('')).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/clerk-js/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export * from './queryStateParams';
export * from './normalizeRoutingOptions';
export * from './image';
export * from './completeSignUpFlow';
export * from './organization';
8 changes: 8 additions & 0 deletions packages/clerk-js/src/utils/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Checks and assumes a string is an organization ID if it starts with 'org_', specifically for
* disambiguating with slugs. `_` is a disallowed character in slug names, so slugs cannot
* start with `org_`.
*/
export function isOrganizationId(id: string | null | undefined): boolean {
return typeof id === 'string' && id.startsWith('org_');
}
2 changes: 1 addition & 1 deletion packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ export type SetActiveParams = {
session?: ActiveSessionResource | string | null;

/**
* The organization resource or organization id (string version) to be set as active in the current session.
* The organization resource or organization ID/slug (string version) to be set as active in the current session.
* If `null`, the currently active organization is removed as active.
*/
organization?: OrganizationResource | string | null;
Expand Down

0 comments on commit 282ba80

Please sign in to comment.