Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-expo-token-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": patch
---

Fix persisted session restoration when the native Clerk singleton is created before `ClerkProvider` receives the app's token cache.
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,75 @@ describe('createClerkInstance', () => {

await expect(tokenCache.getToken(CLERK_CLIENT_JWT_KEY)).resolves.toBe('fresh-token');
});

test('uses the latest explicit tokenCache for request authorization when the singleton is reused', async () => {
const initialTokenCache: TokenCache = {
getToken: vi.fn(() => Promise.resolve(null)),
saveToken: vi.fn(() => Promise.resolve()),
};
const latestTokenCache: TokenCache = {
getToken: vi.fn(() => Promise.resolve('cached-token')),
saveToken: vi.fn(() => Promise.resolve()),
};

const createClerkInstance = await loadCreateClerkInstance();
const getClerkInstance = createClerkInstance(MockClerk as unknown as typeof Clerk);
const clerk = getClerkInstance({
publishableKey: 'pk_test_123',
tokenCache: initialTokenCache,
}) as unknown as MockClerk;

getClerkInstance({
publishableKey: 'pk_test_123',
tokenCache: latestTokenCache,
});
getClerkInstance();

const beforeRequest = clerk.__internal_onBeforeRequest.mock.calls[0][0];
const requestInit = {
headers: new Headers(),
url: new URL('https://clerk.example.com/v1/client'),
};
await beforeRequest(requestInit);

expect(requestInit.headers.get('authorization')).toBe('cached-token');
});

test('uses the latest explicit tokenCache for response authorization when the singleton is reused', async () => {
const initialTokenCache: TokenCache = {
getToken: vi.fn(() => Promise.resolve(null)),
saveToken: vi.fn(() => Promise.resolve()),
};
const latestTokenCache: TokenCache = {
getToken: vi.fn(() => Promise.resolve(null)),
saveToken: vi.fn(() => Promise.resolve()),
};

const createClerkInstance = await loadCreateClerkInstance();
const getClerkInstance = createClerkInstance(MockClerk as unknown as typeof Clerk);
const clerk = getClerkInstance({
publishableKey: 'pk_test_123',
tokenCache: initialTokenCache,
}) as unknown as MockClerk;

getClerkInstance({
publishableKey: 'pk_test_123',
tokenCache: latestTokenCache,
});

const afterResponse = clerk.__internal_onAfterResponse.mock.calls[0][0];
await afterResponse(
{
headers: new Headers(),
url: new URL('https://clerk.example.com/v1/client'),
},
{
headers: new Headers({ authorization: 'fresh-token' }),
payload: null,
},
);

expect(initialTokenCache.saveToken).not.toHaveBeenCalled();
expect(latestTokenCache.saveToken).toHaveBeenCalledWith(CLERK_CLIENT_JWT_KEY, 'fresh-token');
});
});
17 changes: 13 additions & 4 deletions packages/expo/src/provider/singleton/createClerkInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SessionJWTCache,
} from '../../cache';
import { MemoryTokenCache } from '../../cache/MemoryTokenCache';
import type { TokenCache } from '../../cache/types';
import { CLERK_CLIENT_JWT_KEY } from '../../constants';
import { errorThrower } from '../../errorThrower';
import { assertValidProxyUrl, isNative } from '../../utils';
Expand Down Expand Up @@ -51,6 +52,7 @@ function hasOwnOption<Key extends keyof ClerkRuntimeOptions>(

let __internal_clerk: HeadlessBrowserClerk | BrowserClerk | undefined;
let __internal_clerkOptions: ClerkRuntimeOptions | undefined;
let __internal_tokenCache: TokenCache = MemoryTokenCache;

/**
* Resolves the next native singleton config while preserving existing values for omitted options.
Expand Down Expand Up @@ -89,7 +91,9 @@ function getUpdatedClerkOptions(

export function createClerkInstance(ClerkClass: typeof Clerk) {
return (options?: BuildClerkOptions): HeadlessBrowserClerk | BrowserClerk => {
const { tokenCache = MemoryTokenCache, __experimental_resourceCache: createResourceCache } = options || {};
const { __experimental_resourceCache: createResourceCache } = options || {};
const hasNextTokenCache = !!options && Object.prototype.hasOwnProperty.call(options, 'tokenCache');
const nextTokenCache = hasNextTokenCache ? (options.tokenCache ?? MemoryTokenCache) : undefined;
const {
hasConfigChanged,
options: { publishableKey, proxyUrl, domain },
Expand All @@ -99,15 +103,19 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
errorThrower.throwMissingPublishableKeyError();
}

if (nextTokenCache) {
__internal_tokenCache = nextTokenCache;
}

if (!__internal_clerk || hasConfigChanged) {
assertValidProxyUrl(proxyUrl);

if (hasConfigChanged) {
tokenCache.clearToken?.(CLERK_CLIENT_JWT_KEY);
void __internal_tokenCache.clearToken?.(CLERK_CLIENT_JWT_KEY);
}

const getToken = (key: string) => tokenCache.getToken(key);
const saveToken = (key: string, token: string) => tokenCache.saveToken(key, token);
const getToken = (key: string) => __internal_tokenCache.getToken(key);
const saveToken = (key: string, token: string) => __internal_tokenCache.saveToken(key, token);

__internal_clerkOptions = { publishableKey, proxyUrl, domain };
__internal_clerk = new ClerkClass(publishableKey, { proxyUrl, domain }) as unknown as BrowserClerk;
Expand Down Expand Up @@ -244,6 +252,7 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
}
});
}

// At this point __internal_clerk is guaranteed to be defined
return __internal_clerk;
};
Expand Down
Loading