Skip to content

Commit

Permalink
chore(clerk-js): Remove sessions instead of destroying the client (#3941
Browse files Browse the repository at this point in the history
)
  • Loading branch information
panteliselef authored Sep 3, 2024
1 parent 3d23bdb commit e95c281
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 4 deletions.
15 changes: 15 additions & 0 deletions .changeset/funny-monkeys-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@clerk/types": minor
"@clerk/clerk-js": minor
---

**Experimental:** Persist the Clerk client after signing out a user.
This allows for matching a user's device with a client. To try out this new feature, enable it in your `<ClerkProvider />` or `clerk.load()` call.

```js
// React
<ClerkProvider experimental={{ persistClient: true }} />

// Vanilla JS
await clerk.load({ experimental: { persistClient: true } })
```
5 changes: 5 additions & 0 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ 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
.clone()
.setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'true');

const withEmailLinks = base
.clone()
.setId('withEmailLinks')
Expand Down Expand Up @@ -87,6 +91,7 @@ const withDynamicKeys = withEmailCodes
export const envs = {
base,
withEmailCodes,
withEmailCodes_persist_client,
withEmailLinks,
withCustomRoles,
withEmailCodesQuickstart,
Expand Down
6 changes: 6 additions & 0 deletions integration/presets/longRunningApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ 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.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,
},
{ id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles },
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
Expand Down
15 changes: 15 additions & 0 deletions integration/templates/next-app-router/src/app/client-id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';
import { useClerk, useSession } from '@clerk/nextjs';
import React from 'react';

export function ClientId() {
const clerk = useClerk();
// For re-rendering
useSession();
return (
<>
{clerk?.client?.id && <p data-clerk-id>{clerk?.client?.id}</p>}
{clerk?.client?.lastActiveSessionId && <p data-clerk-session>{clerk?.client?.lastActiveSessionId}</p>}
</>
);
}
6 changes: 5 additions & 1 deletion integration/templates/next-app-router/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export const metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<ClerkProvider
experimental={{
persistClient: process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT === 'true',
}}
>
<html lang='en'>
<body className={inter.className}>{children}</body>
</html>
Expand Down
2 changes: 2 additions & 0 deletions integration/templates/next-app-router/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { SignedIn, SignedOut, SignIn, UserButton, Protect } from '@clerk/nextjs';
import Link from 'next/link';
import { ClientId } from './client-id';

export default function Home() {
return (
<main>
<UserButton />
<ClientId />
<SignedIn>SignedIn</SignedIn>
<SignedOut>SignedOut</SignedOut>
<Protect fallback={'SignedOut from protect'}>SignedIn from protect</Protect>
Expand Down
2 changes: 2 additions & 0 deletions integration/templates/react-vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/clerk-react';
import React from 'react';
import { ClientId } from './client-id';

function App() {
return (
<main>
<UserButton afterSignOutUrl={'/'} />
<OrganizationSwitcher />
<ClientId />
<SignedOut>SignedOut</SignedOut>
<SignedIn>SignedIn</SignedIn>
</main>
Expand Down
14 changes: 14 additions & 0 deletions integration/templates/react-vite/src/client-id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useClerk, useSession } from '@clerk/clerk-react';
import React from 'react';

export function ClientId() {
const clerk = useClerk();
// For re-rendering
useSession();
return (
<>
{clerk?.client?.id && <p data-clerk-id>{clerk?.client?.id}</p>}
{clerk?.client?.lastActiveSessionId && <p data-clerk-session>{clerk?.client?.lastActiveSessionId}</p>}
</>
);
}
3 changes: 3 additions & 0 deletions integration/templates/react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const Root = () => {
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string}
routerPush={(to: string) => navigate(to)}
routerReplace={(to: string) => navigate(to, { replace: true })}
experimental={{
persistClient: import.meta.env.VITE_EXPERIMENTAL_PERSIST_CLIENT === 'true',
}}
>
<Outlet />
</ClerkProvider>
Expand Down
66 changes: 65 additions & 1 deletion integration/tests/sign-out-smoke.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
Expand Down Expand Up @@ -45,4 +45,68 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out

await mainTab.po.expect.toBeSignedOut();
});

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);
await u.po.signIn.continue();
await u.po.signIn.setPassword(fakeUser.password);
await u.po.signIn.continue();
await u.po.expect.toBeSignedIn();
await u.page.goToAppHome();

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

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes_persist_client] })(
'sign out with persistClient smoke test @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

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);
await u.po.signIn.continue();
await u.po.signIn.setPassword(fakeUser.password);
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.evaluate(async () => {
await window.Clerk.signOut();
});

await u.po.expect.toBeSignedOut();
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);
});
},
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"test:integration:deployment:nextjs": "npx playwright test --config integration/playwright.deployments.config.ts",
"test:integration:elements": "E2E_APP_ID=elements.* npm run test:integration:base -- --grep @elements",
"test:integration:express": "E2E_APP_ID=express.* npm run test:integration:base -- --grep @express",
"test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes npm run test:integration:base -- --grep @generic",
"test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* npm run test:integration:base -- --grep @generic",
"test:integration:nextjs": "E2E_APP_ID=next.appRouter.* npm run test:integration:base -- --grep @nextjs",
"test:integration:astro": "E2E_APP_ID=astro.* npm run test:integration:base -- --grep @astro",
"test:integration:expo-web": "E2E_APP_ID=expo.expo-web npm run test:integration:base -- --grep @expo-web",
Expand Down
7 changes: 6 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,12 @@ export class Clerk implements ClerkInterface {
const cb = typeof callbackOrOptions === 'function' ? callbackOrOptions : defaultCb;

if (!opts.sessionId || this.client.activeSessions.length === 1) {
await this.client.destroy();
if (this.#options.experimental?.persistClient) {
await this.client.removeSessions();
} else {
await this.client.destroy();
}

return this.setActive({
session: null,
beforeEmit: ignoreEventValue(cb),
Expand Down
7 changes: 7 additions & 0 deletions packages/clerk-js/src/core/resources/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class Client extends BaseResource implements ClientResource {
// TODO: Make it restful by introducing a DELETE /client/:id endpoint
return this._baseDelete({ path: '/client' }).then(() => {
SessionTokenCache.clear();
this.id = '';
this.sessions = [];
this.signUp = new SignUp(null);
this.signIn = new SignIn(null);
Expand All @@ -65,6 +66,12 @@ export class Client extends BaseResource implements ClientResource {
});
}

removeSessions(): Promise<ClientResource> {
return this._baseDelete({
path: this.path() + '/sessions',
}) as unknown as Promise<ClientResource>;
}

clearCache(): void {
return this.sessions.forEach(s => s.clearCache());
}
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,16 @@ export type ClerkOptions = ClerkOptionsNavigation &
};

sdkMetadata?: SDKMetadata;

/**
* Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions.
*/
experimental?: Autocomplete<
{
persistClient: boolean;
},
Record<string, any>
>;
};

export interface NavigateOptions {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ClientResource extends ClerkResource {
isNew: () => boolean;
create: () => Promise<ClientResource>;
destroy: () => Promise<void>;
removeSessions: () => Promise<ClientResource>;
clearCache: () => void;
lastActiveSessionId: string | null;
createdAt: Date | null;
Expand Down

0 comments on commit e95c281

Please sign in to comment.