Skip to content

@clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

Description

@brandonatdashtag

Preliminary Checks

Reproduction

No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

Publishable key

pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

Description

After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

Environment

ComponentVersion
@clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
clerk-ios (SwiftPM)1.0.0
Expo SDK54
React Native0.81
iOS (physical device)26.3.1 (iPhone 15 Pro Max)
macOS26.3

Setup

  • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
  • <AuthView mode="signInOrUp" /> from @clerk/expo/native
  • useAuth({ treatPendingAsSignedOut: false }) everywhere
  • @clerk/expo plugin in app.config.ts plugins array
  • iOS bundle id com.dashtag.app.dev, signed with our developer cert

Diagnosis (with detailed traces)

I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

Cold-start trace (consistent pattern):

mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
bootstrap: configure resolved ← native promise resolves cleanly
mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
bootstrap: poll #1 getSession() -> undefined
bootstrap: poll #5 getSession() -> undefined
bootstrap: poll #15 getSession() -> undefined
bootstrap: poll #30 getSession() -> undefined
bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)

Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

What's interesting

  1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

  2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

  3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

  4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

Suggested fix

Either:

  • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
  • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
  • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

Related issues

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
    Skip to content

    @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

    Description

    @brandonatdashtag

    Preliminary Checks

    Reproduction

    No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

    Publishable key

    pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

    Description

    After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

    Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

    Environment

    ComponentVersion
    @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
    clerk-ios (SwiftPM)1.0.0
    Expo SDK54
    React Native0.81
    iOS (physical device)26.3.1 (iPhone 15 Pro Max)
    macOS26.3

    Setup

    • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
    • <AuthView mode="signInOrUp" /> from @clerk/expo/native
    • useAuth({ treatPendingAsSignedOut: false }) everywhere
    • @clerk/expo plugin in app.config.ts plugins array
    • iOS bundle id com.dashtag.app.dev, signed with our developer cert

    Diagnosis (with detailed traces)

    I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

    Cold-start trace (consistent pattern):

    mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
    bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
    bootstrap: configure resolved ← native promise resolves cleanly
    mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
    bootstrap: poll #1 getSession() -> undefined
    bootstrap: poll #5 getSession() -> undefined
    bootstrap: poll #15 getSession() -> undefined
    bootstrap: poll #30 getSession() -> undefined
    bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
    

    Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

    Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

    Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

    What's interesting

    1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

    2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

    3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

    4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

    Suggested fix

    Either:

    • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
    • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
    • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

    Related issues

    Metadata

    Metadata

    Assignees

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
      Skip to content

      @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

      Description

      @brandonatdashtag

      Preliminary Checks

      Reproduction

      No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

      Publishable key

      pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

      Description

      After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

      Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

      Environment

      ComponentVersion
      @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
      clerk-ios (SwiftPM)1.0.0
      Expo SDK54
      React Native0.81
      iOS (physical device)26.3.1 (iPhone 15 Pro Max)
      macOS26.3

      Setup

      • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
      • <AuthView mode="signInOrUp" /> from @clerk/expo/native
      • useAuth({ treatPendingAsSignedOut: false }) everywhere
      • @clerk/expo plugin in app.config.ts plugins array
      • iOS bundle id com.dashtag.app.dev, signed with our developer cert

      Diagnosis (with detailed traces)

      I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

      Cold-start trace (consistent pattern):

      mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
      bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
      bootstrap: configure resolved ← native promise resolves cleanly
      mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
      bootstrap: poll #1 getSession() -> undefined
      bootstrap: poll #5 getSession() -> undefined
      bootstrap: poll #15 getSession() -> undefined
      bootstrap: poll #30 getSession() -> undefined
      bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
      

      Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

      Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

      Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

      What's interesting

      1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

      2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

      3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

      4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

      Suggested fix

      Either:

      • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
      • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
      • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

      Related issues

      Metadata

      Metadata

      Assignees

      Labels

      No labels
      No labels

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
        Skip to content

        @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

        Description

        @brandonatdashtag

        Preliminary Checks

        Reproduction

        No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

        Publishable key

        pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

        Description

        After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

        Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

        Environment

        ComponentVersion
        @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
        clerk-ios (SwiftPM)1.0.0
        Expo SDK54
        React Native0.81
        iOS (physical device)26.3.1 (iPhone 15 Pro Max)
        macOS26.3

        Setup

        • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
        • <AuthView mode="signInOrUp" /> from @clerk/expo/native
        • useAuth({ treatPendingAsSignedOut: false }) everywhere
        • @clerk/expo plugin in app.config.ts plugins array
        • iOS bundle id com.dashtag.app.dev, signed with our developer cert

        Diagnosis (with detailed traces)

        I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

        Cold-start trace (consistent pattern):

        mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
        bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
        bootstrap: configure resolved ← native promise resolves cleanly
        mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
        bootstrap: poll #1 getSession() -> undefined
        bootstrap: poll #5 getSession() -> undefined
        bootstrap: poll #15 getSession() -> undefined
        bootstrap: poll #30 getSession() -> undefined
        bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
        

        Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

        Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

        Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

        What's interesting

        1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

        2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

        3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

        4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

        Suggested fix

        Either:

        • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
        • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
        • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

        Related issues

        Metadata

        Metadata

        Assignees

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
          Skip to content

          @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

          Description

          @brandonatdashtag

          Preliminary Checks

          Reproduction

          No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

          Publishable key

          pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

          Description

          After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

          Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

          Environment

          ComponentVersion
          @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
          clerk-ios (SwiftPM)1.0.0
          Expo SDK54
          React Native0.81
          iOS (physical device)26.3.1 (iPhone 15 Pro Max)
          macOS26.3

          Setup

          • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
          • <AuthView mode="signInOrUp" /> from @clerk/expo/native
          • useAuth({ treatPendingAsSignedOut: false }) everywhere
          • @clerk/expo plugin in app.config.ts plugins array
          • iOS bundle id com.dashtag.app.dev, signed with our developer cert

          Diagnosis (with detailed traces)

          I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

          Cold-start trace (consistent pattern):

          mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
          bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
          bootstrap: configure resolved ← native promise resolves cleanly
          mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
          bootstrap: poll #1 getSession() -> undefined
          bootstrap: poll #5 getSession() -> undefined
          bootstrap: poll #15 getSession() -> undefined
          bootstrap: poll #30 getSession() -> undefined
          bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
          

          Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

          Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

          Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

          What's interesting

          1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

          2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

          3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

          4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

          Suggested fix

          Either:

          • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
          • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
          • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

          Related issues

          Metadata

          Metadata

          Assignees

          Labels

          No labels
          No labels

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
            Skip to content

            @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

            Description

            @brandonatdashtag

            Preliminary Checks

            Reproduction

            No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

            Publishable key

            pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

            Description

            After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

            Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

            Environment

            ComponentVersion
            @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
            clerk-ios (SwiftPM)1.0.0
            Expo SDK54
            React Native0.81
            iOS (physical device)26.3.1 (iPhone 15 Pro Max)
            macOS26.3

            Setup

            • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
            • <AuthView mode="signInOrUp" /> from @clerk/expo/native
            • useAuth({ treatPendingAsSignedOut: false }) everywhere
            • @clerk/expo plugin in app.config.ts plugins array
            • iOS bundle id com.dashtag.app.dev, signed with our developer cert

            Diagnosis (with detailed traces)

            I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

            Cold-start trace (consistent pattern):

            mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
            bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
            bootstrap: configure resolved ← native promise resolves cleanly
            mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
            bootstrap: poll #1 getSession() -> undefined
            bootstrap: poll #5 getSession() -> undefined
            bootstrap: poll #15 getSession() -> undefined
            bootstrap: poll #30 getSession() -> undefined
            bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
            

            Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

            Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

            Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

            What's interesting

            1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

            2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

            3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

            4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

            Suggested fix

            Either:

            • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
            • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
            • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

            Related issues

            Metadata

            Metadata

            Assignees

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
              Skip to content

              @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

              Description

              @brandonatdashtag

              Preliminary Checks

              Reproduction

              No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

              Publishable key

              pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

              Description

              After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

              Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

              Environment

              ComponentVersion
              @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
              clerk-ios (SwiftPM)1.0.0
              Expo SDK54
              React Native0.81
              iOS (physical device)26.3.1 (iPhone 15 Pro Max)
              macOS26.3

              Setup

              • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
              • <AuthView mode="signInOrUp" /> from @clerk/expo/native
              • useAuth({ treatPendingAsSignedOut: false }) everywhere
              • @clerk/expo plugin in app.config.ts plugins array
              • iOS bundle id com.dashtag.app.dev, signed with our developer cert

              Diagnosis (with detailed traces)

              I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

              Cold-start trace (consistent pattern):

              mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
              bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
              bootstrap: configure resolved ← native promise resolves cleanly
              mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
              bootstrap: poll #1 getSession() -> undefined
              bootstrap: poll #5 getSession() -> undefined
              bootstrap: poll #15 getSession() -> undefined
              bootstrap: poll #30 getSession() -> undefined
              bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
              

              Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

              Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

              Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

              What's interesting

              1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

              2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

              3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

              4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

              Suggested fix

              Either:

              • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
              • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
              • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

              Related issues

              Metadata

              Metadata

              Assignees

              Labels

              No labels
              No labels

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) · Issue #8441 · clerk/javascript · GitHub
                Skip to content

                @clerk/expo v3 iOS: AuthView session not restored on cold start despite valid JWT in keychain (works on Android, same code) #8441

                Description

                @brandonatdashtag

                Preliminary Checks

                Reproduction

                No standalone repro repo, but reproduces in any Expo SDK 54 + @clerk/expo 3.2.7 app on iOS using <AuthView> from @clerk/expo/native. We've reproduced it in two separate apps in our monorepo (one consumer app, one chat app) with completely different code.

                Publishable key

                pk_live_Y2xlcmsudGhlZGFzaHRhZy5jb20k

                Description

                After cold-start (force-quit + relaunch) on iOS only, the user's Clerk session is not restored from the JWT in expo-secure-store. The same code works perfectly on Android — sessions persist as expected.

                Symptoms: user sees the unauthenticated/sign-in screen on every app launch, even though __clerk_client_jwt is intact in keychain.

                Environment

                ComponentVersion
                @clerk/expo3.2.7 (also reproduced on 3.2.8-canary.v20260501194208)
                clerk-ios (SwiftPM)1.0.0
                Expo SDK54
                React Native0.81
                iOS (physical device)26.3.1 (iPhone 15 Pro Max)
                macOS26.3

                Setup

                • <ClerkProvider publishableKey={CLERK_PUBLISHABLE_KEY} tokenCache={tokenCache}> (default tokenCache from @clerk/expo/token-cache)
                • <AuthView mode="signInOrUp" /> from @clerk/expo/native
                • useAuth({ treatPendingAsSignedOut: false }) everywhere
                • @clerk/expo plugin in app.config.ts plugins array
                • iOS bundle id com.dashtag.app.dev, signed with our developer cert

                Diagnosis (with detailed traces)

                I patched node_modules/@clerk/expo/dist/provider/ClerkProvider.js to add console logging and isolated the bug to native ClerkExpo.getSession() returning undefined indefinitely after cold-start, despite configure(pk, jwt) resolving successfully and a valid JWT being passed.

                Cold-start trace (consistent pattern):

                mount | isSignedIn=undefined isLoaded=false jwt=eyJhbGciOiJSUzI1NiIs...(len 518)
                bootstrap: calling native configure with bearerToken= eyJhbGciOiJSUzI1NiIs...(len 518)
                bootstrap: configure resolved ← native promise resolves cleanly
                mount | isSignedIn=false isLoaded=true ← Clerk JS finishes loading
                bootstrap: poll #1 getSession() -> undefined
                bootstrap: poll #5 getSession() -> undefined
                bootstrap: poll #15 getSession() -> undefined
                bootstrap: poll #30 getSession() -> undefined
                bootstrap: NO SESSION after 30 polls (3s timeout — original behavior)
                

                Workaround attempt #1 — extended polling 3s → 30s: marginal improvement, not reliable.

                Workaround attempt #2 — re-call configure(pk, jwt) repeatedly: sometimes triggers eventual restore (~30% of attempts), but unreliable. After 60s of retries (20 × 3s intervals), getSession() still returns undefined for fresh sessions created moments before the test.

                Workaround attempt #3 — block spurious setActive({session: null}): never observed this code path being triggered; the bug is purely in getSession() returning null.

                What's interesting

                1. Same code, same JWT length, same Clerk user — works on Android, fails on iOS. I tested side-by-side with the same Metro instance serving both an Android device and an iPhone using the exact same JS bundle. Android: session restored within 1-2s of cold start, every time. iOS: session restored maybe 30% of the time, often never.

                2. The JWT in keychain is fresh and valid. Just-created sessions (signed in moments before the test) fail to restore.

                3. The ClerkViewFactory.swiftsyncTokenState logic looks suspect. Lines 72-91 of node_modules/@clerk/expo/ios/ClerkViewFactory.swift compare the JS-keychain JWT against the nativeDeviceToken keychain entry; if they differ, clearCachedClerkData() is called to wipe cachedClient and cachedEnvironment. After the cache wipe, the SDK relies on a network round-trip to repopulate. If that round-trip is slow or fails silently, Clerk.shared.session stays nil and getSession() returns null. This iOS-specific keychain-dance code does not have an Android equivalent — which matches our observation that Android always works.

                4. The configure() promise resolving says nothing about whether the session is loaded. Clerk.shared.session might still be nil when configure() resolves.

                Suggested fix

                Either:

                • Make ClerkExpo.configure(pk, bearerToken) not resolve until Clerk.shared.session is non-nil OR a definitive failure has occurred (currently waitForLoadedSession() polls but the promise resolves regardless).
                • Surface an onAuthStateChange event when the native SDK eventually loads the session post-configure(), so the JS bootstrap can react via useNativeAuthEvents instead of polling getSession().
                • Avoid clearCachedClerkData() on cold-start when existingToken == nil (first-launch case) — only clear when the token actually changed and there's a stale cached client to reconcile.

                Related issues

                Metadata

                Metadata

                Assignees

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions