Skip to content

fix(expo): sync native and JS client changes#8879

Merged
wobsoriano merged 15 commits into
mainfrom
mike/expo-native-client-sync
Jun 17, 2026
Merged

fix(expo): sync native and JS client changes#8879
wobsoriano merged 15 commits into
mainfrom
mike/expo-native-client-sync

Conversation

@mikepitre

@mikepitre mikepitre commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR updates @clerk/expo so the JavaScript SDK and native Clerk SDK stay in sync when either runtime changes the active client.

The bridge now uses one sync signal in both directions: client changed.

  • When JS changes, Expo sends native the latest client token if one is available, then native refreshes its client.
  • When native changes, Expo tells JS to refresh its client from Clerk.
  • Neither side interprets the event as "sign out this session". The receiving runtime refreshes from Clerk and treats the refreshed client as the source of truth.

This also removes Expo's native keychain/storage spelunking. Token persistence now belongs to the platform SDKs:

  • iOS uses the Clerk iOS device-token API and configured keychain service from clerk-ios#477.
  • Android uses the Clerk Android device-token storage path.
  • Expo no longer manually reads across native keychain/storage entries.

Why

Expo apps can render both JS auth surfaces (useAuth, useUser, useSession) and native components (AuthView, UserButton, UserProfileView). Before this change, those two runtimes could drift:

  • JS sign-in could leave native components stale until restart/cold start.
  • Native sign-out could leave JS hooks stale.
  • Multi-session native flows could briefly make JS look signed out while another session still existed, unmounting app UI and dismissing native profile sheets.

Main Changes

  • Adds native client-change events on iOS and Android.
  • Adds required syncFromJsClientToken(clientToken, sourceId) native module support.
  • Removes the old JS-callable native refreshClient bridge fallback. Native components are beta, so we do not need to preserve that older bridge shape.
  • Moves sync orchestration out of ClerkProvider into focused native-client-sync code.
  • Wraps the Expo token cache so JS token saves and clears notify native.
  • Refreshes JS from the JS client when native reports a client change.
  • Suppresses echo events to avoid JS/native ping-pong loops.
  • Keeps the remaining JS session active when a multi-session native change removes the previous active session.
  • Removes the public useNativeSession hook and old native-session event plumbing. Public app code should continue using useAuth, useUser, and useSession.

Reviewer Notes

  • The sync model is event-based, not imperative sign-out mirroring.
  • sourceId is only used to identify JS-originated native acknowledgements.
  • The bridge payload stays intentionally small. We do not serialize the full client object over the bridge.
  • JS session selection is derived from the refreshed JS client, including lastActiveSessionId.

Testing

  • rtk pnpm --filter @clerk/expo exec vitest run src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx src/utils/__tests__/native-module.test.ts src/hooks/__tests__/useNativeClientEvents.test.ts src/hooks/__tests__/useSignInWithGoogle.test.ts
  • rtk pnpm --filter @clerk/expo format:check
  • rtk pnpm --filter @clerk/expo lint
  • rtk git diff --check
  • iOS simulator, clean prebuild and build/run:
    • JS sign-in hydrates native UserButton.
    • Native sign-out updates JS.
    • Native multi-session sign-out keeps the remaining JS session active.
  • Android emulator, clean prebuild and expo run:android:
    • JS sign-out updates the app to signed-out state.
    • JS sign-in returns to signed-in state with native UserButton visible.

Notes:

  • Lint passes with existing warnings.
  • Full @clerk/expo declaration build still fails on pre-existing unrelated type errors; the branch-specific native module type error was fixed.

@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 326f194

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 16, 2026 5:20pm
swingset Ready Ready Preview, Comment Jun 16, 2026 5:20pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces the Expo native module's getSession/refreshClient API with a new syncFromJsClientToken protocol and clerkNativeClientChanged event. A new nativeClientSync.tsx module provides token-cache synchronization, client refresh/recovery flows, session reconciliation, and suppression coordination. ClerkProvider delegates to these abstractions using new coordination refs. iOS and Android native modules are updated to emit clerkNativeClientChanged with optional clientToken and sourceId payloads. useNativeSession is removed and the hooks barrel is updated to export useSSO, useOAuth, and useAuth.

