ARTICLE AD BOX
I have an Expo SDK 54 app using Supabase Auth with magic-link sign-in via implicit flow. When the user taps a magic link, my deep-link callback receives the URL, parses the fragment correctly, and calls supabase.auth.setSession() — but the call hangs silently. Never resolves, never throws. App stays stuck on a "Setting up your account" loading screen indefinitely.
Previously hit the same hang at exchangeCodeForSession with PKCE flow, which is why I pivoted to implicit. Same hang, different boundary.
Stack
Expo SDK 54
React Native (Hermes engine)
@supabase/supabase-js v2.x (latest)
expo-router for navigation
iOS dev client built via EAS Build (custom URL scheme)
AsyncStorage adapter for session storage
react-native-get-random-values polyfill installed and imported first
Auth flow
Gate → Email Entry → SEND LINK → magic link arrives → tap link → deep link opens app → callback parses #access_token → setSession() → HANG
What works (verified with diagnostic logs)
Validation queries against application tables (Supabase round-trip works fine)
signInWithOtp magic-link generation
Deep link opens app correctly with myapp://auth/callback#access_token=...&refresh_token=...
URL fragment parsing extracts tokens correctly
Error path works perfectly — when token is invalid (otp_expired), callback receives the error in fragment, parses it, displays the error, routes back to email entry. All [callback] logs fire in sequence.
What hangs
setSession({ access_token, refresh_token }) with valid unconsumed tokens
Same pattern previously observed with PKCE exchangeCodeForSession
Both auth functions hang silently when handed valid tokens
Diagnostic logs
When tapping a clean unconsumed magic link (success path):
[callback] run() entered, url: myapp://auth/callback#access_token=eyJ...&refresh_token=...&token_type=bearer&expires_in=3600 [callback] parsed fragment, has tokens: true [callback] setSession startThen nothing. No setSession done log ever appears.
When tapping a consumed magic link (Gmail pre-fetched it):
[callback] run() entered, url: myapp://auth/callback#error=access_denied&error_code=otp_expired&... [callback] parsed fragment, has tokens: false errorCode: access_denied [callback] error in fragment: access_denied Email link is invalid or has expired [callback] routing to: /(auth)/email (error path)Full happy path here - error case works perfectly. Only the success case hangs.
Things I've tried
PKCE flow with react-native-get-random-values polyfill (provides crypto.getRandomValues but NOT crypto.subtle)
Pivoted to implicit flow (flowType: 'implicit') to avoid PKCE entirely
Replaced SecureStore + chunking adapter with simple AsyncStorage adapter
Fresh EAS dev client builds (multiple)
Clean app installs (uninstall + reinstall to wipe AsyncStorage)
Multiple email delivery methods to ensure token isn't pre-consumed (temp-mail.org, copying link via Notes)
Supabase client config
lib/supabase.ts
import 'react-native-get-random-values'; import 'react-native-url-polyfill/auto'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient } from '@supabase/supabase-js'; export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: AsyncStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, // we handle deep link callback ourselves flowType: 'implicit', }, });Callback handler
app/auth/callback.tsx (relevant excerpt)
const url = Linking.useURL(); useEffect(() => { if (!url) return; if (processedRef.current === url) return; processedRef.current = url; const run = async () => { console.log('[callback] run() entered, url:', url); const fragment = parseAuthFragment(url); console.log('[callback] parsed fragment, has tokens:', !!fragment?.accessToken); if (fragment?.error) { // ... error path works correctly ... return; } if (!fragment?.accessToken || !fragment?.refreshToken) { // ... missing tokens path ... return; } console.log('[callback] setSession start'); const { error } = await supabase.auth.setSession({ access_token: fragment.accessToken, refresh_token: fragment.refreshToken, }); console.log('[callback] setSession done:', error); // ← NEVER PRINTS // ... rest of flow never executes ... }; run(); }, [url]);Metro warning that may be related
Throughout the app I see this warning fire whenever supabase-js initializes:
WARN WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.
This warning makes sense for PKCE (which needs crypto.subtle.digest for S256). But after pivoting to implicit flow, the warning still fires. And setSession still hangs. Suggests supabase-js is touching crypto.subtle somewhere even on implicit flow paths — possibly during session initialization, refresh token nonce generation, or internal state ID generation — and silently hanging on the missing API.
Question
Has anyone hit setSession (or exchangeCodeForSession) hanging silently in React Native + Hermes specifically with Supabase magic link auth?
I suspect missing crypto.subtle (WebCrypto's hash/digest APIs aren't polyfilled by react-native-get-random-values — only crypto.getRandomValues).
If you've solved this:
Did adding a fuller WebCrypto polyfill (expo-crypto-as-shim, crypto-browserify, custom shim using expo-crypto's digestStringAsync) help?
Did downgrading @supabase/supabase-js to a specific version work?
Is there a known-good combination of Expo SDK + supabase-js + auth flow type for React Native that doesn't hit this?
Or should I move to email/password auth as a workaround?
I have the architecture working architecturally (error path, fragment parsing, deep linking) — just blocked on this one silent hang for the success path.
