Skip to content

feat(expo): re-introduce two-way JS/native session sync#8088

Open
chriscanin wants to merge 10 commits intomainfrom
chris/native-session-sync-v2
Open

feat(expo): re-introduce two-way JS/native session sync#8088
chriscanin wants to merge 10 commits intomainfrom
chris/native-session-sync-v2

Conversation

@chriscanin
Copy link
Member

@chriscanin chriscanin commented Mar 16, 2026

Summary

Re-applies the changes from #8032 which was reverted in #8065 due to premature merge.

This PR exists for visibility and review before re-merging. The code is identical to the original #8032:

  • Two-way JS/native token sync for expo native components
  • Native session reliably cleared on sign-out, avoids stale session/state after closing native UI
  • Improved initialization error handling with clearer timeout and failure messages
  • Additional debug logging in development

Context

Test plan

  • Review changes thoroughly before merging
  • Test native sign-in/sign-out flow on iOS
  • Test native sign-in/sign-out flow on Android
  • Verify session state stays in sync between JS and native layers
  • Verify no regressions in existing expo functionality

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Two-way session sync between JS and native Expo components; native auth events now notify JS and JS can push tokens to native.
    • NativeSessionSync integration to auto-reconcile JS↔native sign-in and expose native client token to JS.
  • Improvements

    • Native client-token retrieval and automatic token caching; safer keychain/device-token handling.
    • More robust init/configure/sign-out flows with timeouts, reduced duplicate events, clearer failure handling.
    • Updated Android native dependency versions.

Re-applies the changes from #8032 which was reverted in #8065.
This PR exists for visibility and review before re-merging.

Original changes:
- Two-way JS/native token sync for expo native components
- Native session cleared on sign-out
- Improved initialization error handling with timeout/failure messages
- Additional debug logging in development
@changeset-bot
Copy link

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: dd96357

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
Copy link

vercel bot commented Mar 16, 2026

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Mar 25, 2026 5:15pm

Request Review

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 16, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8088

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8088

@clerk/expo

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: dd96357

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reintroduces two-way JS/native session synchronization for Expo: adds a changeset; bumps Android Clerk API/UI versions; updates Android ClerkExpoModule to guard initialization, write/read device tokens, add timeouts and session-waiting; adds getClientToken and keychain-based token/state tracking, conditional configure/refresh, and revised signOut/getSession behavior in iOS ClerkViewFactory and templates; introduces module-level auth-state emission; and integrates NativeSessionSync and token-sync logic into the React provider, UserButton, and useUserProfileModal.

Possibly related PRs

  • clerk/javascript PR 8032: Implements the same two-way JS/native session synchronization across the same Expo files (ClerkViewFactory/getClientToken, ClerkExpoModule, provider/hooks/components).
  • clerk/javascript PR 8065: Touches the same Expo native session sync codepaths and overlaps modules modified here (ClerkExpoModule, ClerkViewFactory, provider/hooks/components).
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(expo): re-introduce two-way JS/native session sync' directly and clearly describes the main change—restoring bidirectional session synchronization between JavaScript and native layers in the Expo SDK. The changes across multiple files consistently implement this objective.
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.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/expo/src/hooks/useUserProfileModal.ts (1)

61-127: ⚠️ Potential issue | 🟠 Major

Add regression coverage before reintroducing this flow.

This hook now depends on a multi-step contract across JS, the native module, and native UI (getSessionconfigure → modal presentation → post-dismiss sign-out reconciliation), but the PR adds no automated coverage for it. Since this exact auth-sync work was already reverted once, we need regression tests around token sync and native-driven sign-out before merge.