Changes

Expo JS↔Native Client Synchronization

Layer / File(s) Summary
Native module contracts and event API surface
packages/expo/src/specs/NativeClerkModule.ts, packages/expo/ios/ClerkExpoModule.m, packages/expo/src/hooks/index.ts, packages/expo/src/hooks/useNativeClientEvents.ts
TypeScript Spec interface removes getSession/refreshClient and adds syncFromJsClientToken(clientToken, sourceId); Obj-C bridge header updated similarly; useNativeClientEvents exports NativeClientSnapshot/NativeClientEvent interfaces with optional clientToken/sourceId and required issuedAt timestamp; hook subscription wires clerkNativeClientChanged event via DeviceEventEmitter on iOS or native module otherwise; hooks barrel stops exporting useNativeSession and adds useSSO/useOAuth/useAuth.
iOS bridge protocol and module interface
packages/expo/ios/ClerkExpoModule.swift
ClerkNativeBridgeProtocol removes getSession/refreshClient methods and makes getClientToken async; adds syncFromJsClientToken(_:sourceId:) async method; new ClerkNativeBridgeReadyObserver protocol and weak-reference registry enable bridge-ready notifications; module supported events change to "clerkNativeClientChanged"; new emitClerkNativeClientChanged helper and bridge didSet hook for observer emissions.
iOS ClerkNativeBridge synchronization implementation
packages/expo/ios/ClerkNativeBridge.swift
Switches to SPI-gated ClerkKit import and removes keychain/Expo keychain plumbing; introduces ClientStateSnapshot (client + deviceToken) replacing single-field tracking; refactors configure to use async syncTokenState, conditional session-wait, and client-changed emission before ready signal; implements syncFromJsClientToken with optional device-token update and session/client-wait logic; adds waitForLoadedClient and waitForLoadedSessionIfNeeded helpers; makes getClientToken async and guarded; view-controller factories guard on configuration and return nil when not configured.
iOS native view host bridge-ready observer
packages/expo/ios/ClerkNativeViewHost.swift
ClerkNativeViewHost conforms to ClerkNativeBridgeReadyObserver; manages observer registration/unregistration during window lifecycle; triggers hosted-view updates via setNeedsHostedViewUpdate() when the bridge becomes ready.
Android native module event and sync flow
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
Event constant renamed to clerkNativeClientChanged; companion emitClientChanged(sourceId) replaces emitRefreshClient(); introduces jsOriginatedClientSyncDepth counter to suppress emissions during JS-initiated syncs; getSession/refreshClient async functions removed; syncFromJsClientToken added with optional device-token update, 5-second session-wait timeout, sourceId propagation, unified E_SYNC_FROM_JS_FAILED error, and client observer suppression logic.
Native module loader with contract validation
packages/expo/src/utils/native-module.ts, packages/expo/src/utils/__tests__/native-module.test.ts
loadNativeModule gains two-stage resolution: attempts NativeClerkModule import with __DEV__ warning on failure, returns early if isClerkExpoModule type guard passes, falls back to expo.requireNativeModule('ClerkExpo'), validates via same guard, returns null if neither path satisfies sync contract; tests validate both success (returns module) and null-return scenarios with mock isolation.
nativeClientSync utilities: token cache and recovery flows
packages/expo/src/provider/nativeClientSync.tsx
New file provides useSyncableTokenCache hook (in-memory iOS/Android cache with suppression-aware listener notifications), native token extraction/polling helpers, JS client refresh/reload flows driven by native state with optional initial-resource reloading, unauthenticated-error recovery via native token, active session reconciliation via setActive, and runWithSuppressedJsClientChanges suppression wrapper.
NativeClientSync component and lifecycle hooks
packages/expo/src/provider/nativeClientSync.tsx
NativeClientSync component overrides clerkInstance.updateClient to suppress transient signed-out emissions during reconciliation; manages generation-based queued token-cache-triggered syncs via NativeClerkModule.syncFromJsClientToken with unique sourceId; overrides handleUnauthenticated for native-driven JS recovery; registers JS client listener with suppression respect; useNativeClientBootstrap configures native module once, optionally seeds with cached bearer token, waits for JS Clerk load, performs initial native→JS sync; useNativeClientEventSync listens for native events and syncs to JS unless sourceId indicates sync-layer origin (loop prevention).
ClerkProvider refactored to nativeClientSync integration
packages/expo/src/provider/ClerkProvider.tsx
Provider creates coordination refs for token-cache listeners, JS client change suppression, and native refresh orchestration; builds syncableTokenCache via useSyncableTokenCache hook; passes cache to native clerkInstance; delegates bootstrap and event-sync to new useNativeClientBootstrap/useNativeClientEventSync hooks; removes in-file native token polling, sync, and session bootstrap logic; updates NativeClientSync component props with new coordination refs and cache.
Validation tests and release notes
packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx, packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts, packages/expo/src/hooks/__tests__/useSignInWithGoogle.test.ts, .changeset/quiet-ravens-remember.md
851-line ClerkProvider sync test suite covers bootstrap from cached token, bidirectional token-cache sync, native reload with/without tokens, JS listener bounce suppression, session reconciliation with last-active/fallback/switch scenarios, unauthenticated recovery branching, suppression of follow-up updates, queued sync processing, and sourceId-based loop prevention; new useNativeClientEvents test validates clerkNativeClientChanged listener wiring and event payload; useSignInWithGoogle test mock updated to include syncFromJsClientToken method; patch changeset documents the JS↔native auth sync fix preventing stale hooks and cross-runtime sign-out propagation.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over ClerkProvider,NativeClerkModule: Bootstrap (cold start with cached token)
    ClerkProvider->>useNativeClientBootstrap: configure(publishableKey, cachedBearerToken)
    useNativeClientBootstrap->>NativeClerkModule: configure(publishableKey, bearerToken)
    NativeClerkModule->>ClerkNativeBridge: syncTokenState(bearerToken)
    ClerkNativeBridge->>ClerkNativeBridge: Clerk.updateDeviceToken(bearerToken)
    ClerkNativeBridge->>ClerkNativeBridge: waitForLoadedSessionIfNeeded()
    ClerkNativeBridge-->>NativeClerkModule: clerkNativeClientChanged {clientToken}
    NativeClerkModule->>useNativeClientBootstrap: emit event (no sourceId)
    useNativeClientBootstrap->>ClerkInstance: syncNativeClientToJs() + reconcileActiveSession (suppressed)
  end

  rect rgba(144, 238, 144, 0.5)
    Note over TokenCache,useNativeClientEventSync: JS auth change → native sync with sourceId
    TokenCache->>NativeClientSync: listener update (clientJWT saved)
    NativeClientSync->>NativeClerkModule: syncFromJsClientToken(token, sourceId=gen_N)
    NativeClerkModule->>ClerkNativeBridge: updateDeviceToken(token)
    ClerkNativeBridge->>NativeClerkModule: emitClientChanged(sourceId=gen_N)
    NativeClerkModule->>useNativeClientEventSync: clerkNativeClientChanged {clientToken, sourceId}
    useNativeClientEventSync->>useNativeClientEventSync: sourceId check → skip (matches gen prefix)
  end

  rect rgba(255, 200, 150, 0.5)
    Note over ClerkNativeBridge,ClerkInstance: Native auth change → JS sync without sourceId
    ClerkNativeBridge->>NativeClerkModule: emitClientChanged() (no sourceId)
    NativeClerkModule->>useNativeClientEventSync: clerkNativeClientChanged {clientToken}
    useNativeClientEventSync->>useNativeClientEventSync: sourceId check → proceed
    useNativeClientEventSync->>NativeClientSync: syncNativeClientToJs(snapshot)
    NativeClientSync->>ClerkInstance: updateClient (suppressed emit)
    NativeClientSync->>ClerkInstance: reconcileJsActiveSessionFromClient(setActive)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • clerk/javascript#8699: Both PRs refactor the Expo JS↔native auth synchronization around the native client-token bridge, with this PR replacing the prior "refreshClient" event and session/refresh APIs with "clerkNativeClientChanged" and syncFromJsClientToken in the same code paths (ClerkProvider, nativeClientSync, NativeClerkModule, and native iOS/Android modules).

