Skip to content

Commit 1b938fa

Browse files
committed
feat(electron): Add initial React integration export (#8806)
1 parent 74eb5c3 commit 1b938fa

8 files changed

Lines changed: 246 additions & 7 deletions

File tree

‎packages/electron/README.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,29 @@
3131
> [!WARNING]
3232
> `@clerk/electron` is under active development and is not yet ready for production use. The API is incomplete and subject to change.
3333
34-
This package exposes two entrypoints, targeting Electron's distinct runtime contexts:
34+
This package exposes entrypoints for Electron's distinct runtime contexts:
3535

3636
- `@clerk/electron` — for use in the Electron **main** process.
3737
- `@clerk/electron/preload` — for use in Electron **preload** scripts.
38+
- `@clerk/electron/react` — for use in the Electron **renderer** process.
39+
- `@clerk/electron/storage` — default token storage backed by `electron-store`.
40+
41+
```ts
42+
// main.ts
43+
import { setupMain } from '@clerk/electron';
44+
import { storage } from '@clerk/electron/storage';
45+
46+
setupMain({
47+
storage: storage(),
48+
});
49+
```
50+
51+
```tsx
52+
// renderer.tsx
53+
import { ClerkProvider } from '@clerk/electron/react';
54+
55+
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>{/* ... */}</ClerkProvider>;
56+
```
3857

3958
## Support
4059

‎packages/electron/package.json‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@
5454
"default": "./dist/cjs/storage/index.js"
5555
}
5656
},
57+
"./react": {
58+
"import": {
59+
"types": "./dist/types/react/index.d.ts",
60+
"default": "./dist/esm/react/index.js"
61+
},
62+
"require": {
63+
"types": "./dist/types/react/index.d.ts",
64+
"default": "./dist/cjs/react/index.js"
65+
}
66+
},
5767
"./package.json": "./package.json"
5868
},
5969
"main": "./dist/cjs/index.js",
@@ -78,6 +88,9 @@
7888
"test:watch": "vitest"
7989
},
8090
"dependencies": {
91+
"@clerk/clerk-js": "workspace:^",
92+
"@clerk/react": "workspace:^",
93+
"@clerk/ui": "workspace:^",
8194
"tslib": "catalog:repo"
8295
},
8396
"devDependencies": {
@@ -87,11 +100,16 @@
87100
},
88101
"peerDependencies": {
89102
"electron": ">=28",
90-
"electron-store": "^8.2.0"
103+
"electron-store": "^8.2.0",
104+
"react": "catalog:peer-react",
105+
"react-dom": "catalog:peer-react"
91106
},
92107
"peerDependenciesMeta": {
93108
"electron-store": {
94109
"optional": true
110+
},
111+
"react-dom": {
112+
"optional": true
95113
}
96114
},
97115
"engines": {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { renderToStaticMarkup } from 'react-dom/server';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { ClerkProvider } from '../index';
5+
6+
let capturedProviderProps: Record<string, unknown> | null = null;
7+
let beforeRequest:
8+
| ((request: { credentials?: RequestCredentials; headers?: Headers; url?: URL }) => Promise<void>)
9+
| null = null;
10+
let afterResponse: ((request: unknown, response: Response) => Promise<void>) | null = null;
11+
12+
const clerkConstructor = vi.hoisted(() => vi.fn());
13+
14+
vi.mock('@clerk/clerk-js', () => ({
15+
Clerk: class MockClerk {
16+
constructor(publishableKey: string) {
17+
clerkConstructor(publishableKey);
18+
}
19+
20+
__internal_onBeforeRequest(cb: typeof beforeRequest) {
21+
beforeRequest = cb;
22+
}
23+
24+
__internal_onAfterResponse(cb: typeof afterResponse) {
25+
afterResponse = cb;
26+
}
27+
},
28+
}));
29+
30+
vi.mock('@clerk/react/internal', () => ({
31+
InternalClerkProvider: (props: Record<string, unknown>) => {
32+
capturedProviderProps = props;
33+
return <div>{props.children as React.ReactNode}</div>;
34+
},
35+
}));
36+
37+
vi.mock('@clerk/ui', () => ({
38+
ui: { ClerkUI: 'mock-ui' },
39+
}));
40+
41+
describe('Electron ClerkProvider', () => {
42+
const tokenCache = {
43+
clearToken: vi.fn(),
44+
getToken: vi.fn(),
45+
saveToken: vi.fn(),
46+
};
47+
48+
beforeEach(() => {
49+
vi.clearAllMocks();
50+
capturedProviderProps = null;
51+
beforeRequest = null;
52+
afterResponse = null;
53+
vi.stubGlobal('window', {
54+
__clerk_internal_electron: {
55+
tokenCache,
56+
},
57+
});
58+
});
59+
60+
it('renders React ClerkProvider with Electron defaults', () => {
61+
renderToStaticMarkup(
62+
<ClerkProvider
63+
publishableKey='pk_test_provider'
64+
signInUrl='/sign-in'
65+
>
66+
<span>App</span>
67+
</ClerkProvider>,
68+
);
69+
70+
expect(clerkConstructor).toHaveBeenCalledWith('pk_test_provider');
71+
expect(capturedProviderProps).toMatchObject({
72+
publishableKey: 'pk_test_provider',
73+
signInUrl: '/sign-in',
74+
standardBrowser: false,
75+
ui: { ClerkUI: 'mock-ui' },
76+
});
77+
expect(capturedProviderProps?.Clerk).toBeDefined();
78+
expect(capturedProviderProps?.__internal_nativeOAuthHandler).toBeUndefined();
79+
});
80+
81+
it('adds native request params and Authorization from the Electron token cache', async () => {
82+
tokenCache.getToken.mockResolvedValue('client-jwt');
83+
renderToStaticMarkup(<ClerkProvider publishableKey='pk_test_before_request'>App</ClerkProvider>);
84+
85+
const request = {
86+
headers: new Headers(),
87+
url: new URL('https://api.clerk.test/v1/client'),
88+
};
89+
await beforeRequest?.(request);
90+
91+
expect(request.credentials).toBe('omit');
92+
expect(request.url.searchParams.get('_is_native')).toBe('1');
93+
expect(request.headers.get('Authorization')).toBe('Bearer client-jwt');
94+
expect(tokenCache.getToken).toHaveBeenCalledWith('__clerk_client_jwt');
95+
});
96+
97+
it('stores Authorization response headers in the Electron token cache', async () => {
98+
renderToStaticMarkup(<ClerkProvider publishableKey='pk_test_after_response'>App</ClerkProvider>);
99+
100+
await afterResponse?.(
101+
{},
102+
new Response(null, {
103+
headers: {
104+
Authorization: 'Bearer updated-client-jwt',
105+
},
106+
}),
107+
);
108+
109+
expect(tokenCache.saveToken).toHaveBeenCalledWith('__clerk_client_jwt', 'updated-client-jwt');
110+
});
111+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Clerk } from '@clerk/clerk-js';
2+
3+
const CLERK_CLIENT_JWT_KEY = '__clerk_client_jwt';
4+
5+
type ClerkInstance = InstanceType<typeof Clerk>;
6+
7+
let cached: { instance: ClerkInstance; publishableKey: string } | null = null;
8+
9+
export function createClerkInstance(publishableKey: string): ClerkInstance {
10+
if (cached?.publishableKey === publishableKey) {
11+
return cached.instance;
12+
}
13+
14+
const clerk = new Clerk(publishableKey);
15+
16+
clerk.__internal_onBeforeRequest(async request => {
17+
request.credentials = 'omit';
18+
request.url?.searchParams.append('_is_native', '1');
19+
20+
const token = await window.__clerk_internal_electron?.tokenCache.getToken(CLERK_CLIENT_JWT_KEY);
21+
if (token) {
22+
const headers = new Headers(request.headers);
23+
headers.set('Authorization', `Bearer ${token}`);
24+
request.headers = headers;
25+
}
26+
});
27+
28+
clerk.__internal_onAfterResponse(async (_request, response) => {
29+
const authorization = (response as Response).headers.get('Authorization');
30+
if (!authorization) {
31+
return;
32+
}
33+
34+
const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : authorization;
35+
await window.__clerk_internal_electron?.tokenCache.saveToken(CLERK_CLIENT_JWT_KEY, token);
36+
});
37+
38+
cached = { instance: clerk, publishableKey };
39+
return clerk;
40+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { ClerkProviderProps as ReactClerkProviderProps } from '@clerk/react';
2+
import { InternalClerkProvider as ReactClerkProvider } from '@clerk/react/internal';
3+
import { ui } from '@clerk/ui';
4+
import type { ReactNode } from 'react';
5+
6+
import { createClerkInstance } from './create-clerk-instance';
7+
8+
export type ClerkProviderProps = Omit<
9+
ReactClerkProviderProps,
10+
'Clerk' | 'children' | 'publishableKey' | 'standardBrowser' | 'ui'
11+
> & {
12+
children: ReactNode;
13+
/**
14+
* Your Clerk publishable key, available in the Clerk Dashboard.
15+
*/
16+
publishableKey: string;
17+
};
18+
19+
export function ClerkProvider({ children, publishableKey, ...props }: ClerkProviderProps): JSX.Element {
20+
const clerk = createClerkInstance(publishableKey);
21+
22+
return (
23+
<ReactClerkProvider
24+
{...props}
25+
Clerk={clerk}
26+
publishableKey={publishableKey}
27+
standardBrowser={false}
28+
ui={ui}
29+
>
30+
{children}
31+
</ReactClerkProvider>
32+
);
33+
}
34+
35+
export * from '@clerk/react';

‎packages/electron/tsconfig.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
"declarationMap": true,
2020
"emitDeclarationOnly": true
2121
},
22-
"include": ["src"]
22+
"include": ["src"],
23+
"exclude": ["src/**/__tests__/**"]
2324
}

‎packages/electron/tsup.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineConfig(overrideOptions => {
99
const shouldPublish = !!overrideOptions.env?.publish;
1010

1111
const common: Options = {
12-
entry: ['./src/index.ts', './src/preload/index.ts', './src/storage/index.ts'],
12+
entry: ['./src/index.ts', './src/preload/index.ts', './src/react/index.tsx', './src/storage/index.ts'],
1313
bundle: true,
1414
clean: true,
1515
minify: false,

‎pnpm-lock.yaml‎

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)