As per coding guidelines, "If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/src/hooks/useUserProfileModal.ts` around lines 61 - 127, The
new presentUserProfile flow (presentUserProfile hook) requires regression tests
to cover the JS↔native token sync and native-driven sign-out reconciliation; add
tests that mock ClerkExpo.getSession, ClerkExpo.configure,
ClerkExpo.presentUserProfile, ClerkExpo.signOut, tokenCache.getToken, and the
clerk.signOut method to assert: (1) when native has no session but
tokenCache.getToken returns a bearer token, ClerkExpo.configure is called with
clerk.publishableKey and that token and presentUserProfile is invoked; (2) when
post-modal ClerkExpo.getSession returns null but hadNativeSessionBefore was
true, clerk.signOut (and ClerkExpo.signOut if present) are called; and (3) the
inverse case where configure produces a native session avoids JS signOut; target
tests at the unit/integration layer exercising presentUserProfile (the
useUserProfileModal hook) and use Jest mocks/spies for ClerkExpo.getSession,
configure, presentUserProfile, signOut and tokenCache.getToken to verify the
exact call sequence and side effects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Around line 253-257: The signOut(promise: Promise) branch that returns when
!Clerk.isInitialized currently resolves without clearing the stored device
token; update signOut to always remove the stored DEVICE_TOKEN from
SharedPreferences (the same key written by configure()) before resolving, even
when Clerk.isInitialized is false. Locate the signOut method in
ClerkExpoModule.kt and add logic to obtain the module's SharedPreferences and
remove the "DEVICE_TOKEN" entry (or the constant used for that key), then
proceed to promise.resolve(null).

In `@packages/expo/ios/ClerkExpoModule.swift`:
- Around line 23-26: The template is missing the required protocol method
getClientToken() which breaks conformance to ClerkViewFactoryProtocol; add a
concrete implementation of getClientToken() inside the ClerkViewFactory class
(the type that implements ClerkViewFactoryProtocol) that returns the correct
client token string or nil as appropriate, ensuring the method signature exactly
matches getClientToken() -> String? and is public/internal consistent with other
protocol methods; update any backing storage or token retrieval logic used by
configure/getSession to return the same token value.

In `@packages/expo/ios/templates/ClerkViewFactory.swift`:
- Around line 323-330: The signOut() method currently returns early when there
is no live native session, skipping Clerk.clearAllKeychainItems() and
Self.clerkConfigured = false; change the control flow in signOut() so that
clearing native keychain state and resetting Self.clerkConfigured always runs
regardless of whether Clerk.shared.session?.id is present—attempt the signOut
call only if sessionId exists (try await Clerk.shared.auth.signOut(sessionId:
sessionId)), but move Clerk.clearAllKeychainItems() and Self.clerkConfigured =
false outside/after that guard/conditional so they execute unconditionally;
update the signOut() function in ClerkViewFactory.swift accordingly.

---

Outside diff comments:
In `@packages/expo/src/hooks/useUserProfileModal.ts`:
- Around line 61-127: The new presentUserProfile flow (presentUserProfile hook)
requires regression tests to cover the JS↔native token sync and native-driven
sign-out reconciliation; add tests that mock ClerkExpo.getSession,
ClerkExpo.configure, ClerkExpo.presentUserProfile, ClerkExpo.signOut,
tokenCache.getToken, and the clerk.signOut method to assert: (1) when native has
no session but tokenCache.getToken returns a bearer token, ClerkExpo.configure
is called with clerk.publishableKey and that token and presentUserProfile is
invoked; (2) when post-modal ClerkExpo.getSession returns null but
hadNativeSessionBefore was true, clerk.signOut (and ClerkExpo.signOut if
present) are called; and (3) the inverse case where configure produces a native
session avoids JS signOut; target tests at the unit/integration layer exercising
presentUserProfile (the useUserProfileModal hook) and use Jest mocks/spies for
ClerkExpo.getSession, configure, presentUserProfile, signOut and
tokenCache.getToken to verify the exact call sequence and side effects.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro

Run ID: e2579af5-b567-4b39-af9d-c538c6937b51

📥 Commits

Reviewing files that changed from the base of the PR and between da2b239 and 4685f92.

📒 Files selected for processing (9)
  • .changeset/native-session-sync-v2.md
  • packages/expo/android/build.gradle
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/ios/ClerkExpoModule.swift
  • packages/expo/ios/ClerkViewFactory.swift
  • packages/expo/ios/templates/ClerkViewFactory.swift
  • packages/expo/src/hooks/useUserProfileModal.ts
  • packages/expo/src/native/UserButton.tsx
  • packages/expo/src/provider/ClerkProvider.tsx