Suggested labels

react, native

Suggested reviewers

  • swolfand
  • jeremy-clerk

🐇 From JS to native, a token takes flight,
syncFromJsClientToken sets each session right.
No more stale hooks when sign-out takes hold,
sourceId guards loops so the story unfolds.
Cold start hydrates, multi-session aligns—
This bunny rejoices at synchronized signs! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly addresses the main change: implementing synchronization between native and JavaScript client state changes in the Clerk Expo SDK.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8879

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8879

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8879

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8879

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8879

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8879

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8879

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8879

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8879

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8879

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8879

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8879

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8879

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8879

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8879

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8879

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8879

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8879

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8879

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8879

commit: 326f194

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-16T17:20:02.398Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 1
🔴 Breaking changes 8
🟡 Non-breaking changes 0
🟢 Additions 0

Warning
8 breaking change(s) detected - Major version bump required

🤖 This report was reviewed by claude-sonnet-4-6.

🔴 Breaking changes index (8)

Every breaking change, up front. Full diffs are in the package sections below.

Package Subpath Change
@clerk/expo . useNativeSession
@clerk/expo . UseNativeSessionReturn
@clerk/expo . UseNativeSessionReturn.isAvailable
@clerk/expo . UseNativeSessionReturn.isLoading
@clerk/expo . UseNativeSessionReturn.isSignedIn
@clerk/expo . UseNativeSessionReturn.refresh
@clerk/expo . UseNativeSessionReturn.sessionId
@clerk/expo . UseNativeSessionReturn.user

