From f0d9553ff900c229ec9746e21af3d104056a8c0d Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 31 Oct 2025 14:02:41 -0500 Subject: [PATCH 1/8] fix(clerk-js): ensure sessionId is available immeditately after sign in --- packages/clerk-js/src/core/clerk.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 75d2a2b28c7..af6772e1574 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1412,7 +1412,7 @@ export class Clerk implements ClerkInterface { return; } - if (newSession?.status !== 'pending') { + if (newSession?.status !== 'pending' && this.session?.id !== newSession?.id) { this.#setTransitiveState(); } @@ -2351,7 +2351,9 @@ export class Clerk implements ClerkInterface { } updateClient = (newClient: ClientResource): void => { - if (!this.client) { + const isFirstClientSet = !this.client; + + if (isFirstClientSet) { // This is the first time client is being // set, so we also need to set session const session = this.#options.selectInitialSession @@ -2381,6 +2383,11 @@ export class Clerk implements ClerkInterface { // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.emit(events.TokenUpdate, { token: this.session?.lastActiveToken }); + } else if (!isFirstClientSet && newClient.sessions?.length > 0) { + const session = this.#options.selectInitialSession + ? this.#options.selectInitialSession(newClient) + : this.#defaultSession(newClient); + this.#setAccessors(session); } this.#emit(); @@ -2591,6 +2598,12 @@ export class Clerk implements ClerkInterface { }); const initClient = async () => { + const jwtInCookie = this.#authService?.getSessionCookie(); + if (jwtInCookie) { + const preliminaryClient = createClientFromJwt(jwtInCookie); + this.updateClient(preliminaryClient); + } + return Client.getOrCreateInstance() .fetch() .then(res => this.updateClient(res)) From 7964a900f06c37545daa26b276ef5d423c0e6da1 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 31 Oct 2025 17:11:44 -0500 Subject: [PATCH 2/8] set transitive state when switching orgs --- packages/clerk-js/src/core/clerk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index af6772e1574..3fef7701ceb 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1412,7 +1412,7 @@ export class Clerk implements ClerkInterface { return; } - if (newSession?.status !== 'pending' && this.session?.id !== newSession?.id) { + if (newSession?.status !== 'pending' && (this.session?.id !== newSession?.id || shouldSwitchOrganization)) { this.#setTransitiveState(); } From 74575b8dc7e76fbf076bc2252966be60c8bef891 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 3 Nov 2025 22:37:49 -0600 Subject: [PATCH 3/8] remove preliminary client optimization --- packages/clerk-js/src/core/clerk.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 5a692f36267..a52669760ef 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2612,12 +2612,6 @@ export class Clerk implements ClerkInterface { }); const initClient = async () => { - const jwtInCookie = this.#authService?.getSessionCookie(); - if (jwtInCookie) { - const preliminaryClient = createClientFromJwt(jwtInCookie); - this.updateClient(preliminaryClient); - } - return Client.getOrCreateInstance() .fetch() .then(res => this.updateClient(res)) From d29ec53f2ac7ce546895253dd0fe5960a004d438 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 3 Nov 2025 23:15:10 -0600 Subject: [PATCH 4/8] changeset --- .changeset/upset-words-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/upset-words-fly.md diff --git a/.changeset/upset-words-fly.md b/.changeset/upset-words-fly.md new file mode 100644 index 00000000000..035ff88fda2 --- /dev/null +++ b/.changeset/upset-words-fly.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fix session setting immediately after sign in From fc11bb5655cc330f959b2d90dc4995e0324c302d Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 4 Nov 2025 09:55:51 -0600 Subject: [PATCH 5/8] add explanation --- packages/clerk-js/src/core/clerk.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index a52669760ef..064fe618da5 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2398,6 +2398,9 @@ export class Clerk implements ClerkInterface { } eventBus.emit(events.TokenUpdate, { token: this.session?.lastActiveToken }); } else if (!isFirstClientSet && newClient.sessions?.length > 0) { + // Handles the case where updateClient() is called (e.g., from a touch() response) with session data, + // but this.session is falsy. This commonly occurs after sign-in when the touch() response + // includes fresh client data before setActive() completes, preventing a session null flash. const session = this.#options.selectInitialSession ? this.#options.selectInitialSession(newClient) : this.#defaultSession(newClient); From 8b3df5102468123a323e00adbd6df78fdf842911 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 4 Nov 2025 10:05:52 -0600 Subject: [PATCH 6/8] add test coverage --- .../clerk-js/src/core/__tests__/clerk.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 1cccffbbcd8..d85040a47e0 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2507,5 +2507,37 @@ describe('Clerk singleton', () => { expect(mockOnAfterSetActive).toHaveBeenCalledTimes(1); }); }); + + it('sets session after sign-in when touch() response triggers updateClient', () => { + const mockSession = { + id: 'session_1', + status: 'active', + user: { id: 'user_1' }, + lastActiveToken: { getRawString: () => 'token_1' }, + }; + + const mockInitialClient = { + sessions: [], + signedInSessions: [], + lastActiveSessionId: null, + }; + + const mockClientWithSession = { + sessions: [mockSession], + signedInSessions: [mockSession], + lastActiveSessionId: 'session_1', + }; + + const sut = new Clerk(productionPublishableKey); + + sut.updateClient(mockInitialClient as any); + expect(sut.session).toBe(null); + + sut.updateClient(mockClientWithSession as any); + + expect(sut.session).toBeDefined(); + expect(sut.session?.id).toBe('session_1'); + expect(sut.session?.status).toBe('active'); + }); }); }); From 89fc47e96af8b65f0b9705df035035bfd53370e6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 4 Nov 2025 10:17:48 -0600 Subject: [PATCH 7/8] more tests --- .../clerk-js/src/core/__tests__/clerk.test.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index d85040a47e0..5321cea0e3f 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -233,6 +233,92 @@ describe('Clerk singleton', () => { expect(mockSession.touch).toHaveBeenCalled(); }); + it('does not call setTransitiveState when session ID stays the same during setActive with navigation', async () => { + const mockSession1 = { + id: 'session_1', + status: 'active', + user: { id: 'user_1' }, + touch: vi.fn(() => Promise.resolve()), + getToken: vi.fn(() => Promise.resolve('token_1')), + lastActiveToken: { getRawString: () => 'token_1' }, + }; + + mockClientFetch.mockReturnValue( + Promise.resolve({ signedInSessions: [mockSession1], isEligibleForTouch: () => false }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + await sut.setActive({ session: mockSession1 as any as ActiveSessionResource }); + expect(sut.session?.id).toBe('session_1'); + + // Track if session becomes undefined (transitive state) + let sessionBecameUndefined = false; + sut.addListener(state => { + if (state.session === undefined) { + sessionBecameUndefined = true; + } + }); + + // Call setActive with SAME session ID and redirectUrl + // Should NOT trigger setTransitiveState since session ID is not changing + await sut.setActive({ + session: mockSession1 as any as ActiveSessionResource, + redirectUrl: '/dashboard', + }); + + expect(sessionBecameUndefined).toBe(false); + expect(sut.session?.id).toBe('session_1'); + }); + + it('calls setTransitiveState when session ID changes during setActive with navigation', async () => { + const mockSession1 = { + id: 'session_1', + status: 'active', + user: { id: 'user_1' }, + touch: vi.fn(() => Promise.resolve()), + getToken: vi.fn(() => Promise.resolve('token_1')), + lastActiveToken: { getRawString: () => 'token_1' }, + }; + + const mockSession2 = { + id: 'session_2', + status: 'active', + user: { id: 'user_2' }, + touch: vi.fn(() => Promise.resolve()), + getToken: vi.fn(() => Promise.resolve('token_2')), + lastActiveToken: { getRawString: () => 'token_2' }, + }; + + mockClientFetch.mockReturnValue( + Promise.resolve({ signedInSessions: [mockSession1, mockSession2], isEligibleForTouch: () => false }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + await sut.setActive({ session: mockSession1 as any as ActiveSessionResource }); + expect(sut.session?.id).toBe('session_1'); + + // Track if session becomes undefined (transitive state) + let sessionBecameUndefined = false; + sut.addListener(state => { + if (state.session === undefined) { + sessionBecameUndefined = true; + } + }); + + // Call setActive with different session ID and redirectUrl + await sut.setActive({ + session: mockSession2 as any as ActiveSessionResource, + redirectUrl: '/dashboard', + }); + + expect(sessionBecameUndefined).toBe(true); + expect(sut.session?.id).toBe('session_2'); + }); + it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { mockSession.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); From d7b3918308eed712c45d427c94fcb560b7d5900c Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 4 Nov 2025 11:51:13 -0600 Subject: [PATCH 8/8] CR suggestion --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 8 ++++++++ packages/clerk-js/src/core/clerk.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 5321cea0e3f..79ffc0c71c4 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2619,11 +2619,19 @@ describe('Clerk singleton', () => { sut.updateClient(mockInitialClient as any); expect(sut.session).toBe(null); + const eventBusSpy = vi.spyOn(eventBus, 'emit'); + sut.updateClient(mockClientWithSession as any); expect(sut.session).toBeDefined(); expect(sut.session?.id).toBe('session_1'); expect(sut.session?.status).toBe('active'); + + expect(eventBusSpy).toHaveBeenCalledWith(events.TokenUpdate, { + token: mockSession.lastActiveToken, + }); + + eventBusSpy.mockRestore(); }); }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 064fe618da5..f44450d46cd 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -2405,6 +2405,8 @@ export class Clerk implements ClerkInterface { ? this.#options.selectInitialSession(newClient) : this.#defaultSession(newClient); this.#setAccessors(session); + + eventBus.emit(events.TokenUpdate, { token: session?.lastActiveToken || null }); } this.#emit();