Skip to content

Commit 166f3ae

Browse files
committed
feat(repo): Add a library-agnostic getToken helper
1 parent 4da7668 commit 166f3ae

File tree

9 files changed

+488
-0
lines changed

9 files changed

+488
-0
lines changed

packages/astro/src/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { updateClerkOptions } from '../internal/create-clerk-instance';
22
export * from '../stores/external';
3+
export { getToken } from '@clerk/shared/getToken';

packages/nextjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export {
5959
useUser,
6060
} from './client-boundary/hooks';
6161

62+
export { getToken } from '@clerk/shared/getToken';
63+
6264
/**
6365
* Conditionally export components that exhibit different behavior
6466
* when used in /app vs /pages.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { createRouteMatcher } from './routeMatcher';
22
export { updateClerkOptions } from '@clerk/vue';
3+
export { getToken } from '@clerk/shared/getToken';

packages/react-router/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ if (typeof window !== 'undefined' && typeof (window as any).global === 'undefine
33
}
44

55
export * from './client';
6+
export { getToken } from '@clerk/shared/getToken';
67

78
// Override Clerk React error thrower to show that errors come from @clerk/react-router
89
import { setErrorThrowerOptions } from '@clerk/react/internal';

packages/react/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './components';
88
export * from './contexts';
99

1010
export * from './hooks';
11+
export { getToken } from '@clerk/shared/getToken';
1112
export type {
1213
BrowserClerk,
1314
BrowserClerkConstructor,
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { getToken } from '../getToken';
4+
5+
type StatusHandler = (status: string) => void;
6+
7+
describe('getToken', () => {
8+
const originalWindow = global.window;
9+
10+
beforeEach(() => {
11+
vi.useFakeTimers();
12+
});
13+
14+
afterEach(() => {
15+
vi.useRealTimers();
16+
vi.restoreAllMocks();
17+
global.window = originalWindow;
18+
});
19+
20+
describe('when Clerk is already ready', () => {
21+
it('should return token immediately', async () => {
22+
const mockToken = 'mock-jwt-token';
23+
const mockClerk = {
24+
status: 'ready',
25+
session: {
26+
getToken: vi.fn().mockResolvedValue(mockToken),
27+
},
28+
};
29+
30+
global.window = { Clerk: mockClerk } as any;
31+
32+
const token = await getToken();
33+
expect(token).toBe(mockToken);
34+
expect(mockClerk.session.getToken).toHaveBeenCalledWith(undefined);
35+
});
36+
37+
it('should pass options to session.getToken', async () => {
38+
const mockClerk = {
39+
status: 'ready',
40+
session: {
41+
getToken: vi.fn().mockResolvedValue('token'),
42+
},
43+
};
44+
45+
global.window = { Clerk: mockClerk } as any;
46+
47+
await getToken({ template: 'custom-template' });
48+
expect(mockClerk.session.getToken).toHaveBeenCalledWith({ template: 'custom-template' });
49+
});
50+
51+
it('should pass organizationId option to session.getToken', async () => {
52+
const mockClerk = {
53+
status: 'ready',
54+
session: {
55+
getToken: vi.fn().mockResolvedValue('token'),
56+
},
57+
};
58+
59+
global.window = { Clerk: mockClerk } as any;
60+
61+
await getToken({ organizationId: 'org_123' });
62+
expect(mockClerk.session.getToken).toHaveBeenCalledWith({ organizationId: 'org_123' });
63+
});
64+
});
65+
66+
describe('when Clerk is loading', () => {
67+
it('should wait for ready status via event listener', async () => {
68+
const mockToken = 'delayed-token';
69+
let statusHandler: StatusHandler | null = null;
70+
71+
const mockClerk = {
72+
status: 'loading' as string,
73+
on: vi.fn((event: string, handler: StatusHandler) => {
74+
if (event === 'status') {
75+
statusHandler = handler;
76+
}
77+
}),
78+
off: vi.fn(),
79+
session: {
80+
getToken: vi.fn().mockResolvedValue(mockToken),
81+
},
82+
};
83+
84+
global.window = { Clerk: mockClerk } as any;
85+
86+
const tokenPromise = getToken();
87+
88+
// Simulate Clerk becoming ready
89+
await vi.advanceTimersByTimeAsync(100);
90+
mockClerk.status = 'ready';
91+
if (statusHandler) {
92+
(statusHandler as StatusHandler)('ready');
93+
}
94+
95+
const token = await tokenPromise;
96+
expect(token).toBe(mockToken);
97+
});
98+
99+
it('should resolve when status changes to degraded', async () => {
100+
const mockToken = 'degraded-token';
101+
let statusHandler: StatusHandler | null = null;
102+
103+
const mockClerk = {
104+
status: 'loading' as string,
105+
on: vi.fn((event: string, handler: StatusHandler) => {
106+
if (event === 'status') {
107+
statusHandler = handler;
108+
}
109+
}),
110+
off: vi.fn(),
111+
session: {
112+
getToken: vi.fn().mockResolvedValue(mockToken),
113+
},
114+
};
115+
116+
global.window = { Clerk: mockClerk } as any;
117+
118+
const tokenPromise = getToken();
119+
120+
// Simulate Clerk becoming degraded
121+
await vi.advanceTimersByTimeAsync(100);
122+
mockClerk.status = 'degraded';
123+
if (statusHandler) {
124+
(statusHandler as StatusHandler)('degraded');
125+
}
126+
127+
const token = await tokenPromise;
128+
expect(token).toBe(mockToken);
129+
});
130+
});
131+
132+
describe('when window.Clerk does not exist', () => {
133+
it('should poll until Clerk is available', async () => {
134+
const mockToken = 'polled-token';
135+
136+
global.window = {} as any;
137+
138+
const tokenPromise = getToken();
139+
140+
// Simulate Clerk loading after 200ms
141+
await vi.advanceTimersByTimeAsync(200);
142+
143+
(global.window as any).Clerk = {
144+
status: 'ready',
145+
session: {
146+
getToken: vi.fn().mockResolvedValue(mockToken),
147+
},
148+
};
149+
150+
await vi.advanceTimersByTimeAsync(100);
151+
152+
const token = await tokenPromise;
153+
expect(token).toBe(mockToken);
154+
});
155+
156+
it('should timeout and return null if Clerk never loads', async () => {
157+
global.window = {} as any;
158+
159+
const tokenPromise = getToken();
160+
161+
// Fast-forward past timeout (10 seconds)
162+
await vi.advanceTimersByTimeAsync(15000);
163+
164+
const token = await tokenPromise;
165+
expect(token).toBeNull();
166+
});
167+
});
168+
169+
describe('when user is not signed in', () => {
170+
it('should return null when session is null', async () => {
171+
const mockClerk = {
172+
status: 'ready',
173+
session: null,
174+
};
175+
176+
global.window = { Clerk: mockClerk } as any;
177+
178+
const token = await getToken();
179+
expect(token).toBeNull();
180+
});
181+
182+
it('should return null when session is undefined', async () => {
183+
const mockClerk = {
184+
status: 'ready',
185+
session: undefined,
186+
};
187+
188+
global.window = { Clerk: mockClerk } as any;
189+
190+
const token = await getToken();
191+
expect(token).toBeNull();
192+
});
193+
});
194+
195+
describe('when Clerk status is degraded', () => {
196+
it('should still return token', async () => {
197+
const mockToken = 'degraded-token';
198+
const mockClerk = {
199+
status: 'degraded',
200+
session: {
201+
getToken: vi.fn().mockResolvedValue(mockToken),
202+
},
203+
};
204+
205+
global.window = { Clerk: mockClerk } as any;
206+
207+
const token = await getToken();
208+
expect(token).toBe(mockToken);
209+
});
210+
});
211+
212+
describe('in non-browser environment', () => {
213+
it('should return null when window is undefined', async () => {
214+
global.window = undefined as any;
215+
216+
const token = await getToken();
217+
expect(token).toBeNull();
218+
});
219+
});
220+
221+
describe('when Clerk enters error status', () => {
222+
it('should return null', async () => {
223+
let statusHandler: StatusHandler | null = null;
224+
225+
const mockClerk = {
226+
status: 'loading' as string,
227+
on: vi.fn((event: string, handler: StatusHandler) => {
228+
if (event === 'status') {
229+
statusHandler = handler;
230+
}
231+
}),
232+
off: vi.fn(),
233+
session: null,
234+
};
235+
236+
global.window = { Clerk: mockClerk } as any;
237+
238+
const tokenPromise = getToken();
239+
240+
// Simulate Clerk entering error state
241+
await vi.advanceTimersByTimeAsync(100);
242+
mockClerk.status = 'error';
243+
if (statusHandler) {
244+
(statusHandler as StatusHandler)('error');
245+
}
246+
247+
const token = await tokenPromise;
248+
expect(token).toBeNull();
249+
});
250+
});
251+
252+
describe('when session.getToken throws', () => {
253+
it('should return null and not propagate the error', async () => {
254+
const mockClerk = {
255+
status: 'ready',
256+
session: {
257+
getToken: vi.fn().mockRejectedValue(new Error('Token fetch failed')),
258+
},
259+
};
260+
261+
global.window = { Clerk: mockClerk } as any;
262+
263+
const token = await getToken();
264+
expect(token).toBeNull();
265+
});
266+
});
267+
268+
describe('fallback for older clerk-js versions', () => {
269+
it('should resolve when clerk.loaded is true but status is undefined', async () => {
270+
const mockToken = 'legacy-token';
271+
const mockClerk = {
272+
loaded: true,
273+
status: undefined,
274+
session: {
275+
getToken: vi.fn().mockResolvedValue(mockToken),
276+
},
277+
};
278+
279+
global.window = { Clerk: mockClerk } as any;
280+
281+
const token = await getToken();
282+
expect(token).toBe(mockToken);
283+
});
284+
});
285+
286+
describe('cleanup', () => {
287+
it('should unsubscribe from status listener on success', async () => {
288+
const mockToken = 'cleanup-token';
289+
let statusHandler: StatusHandler | null = null;
290+
291+
const mockClerk = {
292+
status: 'loading' as string,
293+
on: vi.fn((event: string, handler: StatusHandler) => {
294+
if (event === 'status') {
295+
statusHandler = handler;
296+
}
297+
}),
298+
off: vi.fn(),
299+
session: {
300+
getToken: vi.fn().mockResolvedValue(mockToken),
301+
},
302+
};
303+
304+
global.window = { Clerk: mockClerk } as any;
305+
306+
const tokenPromise = getToken();
307+
308+
await vi.advanceTimersByTimeAsync(50);
309+
mockClerk.status = 'ready';
310+
if (statusHandler) {
311+
(statusHandler as StatusHandler)('ready');
312+
}
313+
314+
await tokenPromise;
315+
316+
// Verify cleanup was called
317+
expect(mockClerk.off).toHaveBeenCalledWith('status', statusHandler);
318+
});
319+
});
320+
});

0 commit comments

Comments
 (0)