@clerk/expo

Current version: 3.4.3
Recommended bump: MAJOR → 4.0.0

🔴 Breaking Changes (8)

Changed: useNativeSession

- export declare function useNativeSession(): UseNativeSessionReturn;

Static analyzer: Removed function useNativeSession

🤖 AI review (confirmed) (100%): The exported function useNativeSession was removed entirely, breaking any consumer that imports and calls it.

Migration: Remove all calls to useNativeSession and migrate to the replacement API provided by @clerk/expo.

Changed: UseNativeSessionReturn

- export interface UseNativeSessionReturn

Static analyzer: Removed interface UseNativeSessionReturn

🤖 AI review (confirmed) (100%): The exported interface UseNativeSessionReturn was removed entirely, breaking any consumer that references it as a type.

Migration: Remove or replace any type annotations referencing UseNativeSessionReturn with the new return type from the replacement hook.

Changed: UseNativeSessionReturn.isAvailable

- isAvailable: boolean;

Static analyzer: Removed property UseNativeSessionReturn.isAvailable

🤖 AI review (confirmed) (100%): The isAvailable property was removed from UseNativeSessionReturn, breaking consumers that read this field.

Migration: Stop reading isAvailable from the hook result and migrate to the replacement API.

Changed: UseNativeSessionReturn.isLoading

- isLoading: boolean;

Static analyzer: Removed property UseNativeSessionReturn.isLoading

🤖 AI review (confirmed) (100%): The isLoading property was removed from UseNativeSessionReturn, breaking consumers that read this field.

Migration: Stop reading isLoading from the hook result and migrate to the replacement API.

Changed: UseNativeSessionReturn.isSignedIn

- isSignedIn: boolean;

Static analyzer: Removed property UseNativeSessionReturn.isSignedIn

🤖 AI review (confirmed) (100%): The isSignedIn property was removed from UseNativeSessionReturn, breaking consumers that read this field.

Migration: Stop reading isSignedIn from the hook result and migrate to the replacement API.

Changed: UseNativeSessionReturn.refresh

- refresh: () => Promise<void>;

Static analyzer: Removed property UseNativeSessionReturn.refresh

🤖 AI review (confirmed) (100%): The refresh method was removed from UseNativeSessionReturn, breaking consumers that call it.

Migration: Stop calling refresh from the hook result and migrate to the replacement API.

Changed: UseNativeSessionReturn.sessionId

- sessionId: string | null;

Static analyzer: Removed property UseNativeSessionReturn.sessionId

