diff --git a/.changeset/expo-refresh-native-after-pull.md b/.changeset/expo-refresh-native-after-pull.md new file mode 100644 index 00000000000..88a650ea5fe --- /dev/null +++ b/.changeset/expo-refresh-native-after-pull.md @@ -0,0 +1,5 @@ +--- +'@clerk/expo': patch +--- + +Fix native components such as `UserButton` occasionally staying signed out after a JS sign-in. When the sign-in finished while the SDK was syncing a change from the native SDK, the native SDK was never refreshed with the new session. diff --git a/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx b/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx index 19404694f4c..18b923bdfc6 100644 --- a/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx +++ b/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx @@ -213,7 +213,7 @@ describe('native client sync behavior', () => { await expectConverged(h); expect(server.requestCount('js', 'GET /client') - jsClientFetchesBefore).toBe(1); - expect(h.native.clientRefreshCount - nativeRefreshesBefore).toBe(0); + expect(h.native.clientRefreshCount - nativeRefreshesBefore).toBe(1); }); }); @@ -307,6 +307,29 @@ describe('native client sync behavior', () => { await expectConverged(h); }); + it('refreshes native after a JS sign-in that lands while JS refetches a native change', async () => { + const { server, token } = seedDevice(); + const h = await start({ server, jsDeviceToken: token, nativeDeviceToken: token }); + const client = h.js.client; + if (!client) { + throw new Error('JS has no client'); + } + const fetchClient = client.fetch; + let jsSessionId: string | undefined; + client.fetch = async options => { + client.fetch = fetchClient; + jsSessionId = await h.js.signIn('user_2'); + return fetchClient(options); + }; + + await h.run(() => h.native.signIn('user_1')); + await h.settle(); + + expect(h.js.session?.id).toBe(jsSessionId); + expect(h.native.activeSessionId).toBe(jsSessionId); + await expectConverged(h); + }); + it('settles a JS change without an echo loop', async () => { const { server, token, sessionIds } = seedDevice(['user_1', 'user_2'], 'user_1'); const h = await start({ server, jsDeviceToken: token, nativeDeviceToken: token }); diff --git a/packages/expo/src/provider/nativeClientSync.ts b/packages/expo/src/provider/nativeClientSync.ts index e55c754dc9d..ca58041900d 100644 --- a/packages/expo/src/provider/nativeClientSync.ts +++ b/packages/expo/src/provider/nativeClientSync.ts @@ -271,12 +271,11 @@ export function createNativeClientSync( if (await ready()) { await pull(); await reconciling; - lastFingerprint = fingerprint(clerk); } }); const toNative = singleFlight(async () => { - // Let an in-flight native pull settle first so its own emissions are not echoed back. + // Let an in-flight native pull settle first so one refresh covers what it applied. await fromNative.current()?.catch(noop); const next = fingerprint(clerk); if (next === lastFingerprint || !(await ready())) {