diff --git a/.changeset/shy-sloths-laugh.md b/.changeset/shy-sloths-laugh.md new file mode 100644 index 0000000000..3459ac9842 --- /dev/null +++ b/.changeset/shy-sloths-laugh.md @@ -0,0 +1,16 @@ +--- +"@clerk/clerk-js": minor +--- + +We recently shipped an experimental feature to persist the Clerk client (under `persistClient` flag) as an opt-in. This allows for matching a user's device with a client. We want to test this behavior with more users, so we're making it opt-out as the next step. After more successful testing we'll remove the experimental flag and enable it by default. + +If you're encountering issues, please open an issue. You can disable this new behavior like so: + +```js +// React + + +// Vanilla JS +await clerk.load({ experimental: { persistClient: false } }) +``` + diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index e2a9f8030d..21b1794e7b 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -35,9 +35,9 @@ const withEmailCodes = base .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); -const withEmailCodes_persist_client = withEmailCodes +const withEmailCodes_destroy_client = withEmailCodes .clone() - .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'true'); + .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'false'); const withEmailLinks = base .clone() @@ -91,7 +91,7 @@ const withDynamicKeys = withEmailCodes export const envs = { base, withEmailCodes, - withEmailCodes_persist_client, + withEmailCodes_destroy_client, withEmailLinks, withCustomRoles, withEmailCodesQuickstart, diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index 718b2915ca..eee5510608 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -19,14 +19,14 @@ export const createLongRunningApps = () => { const configs = [ { id: 'express.vite.withEmailCodes', config: express.vite, env: envs.withEmailCodes }, { id: 'react.vite.withEmailCodes', config: react.vite, env: envs.withEmailCodes }, - { id: 'react.vite.withEmailCodes_persist_client', config: react.vite, env: envs.withEmailCodes_persist_client }, + { id: 'react.vite.withEmailCodes_persist_client', config: react.vite, env: envs.withEmailCodes_destroy_client }, { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes }, { id: 'next.appRouter.withEmailCodes_persist_client', config: next.appRouter, - env: envs.withEmailCodes_persist_client, + env: envs.withEmailCodes_destroy_client, }, { id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles }, { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart }, diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index d73009ca87..b8b377146c 100644 --- a/integration/templates/next-app-router/src/app/layout.tsx +++ b/integration/templates/next-app-router/src/app/layout.tsx @@ -13,7 +13,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( diff --git a/integration/templates/react-vite/src/client-id.tsx b/integration/templates/react-vite/src/client-id.tsx index 49a76a45dc..88ccc8cf7c 100644 --- a/integration/templates/react-vite/src/client-id.tsx +++ b/integration/templates/react-vite/src/client-id.tsx @@ -1,5 +1,4 @@ import { useClerk, useSession } from '@clerk/clerk-react'; -import React from 'react'; export function ClientId() { const clerk = useClerk(); diff --git a/integration/templates/react-vite/src/main.tsx b/integration/templates/react-vite/src/main.tsx index 2841869bf4..21366dff08 100644 --- a/integration/templates/react-vite/src/main.tsx +++ b/integration/templates/react-vite/src/main.tsx @@ -21,7 +21,9 @@ const Root = () => { routerPush={(to: string) => navigate(to)} routerReplace={(to: string) => navigate(to, { replace: true })} experimental={{ - persistClient: import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT === 'true', + persistClient: import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT + ? import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT === 'true' + : undefined, }} > diff --git a/integration/tests/sign-out-smoke.test.ts b/integration/tests/sign-out-smoke.test.ts index 27381295be..129947774c 100644 --- a/integration/tests/sign-out-smoke.test.ts +++ b/integration/tests/sign-out-smoke.test.ts @@ -20,7 +20,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out await app.teardown(); }); - test('sign out throught all open tabs at once', async ({ page, context }) => { + test('sign out through all open tabs at once', async ({ page, context }) => { const mainTab = createTestUtils({ app, page, context }); await mainTab.po.signIn.goTo(); await mainTab.po.signIn.setIdentifier(fakeUser.email); @@ -46,7 +46,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out await mainTab.po.expect.toBeSignedOut(); }); - test('sign out destroying client', async ({ page, context }) => { + test('sign out persisting client', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.signIn.goTo(); await u.po.signIn.setIdentifier(fakeUser.email); @@ -55,21 +55,23 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out await u.po.signIn.continue(); await u.po.expect.toBeSignedIn(); await u.page.goToAppHome(); - - await u.page.waitForSelector('p[data-clerk-id]', { state: 'attached' }); + const client_id_element = await u.page.waitForSelector('p[data-clerk-id]', { state: 'attached' }); + const client_id = await client_id_element.innerHTML(); await u.page.evaluate(async () => { await window.Clerk.signOut(); }); await u.po.expect.toBeSignedOut(); - await u.page.waitForSelector('p[data-clerk-id]', { state: 'detached' }); await u.page.waitForSelector('p[data-clerk-session]', { state: 'detached' }); + + const client_id_after_sign_out = await u.page.locator('p[data-clerk-id]').innerHTML(); + expect(client_id).toEqual(client_id_after_sign_out); }); }); -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes_persist_client] })( - 'sign out with persistClient smoke test @generic', +testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes_destroy_client] })( + 'sign out with destroy client smoke test @generic', ({ app }) => { test.describe.configure({ mode: 'serial' }); @@ -86,7 +88,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes_persist_client await app.teardown(); }); - test('sign out persisting client', async ({ page, context }) => { + test('sign out destroying client', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.signIn.goTo(); await u.po.signIn.setIdentifier(fakeUser.email); @@ -95,18 +97,16 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes_persist_client await u.po.signIn.continue(); await u.po.expect.toBeSignedIn(); await u.page.goToAppHome(); - const client_id_element = await u.page.waitForSelector('p[data-clerk-id]', { state: 'attached' }); - const client_id = await client_id_element.innerHTML(); + + await u.page.waitForSelector('p[data-clerk-id]', { state: 'attached' }); await u.page.evaluate(async () => { await window.Clerk.signOut(); }); await u.po.expect.toBeSignedOut(); + await u.page.waitForSelector('p[data-clerk-id]', { state: 'detached' }); await u.page.waitForSelector('p[data-clerk-session]', { state: 'detached' }); - - const client_id_after_sign_out = await u.page.locator('p[data-clerk-id]').innerHTML(); - expect(client_id).toEqual(client_id_after_sign_out); }); }, ); diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 31da1d950d..bbea30c621 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -448,11 +448,13 @@ describe('Clerk singleton', () => { describe('.signOut()', () => { const mockClientDestroy = jest.fn(); + 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() }; beforeEach(() => { mockClientDestroy.mockReset(); + mockClientRemoveSessions.mockReset(); mockSession1.remove.mockReset(); mockSession2.remove.mockReset(); }); @@ -480,6 +482,7 @@ describe('Clerk singleton', () => { activeSessions: [mockSession1, mockSession2], sessions: [mockSession1, mockSession2], destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, }), ); @@ -488,7 +491,8 @@ describe('Clerk singleton', () => { await sut.load(); await sut.signOut(); await waitFor(() => { - expect(mockClientDestroy).toHaveBeenCalled(); + expect(mockClientDestroy).not.toHaveBeenCalled(); + expect(mockClientRemoveSessions).toHaveBeenCalled(); expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function), @@ -502,6 +506,7 @@ describe('Clerk singleton', () => { activeSessions: [mockSession1], sessions: [mockSession1], destroy: mockClientDestroy, + removeSessions: mockClientRemoveSessions, }), ); @@ -510,7 +515,8 @@ describe('Clerk singleton', () => { await sut.load(); await sut.signOut(); await waitFor(() => { - expect(mockClientDestroy).toHaveBeenCalled(); + expect(mockClientDestroy).not.toHaveBeenCalled(); + expect(mockClientRemoveSessions).toHaveBeenCalled(); expect(mockSession1.remove).not.toHaveBeenCalled(); expect(sut.setActive).toHaveBeenCalledWith({ session: null, beforeEmit: expect.any(Function) }); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index ddd5e93a50..a5d961e91c 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -331,7 +331,7 @@ export class Clerk implements ClerkInterface { const cb = typeof callbackOrOptions === 'function' ? callbackOrOptions : defaultCb; if (!opts.sessionId || this.client.activeSessions.length === 1) { - if (this.#options.experimental?.persistClient) { + if (this.#options.experimental?.persistClient ?? true) { await this.client.removeSessions(); } else { await this.client.destroy();