@chriscanin chriscanin self-assigned this Mar 18, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/expo/src/provider/ClerkProvider.tsx (2)

364-367: ⚠️ Potential issue | 🟡 Minor

Missing tokenCache in dependency array.

Same issue as above - the effect uses tokenCache (line 330) for saving native client tokens but doesn't include it in dependencies. This could cause tokens to be saved to a stale cache reference.

Suggested fix
-  }, [nativeAuthState, clerkInstance]);
+  }, [nativeAuthState, clerkInstance, tokenCache]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/src/provider/ClerkProvider.tsx` around lines 364 - 367, The
effect that calls syncNativeAuthToJs uses tokenCache but doesn't list it in the
dependency array, risking saving tokens to a stale cache; update the useEffect
(the one that invokes syncNativeAuthToJs) to include tokenCache alongside
nativeAuthState and clerkInstance in its dependency array so the effect re-runs
when tokenCache changes, ensuring syncNativeAuthToJs always captures the current
tokenCache reference.

308-311: ⚠️ Potential issue | 🟡 Minor

Missing tokenCache in dependency array.

The effect uses tokenCache (line 192) but it's not included in the dependency array. If tokenCache changes after mount, the effect will use a stale reference, potentially reading tokens from the wrong cache.

Suggested fix
-  }, [pk, clerkInstance]);
+  }, [pk, clerkInstance, tokenCache]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/src/provider/ClerkProvider.tsx` around lines 308 - 311, The
effect that sets isMountedRef.current to false in the cleanup references
tokenCache elsewhere in the effect but the dependency array only includes pk and
clerkInstance; update the effect's dependency array to include tokenCache (or a
stable identifier derived from it) so the effect re-runs when tokenCache
changes, ensuring the effect does not capture a stale tokenCache; locate the
useEffect that returns the cleanup setting isMountedRef.current = false (the
closure referencing isMountedRef, pk, clerkInstance, and tokenCache) and add
tokenCache to its dependency list (or memoize tokenCache where used) to fix the
stale reference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/expo/src/provider/ClerkProvider.tsx`:
- Around line 66-136: Add unit/integration tests exercising the
NativeSessionSync component's sync paths: write tests that mount
NativeSessionSync (or the parent ClerkProvider) and simulate JS→native sync by
toggling useAuth isSignedIn, mocking
NativeClerkModule.configure/getSession/signOut and defaultTokenCache.getToken
(use CLERK_CLIENT_JWT_KEY), asserting configure is called with publishableKey
and token and that signOut is invoked on sign-out; also add tests for native→JS
token sync by mocking getSession to return a native session and ensuring
hasSyncedRef prevents redundant configure calls; target tests around
NativeSessionSync, syncToNative, and NativeClerkModule behaviors to cover error
branches and the effectiveTokenCache fallback.

---

Outside diff comments:
In `@packages/expo/src/provider/ClerkProvider.tsx`:
- Around line 364-367: The effect that calls syncNativeAuthToJs uses tokenCache
but doesn't list it in the dependency array, risking saving tokens to a stale
cache; update the useEffect (the one that invokes syncNativeAuthToJs) to include
tokenCache alongside nativeAuthState and clerkInstance in its dependency array
so the effect re-runs when tokenCache changes, ensuring syncNativeAuthToJs
always captures the current tokenCache reference.
- Around line 308-311: The effect that sets isMountedRef.current to false in the
cleanup references tokenCache elsewhere in the effect but the dependency array
only includes pk and clerkInstance; update the effect's dependency array to
include tokenCache (or a stable identifier derived from it) so the effect
re-runs when tokenCache changes, ensuring the effect does not capture a stale
tokenCache; locate the useEffect that returns the cleanup setting
isMountedRef.current = false (the closure referencing isMountedRef, pk,
clerkInstance, and tokenCache) and add tokenCache to its dependency list (or
memoize tokenCache where used) to fix the stale reference.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro

Run ID: fed743a6-c865-4570-98a6-5768d0a655e0

📥 Commits

Reviewing files that changed from the base of the PR and between 4685f92 and 5b3a88f.

📒 Files selected for processing (1)
  • packages/expo/src/provider/ClerkProvider.tsx

Comment on lines +66 to +136
function NativeSessionSync({
publishableKey,
tokenCache,
}: {
publishableKey: string;
tokenCache: TokenCache | undefined;
}) {
const { isSignedIn } = useAuth();
const hasSyncedRef = useRef(false);
// Use the provided tokenCache, falling back to the default SecureStore cache
const effectiveTokenCache = tokenCache ?? defaultTokenCache;

useEffect(() => {
if (!isSignedIn) {
hasSyncedRef.current = false;

// Clear the native session so native components (UserButton, etc.)
// don't continue showing a signed-in state after JS-side sign out.
const ClerkExpo = NativeClerkModule;
if (ClerkExpo?.signOut) {
void ClerkExpo.signOut().catch((error: unknown) => {
if (__DEV__) {
console.warn('[NativeSessionSync] Failed to clear native session:', error);
}
});
}

return;
}

if (hasSyncedRef.current) {
return;
}

const syncToNative = async () => {
try {
const ClerkExpo = NativeClerkModule;
if (!ClerkExpo?.configure || !ClerkExpo?.getSession) {
return;
}

// Check if native already has a session (e.g. auth via AuthView or initial load)
const nativeSession = (await ClerkExpo.getSession()) as {
sessionId?: string;
session?: { id: string };
} | null;
const hasNativeSession = !!(nativeSession?.sessionId || nativeSession?.session?.id);

if (hasNativeSession) {
hasSyncedRef.current = true;
return;
}

// Read the JS SDK's client JWT and push it to the native SDK
const bearerToken = (await effectiveTokenCache?.getToken(CLERK_CLIENT_JWT_KEY)) ?? null;
if (bearerToken) {
await ClerkExpo.configure(publishableKey, bearerToken);
hasSyncedRef.current = true;
}
} catch (error) {
if (__DEV__) {
console.warn('[NativeSessionSync] Failed to sync JS session to native:', error);
}
}
};

void syncToNative();
}, [isSignedIn, publishableKey, effectiveTokenCache]);

return null;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 20, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding tests for the new session sync logic.

The NativeSessionSync component and the two-way sync paths introduce significant new functionality. Tests covering JS→native sync (sign-in/sign-out flows) and native→JS token sync would help ensure this behavior doesn't regress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/src/provider/ClerkProvider.tsx` around lines 66 - 136, Add
unit/integration tests exercising the NativeSessionSync component's sync paths:
write tests that mount NativeSessionSync (or the parent ClerkProvider) and
simulate JS→native sync by toggling useAuth isSignedIn, mocking
NativeClerkModule.configure/getSession/signOut and defaultTokenCache.getToken
(use CLERK_CLIENT_JWT_KEY), asserting configure is called with publishableKey
and token and that signOut is invoked on sign-out; also add tests for native→JS
token sync by mocking getSession to return a native session and ensuring
hasSyncedRef prevents redundant configure calls; target tests around
NativeSessionSync, syncToNative, and NativeClerkModule behaviors to cover error
branches and the effectiveTokenCache fallback.

