From 7ff078ee7f2f3f8e0c4a35fb8b50c83807675302 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Thu, 25 Jul 2024 13:56:32 -0700 Subject: [PATCH 1/7] feat(clerk-js): Add ability to set active organization by slug chore(clerk-js): Remove deprecated test text chore(clerk-js): Simplify organization slug or id checking chore(clerk-js): Clean up org id condition chore(clerk-js): Use updated or current session when checking for org id test(clerk-js): Update org IDs in tests to use correct format chore(clerk-js): Use organization id instead of org membership id test(clerk-js): Add organization id checker test test(clerk-js): Test setting active organization by slug test(clerk-js): Move new test to last of setActive --- .../clerk-js/src/core/__tests__/clerk.test.ts | 50 ++++++++++++++++--- packages/clerk-js/src/core/clerk.ts | 14 +++++- .../src/utils/__tests__/organization.test.ts | 15 ++++++ packages/clerk-js/src/utils/index.ts | 1 + packages/clerk-js/src/utils/organization.ts | 6 +++ 5 files changed, 78 insertions(+), 8 deletions(-) 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/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 71ceb63e98e..a83e1e1097a 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..1286c225363 --- /dev/null +++ b/packages/clerk-js/src/utils/__tests__/organization.test.ts @@ -0,0 +1,15 @@ +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); + 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..bebd32f8fea --- /dev/null +++ b/packages/clerk-js/src/utils/organization.ts @@ -0,0 +1,6 @@ +/** + * Checks and assumes a string is an organization ID if it starts with 'org_'. + */ +export function isOrganizationId(id: string) { + return id.startsWith('org_'); +} From 7c61adf6c7a2fbc83640b0a8c48f55e417396193 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Fri, 26 Jul 2024 09:21:22 -0700 Subject: [PATCH 2/7] chore(clerk-js): Add changeset --- .changeset/wicked-seahorses-juggle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wicked-seahorses-juggle.md diff --git a/.changeset/wicked-seahorses-juggle.md b/.changeset/wicked-seahorses-juggle.md new file mode 100644 index 00000000000..46e91d06f3c --- /dev/null +++ b/.changeset/wicked-seahorses-juggle.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-js": patch +--- + +Introduce ability to set an active organization by slug From 3414e6fe533e80682f5d393ec508f89db3ab7b44 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Mon, 29 Jul 2024 11:36:56 -0700 Subject: [PATCH 3/7] chore(clerk-js): Update organization ID utility function description Co-authored-by: Izaak Lauer <8404559+izaaklauer@users.noreply.github.com> --- packages/clerk-js/src/utils/organization.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/organization.ts b/packages/clerk-js/src/utils/organization.ts index bebd32f8fea..097dc3bdb7d 100644 --- a/packages/clerk-js/src/utils/organization.ts +++ b/packages/clerk-js/src/utils/organization.ts @@ -1,5 +1,7 @@ /** - * Checks and assumes a string is an organization ID if it starts with 'org_'. + * 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) { return id.startsWith('org_'); From 3aae4ebbfbb5294b8cfbe80ae49efbf9cbdf03a1 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 29 Jul 2024 12:40:57 -0700 Subject: [PATCH 4/7] chore(clerk-js,types): Document SetActiveParams type to include slug when setting active org --- packages/types/src/clerk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 62ce64ccb46..61858a26df9 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -681,7 +681,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; From fdb2c084c6ae1394062c91572b46fa753f40c8c0 Mon Sep 17 00:00:00 2001 From: Robert Soriano Date: Mon, 29 Jul 2024 13:33:22 -0700 Subject: [PATCH 5/7] chore: Update changeset --- .changeset/wicked-seahorses-juggle.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/wicked-seahorses-juggle.md b/.changeset/wicked-seahorses-juggle.md index 46e91d06f3c..92aed29cd2e 100644 --- a/.changeset/wicked-seahorses-juggle.md +++ b/.changeset/wicked-seahorses-juggle.md @@ -1,5 +1,6 @@ --- "@clerk/clerk-js": patch +"@clerk/types": patch --- Introduce ability to set an active organization by slug From a9c10519155052dab0f675e80203d0726f48b657 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 06:31:48 -0700 Subject: [PATCH 6/7] chore(clerk-js): Handle falsy values when checking organization ID --- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/utils/__tests__/organization.test.ts | 5 +++++ packages/clerk-js/src/utils/organization.ts | 6 +++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index a83e1e1097a..3668f245ffd 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -712,7 +712,7 @@ export class Clerk implements ClerkInterface { if (newSession && shouldSwitchOrganization) { const organizationIdOrSlug = typeof organization === 'string' ? organization : organization?.id; - if (isOrganizationId(organizationIdOrSlug!)) { + if (isOrganizationId(organizationIdOrSlug)) { newSession.lastActiveOrganizationId = organizationIdOrSlug || null; } else { const matchingOrganization = newSession.user.organizationMemberships.find( diff --git a/packages/clerk-js/src/utils/__tests__/organization.test.ts b/packages/clerk-js/src/utils/__tests__/organization.test.ts index 1286c225363..4262f073372 100644 --- a/packages/clerk-js/src/utils/__tests__/organization.test.ts +++ b/packages/clerk-js/src/utils/__tests__/organization.test.ts @@ -10,6 +10,11 @@ describe('isOrganizationId(string)', () => { 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/organization.ts b/packages/clerk-js/src/utils/organization.ts index 097dc3bdb7d..25a689ff445 100644 --- a/packages/clerk-js/src/utils/organization.ts +++ b/packages/clerk-js/src/utils/organization.ts @@ -1,8 +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 + disambiguating with slugs. `_` is a disallowed character in slug names, so slugs cannot start with `org_`. */ -export function isOrganizationId(id: string) { - return id.startsWith('org_'); +export function isOrganizationId(id: string | null | undefined): boolean { + return typeof id === 'string' && id.startsWith('org_'); } From 348306881bfa5c20fa50bf025af300acd3d4ce7c Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Tue, 30 Jul 2024 06:51:34 -0700 Subject: [PATCH 7/7] chore(clerk-js): Format function definition --- packages/clerk-js/src/utils/organization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/utils/organization.ts b/packages/clerk-js/src/utils/organization.ts index 25a689ff445..5106fb93d9f 100644 --- a/packages/clerk-js/src/utils/organization.ts +++ b/packages/clerk-js/src/utils/organization.ts @@ -1,7 +1,7 @@ /** * 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_`. + * 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_');