Skip to content

Commit 6b05308

Browse files
authored
feat(chrome-extension): Add onChange cookie listeners (clerk#4962)
1 parent bdcc1d5 commit 6b05308

File tree

11 files changed

+150
-7202
lines changed

11 files changed

+150
-7202
lines changed

.changeset/green-bikes-end.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/chrome-extension': minor
3+
---
4+
5+
Add experimental support for listening to cookie changes on a synced host via the `__experimental_syncHostListener`

packages/chrome-extension/src/internal/clerk.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import browser from 'webextension-polyfill';
66
import { SCOPE, type Scope } from '../types';
77
import { CLIENT_JWT_KEY, DEFAULT_LOCAL_HOST_PERMISSION } from './constants';
88
import { assertPublishableKey } from './utils/errors';
9+
import type { JWTHandlerParams } from './utils/jwt-handler';
910
import { JWTHandler } from './utils/jwt-handler';
1011
import { validateManifest } from './utils/manifest';
1112
import { requestHandler } from './utils/request-handler';
@@ -20,13 +21,15 @@ Clerk.sdkMetadata = {
2021
};
2122

2223
export type CreateClerkClientOptions = {
24+
__experimental_syncHostListener?: boolean;
2325
publishableKey: string;
2426
scope?: Scope;
2527
storageCache?: StorageCache;
2628
syncHost?: string;
2729
};
2830

2931
export async function createClerkClient({
32+
__experimental_syncHostListener = false,
3033
publishableKey,
3134
scope,
3235
storageCache = BrowserStorageCache,
@@ -60,17 +63,35 @@ export async function createClerkClient({
6063
// Set up JWT handler and attempt to get JWT from storage on initialization
6164
const url = syncHost ? syncHost : DEFAULT_LOCAL_HOST_PERMISSION;
6265

63-
const jwtOptions = {
66+
// Create Clerk instance
67+
clerk = new Clerk(publishableKey);
68+
69+
// @ts-expect-error - TODO: sync is evaluating to true vs boolean
70+
const jwtOptions: JWTHandlerParams = {
6471
frontendApi: key.frontendApi,
6572
name: isProd ? CLIENT_JWT_KEY : DEV_BROWSER_JWT_KEY,
66-
sync,
6773
url,
74+
sync: sync,
6875
};
6976

77+
if (jwtOptions.sync && __experimental_syncHostListener) {
78+
jwtOptions.onListenerCallback = () => {
79+
if (clerk.user) {
80+
clerk.user.reload();
81+
} else {
82+
window.location.reload();
83+
}
84+
};
85+
}
86+
7087
const jwt = JWTHandler(storageCache, jwtOptions);
7188

72-
// Create Clerk instance
73-
clerk = new Clerk(publishableKey);
89+
// Add listener to sync host cookies if enabled
90+
if (jwtOptions.sync && __experimental_syncHostListener) {
91+
const listener = jwt.listener();
92+
listener?.add();
93+
}
94+
7495
clerk.__unstable__onAfterResponse(responseHandler(jwt, { isProd }));
7596
clerk.__unstable__onBeforeRequest(requestHandler(jwt, { isProd }));
7697

packages/chrome-extension/src/internal/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export const AUTH_HEADER = {
44
};
55

66
export const CLIENT_JWT_KEY = '__client';
7+
export const CLIENT_UAT_KEY = '__clerk_uat';
78
export const DEFAULT_LOCAL_HOST_PERMISSION = 'http://localhost';
89
export const STORAGE_KEY_CLIENT_JWT = '__clerk_client_jwt';

packages/chrome-extension/src/internal/utils/cookies.ts

+26
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import browser from 'webextension-polyfill';
22

33
export type FormattedUrl = `http${string}`;
4+
45
export type GetClientCookieParams = {
56
name: string;
67
url: string;
8+
callback: (changeInfo: ChangeInfo) => Promise<void>;
9+
onListenerCallback?: () => void;
10+
};
11+
12+
export type ChangeInfo = {
13+
cookie: browser.Cookies.Cookie;
14+
cause: browser.Cookies.OnChangedCause;
15+
removed: boolean;
716
};
817

918
function ensureFormattedUrl(url: string): FormattedUrl {
@@ -13,3 +22,20 @@ function ensureFormattedUrl(url: string): FormattedUrl {
1322
export async function getClientCookie({ url, name }: GetClientCookieParams) {
1423
return await browser.cookies.get({ name, url: ensureFormattedUrl(url) });
1524
}
25+
26+
export function createClientCookieListener({ url, name, callback }: GetClientCookieParams) {
27+
const domain = new URL(url).hostname;
28+
const cookieDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
29+
30+
const listener = (changeInfo: ChangeInfo) => {
31+
if (changeInfo.cookie.domain === cookieDomain && changeInfo.cookie.name === name) {
32+
void callback(changeInfo);
33+
}
34+
};
35+
36+
return {
37+
add: () => browser.cookies.onChanged.addListener(listener),
38+
has: () => browser.cookies.onChanged.hasListener(listener),
39+
remove: () => browser.cookies.onChanged.removeListener(listener),
40+
};
41+
}

packages/chrome-extension/src/internal/utils/jwt-handler.ts

+28-19
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { Poller } from '@clerk/shared/poller';
2-
3-
import { STORAGE_KEY_CLIENT_JWT } from '../constants';
4-
import type { GetClientCookieParams } from './cookies';
5-
import { getClientCookie } from './cookies';
1+
import { CLIENT_UAT_KEY, STORAGE_KEY_CLIENT_JWT } from '../constants';
2+
import type { ChangeInfo, GetClientCookieParams } from './cookies';
3+
import { createClientCookieListener, getClientCookie } from './cookies';
64
import { errorLogger } from './errors';
75
import type { StorageCache } from './storage';
86

9-
type JWTHandlerParams = { frontendApi: string } & (
7+
export type JWTHandlerParams = { frontendApi: string } & (
108
| {
119
sync?: false;
1210
}
@@ -59,22 +57,33 @@ export function JWTHandler(store: StorageCache, params: JWTHandlerParams) {
5957
return await store.get<string>(CACHE_KEY);
6058
};
6159

62-
/**
63-
* Polls for the synced session JWT via the get() function.
64-
*
65-
* @param delayInMs: Polling delay in milliseconds (default: 1500ms)
66-
*/
67-
const poll = async (delayInMs = 1500) => {
68-
const { run, stop } = Poller({ delayInMs });
60+
const listener = () => {
61+
if (!shouldSync(sync, cookieParams)) {
62+
return;
63+
}
6964

70-
void run(async () => {
71-
const currentJWT = await get();
65+
const { onListenerCallback, ...restCookieParams } = cookieParams;
7266

73-
if (currentJWT) {
74-
stop();
75-
}
67+
return createClientCookieListener({
68+
...restCookieParams,
69+
callback: async (changeInfo: ChangeInfo) => {
70+
const existingJWT = await get();
71+
72+
if (existingJWT === changeInfo.cookie.value) {
73+
const syncedUAT = await getClientCookie({ ...restCookieParams, name: CLIENT_UAT_KEY }).catch(errorLogger);
74+
75+
if (!syncedUAT || syncedUAT?.value === '0') {
76+
onListenerCallback?.();
77+
}
78+
79+
return;
80+
}
81+
82+
await set(changeInfo.cookie.value);
83+
onListenerCallback?.();
84+
},
7685
});
7786
};
7887

79-
return { get, poll, set, remove };
88+
return { get, listener, set, remove };
8089
}

packages/chrome-extension/src/react/ClerkProvider.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@ import { createClerkClient } from '../internal/clerk';
77
import type { StorageCache } from '../internal/utils/storage';
88

99
type ChromeExtensionClerkProviderProps = ClerkReactProviderProps & {
10+
/**
11+
* @experimental
12+
* @description Enables the listener to sync host cookies on changes.
13+
*/
14+
__experimental_syncHostListener?: boolean;
1015
storageCache?: StorageCache;
1116
syncHost?: string;
1217
};
1318

1419
export function ClerkProvider(props: ChromeExtensionClerkProviderProps): JSX.Element | null {
15-
const { children, storageCache, syncHost, ...rest } = props;
20+
const { children, storageCache, syncHost, __experimental_syncHostListener, ...rest } = props;
1621
const { publishableKey = '' } = props;
1722

1823
const [clerkInstance, setClerkInstance] = React.useState<Clerk | null>(null);
1924

2025
React.useEffect(() => {
2126
void (async () => {
22-
setClerkInstance(await createClerkClient({ publishableKey, storageCache, syncHost }));
27+
setClerkInstance(
28+
await createClerkClient({ publishableKey, storageCache, syncHost, __experimental_syncHostListener }),
29+
);
2330
})();
24-
}, [publishableKey, storageCache, syncHost]);
31+
}, [publishableKey, storageCache, syncHost, __experimental_syncHostListener]);
2532

2633
if (!clerkInstance) {
2734
return null;

playground/browser-extension/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ After following the quickstart you'll have learned how to:
2626
- Load your Chrome Extension into your Chromium-based browser
2727
- Test your Chrome Extension
2828

29+
30+
## Connect with developement package
31+
32+
Run the development server:
33+
```bash
34+
> cd playground/browser-extension
35+
> npm i
36+
> npm dev
37+
```
38+
39+
In a separate terminal, build and publish the development package to your local registry:
40+
```bash
41+
> cd packages/chrome-extension
42+
> pnpm build && pnpm publish:local
43+
```
44+
2945
## Running the template
3046

3147
```bash

playground/browser-extension/package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"name": "chrome-extension",
2+
"name": "browser-extension",
3+
"private": true,
34
"displayName": "Clerk Chrome Extension Demo",
45
"version": "0.0.1",
56
"description": "A feature rich implementation of Clerk with a Plasmo-based Chrome Extension.",
@@ -11,10 +12,11 @@
1112
"debug:firefox": "plasmo dev --target=firefox-mv2 --verbose",
1213
"dev": "plasmo dev",
1314
"dev:firefox": "plasmo dev --target=firefox-mv2",
14-
"start:firefox": "web-ext run --source-dir ./build/firefox-mv2-dev"
15+
"start:firefox": "web-ext run --source-dir ./build/firefox-mv2-dev",
16+
"yalc:add": "pnpm yalc add @clerk/chrome-extension"
1517
},
1618
"dependencies": {
17-
"@clerk/chrome-extension": "file:../../packages/chrome-extension",
19+
"@clerk/chrome-extension": "file:.yalc/@clerk/chrome-extension",
1820
"plasmo": "0.89.4",
1921
"react": "^18.3.1",
2022
"react-dom": "^18.3.1",
@@ -41,6 +43,7 @@
4143
],
4244
"host_permissions": [
4345
"$CLERK_FRONTEND_API/*",
46+
"$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
4447
"http://localhost/*"
4548
],
4649
"key": "$CRX_PUBLIC_KEY"

0 commit comments

Comments
 (0)