Copy link
Member Author

Choose a reason for hiding this comment

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

Will leave testing to another PR, test coverage needs to be added in many places, not just here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@chriscanin, understood — happy to defer that to a follow-up PR. Would you like me to open a GitHub issue to track adding test coverage for NativeSessionSync and the broader two-way session sync paths so it doesn't get lost?

On cold start with a bearer token, configure() resolved as soon as
Clerk.isInitialized flipped. Callers like useUserProfileModal and
UserButton that immediately call getSession() could observe null.
Now we wait for Clerk.sessionFlow to emit a non-null session (up to
5s) before resolving, matching the already-initialized path.
The session sync code uses Clerk.updateDeviceToken() which was added
in clerk-android 1.0.10. Bumps from 1.0.9 to 1.0.10.
- Android: clear DEVICE_TOKEN from SharedPreferences in signOut() even
  when native SDK is not initialized, preventing stale token on next init
- iOS: signOut() now always clears keychain data and resets state, even
  when there is no active native session
- iOS templates: add missing getClientToken() for protocol conformance,
  fix signOut() to always clear keychain
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt (1)

271-280: ⚠️ Potential issue | 🟠 Major

Android signOut does not clear DEVICE_TOKEN when Clerk is initialized.

When Clerk is initialized, signOut() calls Clerk.auth.signOut() without clearing DEVICE_TOKEN from SharedPreferences. iOS always clears keychain items regardless of session state with an explicit comment: "This ensures the native SDK doesn't boot with a stale token on next launch."

