Summary
On a real Android device, @clerk/expo's useAuth().isLoaded never becomes true on a cold start when the device is already connected to Wi‑Fi, so any gate like if (!isLoaded) return <Spinner/> spins forever. The device network is completely healthy. Toggling Wi‑Fi off/on while it's stuck immediately unblocks it and the app proceeds. It does not reproduce on the Android emulator (its network state flickers during boot), which makes it easy to miss in QA but it hits real users.
This looks like the native clerk-android connectivity monitor waiting for a ConnectivityManager onAvailable callback that never fires for a network that was already available before the callback was registered (i.e. every normal cold start on stable Wi‑Fi).
Environment
|
|
@clerk/expo |
3.5.2 (also repro'd intent on 3.4.3; canary 3.5.3 unchanged) |
| native |
com.clerk:clerk-android-api / -ui 1.0.28 (also 1.0.30) |
| Expo SDK |
56 (New Architecture / bridgeless, React 19, RN 0.85.3) |
| Device |
Samsung Galaxy Z Fold 3 (SM‑F926B), real device, stable Wi‑Fi |
| Clerk instance |
development (pk_test_…) |
| Build |
EAS preview (release-style standalone) |
Steps to reproduce
@clerk/expo with a standard <ClerkProvider publishableKey tokenCache> and an isLoaded gate (e.g. expo-router group layout returns a spinner while !isLoaded).
- Build a release/standalone Android build, install on a real device on stable Wi‑Fi.
- Cold-launch the app (let Wi‑Fi settle first; don't toggle anything).
Actual: stuck on the loading state forever; isLoaded stays false.
Expected: isLoaded resolves (network is reachable) and the app proceeds, as it does on web/emulator.
Workaround that proves the cause: while stuck, toggle Wi‑Fi (or airplane mode) off→on. isLoaded resolves within ~1s and the app proceeds. Verified via adb shell svc wifi disable && svc wifi enable.
Evidence the network is fine
From the same device, while the app is hung:
# DNS + HTTPS to the Frontend API, x3
$ adb shell curl -s -m 12 -o /dev/null -w '%{http_code} %{time_total}s' \
'https://<instance>.clerk.accounts.dev/v1/environment?_clerk_js_version=5'
200 1.07s
200 0.62s
200 0.62s
DNS resolves, TLS connects, ping to the Clerk CDN ~7ms, Private DNS off, device clock correct.
Relevant logcat (cold start, app pid)
ReactNativeJS: Running "main"
ReactNativeJS: Clerk: Clerk has been loaded with development keys...
ConnectivityManager: registerNetworkCallback(...)
com.clerk.api.configuration.connectivity.NetworkConnectivityMonitor.configure(...)
com.clerk.api.configuration.ConfigurationManager.configure(...)
com.clerk.api.Clerk.initialize(...)
expo.modules.clerk.ClerkExpoModule$configure$1.invokeSuspend(ClerkExpoModule.kt:124)
…then nothing further — init never completes, so isLoaded never flips. After a Wi‑Fi toggle, init completes and the sign-in screen renders.
Things we tried (that did NOT fix it)
__experimental_resourceCache (offline-support): no change on a clean install — it only helps once a successful load has seeded the cache, but the initial load is exactly what hangs, so the cache never seeds.
- Excluding the native module to run JS-only (
expo.autolinking.android.exclude: ["@clerk/expo"]): crashes with Error: Cannot find native module 'ClerkExpo' because dist/specs/NativeClerkModule.android.js calls requireNativeModule("ClerkExpo") at module-eval (top level, no try/catch), so importing @clerk/expo throws when the native module is absent. (Separately: a top-level requireNativeModule with no graceful fallback seems risky.)
- Upgrading to canary
3.5.3 / native 1.0.30 — same clerk-android 1.0.28-era connectivity code, no change.
Likely root cause / suggested fix
The native connectivity monitor appears to gate initialization on a network‑availability event rather than reading the current network synchronously. On a cold start where the network is already connected, registerNetworkCallback's onAvailable may not fire (it fires for networks that become available after registration), so init waits indefinitely. Reading the active network at registration time (e.g. ConnectivityManager.getActiveNetwork() / registerDefaultNetworkCallback, or seeding the initial online state) — and/or adding a timeout so init resolves even if the monitor never reports — would fix the cold-start hang.
Happy to provide a minimal repro or more logs. Thanks!
Summary
On a real Android device,
@clerk/expo'suseAuth().isLoadednever becomestrueon a cold start when the device is already connected to Wi‑Fi, so any gate likeif (!isLoaded) return <Spinner/>spins forever. The device network is completely healthy. Toggling Wi‑Fi off/on while it's stuck immediately unblocks it and the app proceeds. It does not reproduce on the Android emulator (its network state flickers during boot), which makes it easy to miss in QA but it hits real users.This looks like the native
clerk-androidconnectivity monitor waiting for aConnectivityManageronAvailablecallback that never fires for a network that was already available before the callback was registered (i.e. every normal cold start on stable Wi‑Fi).Environment
@clerk/expo3.5.3unchanged)com.clerk:clerk-android-api/-ui1.0.28 (also 1.0.30)pk_test_…)preview(release-style standalone)Steps to reproduce
@clerk/expowith a standard<ClerkProvider publishableKey tokenCache>and anisLoadedgate (e.g. expo-router group layout returns a spinner while!isLoaded).Actual: stuck on the loading state forever;
isLoadedstaysfalse.Expected:
isLoadedresolves (network is reachable) and the app proceeds, as it does on web/emulator.Workaround that proves the cause: while stuck, toggle Wi‑Fi (or airplane mode) off→on.
isLoadedresolves within ~1s and the app proceeds. Verified viaadb shell svc wifi disable && svc wifi enable.Evidence the network is fine
From the same device, while the app is hung:
DNS resolves, TLS connects, ping to the Clerk CDN ~7ms, Private DNS off, device clock correct.
Relevant logcat (cold start, app pid)
…then nothing further — init never completes, so
isLoadednever flips. After a Wi‑Fi toggle, init completes and the sign-in screen renders.Things we tried (that did NOT fix it)
__experimental_resourceCache(offline-support): no change on a clean install — it only helps once a successful load has seeded the cache, but the initial load is exactly what hangs, so the cache never seeds.expo.autolinking.android.exclude: ["@clerk/expo"]): crashes withError: Cannot find native module 'ClerkExpo'becausedist/specs/NativeClerkModule.android.jscallsrequireNativeModule("ClerkExpo")at module-eval (top level, no try/catch), so importing@clerk/expothrows when the native module is absent. (Separately: a top-levelrequireNativeModulewith no graceful fallback seems risky.)3.5.3/ native1.0.30— sameclerk-android1.0.28-era connectivity code, no change.Likely root cause / suggested fix
The native connectivity monitor appears to gate initialization on a network‑availability event rather than reading the current network synchronously. On a cold start where the network is already connected,
registerNetworkCallback'sonAvailablemay not fire (it fires for networks that become available after registration), so init waits indefinitely. Reading the active network at registration time (e.g.ConnectivityManager.getActiveNetwork()/registerDefaultNetworkCallback, or seeding the initial online state) — and/or adding a timeout so init resolves even if the monitor never reports — would fix the cold-start hang.Happy to provide a minimal repro or more logs. Thanks!