From 12657ffe9c95dd9647dff8dea97f8f488e136ba3 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 26 Oct 2026 11:23:59 -0700 Subject: [PATCH 1/2] test(expo): cover a JS sign-in that lands during a native pull --- .../nativeClientSync.behavior.test.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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..7f8d948a26e 100644 --- a/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx +++ b/packages/expo/src/provider/__tests__/nativeClientSyncBehavior/nativeClientSync.behavior.test.tsx @@ -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 }); From 78ec18d4f9a442210bcfd071f095f08473ea1ad5 Mon Sep 17 00:00:00 2001 From: wobsoriano Date: Mon, 26 Oct 2026 11:24:00 -0700 Subject: [PATCH 2/2] fix(expo): refresh native after a JS change that lands during a native pull --- .changeset/expo-refresh-native-after-pull.md | 5 +++++ .../nativeClientSync.behavior.test.tsx | 2 +- packages/expo/src/provider/nativeClientSync.ts | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/expo-refresh-native-after-pull.md 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 7f8d948a26e..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); }); }); 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())) {