If the native SDK's signOut() doesn't automatically clear the persisted device token, the next configure() call could rehydrate a stale client token, creating a security and data isolation issue.

The Android code already demonstrates awareness of this requirement—it clears the token when Clerk is not initialized with the exact same reasoning. The initialized path should match this behavior to be consistent with iOS.

Suggested fix to match iOS behavior
     coroutineScope.launch {
         try {
             Clerk.auth.signOut()
+            // Clear DEVICE_TOKEN to match iOS behavior (Clerk.clearAllKeychainItems)
+            reactApplicationContext.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE)
+                .edit()
+                .remove("DEVICE_TOKEN")
+                .apply()
             promise.resolve(null)
         } catch (e: Exception) {
             promise.reject("E_SIGN_OUT_FAILED", e.message ?: "Sign out failed", e)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`
around lines 271 - 280, In signOut() inside ClerkExpoModule.kt, after calling
Clerk.auth.signOut() make sure to also remove the persisted DEVICE_TOKEN from
Android storage (same SharedPreferences key/mechanism used elsewhere) so the
native SDK doesn't rehydrate a stale token; update the Clerk.auth.signOut()
success path to call the same SharedPreferences removal logic that the "not
initialized" branch uses (i.e., retrieve the same prefs, remove the DEVICE_TOKEN
key and apply/commit) before resolving the promise. Ensure you reference the
DEVICE_TOKEN constant and reuse the existing SharedPreferences helper code (or
mirror its remove logic) so both initialized and uninitialized signOut paths
behave identically.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/expo/ios/ClerkViewFactory.swift`:
- Around line 199-205: The getSession() method currently only guards on
Clerk.shared.session and can return stale data if Self.clerkConfigured is false;
update the guard in public func getSession() async -> [String: Any]? to check
both Self.clerkConfigured and let session = Clerk.shared.session (i.e., use
guard Self.clerkConfigured, let session = Clerk.shared.session else { return nil
}) before calling Self.sessionPayload(from: session, user: session.user ??
Clerk.shared.user) so it matches the template behavior.

---

Outside diff comments:
In `@packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt`:
- Around line 271-280: In signOut() inside ClerkExpoModule.kt, after calling
Clerk.auth.signOut() make sure to also remove the persisted DEVICE_TOKEN from
Android storage (same SharedPreferences key/mechanism used elsewhere) so the
native SDK doesn't rehydrate a stale token; update the Clerk.auth.signOut()
success path to call the same SharedPreferences removal logic that the "not
initialized" branch uses (i.e., retrieve the same prefs, remove the DEVICE_TOKEN
key and apply/commit) before resolving the promise. Ensure you reference the
DEVICE_TOKEN constant and reuse the existing SharedPreferences helper code (or
mirror its remove logic) so both initialized and uninitialized signOut paths
behave identically.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ba48ae8-ab0b-4854-ade3-4fed915c855c

📥 Commits

Reviewing files that changed from the base of the PR and between 5b3a88f and c26e1ce.

📒 Files selected for processing (4)
  • packages/expo/android/build.gradle
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/ios/ClerkViewFactory.swift
  • packages/expo/ios/templates/ClerkViewFactory.swift

Match the template's guard so getSession() returns nil after signOut()
resets clerkConfigured, preventing stale session data.
@chriscanin

This comment was marked as outdated.

@github-actions

This comment has been minimized.

Clerk.shared force-unwraps and crashes if accessed before configure().
The JS provider calls signOut() on native before Clerk is initialized,
causing an immediate crash on launch. Guard all Clerk.shared access
behind Self.clerkConfigured check.
@chriscanin
Copy link
Member Author

