diff --git a/.changeset/expo-resource-cache-key-per-instance.md b/.changeset/expo-resource-cache-key-per-instance.md new file mode 100644 index 00000000000..09f8f5784a5 --- /dev/null +++ b/.changeset/expo-resource-cache-key-per-instance.md @@ -0,0 +1,5 @@ +--- +'@clerk/expo': patch +--- + +Fix the offline resource cache so an app that switches Clerk instances no longer starts offline with the previous instance's cached user and session. diff --git a/packages/expo/src/cache/ResourceCache.ts b/packages/expo/src/cache/ResourceCache.ts index ca5ddf8fb15..b2b0b51e56f 100644 --- a/packages/expo/src/cache/ResourceCache.ts +++ b/packages/expo/src/cache/ResourceCache.ts @@ -14,7 +14,7 @@ function createResourceCache(key: string): ResourceCache { if (!opts.storage || !opts.publishableKey) { throw new Error(`Clerk: ResourceCache for ${key} requires storage and publishableKey!`); } - itemKey = `${key}_${opts.publishableKey.slice(-5)}`; + itemKey = `${key}_${opts.publishableKey.split('=').join('')}`; storage = opts.storage(); }; diff --git a/packages/expo/src/cache/__tests__/ResourceCache.test.ts b/packages/expo/src/cache/__tests__/ResourceCache.test.ts new file mode 100644 index 00000000000..035282c0e80 --- /dev/null +++ b/packages/expo/src/cache/__tests__/ResourceCache.test.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, test } from 'vitest'; + +import type { IStorage } from '../../provider/singleton/types'; +import { SessionJWTCache } from '../ResourceCache'; + +const store = new Map(); +const storage = (): IStorage => ({ + get: key => Promise.resolve(store.get(key) ?? null), + set: (key, value) => { + store.set(key, value); + return Promise.resolve(); + }, +}); + +const buildKey = (frontendApi: string) => `pk_test_${btoa(`${frontendApi}$`)}`; + +beforeEach(() => { + store.clear(); +}); + +describe('ResourceCache', () => { + test('isolates entries between instances whose publishable keys share a suffix', async () => { + const first = buildKey('a.clerk.accounts.dev'); + const second = buildKey('foo-bar-12.clerk.accounts.dev'); + expect(first.slice(-5)).toBe(second.slice(-5)); + + SessionJWTCache.init({ publishableKey: first, storage }); + await SessionJWTCache.save('jwt-for-first'); + + SessionJWTCache.init({ publishableKey: second, storage }); + await expect(SessionJWTCache.load()).resolves.toBeNull(); + + SessionJWTCache.init({ publishableKey: first, storage }); + await expect(SessionJWTCache.load()).resolves.toBe('jwt-for-first'); + }); + + test('uses storage keys that expo-secure-store accepts', async () => { + const padded = buildKey('ab.clerk.accounts.dev'); + expect(padded.endsWith('=')).toBe(true); + + SessionJWTCache.init({ publishableKey: padded, storage }); + await SessionJWTCache.save('jwt'); + + for (const storedKey of store.keys()) { + expect(storedKey).toMatch(/^[\w.-]+$/); + } + }); +});