diff --git a/.changeset/nasty-mangos-live.md b/.changeset/nasty-mangos-live.md new file mode 100644 index 00000000000..1206fda3bd8 --- /dev/null +++ b/.changeset/nasty-mangos-live.md @@ -0,0 +1,11 @@ +--- +'@clerk/elements': minor +'@clerk/shared': minor +'@clerk/astro': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +'@clerk/clerk-expo': minor +'@clerk/vue': minor +--- + +Surface new `pending` session as a signed-in state diff --git a/.changeset/proud-cycles-roll.md b/.changeset/proud-cycles-roll.md new file mode 100644 index 00000000000..b15a69c5577 --- /dev/null +++ b/.changeset/proud-cycles-roll.md @@ -0,0 +1,20 @@ +--- +'@clerk/clerk-js': minor +--- + +- Initialize new `pending` session status as an signed-in state +- Deprecate `Clerk.client.activeSessions` in favor of `Clerk.client.signedInSessions` +- Introduce `Clerk.isSignedIn` property as an explicit signed-in state check, instead of `!!Clerk.session` or `!!Clerk.user`: + +```ts +- if (Clerk.user) { ++ if (Clerk.isSignedIn) { + // Mount user button component + document.getElementById('signed-in').innerHTML = ` +
+ ` + + const userbuttonDiv = document.getElementById('user-button') + + clerk.mountUserButton(userbuttonDiv) +} diff --git a/packages/astro/src/stores/internal.ts b/packages/astro/src/stores/internal.ts index d9ea37d6902..4f9a9d50dc4 100644 --- a/packages/astro/src/stores/internal.ts +++ b/packages/astro/src/stores/internal.ts @@ -1,9 +1,9 @@ import type { - ActiveSessionResource, Clerk, ClientResource, InitialState, OrganizationResource, + SignedInSessionResource, UserResource, } from '@clerk/types'; import { atom, map } from 'nanostores'; @@ -12,7 +12,7 @@ export const $csrState = map<{ isLoaded: boolean; client: ClientResource | undefined | null; user: UserResource | undefined | null; - session: ActiveSessionResource | undefined | null; + session: SignedInSessionResource | undefined | null; organization: OrganizationResource | undefined | null; }>({ isLoaded: false, diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 339dd41e783..c744fffd27f 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -91,7 +91,7 @@ describe('Clerk singleton - Redirects', () => { beforeEach(() => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], }), ); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index eb085dce250..dc233ebfbc7 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -1,4 +1,10 @@ -import type { ActiveSessionResource, SignInJSON, SignUpJSON, TokenResource } from '@clerk/types'; +import type { + ActiveSessionResource, + SignedInSessionResource, + SignInJSON, + SignUpJSON, + TokenResource, +} from '@clerk/types'; import { waitFor } from '@testing-library/dom'; import { mockNativeRuntime } from '../../testUtils'; @@ -121,7 +127,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], }), ); @@ -149,370 +155,393 @@ describe('Clerk singleton', () => { }); describe('.setActive', () => { - const mockSession = { - id: '1', - remove: jest.fn(), - status: 'active', - user: {}, - touch: jest.fn(() => Promise.resolve()), - getToken: jest.fn(), - lastActiveToken: { getRawString: () => 'mocked-token' }, - }; - let eventBusSpy; - - beforeEach(() => { - eventBusSpy = jest.spyOn(eventBus, 'dispatch'); - }); - - afterEach(() => { - mockSession.remove.mockReset(); - mockSession.touch.mockReset(); - - eventBusSpy?.mockRestore(); - // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; - }); - - it('does not call session touch on signOut', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: null }); - await waitFor(() => { - expect(mockSession.touch).not.toHaveBeenCalled(); - expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null }); - }); - }); - - it('calls session.touch by default', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - expect(mockSession.touch).toHaveBeenCalled(); - }); - - it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - mockSession.getToken.mockResolvedValue('mocked-token'); - - const sut = new Clerk(productionPublishableKey); - await sut.load({ touchSession: false }); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - await waitFor(() => { - expect(mockSession.touch).not.toHaveBeenCalled(); - expect(mockSession.getToken).toHaveBeenCalled(); - }); - }); - - it('calls __unstable__onBeforeSetActive before session.touch', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + describe.each(['active', 'pending'] satisfies Array)( + 'when session has %s status', + status => { + const mockSession = { + id: '1', + remove: jest.fn(), + status, + user: {}, + touch: jest.fn(() => Promise.resolve()), + getToken: jest.fn(), + lastActiveToken: { getRawString: () => 'mocked-token' }, + }; + let eventBusSpy; + + beforeEach(() => { + eventBusSpy = jest.spyOn(eventBus, 'dispatch'); + }); - (window as any).__unstable__onBeforeSetActive = () => { - expect(mockSession.touch).not.toHaveBeenCalled(); - }; + afterEach(() => { + mockSession.remove.mockReset(); + mockSession.touch.mockReset(); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - expect(mockSession.touch).toHaveBeenCalled(); - }); + eventBusSpy?.mockRestore(); + // cleanup global window pollution + (window as any).__unstable__onBeforeSetActive = null; + (window as any).__unstable__onAfterSetActive = null; + }); - it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('does not call session touch on signOut', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: null }); + await waitFor(() => { + expect(mockSession.touch).not.toHaveBeenCalled(); + expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null }); + }); + }); - (window as any).__unstable__onBeforeSetActive = () => { - expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); - }; + it('calls session.touch by default', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource }); - }); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + expect(mockSession.touch).toHaveBeenCalled(); + }); - it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { - const beforeEmitMock = jest.fn(); - mockSession.touch.mockReturnValueOnce(Promise.resolve()); - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); + mockSession.getToken.mockResolvedValue('mocked-token'); + + const sut = new Clerk(productionPublishableKey); + await sut.load({ touchSession: false }); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + await waitFor(() => { + expect(mockSession.touch).not.toHaveBeenCalled(); + expect(mockSession.getToken).toHaveBeenCalled(); + }); + }); - (window as any).__unstable__onAfterSetActive = () => { - expect(mockSession.touch).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalled(); - }; + it('calls __unstable__onBeforeSetActive before session.touch', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); - }); + (window as any).__unstable__onBeforeSetActive = () => { + expect(mockSession.touch).not.toHaveBeenCalled(); + }; - // TODO: @dimkl include set transitive state - it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => { - const mockSession2 = { - id: '2', - remove: jest.fn(), - status: 'active', - user: {}, - touch: jest.fn(), - getToken: jest.fn(), - }; - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession, mockSession2] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + expect(mockSession.touch).toHaveBeenCalled(); + }); - const executionOrder: string[] = []; - mockSession2.touch.mockImplementationOnce(() => { - sut.session = mockSession2 as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession2.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token-2'; - }); - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); + it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => { + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + (window as any).__unstable__onBeforeSetActive = () => { + expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken }); + }; - await waitFor(() => { - expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); - expect(mockSession2.touch).toHaveBeenCalled(); - expect(mockSession2.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2); - expect(sut.session).toMatchObject(mockSession2); - }); - }); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource }); + }); - // TODO: @dimkl include set transitive state - it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => { + const beforeEmitMock = jest.fn(); + mockSession.touch.mockReturnValueOnce(Promise.resolve()); + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const executionOrder: string[] = []; - mockSession.touch.mockImplementationOnce(() => { - sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); - }); - mockSession.getToken.mockImplementation(() => { - executionOrder.push('set cookie'); - return 'mocked-token'; - }); + (window as any).__unstable__onAfterSetActive = () => { + expect(mockSession.touch).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalled(); + }; - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); - }); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + }); - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + // TODO: @dimkl include set transitive state + it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => { + const mockSession2 = { + id: '2', + remove: jest.fn(), + status: 'active', + user: {}, + touch: jest.fn(), + getToken: jest.fn(), + }; + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [mockSession, mockSession2], + }), + ); + + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + const executionOrder: string[] = []; + mockSession2.touch.mockImplementationOnce(() => { + sut.session = mockSession2 as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + mockSession2.getToken.mockImplementation(() => { + executionOrder.push('set cookie'); + return 'mocked-token-2'; + }); + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock }); + + await waitFor(() => { + expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']); + expect(mockSession2.touch).toHaveBeenCalled(); + expect(mockSession2.getToken).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2); + expect(sut.session).toMatchObject(mockSession2); + }); + }); - 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(beforeEmitMock).toHaveBeenCalledWith(mockSession); - expect(sut.session).toMatchObject(mockSession); - }); - }); + // TODO: @dimkl include set transitive state + it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); + const sut = new Clerk(productionPublishableKey); + await sut.load(); + + const executionOrder: string[] = []; + mockSession.touch.mockImplementationOnce(() => { + sut.session = mockSession as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + mockSession.getToken.mockImplementation(() => { + executionOrder.push('set cookie'); + return 'mocked-token'; + }); + + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + 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(beforeEmitMock).toHaveBeenCalledWith(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', - }, + it('sets active organization by slug', async () => { + const mockSession2 = { + id: '1', + status, + 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); - }); - }); - - it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now - isEligibleForTouch: () => true, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]); - expect(redirectUrl.pathname).toEqual('/v1/client/touch'); - expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`); - }); - - it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now - isEligibleForTouch: () => false, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); - }); - - it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => { - mockSession.touch.mockReturnValue(Promise.resolve()); - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession], - cookieExpiresAt: null, - isEligibleForTouch: () => false, - buildTouchUrl: () => - `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, - }), - ); - - const sut = new Clerk(productionPublishableKey); - sut.navigate = jest.fn(); - await sut.load(); - await sut.setActive({ session: mockSession as any as ActiveSessionResource, redirectUrl: '/redirect-url-path' }); - expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); - }); - - mockNativeRuntime(() => { - it('calls session.touch in a non-standard browser', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); - - const sut = new Clerk(productionPublishableKey); - await sut.load({ standardBrowser: false }); + touch: jest.fn(), + getToken: jest.fn(), + }; + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [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); + }); + }); - const executionOrder: string[] = []; - mockSession.touch.mockImplementationOnce(() => { - sut.session = mockSession as any; - executionOrder.push('session.touch'); - return Promise.resolve(); + it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + isEligibleForTouch: () => true, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]); + expect(redirectUrl.pathname).toEqual('/v1/client/touch'); + expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`); }); - const beforeEmitMock = jest.fn().mockImplementationOnce(() => { - executionOrder.push('before emit'); - return Promise.resolve(); + + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [mockSession], + cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now + isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); }); - await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock }); + it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => { + mockSession.touch.mockReturnValue(Promise.resolve()); + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [mockSession], + cookieExpiresAt: null, + isEligibleForTouch: () => false, + buildTouchUrl: () => + `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`, + }), + ); + + const sut = new Clerk(productionPublishableKey); + sut.navigate = jest.fn(); + await sut.load(); + await sut.setActive({ + session: mockSession as any as ActiveSessionResource, + redirectUrl: '/redirect-url-path', + }); + expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path'); + }); - expect(executionOrder).toEqual(['session.touch', 'before emit']); - expect(mockSession.touch).toHaveBeenCalled(); - expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id'); - expect(mockSession.getToken).toHaveBeenCalled(); - expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); - expect(sut.session).toMatchObject(mockSession); - }); - }); + mockNativeRuntime(() => { + it('calls session.touch in a non-standard browser', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); + + const sut = new Clerk(productionPublishableKey); + await sut.load({ standardBrowser: false }); + + const executionOrder: string[] = []; + mockSession.touch.mockImplementationOnce(() => { + sut.session = mockSession as any; + executionOrder.push('session.touch'); + return Promise.resolve(); + }); + const beforeEmitMock = jest.fn().mockImplementationOnce(() => { + executionOrder.push('before emit'); + return Promise.resolve(); + }); + + 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.getToken).toHaveBeenCalled(); + expect(beforeEmitMock).toHaveBeenCalledWith(mockSession); + expect(sut.session).toMatchObject(mockSession); + }); + }); + }, + ); }); describe('.load()', () => { - const mockSession = { - id: '1', - status: 'active', - user: {}, - getToken: jest.fn(), - lastActiveToken: { getRawString: () => mockJwt }, - }; - - afterEach(() => { - // cleanup global window pollution - (window as any).__unstable__onBeforeSetActive = null; - (window as any).__unstable__onAfterSetActive = null; - }); - - it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [], - }), - ); - - // any is intentional here. We simulate a runtime value that should not exist - const mockSelectInitialSession = jest.fn(() => undefined) as any; - const sut = new Clerk(productionPublishableKey); - await sut.load({ - selectInitialSession: mockSelectInitialSession, - }); + describe.each(['active', 'pending'] satisfies Array)( + 'when session has %s status', + status => { + const mockSession = { + id: '1', + status, + user: {}, + getToken: jest.fn(), + lastActiveToken: { getRawString: () => mockJwt }, + }; + + afterEach(() => { + // cleanup global window pollution + (window as any).__unstable__onBeforeSetActive = null; + (window as any).__unstable__onAfterSetActive = null; + }); - await waitFor(() => { - expect(sut.session).not.toBe(undefined); - expect(sut.session).toBe(null); - }); - }); + it('gracefully handles an incorrect value returned from the user provided selectInitialSession', async () => { + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [], + }), + ); + + // any is intentional here. We simulate a runtime value that should not exist + const mockSelectInitialSession = jest.fn(() => undefined) as any; + const sut = new Clerk(productionPublishableKey); + await sut.load({ + selectInitialSession: mockSelectInitialSession, + }); + + await waitFor(() => { + expect(sut.session).not.toBe(undefined); + expect(sut.session).toBe(null); + }); + }); - it('updates auth cookie on load from fetched session', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('updates auth cookie on load from fetched session', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); - expect(document.cookie).toContain(mockJwt); - }); + expect(document.cookie).toContain(mockJwt); + }); - it('updates auth cookie on token:update event', async () => { - mockClientFetch.mockReturnValue(Promise.resolve({ activeSessions: [mockSession] })); + it('updates auth cookie on token:update event', async () => { + mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] })); - const sut = new Clerk(productionPublishableKey); - await sut.load(); + const sut = new Clerk(productionPublishableKey); + await sut.load(); - const token = { - jwt: {}, - getRawString: () => 'updated-jwt', - } as TokenResource; - eventBus.dispatch(events.TokenUpdate, { token }); + const token = { + jwt: {}, + getRawString: () => 'updated-jwt', + } as TokenResource; + eventBus.dispatch(events.TokenUpdate, { token }); - expect(document.cookie).toContain('updated-jwt'); - }); + expect(document.cookie).toContain('updated-jwt'); + }); + }, + ); }); describe('.signOut()', () => { @@ -520,19 +549,21 @@ describe('Clerk singleton', () => { const mockClientRemoveSessions = jest.fn(); const mockSession1 = { id: '1', remove: jest.fn(), status: 'active', user: {}, getToken: jest.fn() }; const mockSession2 = { id: '2', remove: jest.fn(), status: 'active', user: {}, getToken: jest.fn() }; + const mockSession3 = { id: '2', remove: jest.fn(), status: 'pending', user: {}, getToken: jest.fn() }; beforeEach(() => { mockClientDestroy.mockReset(); mockClientRemoveSessions.mockReset(); mockSession1.remove.mockReset(); mockSession2.remove.mockReset(); + mockSession3.remove.mockReset(); }); - it('has no effect if called when no active sessions exist', async () => { + it('has no effect if called when no sessions exist', async () => { const sut = new Clerk(productionPublishableKey); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], destroy: mockClientDestroy, }), @@ -545,11 +576,11 @@ describe('Clerk singleton', () => { }); }); - it('signs out all sessions if no sessionId is passed and multiple sessions are active', async () => { + it('signs out all sessions if no sessionId is passed and multiple sessions have authenticated status', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + signedInSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, removeSessions: mockClientRemoveSessions, }), @@ -569,36 +600,39 @@ describe('Clerk singleton', () => { }); }); - it('signs out all sessions if no sessionId is passed and only one session is active', async () => { - mockClientFetch.mockReturnValue( - Promise.resolve({ - activeSessions: [mockSession1], - sessions: [mockSession1], - destroy: mockClientDestroy, - removeSessions: mockClientRemoveSessions, - }), - ); + it.each(['active', 'pending'] satisfies Array)( + 'signs out all sessions if no sessionId is passed and only one session has %s status', + async status => { + mockClientFetch.mockReturnValue( + Promise.resolve({ + signedInSessions: [{ ...mockSession1, status }], + sessions: [{ ...mockSession1, status }], + destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, + }), + ); - const sut = new Clerk(productionPublishableKey); - sut.setActive = jest.fn(); - await sut.load(); - await sut.signOut(); - await waitFor(() => { - expect(mockClientDestroy).not.toHaveBeenCalled(); - expect(mockClientRemoveSessions).toHaveBeenCalled(); - expect(mockSession1.remove).not.toHaveBeenCalled(); - expect(sut.setActive).toHaveBeenCalledWith({ - session: null, - redirectUrl: '/', + const sut = new Clerk(productionPublishableKey); + sut.setActive = jest.fn(); + await sut.load(); + await sut.signOut(); + await waitFor(() => { + expect(mockClientDestroy).not.toHaveBeenCalled(); + expect(mockClientRemoveSessions).toHaveBeenCalled(); + expect(mockSession1.remove).not.toHaveBeenCalled(); + expect(sut.setActive).toHaveBeenCalledWith({ + session: null, + redirectUrl: '/', + }); }); - }); - }); + }, + ); it('only removes the session that corresponds to the passed sessionId if it is not the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + signedInSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -619,8 +653,8 @@ describe('Clerk singleton', () => { it('removes and signs out the session that corresponds to the passed sessionId if it is the current', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + signedInSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -642,8 +676,8 @@ describe('Clerk singleton', () => { it('removes and signs out the session and redirects to the provided redirectUrl ', async () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [mockSession1, mockSession2], - sessions: [mockSession1, mockSession2], + signedInSessions: [mockSession1, mockSession2, mockSession3], + sessions: [mockSession1, mockSession2, mockSession3], destroy: mockClientDestroy, }), ); @@ -763,7 +797,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -822,7 +856,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -884,7 +918,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -946,7 +980,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1013,7 +1047,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1064,7 +1098,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_identifier', first_factor_verification: { @@ -1119,7 +1153,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1159,7 +1193,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_second_factor', first_factor_verification: { @@ -1213,7 +1247,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1253,7 +1287,7 @@ describe('Clerk singleton', () => { const mockSession = { id: sessionId, remove: jest.fn(), - status: 'active', + status, user: {}, touch: jest.fn(() => Promise.resolve()), getToken: jest.fn(), @@ -1274,7 +1308,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ sessions: [mockSession], - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1326,7 +1360,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1374,7 +1408,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1422,7 +1456,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1463,7 +1497,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1510,7 +1544,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_first_factor', } as unknown as SignInJSON), @@ -1542,7 +1576,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1590,7 +1624,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn(null), signUp: new SignUp({ status: 'missing_requirements', @@ -1650,7 +1684,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_first_factor', first_factor_verification: { @@ -1700,7 +1734,7 @@ describe('Clerk singleton', () => { mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], signIn: new SignIn({ status: 'needs_new_password', } as unknown as SignInJSON), @@ -1748,7 +1782,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [{ id: createdSessionId }], signIn: new SignIn({ status: 'completed', @@ -1777,7 +1811,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signIn: new SignIn({ status: 'needs_second_factor', @@ -1808,7 +1842,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [{ id: createdSessionId }], signUp: new SignUp({ status: 'completed', @@ -1837,7 +1871,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'verified']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp({ status: 'missing_requirements', @@ -1864,7 +1898,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'expired']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1886,7 +1920,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_status', 'failed']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1911,7 +1945,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1934,7 +1968,7 @@ describe('Clerk singleton', () => { setWindowQueryParams([['__clerk_created_session', 'sess_123']]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [], signUp: new SignUp(null), signIn: new SignIn(null), @@ -1957,7 +1991,7 @@ describe('Clerk singleton', () => { ]); mockClientFetch.mockReturnValue( Promise.resolve({ - activeSessions: [], + signedInSessions: [], sessions: [{ id: 'sess_123' }], signIn: new SignIn({ status: 'completed', diff --git a/packages/clerk-js/src/core/auth/cookies/clientUat.ts b/packages/clerk-js/src/core/auth/cookies/clientUat.ts index ed87e2a6ae8..03144edabd1 100644 --- a/packages/clerk-js/src/core/auth/cookies/clientUat.ts +++ b/packages/clerk-js/src/core/auth/cookies/clientUat.ts @@ -38,7 +38,7 @@ export const createClientUatCookie = (cookieSuffix: string): ClientUatCookieHand // '0' indicates the user is signed out let val = '0'; - if (client && client.updatedAt && client.activeSessions.length > 0) { + if (client && client.updatedAt && client.signedInSessions.length > 0) { // truncate timestamp to seconds, since this is a unix timestamp val = Math.floor(client.updatedAt.getTime() / 1000).toString(); } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d74720f50af..8c205ab9909 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -10,7 +10,6 @@ import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, - ActiveSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -47,6 +46,7 @@ import type { Resources, SDKMetadata, SetActiveParams, + SignedInSessionResource, SignInProps, SignInRedirectOptions, SignInResource, @@ -164,7 +164,7 @@ export class Clerk implements ClerkInterface { }; public client: ClientResource | undefined; - public session: ActiveSessionResource | null | undefined; + public session: SignedInSessionResource | null | undefined; public organization: OrganizationResource | null | undefined; public user: UserResource | null | undefined; public __internal_country?: string | null; @@ -288,6 +288,10 @@ export class Clerk implements ClerkInterface { return this.#options[key]; } + get isSignedIn(): boolean { + return !!this.session; + } + public constructor(key: string, options?: DomainOrProxyUrl) { key = (key || '').trim(); @@ -387,7 +391,7 @@ export class Clerk implements ClerkInterface { }); }; - if (!opts.sessionId || this.client.activeSessions.length === 1) { + if (!opts.sessionId || this.client.signedInSessions.length === 1) { if (this.#options.experimental?.persistClient ?? true) { await this.client.removeSessions(); } else { @@ -397,7 +401,7 @@ export class Clerk implements ClerkInterface { return handleSetActive(); } - const session = this.client.activeSessions.find(s => s.id === opts.sessionId); + const session = this.client.signedInSessions.find(s => s.id === opts.sessionId); const shouldSignOutCurrent = session?.id && this.session?.id === session.id; await session?.remove(); if (shouldSignOutCurrent) { @@ -877,12 +881,12 @@ export class Clerk implements ClerkInterface { : noop; if (typeof session === 'string') { - session = (this.client.sessions.find(x => x.id === session) as ActiveSessionResource) || null; + session = (this.client.sessions.find(x => x.id === session) as SignedInSessionResource) || null; } let newSession = session === undefined ? this.session : session; - // At this point, the `session` variable should contain either an `ActiveSessionResource` + // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. // We now want to set the last active organization id on that session (if it exists). // However, if the `organization` parameter is not given (i.e. `undefined`), we want @@ -920,7 +924,7 @@ export class Clerk implements ClerkInterface { // Note that this will also update the session's active organization // id. if (inActiveBrowserTab() || !this.#options.standardBrowser) { - await this.#touchLastActiveSession(newSession); + await this.#touchCurrentSession(newSession); // reload session from updated client newSession = this.#getSessionFromClient(newSession?.id); } @@ -2028,14 +2032,14 @@ export class Clerk implements ClerkInterface { this.#emit(); }; - #defaultSession = (client: ClientResource): ActiveSessionResource | null => { + #defaultSession = (client: ClientResource): SignedInSessionResource | null => { if (client.lastActiveSessionId) { - const lastActiveSession = client.activeSessions.find(s => s.id === client.lastActiveSessionId); - if (lastActiveSession) { - return lastActiveSession; + const currentSession = client.signedInSessions.find(s => s.id === client.lastActiveSessionId); + if (currentSession) { + return currentSession; } } - const session = client.activeSessions[0]; + const session = client.signedInSessions[0]; return session || null; }; @@ -2055,7 +2059,7 @@ export class Clerk implements ClerkInterface { } this.#touchThrottledUntil = Date.now() + 5_000; - return this.#touchLastActiveSession(this.session); + return this.#touchCurrentSession(this.session); }; this.#sessionTouchOfflineScheduler.schedule(performTouch); @@ -2069,7 +2073,7 @@ export class Clerk implements ClerkInterface { }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc - #touchLastActiveSession = async (session?: ActiveSessionResource | null): Promise => { + #touchCurrentSession = async (session?: SignedInSessionResource | null): Promise => { if (!session || !this.#options.touchSession) { return Promise.resolve(); } @@ -2118,14 +2122,14 @@ export class Clerk implements ClerkInterface { ); }; - #setAccessors = (session?: ActiveSessionResource | null) => { + #setAccessors = (session?: SignedInSessionResource | null) => { this.session = session || null; this.organization = this.#getLastActiveOrganizationFromSession(); this.#aliasUser(); }; - #getSessionFromClient = (sessionId: string | undefined): ActiveSessionResource | null => { - return this.client?.activeSessions.find(x => x.id === sessionId) || null; + #getSessionFromClient = (sessionId: string | undefined): SignedInSessionResource | null => { + return this.client?.signedInSessions.find(x => x.id === sessionId) || null; }; #aliasUser = () => { diff --git a/packages/clerk-js/src/core/resources/Client.ts b/packages/clerk-js/src/core/resources/Client.ts index b7558c49a07..b46fc02a42f 100644 --- a/packages/clerk-js/src/core/resources/Client.ts +++ b/packages/clerk-js/src/core/resources/Client.ts @@ -1,10 +1,11 @@ -import type { - ActiveSessionResource, - ClientJSON, - ClientJSONSnapshot, - ClientResource, - SignInResource, - SignUpResource, +import { + type ActiveSessionResource, + type ClientJSON, + type ClientJSONSnapshot, + type ClientResource, + type SignedInSessionResource, + type SignInResource, + type SignUpResource, } from '@clerk/types'; import { unixEpochToDate } from '../../utils/date'; @@ -53,10 +54,17 @@ export class Client extends BaseResource implements ClientResource { return this.signIn; } + /** + * @deprecated Use `signedInSessions` instead + */ get activeSessions(): ActiveSessionResource[] { return this.sessions.filter(s => s.status === 'active') as ActiveSessionResource[]; } + get signedInSessions(): SignedInSessionResource[] { + return this.sessions.filter(s => s.status === 'active' || s.status === 'pending') as SignedInSessionResource[]; + } + create(): Promise { return this._basePut(); } diff --git a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx index d9d7299424c..4c606c06498 100644 --- a/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx +++ b/packages/clerk-js/src/ui/components/ImpersonationFab/ImpersonationFab.tsx @@ -1,5 +1,5 @@ import { useClerk, useSession, useUser } from '@clerk/shared/react'; -import type { ActiveSessionResource } from '@clerk/types'; +import type { SignedInSessionResource } from '@clerk/types'; import type { PointerEventHandler } from 'react'; import React, { useEffect, useRef } from 'react'; @@ -66,7 +66,7 @@ const FabContent = ({ title, signOutText }: FabContentProps) => { const { otherSessions } = useMultipleSessions({ user }); const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext(); - const handleSignOutSessionClicked = (session: ActiveSessionResource) => () => { + const handleSignOutSessionClicked = (session: SignedInSessionResource) => () => { if (otherSessions.length === 0) { return signOut(navigateAfterSignOut); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx index 1bf0a3d8069..f5f8e6d0ee3 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInAccountSwitcher.tsx @@ -12,7 +12,7 @@ const _SignInAccountSwitcher = () => { const { userProfileUrl } = useEnvironment().displayConfig; const { afterSignInUrl, path: signInPath } = useSignInContext(); const { navigateAfterSignOut } = useSignOutContext(); - const { handleSignOutAllClicked, handleSessionClicked, activeSessions, handleAddAccountClicked } = + const { handleSignOutAllClicked, handleSessionClicked, signedInSessions, handleAddAccountClicked } = useMultisessionActions({ navigateAfterSignOut, afterSwitchSessionUrl: afterSignInUrl, @@ -40,7 +40,7 @@ const _SignInAccountSwitcher = () => { })} > - {activeSessions.map(s => ( + {signedInSessions.map(s => ( Promise | void; - handleSignOutSessionClicked: (session: ActiveSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: SignedInSessionResource) => () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: ActiveSessionResource; + session: SignedInSessionResource; completedCallback: () => void; }; @@ -113,12 +113,12 @@ export const SingleSessionActions = (props: SingleSessionActionsProps) => { type MultiSessionActionsProps = { handleManageAccountClicked: () => Promise | void; - handleSignOutSessionClicked: (session: ActiveSessionResource) => () => Promise | void; - handleSessionClicked: (session: ActiveSessionResource) => () => Promise | void; + handleSignOutSessionClicked: (session: SignedInSessionResource) => () => Promise | void; + handleSessionClicked: (session: SignedInSessionResource) => () => Promise | void; handleAddAccountClicked: () => Promise | void; handleUserProfileActionClicked: (startPath?: string) => Promise | void; - session: ActiveSessionResource; - otherSessions: ActiveSessionResource[]; + session: SignedInSessionResource; + otherSessions: SignedInSessionResource[]; completedCallback: () => void; }; diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx index e773bdf2e08..1b3fe1a0077 100644 --- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx @@ -1,5 +1,5 @@ import { useSession, useUser } from '@clerk/shared/react'; -import type { ActiveSessionResource } from '@clerk/types'; +import type { SignedInSessionResource } from '@clerk/types'; import React from 'react'; import { useEnvironment, useUserButtonContext } from '../../contexts'; @@ -14,7 +14,7 @@ type UserButtonPopoverProps = { close?: (open: boolean) => void } & PropsOfCompo export const UserButtonPopover = React.forwardRef((props, ref) => { const { close: unsafeClose, ...rest } = props; const close = () => unsafeClose?.(false); - const { session } = useSession() as { session: ActiveSessionResource }; + const { session } = useSession() as { session: SignedInSessionResource }; const userButtonContext = useUserButtonContext(); const { __experimental_asStandalone } = userButtonContext; const { authConfig } = useEnvironment(); diff --git a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx index c5acbd5ed2b..0b824e2999b 100644 --- a/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/useMultisessionActions.tsx @@ -1,5 +1,5 @@ import { useClerk } from '@clerk/shared/react'; -import type { ActiveSessionResource, UserButtonProps, UserResource } from '@clerk/types'; +import type { SignedInSessionResource, UserButtonProps, UserResource } from '@clerk/types'; import { windowNavigate } from '../../../utils/windowNavigate'; import { useCardState } from '../../elements'; @@ -20,10 +20,10 @@ type UseMultisessionActionsParams = { export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { const { setActive, signOut, openUserProfile } = useClerk(); const card = useCardState(); - const { activeSessions, otherSessions } = useMultipleSessions({ user: opts.user }); + const { signedInSessions, otherSessions } = useMultipleSessions({ user: opts.user }); const { navigate } = useRouter(); - const handleSignOutSessionClicked = (session: ActiveSessionResource) => () => { + const handleSignOutSessionClicked = (session: SignedInSessionResource) => () => { if (otherSessions.length === 0) { return signOut(opts.navigateAfterSignOut); } @@ -66,7 +66,7 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { return signOut(opts.navigateAfterSignOut); }; - const handleSessionClicked = (session: ActiveSessionResource) => async () => { + const handleSessionClicked = (session: SignedInSessionResource) => async () => { card.setLoading(); return setActive({ session, redirectUrl: opts.afterSwitchSessionUrl }).finally(() => { card.setIdle(); @@ -87,6 +87,6 @@ export const useMultisessionActions = (opts: UseMultisessionActionsParams) => { handleSessionClicked, handleAddAccountClicked, otherSessions, - activeSessions, + signedInSessions, }; }; diff --git a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx index d13aa8a5b31..d26820b64fe 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx @@ -1,5 +1,5 @@ import { useReverification, useSession, useUser } from '@clerk/shared/react'; -import type { SessionWithActivitiesResource } from '@clerk/types'; +import type { SessionWithActivitiesResource, SignedInSessionResource } from '@clerk/types'; import { Badge, Col, descriptors, Flex, Icon, localizationKeys, Text, useLocalizations } from '../../customizables'; import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements'; @@ -29,7 +29,7 @@ export const ActiveDevicesSection = () => { ) : ( sessions?.sort(currentSessionFirst(session!.id)).map(sa => { - if (sa.status !== 'active') { + if (!isSignedInStatus(sa.status)) { return null; } return ( @@ -45,6 +45,12 @@ export const ActiveDevicesSection = () => { ); }; +const isSignedInStatus = (status: string): status is SignedInSessionResource['status'] => { + return (['active', 'pending'] satisfies Array).includes( + status as SignedInSessionResource['status'], + ); +}; + const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) => { const isCurrent = useSession().session?.id === session.id; const status = useLoadingStatus(); diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx index d5927dfc576..fcb84d29bbf 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/SecurityPage.test.tsx @@ -134,6 +134,25 @@ describe('SecurityPage', () => { actor: null, revoke: jest.fn().mockResolvedValue({}), } as any as SessionWithActivitiesResource, + { + pathRoot: '/me/sessions', + id: 'sess_2HyQfBh8wRJUbpvCtPNllWdsHFi', + status: 'pending', + expireAt: '2022-12-01T01:55:44.636Z', + abandonAt: '2022-12-24T01:55:44.636Z', + lastActiveAt: '2022-11-24T12:11:49.328Z', + latestActivity: { + id: 'sess_activity_2HyQwElm529O5NDL1KNpJAGWVJZ', + deviceType: 'Macintosh', + browserName: 'Chrome', + browserVersion: '107.0.0.0', + country: 'Greece', + city: 'Athens', + isMobile: false, + }, + actor: null, + revoke: jest.fn().mockResolvedValue({}), + } as any as SessionWithActivitiesResource, ]), ); diff --git a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts index c62caba2936..b70217d8b88 100644 --- a/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts +++ b/packages/clerk-js/src/ui/hooks/useMultipleSessions.ts @@ -1,18 +1,17 @@ -import { useSessionList } from '@clerk/shared/react'; -import type { ActiveSessionResource, UserResource } from '@clerk/types'; +import { useClerk } from '@clerk/shared/react'; +import type { UserResource } from '@clerk/types'; type UseMultipleSessionsParam = { user: UserResource | null | undefined; }; const useMultipleSessions = (params: UseMultipleSessionsParam) => { - const { sessions } = useSessionList(); - const activeSessions = sessions?.filter(s => s.status === 'active') as ActiveSessionResource[]; - const otherSessions = activeSessions.filter(s => s.user?.id !== params.user?.id); + const clerk = useClerk(); + const signedInSessions = clerk.client.signedInSessions; return { - activeSessions, - otherSessions, + signedInSessions, + otherSessions: signedInSessions.filter(s => s.user?.id !== params.user?.id), }; }; diff --git a/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx b/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx index e3a7b8b7cb0..c39f64627dc 100644 --- a/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx +++ b/packages/elements/src/react/sign-in/choose-session/__tests__/choose-session.test.tsx @@ -8,7 +8,7 @@ import * as Hooks from '../choose-session.hooks'; describe('SignInSessionList/SignInSessionListItem', () => { beforeAll(() => { jest.spyOn(Hooks, 'useSignInChooseSessionIsActive').mockImplementation(() => true); - jest.spyOn(Hooks, 'useSignInActiveSessionList').mockImplementation(() => [ + jest.spyOn(Hooks, 'useSignInSessionList').mockImplementation(() => [ { id: 'abc123', firstName: 'firstName', diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts index dceea5ea7d2..ee7cfb697a4 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.hooks.ts @@ -23,17 +23,17 @@ export function useSignInChooseSessionIsActive() { return useActiveTags(routerRef, 'step:choose-session'); } -export type UseSignInActiveSessionListParams = { +export type UseSignInSessionListParams = { omitCurrent: boolean; }; -export function useSignInActiveSessionList(params?: UseSignInActiveSessionListParams): SignInActiveSessionListItem[] { +export function useSignInSessionList(params?: UseSignInSessionListParams): SignInActiveSessionListItem[] { const { omitCurrent = true } = params || {}; return SignInRouterCtx.useSelector(state => { - const activeSessions = state.context.clerk?.client?.activeSessions || []; + const signedInSessions = state.context.clerk?.client?.signedInSessions || []; const currentSessionId = state.context.clerk?.session?.id; - const filteredSessions = omitCurrent ? activeSessions.filter(s => s.id !== currentSessionId) : activeSessions; + const filteredSessions = omitCurrent ? signedInSessions.filter(s => s.id !== currentSessionId) : signedInSessions; return filteredSessions.map(s => ({ id: s.id, diff --git a/packages/elements/src/react/sign-in/choose-session/choose-session.tsx b/packages/elements/src/react/sign-in/choose-session/choose-session.tsx index 020464b9bd8..6ce9860e74e 100644 --- a/packages/elements/src/react/sign-in/choose-session/choose-session.tsx +++ b/packages/elements/src/react/sign-in/choose-session/choose-session.tsx @@ -8,8 +8,8 @@ import { SignInActiveSessionContext, type SignInActiveSessionListItem, useSignInActiveSessionContext, - useSignInActiveSessionList, useSignInChooseSessionIsActive, + useSignInSessionList, } from './choose-session.hooks'; // ----------------------------------- TYPES ------------------------------------ @@ -44,7 +44,7 @@ export function SignInChooseSession({ asChild, children, ...props }: SignInChoos } export function SignInSessionList({ asChild, children, includeCurrentSession, ...props }: SignInSessionListProps) { - const sessions = useSignInActiveSessionList({ omitCurrent: !includeCurrentSession }); + const sessions = useSignInSessionList({ omitCurrent: !includeCurrentSession }); if (!children || !sessions?.length) { return null; diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index a617471fe7d..b62d3d42c50 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -123,8 +123,8 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { if (client) { void ClientResourceCache.save(client.__internal_toSnapshot()); if (client.lastActiveSessionId) { - const lastActiveSession = client.activeSessions.find(s => s.id === client.lastActiveSessionId); - const token = lastActiveSession?.lastActiveToken?.getRawString(); + const currentSession = client.signedInSessions.find(s => s.id === client.lastActiveSessionId); + const token = currentSession?.lastActiveToken?.getRawString(); if (token) { void SessionJWTCache.save(token); } diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index e8b71d770d8..91c9d7ee00e 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -143,10 +143,10 @@ export const Protect = ({ children, fallback, ...restAuthorizedParams }: Protect export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { const { client, session } = clerk; - const hasActiveSessions = client.activeSessions && client.activeSessions.length > 0; + const hasSignedInSessions = client.signedInSessions && client.signedInSessions.length > 0; React.useEffect(() => { - if (session === null && hasActiveSessions) { + if (session === null && hasSignedInSessions) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 736570ae8a8..61b8c439cc9 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -5,7 +5,6 @@ import { handleValueOrFn } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, __internal_UserVerificationProps, - ActiveSessionResource, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, @@ -31,6 +30,7 @@ import type { RedirectOptions, SDKMetadata, SetActiveParams, + SignedInSessionResource, SignInProps, SignInRedirectOptions, SignInResource, @@ -616,7 +616,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - get session(): ActiveSessionResource | undefined | null { + get session(): SignedInSessionResource | undefined | null { if (this.clerkjs) { return this.clerkjs.session; } else { @@ -658,6 +658,14 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } + get isSignedIn(): boolean { + if (this.clerkjs) { + return this.clerkjs.isSignedIn; + } else { + return false; + } + } + __unstable__setEnvironment(...args: any): void { if (this.clerkjs && '__unstable__setEnvironment' in this.clerkjs) { (this.clerkjs as any).__unstable__setEnvironment(args); diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts index 771c8040839..d23c70ffe31 100644 --- a/packages/shared/src/deriveState.ts +++ b/packages/shared/src/deriveState.ts @@ -1,10 +1,10 @@ import type { - ActiveSessionResource, InitialState, OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, Resources, + SignedInSessionResource, UserResource, } from '@clerk/types'; @@ -22,7 +22,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { const userId = initialState.userId; const user = initialState.user as UserResource; const sessionId = initialState.sessionId; - const session = initialState.session as ActiveSessionResource; + const session = initialState.session as SignedInSessionResource; const organization = initialState.organization as OrganizationResource; const orgId = initialState.orgId; const orgRole = initialState.orgRole as OrganizationCustomRoleKey; diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index ad178e59202..e3170145c09 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -1,11 +1,11 @@ 'use client'; import type { - ActiveSessionResource, ClerkOptions, ClientResource, LoadedClerk, OrganizationResource, + SignedInSessionResource, UserResource, } from '@clerk/types'; import type { PropsWithChildren } from 'react'; @@ -17,7 +17,7 @@ import { createContextAndHook } from './hooks/createContextAndHook'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); const [UserContext, useUserContext] = createContextAndHook('UserContext'); const [ClientContext, useClientContext] = createContextAndHook('ClientContext'); -const [SessionContext, useSessionContext] = createContextAndHook( +const [SessionContext, useSessionContext] = createContextAndHook( 'SessionContext', ); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 2b0e8ff27c2..1296685e084 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -31,7 +31,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { ActiveSessionResource } from './session'; +import type { SignedInSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -63,7 +63,7 @@ export type SDKMetadata = { export type ListenerCallback = (emission: Resources) => void; export type UnsubscribeCallback = () => void; -export type BeforeEmitCallback = (session?: ActiveSessionResource | null) => void | Promise; +export type BeforeEmitCallback = (session?: SignedInSessionResource | null) => void | Promise; export type SignOutCallback = () => void | Promise; @@ -127,11 +127,16 @@ export interface Clerk { /** Clerk flag for loading Clerk in a standard browser setup */ isStandardBrowser: boolean | undefined; + /** + * Indicates whether the current user has a valid signed-in client session + */ + isSignedIn: boolean; + /** Client handling most Clerk operations. */ client: ClientResource | undefined; - /** Active Session. */ - session: ActiveSessionResource | null | undefined; + /** Current Session. */ + session: SignedInSessionResource | null | undefined; /** Active Organization */ organization: OrganizationResource | null | undefined; @@ -708,9 +713,9 @@ export type ClerkOptions = ClerkOptionsNavigation & localization?: LocalizationResource; polling?: boolean; /** - * By default, the last active session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. + * By default, the last signed-in session is used during client initialization. This option allows you to override that behavior, e.g. by selecting a specific session. */ - selectInitialSession?: (client: ClientResource) => ActiveSessionResource | null; + selectInitialSession?: (client: ClientResource) => SignedInSessionResource | null; /** * By default, ClerkJS is loaded with the assumption that cookies can be set (browser setup). On native platforms this value must be set to `false`. */ @@ -796,7 +801,7 @@ export interface NavigateOptions { export interface Resources { client: ClientResource; - session?: ActiveSessionResource | null; + session?: SignedInSessionResource | null; user?: UserResource | null; organization?: OrganizationResource | null; } @@ -869,10 +874,10 @@ export type SignUpRedirectOptions = RedirectOptions & export type SetActiveParams = { /** - * The session resource or session id (string version) to be set as active. + * The session resource or session id (string version) to be set on the client. * If `null`, the current session is deleted. */ - session?: ActiveSessionResource | string | null; + session?: SignedInSessionResource | string | null; /** * The organization resource or organization ID/slug (string version) to be set as active in the current session. diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index d9b235bade1..f5374e8af54 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,12 +1,12 @@ import type { ClerkResource } from './resource'; -import type { ActiveSessionResource, SessionResource } from './session'; +import type { ActiveSessionResource, SessionResource, SignedInSessionResource } from './session'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; import type { ClientJSONSnapshot } from './snapshots'; export interface ClientResource extends ClerkResource { sessions: SessionResource[]; - activeSessions: ActiveSessionResource[]; + signedInSessions: SignedInSessionResource[]; signUp: SignUpResource; signIn: SignInResource; isNew: () => boolean; @@ -23,4 +23,8 @@ export interface ClientResource extends ClerkResource { createdAt: Date | null; updatedAt: Date | null; __internal_toSnapshot: () => ClientJSONSnapshot; + /** + * @deprecated Use `signedInSessions` instead + */ + activeSessions: ActiveSessionResource[]; } diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 4a1a05c66ba..3d1a3d9f04f 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -4,10 +4,10 @@ import type { SignInResource } from 'signIn'; import type { SetActive, SignOut } from './clerk'; import type { ActJWTClaim } from './jwt'; import type { - ActiveSessionResource, CheckAuthorizationWithCustomPermissions, GetToken, SessionResource, + SignedInSessionResource, } from './session'; import type { SignUpResource } from './signUp'; import type { UserResource } from './user'; @@ -180,7 +180,7 @@ export type UseSessionReturn = | { isLoaded: true; isSignedIn: true; - session: ActiveSessionResource; + session: SignedInSessionResource; }; /** diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 32f18a93260..db4772c3813 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -131,11 +131,29 @@ export interface SessionResource extends ClerkResource { __internal_toSnapshot: () => SessionJSONSnapshot; } +/** + * Represents a session resource that has completed all pending tasks + * and authentication factors + */ export interface ActiveSessionResource extends SessionResource { status: 'active'; user: UserResource; } +/** + * Represents a session resource that has completed sign-in but has pending tasks + */ +export interface PendingSessionResource extends SessionResource { + status: 'pending'; + user: UserResource; +} + +/** + * Represents session resources for users who have completed + * the full sign-in flow + */ +export type SignedInSessionResource = ActiveSessionResource | PendingSessionResource; + export interface SessionWithActivitiesResource extends ClerkResource { id: string; status: string; @@ -159,7 +177,15 @@ export interface SessionActivity { isMobile?: boolean; } -export type SessionStatus = 'abandoned' | 'active' | 'ended' | 'expired' | 'removed' | 'replaced' | 'revoked'; +export type SessionStatus = + | 'abandoned' + | 'active' + | 'ended' + | 'expired' + | 'removed' + | 'replaced' + | 'revoked' + | 'pending'; export interface PublicUserData { firstName: string | null; diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 218e0c5b8cc..c48925befae 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -41,9 +41,9 @@ export const RedirectToSignIn = defineComponent((props: RedirectOptions) => { const { sessionCtx, clientCtx } = useClerkContext(); useClerkLoaded(clerk => { - const hasActiveSessions = clientCtx.value?.activeSessions && clientCtx.value.activeSessions.length > 0; + const hasSignedInSessions = clientCtx.value?.signedInSessions && clientCtx.value.signedInSessions.length > 0; - if (sessionCtx.value === null && hasActiveSessions) { + if (sessionCtx.value === null && hasSignedInSessions) { void clerk.redirectToAfterSignOut(); } else { void clerk.redirectToSignIn(props); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 213c909b658..032183d8e10 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,5 +1,4 @@ import type { - ActiveSessionResource, ActJWTClaim, Clerk, ClerkOptions, @@ -9,6 +8,7 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey, OrganizationResource, + SignedInSessionResource, UserResource, Without, } from '@clerk/types'; @@ -26,7 +26,7 @@ export interface VueClerkInjectionKeyType { orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; }>; clientCtx: ComputedRef; - sessionCtx: ComputedRef; + sessionCtx: ComputedRef; userCtx: ComputedRef; organizationCtx: ComputedRef; }