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;
}