From cfac5e8e84906eff29dfa84c5825063aabb2b5f7 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Wed, 31 Jul 2024 00:18:47 -0700 Subject: [PATCH] feat(clerk-js): Add ability to set active organization by slug (#3825) Co-authored-by: Izaak Lauer <8404559+izaaklauer@users.noreply.github.com> --- .changeset/wicked-seahorses-juggle.md | 6 +++ .../clerk-js/src/core/__tests__/clerk.test.ts | 50 ++++++++++++++++--- packages/clerk-js/src/core/clerk.ts | 14 +++++- .../src/utils/__tests__/organization.test.ts | 20 ++++++++ packages/clerk-js/src/utils/index.ts | 1 + packages/clerk-js/src/utils/organization.ts | 8 +++ packages/types/src/clerk.ts | 2 +- 7 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 .changeset/wicked-seahorses-juggle.md create mode 100644 packages/clerk-js/src/utils/__tests__/organization.test.ts create mode 100644 packages/clerk-js/src/utils/organization.ts diff --git a/.changeset/wicked-seahorses-juggle.md b/.changeset/wicked-seahorses-juggle.md new file mode 100644 index 00000000000..92aed29cd2e --- /dev/null +++ b/.changeset/wicked-seahorses-juggle.md @@ -0,0 +1,6 @@ +--- +"@clerk/clerk-js": patch +"@clerk/types": patch +--- + +Introduce ability to set an active organization by slug diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 33971961675..3178012b225 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -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] })); @@ -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); @@ -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', }); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 1fa4dde3ed1..6495129a4ad 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -75,6 +75,7 @@ import { inBrowser, isDevAccountPortalOrigin, isError, + isOrganizationId, isRedirectForFAPIInitiatedFlow, noOrganizationExists, noUserExists, @@ -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 diff --git a/packages/clerk-js/src/utils/__tests__/organization.test.ts b/packages/clerk-js/src/utils/__tests__/organization.test.ts new file mode 100644 index 00000000000..4262f073372 --- /dev/null +++ b/packages/clerk-js/src/utils/__tests__/organization.test.ts @@ -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); + }); +}); diff --git a/packages/clerk-js/src/utils/index.ts b/packages/clerk-js/src/utils/index.ts index 2defd829ff2..cdffa71c21c 100644 --- a/packages/clerk-js/src/utils/index.ts +++ b/packages/clerk-js/src/utils/index.ts @@ -25,3 +25,4 @@ export * from './queryStateParams'; export * from './normalizeRoutingOptions'; export * from './image'; export * from './completeSignUpFlow'; +export * from './organization'; diff --git a/packages/clerk-js/src/utils/organization.ts b/packages/clerk-js/src/utils/organization.ts new file mode 100644 index 00000000000..5106fb93d9f --- /dev/null +++ b/packages/clerk-js/src/utils/organization.ts @@ -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_'); +} diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 177e63394b4..16ebcdba790 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -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;