!snapshot

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/expo/ios/templates/ClerkViewFactory.swift (1)

327-334: ⚠️ Potential issue | 🔴 Critical

Always clear native state even when auth.signOut throws.

Line 330 can throw before Lines 332-334 run. A transient sign-out failure would leave the keychain and clerkConfigured flag intact, so stale native auth can rehydrate on the next open.

Suggested fix
   `@MainActor`
   public func signOut() async throws {
-    if Self.clerkConfigured {
-      if let sessionId = Clerk.shared.session?.id {
-        try await Clerk.shared.auth.signOut(sessionId: sessionId)
-      }
-      Clerk.clearAllKeychainItems()
-    }
-    Self.clerkConfigured = false
+    guard Self.clerkConfigured else { return }
+    defer {
+      Clerk.clearAllKeychainItems()
+      Self.clerkConfigured = false
+    }
+
+    if let sessionId = Clerk.shared.session?.id {
+      try await Clerk.shared.auth.signOut(sessionId: sessionId)
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/expo/ios/templates/ClerkViewFactory.swift` around lines 327 - 334,
In signOut(), ensure native state is always cleared even if
Clerk.shared.auth.signOut(sessionId:) throws by moving
Clerk.clearAllKeychainItems() and Self.clerkConfigured = false into a
guaranteed-finalization path (e.g., a defer block at the start of public func
signOut() or a finally-equivalent after a do/catch) so that regardless of errors
from Clerk.shared.auth.signOut the keychain is cleared and the clerkConfigured
flag is reset; reference the signOut function, Clerk.shared.auth.signOut,
Clerk.clearAllKeychainItems(), and Self.clerkConfigured when locating and
applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/expo/ios/ClerkViewFactory.swift`:
- Around line 56-58: The code in ClerkViewFactory silently swallows errors from
Clerk.shared.refreshClient() (called in shouldRefreshConfiguredClient(for:)) and
only clears keychain credentials in a conditional block that can be skipped if
clerkConfigured is false or if auth.signOut() throws; change the refresh call to
propagate failures (replace try? await Clerk.shared.refreshClient() with a
throwing try await and let configure() surface the error or handle it
explicitly), and ensure keychain cleanup always runs on sign-out by moving the
keychain deletion into a guaranteed cleanup path (use defer or a
finally-equivalent) so that keychain removal executes regardless of
clerkConfigured or auth.signOut() throwing; update the related methods
(configure(), the branch that calls shouldRefreshConfiguredClient(for:), and the
sign-out flow invoking auth.signOut()) accordingly and add tests for
pre-configure sign-out and refresh/sign-out error propagation.

In `@packages/expo/ios/templates/ClerkViewFactory.swift`:
- Around line 61-63: In ClerkViewFactory.configure(), do not swallow errors from
Clerk.shared.refreshClient(): replace the 'try? await' usage with a propagating
call (use 'try await' so errors bubble up from refreshClient()) inside the
existing early-return branch that checks Self.clerkConfigured and bearerToken;
this ensures the async throws contract of configure() is honored and callers
(e.g., ClerkExpoModule) can handle failures.

---

Duplicate comments:
In `@packages/expo/ios/templates/ClerkViewFactory.swift`:
- Around line 327-334: In signOut(), ensure native state is always cleared even
if Clerk.shared.auth.signOut(sessionId:) throws by moving
Clerk.clearAllKeychainItems() and Self.clerkConfigured = false into a
guaranteed-finalization path (e.g., a defer block at the start of public func
signOut() or a finally-equivalent after a do/catch) so that regardless of errors
from Clerk.shared.auth.signOut the keychain is cleared and the clerkConfigured
flag is reset; reference the signOut function, Clerk.shared.auth.signOut,
Clerk.clearAllKeychainItems(), and Self.clerkConfigured when locating and
applying the change.

ℹ️ Review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17a948ad-051e-4b69-81ec-445bf8a23a37

📥 Commits

Reviewing files that changed from the base of the PR and between e0c2b62 and df51c1e.

📒 Files selected for processing (2)
  • packages/expo/ios/ClerkViewFactory.swift
  • packages/expo/ios/templates/ClerkViewFactory.swift

If auth.signOut() throws (e.g. network error), clearAllKeychainItems()
was skipped, leaving stale tokens. Using defer ensures keychain is
always cleared regardless of whether the remote sign-out succeeds.
@chriscanin
Copy link
Member Author

!snapshot

@github-actions
Copy link
Contributor

Hey @chriscanin - the snapshot version command generated the following package versions:

Package Version
@clerk/agent-toolkit 0.3.7-snapshot.v20260325172303
@clerk/astro 3.0.7-snapshot.v20260325172303
@clerk/backend 3.2.3-snapshot.v20260325172303
@clerk/chrome-extension 3.1.5-snapshot.v20260325172303
@clerk/clerk-js 6.3.3-snapshot.v20260325172303
@clerk/dev-cli 0.1.1-snapshot.v20260325172303
@clerk/expo 3.1.5-snapshot.v20260325172303
@clerk/expo-passkeys 1.0.7-snapshot.v20260325172303
@clerk/express 2.0.7-snapshot.v20260325172303
@clerk/fastify 3.1.5-snapshot.v20260325172303
@clerk/hono 0.1.5-snapshot.v20260325172303
@clerk/localizations 4.2.4-snapshot.v20260325172303
@clerk/msw 0.0.7-snapshot.v20260325172303
@clerk/nextjs 7.0.7-snapshot.v20260325172303
@clerk/nuxt 2.0.7-snapshot.v20260325172303
@clerk/react 6.1.3-snapshot.v20260325172303
@clerk/react-router 3.0.7-snapshot.v20260325172303
@clerk/shared 4.3.3-snapshot.v20260325172303
@clerk/tanstack-react-start 1.0.7-snapshot.v20260325172303
@clerk/testing 2.0.7-snapshot.v20260325172303
@clerk/ui 1.2.4-snapshot.v20260325172303
@clerk/upgrade 2.0.3-snapshot.v20260325172303
@clerk/vue 2.0.7-snapshot.v20260325172303

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.3.7-snapshot.v20260325172303 --save-exact

@clerk/astro

npm i @clerk/astro@3.0.7-snapshot.v20260325172303 --save-exact

@clerk/backend

npm i @clerk/backend@3.2.3-snapshot.v20260325172303 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@3.1.5-snapshot.v20260325172303 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@6.3.3-snapshot.v20260325172303 --save-exact

@clerk/dev-cli

npm i @clerk/dev-cli@0.1.1-snapshot.v20260325172303 --save-exact

@clerk/expo

npm i @clerk/expo@3.1.5-snapshot.v20260325172303 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@1.0.7-snapshot.v20260325172303 --save-exact

@clerk/express

npm i @clerk/express@2.0.7-snapshot.v20260325172303 --save-exact

@clerk/fastify

npm i @clerk/fastify@3.1.5-snapshot.v20260325172303 --save-exact

@clerk/hono

npm i @clerk/hono@0.1.5-snapshot.v20260325172303 --save-exact

@clerk/localizations

npm i @clerk/localizations@4.2.4-snapshot.v20260325172303 --save-exact

@clerk/msw

npm i @clerk/msw@0.0.7-snapshot.v20260325172303 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@7.0.7-snapshot.v20260325172303 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@2.0.7-snapshot.v20260325172303 --save-exact

@clerk/react

npm i @clerk/react@6.1.3-snapshot.v20260325172303 --save-exact

@clerk/react-router

npm i @clerk/react-router@3.0.7-snapshot.v20260325172303 --save-exact

@clerk/shared

npm i @clerk/shared@4.3.3-snapshot.v20260325172303 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@1.0.7-snapshot.v20260325172303 --save-exact

@clerk/testing

npm i @clerk/testing@2.0.7-snapshot.v20260325172303 --save-exact

@clerk/ui

npm i @clerk/ui@1.2.4-snapshot.v20260325172303 --save-exact

@clerk/upgrade

npm i @clerk/upgrade@2.0.3-snapshot.v20260325172303 --save-exact

@clerk/vue

npm i @clerk/vue@2.0.7-snapshot.v20260325172303 --save-exact

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