🤖 AI review (confirmed) (100%): The sessionId property was removed from UseNativeSessionReturn, breaking consumers that read this field.

Migration: Stop reading sessionId from the hook result and migrate to the replacement API.

Changed: UseNativeSessionReturn.user

- user: NativeSessionData['user'] | null;

Static analyzer: Removed property UseNativeSessionReturn.user

🤖 AI review (confirmed) (100%): The user property was removed from UseNativeSessionReturn, breaking consumers that read this field.

Migration: Stop reading user from the hook result and migrate to the replacement API.


Report generated by Break Check

Last ran on 326f194.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts (1)

64-65: ⚡ Quick win

Assert listener cleanup on unmount.

Line 64 calls unmount(), but the test never verifies remove was invoked. Add an assertion to lock in cleanup behavior and prevent event-listener leaks from regressing.

Suggested test hardening
     unmount();
+    expect(mocks.remove).toHaveBeenCalledTimes(1);
   });

As per coding guidelines, “Implement proper test cleanup in React component tests” and “Use proper test assertions in React component tests.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts` around lines
64 - 65, The test in useNativeClientEvents.test.ts calls unmount() at line 64
but does not verify that the remove listener cleanup function was actually
invoked, which leaves a gap in test coverage for preventing event listener
leaks. After the unmount() call, add an assertion that verifies the remove mock
function was called, ensuring the cleanup behavior is properly tested and locked
in. This assertion should check that remove was invoked exactly once to confirm
proper listener removal on unmount.

Source: Coding guidelines

packages/expo/src/hooks/useNativeClientEvents.ts (1)

52-53: ⚡ Quick win

Stamp issuedAt after applying the native snapshot.

NativeClientSnapshot arrives from an untyped native emitter. Spreading it after issuedAt lets an accidental payload field override the local event marker downstream sync code uses for freshness.

Proposed fix
-      subscription = eventEmitter.addListener(nativeClientChangedEvent, snapshot => {
-        setNativeClientEvent({ issuedAt: Date.now(), ...snapshot });
+      subscription = eventEmitter.addListener(nativeClientChangedEvent, snapshot => {
+        setNativeClientEvent({ ...snapshot, issuedAt: Date.now() });
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/src/hooks/useNativeClientEvents.ts` around lines 52 - 53, The
setNativeClientEvent call in the eventEmitter.addListener callback is spreading
the native snapshot before setting issuedAt, which allows fields from the
untyped native snapshot to override the local issuedAt timestamp. Move the
issuedAt assignment to occur after the snapshot spread so it takes precedence
and cannot be overridden by accidental payload fields from the native emitter.
Change the order from { issuedAt: Date.now(), ...snapshot } to { ...snapshot,
issuedAt: Date.now() } in the setNativeClientEvent call.
packages/expo/src/provider/nativeClientSync.tsx (1)

683-693: ⚡ Quick win

Add explicit return types to the exported hooks.

useNativeClientBootstrap returns the mounted ref and useNativeClientEventSync is side-effect-only; both are exported functions, so their return types should be explicit.

Proposed fix
 export function useNativeClientBootstrap({
@@
-}) {
+}): MutableRefObject<boolean> {
@@
 export function useNativeClientEventSync({
@@
-}) {
+}): void {

As per coding guidelines, “Always define explicit return types for functions, especially public APIs.” Based on learnings, “enforce explicit return type annotations for exported functions and public APIs.”

Also applies to: 797-811

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/src/provider/nativeClientSync.tsx` around lines 683 - 693, Add
explicit return type annotations to the exported hook functions to comply with
coding guidelines for public APIs. The `useNativeClientBootstrap` function
should have an explicit return type annotation indicating it returns the mounted
ref, and the `useNativeClientEventSync` function should have an explicit return
type annotation of void since it is side-effect only. Review the function
implementations to determine the correct return types and add them to the
function signatures.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Line 39: The module-wide `pendingClientChangeSourceId` field can be
overwritten by concurrent calls to `syncFromJsClientToken` before observers
consume the previous sourceId, causing incorrect sourceId stamping that breaks
feedback-loop suppression. Instead of storing the sourceId in a shared pending
slot at line 39, bind the sourceId directly to each specific sync operation or
client transition. This requires either serializing JS-originated syncs so they
execute one at a time, or redesigning how sourceId is passed through the client
transition flow (affecting the usages at lines 98-100 and 286-337 where
`pendingClientChangeSourceId` is read/written). Ensure that each
`syncFromJsClientToken` call carries its sourceId through the entire transition
lifecycle without risk of overwriting or being overwritten by concurrent
operations.

In `@packages/expo/src/provider/nativeClientSync.tsx`:
- Around line 551-557: The refreshNativeFromJsClient function is incorrectly
sending a null bearer token to native when clientToken is missing from options,
which clears the native state instead of keeping it in sync. Instead of
converting missing clientToken to null, read from a cached token value (the
latest client token) and send that to native via syncFromJsClientToken. This
ensures that ordinary JS client changes propagate the actual current token to
native. Apply the same fix at lines 664-671 where this pattern also occurs.
- Around line 134-138: The function `syncClientTokenToCache` currently only
saves the token when clientToken is truthy, but ignores the null case. This
leaves stale cached tokens in storage when native clears the client token (e.g.,
during sign-out). Modify the function to handle the null case by explicitly
removing the token from the cache when clientToken is null, ensuring that the
CLERK_CLIENT_JWT_KEY is cleared from tokenCache in addition to saving it when a
token is present.

In `@packages/expo/src/utils/native-module.ts`:
- Around line 21-37: The function returns a native module that may not satisfy
the sync contract required by downstream code that calls `syncFromJsClientToken`
directly. The issue is that `nativeModule` can be returned after failing the
`configure` check, and the fallback from `requireNativeModule` is not verified
to have the required sync methods. Modify the return statements to only return a
module that has the required sync methods (like `syncFromJsClientToken` or
`getClientToken`). Check the result of the Expo `requireNativeModule` call to
ensure it satisfies the contract before returning it, and remove the fallback to
the incomplete `nativeModule` that failed the initial validation check. In the
catch block, if `nativeModule` exists but doesn't satisfy the contract, return
null or a placeholder instead of returning an incomplete module.

---

Nitpick comments:
In `@packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts`:
- Around line 64-65: The test in useNativeClientEvents.test.ts calls unmount()
at line 64 but does not verify that the remove listener cleanup function was
actually invoked, which leaves a gap in test coverage for preventing event
listener leaks. After the unmount() call, add an assertion that verifies the
remove mock function was called, ensuring the cleanup behavior is properly
tested and locked in. This assertion should check that remove was invoked
exactly once to confirm proper listener removal on unmount.

In `@packages/expo/src/hooks/useNativeClientEvents.ts`:
- Around line 52-53: The setNativeClientEvent call in the
eventEmitter.addListener callback is spreading the native snapshot before
setting issuedAt, which allows fields from the untyped native snapshot to
override the local issuedAt timestamp. Move the issuedAt assignment to occur
after the snapshot spread so it takes precedence and cannot be overridden by
accidental payload fields from the native emitter. Change the order from {
issuedAt: Date.now(), ...snapshot } to { ...snapshot, issuedAt: Date.now() } in
the setNativeClientEvent call.

In `@packages/expo/src/provider/nativeClientSync.tsx`:
- Around line 683-693: Add explicit return type annotations to the exported hook
functions to comply with coding guidelines for public APIs. The
`useNativeClientBootstrap` function should have an explicit return type
annotation indicating it returns the mounted ref, and the
`useNativeClientEventSync` function should have an explicit return type
annotation of void since it is side-effect only. Review the function
implementations to determine the correct return types and add them to the
function signatures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 129460af-d109-4092-bec0-ce620cf8c184

📥 Commits

Reviewing files that changed from the base of the PR and between d5968d0 and 228b0e4.

📒 Files selected for processing (17)
  • .changeset/quiet-ravens-remember.md
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/ios/ClerkExpoModule.m
  • packages/expo/ios/ClerkExpoModule.swift
  • packages/expo/ios/ClerkNativeBridge.swift
  • packages/expo/ios/ClerkNativeViewHost.swift
  • packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts
  • packages/expo/src/hooks/__tests__/useSignInWithGoogle.test.ts
  • packages/expo/src/hooks/index.ts
  • packages/expo/src/hooks/useNativeClientEvents.ts
  • packages/expo/src/hooks/useNativeSession.ts
  • packages/expo/src/provider/ClerkProvider.tsx
  • packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx
  • packages/expo/src/provider/nativeClientSync.tsx
  • packages/expo/src/specs/NativeClerkModule.android.ts
  • packages/expo/src/specs/NativeClerkModule.ts
  • packages/expo/src/utils/native-module.ts
💤 Files with no reviewable changes (2)
  • packages/expo/src/hooks/index.ts
  • packages/expo/src/hooks/useNativeSession.ts

Comment thread packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt Outdated
Comment thread packages/expo/src/provider/nativeClientSync.tsx
Comment thread packages/expo/src/provider/nativeClientSync.tsx
Comment thread packages/expo/src/utils/native-module.ts Outdated
@mikepitre mikepitre force-pushed the mike/expo-native-client-sync branch from 462b7a4 to 326f194 Compare June 16, 2026 17:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/expo/src/utils/__tests__/native-module.test.ts (1)

35-67: ⚡ Quick win

Add an explicit test for the Expo fallback success path.

You test “generated module valid” and “both invalid,” but not “generated invalid + Expo fallback valid,” which is a key branch in the new loader behavior.

Proposed test addition
 describe('native module loader', () => {
@@
   test('returns null when no native module satisfies the sync contract', async () => {
@@
     expect(ClerkExpoModule).toBeNull();
   });
+
+  test('returns Expo fallback module when generated module is invalid but fallback satisfies contract', async () => {
+    mocks.nativeModule = {
+      configure: vi.fn(),
+    };
+    mocks.expoModule = makeNativeModule();
+
+    const { ClerkExpoModule } = await importNativeModule();
+
+    expect(ClerkExpoModule).toBe(mocks.expoModule);
+  });
 });

As per coding guidelines, **/*.{test,spec}.{js,ts,jsx,tsx} requires comprehensive testing and **/*.{test,spec}.{ts,tsx} requires edge-case verification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/expo/src/utils/__tests__/native-module.test.ts` around lines 35 -
67, The test suite for the native module loader is missing coverage for the Expo
fallback success path. Currently there are tests for "generated module valid"
(the first test) and "both invalid" (the second test), but no test for when the
generated module is invalid but the Expo fallback module is valid, which is a
key execution branch. Add a new test case after the existing tests that sets up
mocks.nativeModule to be incomplete or invalid (missing required methods like
getClientToken), while setting up mocks.expoModule as a complete valid module
matching the sync contract, then verify that importNativeModule returns
ClerkExpoModule as the expoModule fallback.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/expo/src/utils/__tests__/native-module.test.ts`:
- Around line 35-67: The test suite for the native module loader is missing
coverage for the Expo fallback success path. Currently there are tests for
"generated module valid" (the first test) and "both invalid" (the second test),
but no test for when the generated module is invalid but the Expo fallback
module is valid, which is a key execution branch. Add a new test case after the
existing tests that sets up mocks.nativeModule to be incomplete or invalid
(missing required methods like getClientToken), while setting up
mocks.expoModule as a complete valid module matching the sync contract, then
verify that importNativeModule returns ClerkExpoModule as the expoModule
fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 770d642f-7061-4e15-933b-ad79fba9945c

📥 Commits

Reviewing files that changed from the base of the PR and between 228b0e4 and 462b7a4.

📒 Files selected for processing (5)
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx
  • packages/expo/src/provider/nativeClientSync.tsx
  • packages/expo/src/utils/__tests__/native-module.test.ts
  • packages/expo/src/utils/native-module.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/expo/src/provider/tests/ClerkProvider.nativeClientSync.test.tsx
  • packages/expo/src/provider/nativeClientSync.tsx

@mikepitre

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 326f194f9d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .changeset/quiet-ravens-remember.md
@wobsoriano wobsoriano merged commit b803274 into main Jun 17, 2026
51 checks passed
@wobsoriano wobsoriano deleted the mike/expo-native-client-sync branch June 17, 2026 19:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants