This report was written by Claude, but the behavior and the source references were verified manually on real devices.
Email link (magic link) sign-in completes on Android but silently fails on iOS when using the prebuilt <AuthView /> from @clerk/expo/native. The app reopens on the callback URL and no session is created.
Filing without a minimal reproduction — apologies. This was found and verified on a production Expo app, and the analysis below is from reading the shipped SDK source, so I wanted to get it in front of you rather than sit on it. Happy to build a standalone repro if that's what it takes to get it looked at. Publishable key omitted for the same reason (production instance).
What happens
- User starts email link sign-in from
<AuthView /> in an Expo app.
- The email opens Clerk's hosted page, which verifies and shows "Return to App".
- That button deep-links back to the app on the bundle identifier scheme, e.g.
com.myapp.bundleid://callback?flow_id=…&approval_token=….
- Android: the session is created and the user is signed in.
- iOS: the app comes to the foreground, but no session is created and the user is still signed out. No error surfaces.
Why (as far as I can tell)
clerk-ios completes the flow only from the callback's flow_id + approval_token — MagicLink.swift has no polling fallback — and its single intake path is Clerk.handle(_ url:). In AuthView.swift, the only caller is SwiftUI's .onOpenURL:
.onOpenURL { url in
Task {
do { try await clerk.handle(url) }
catch { self.error = error }
}
}
.onOpenURL is driven by the SwiftUI app lifecycle. Expo/React Native apps use the UIKit app delegate, so URLs arrive via application(_:open:options:) and that modifier never fires for the hosted SwiftUI view — nothing ever calls Clerk.handle(url).
Nothing in @clerk/expo bridges the gap: searching packages/expo on main returns zero references to onOpenURL, openURL, handleURL, or onNewIntent. Android is unaffected because clerk-android registers its own clerk:// intent filter and receives the callback natively — hence the platform split.
Expected behavior
@clerk/expo forwards inbound URLs to the native SDK on iOS the way clerk-android already does on Android, so email link sign-in completes on both platforms.
An ExpoAppDelegateSubscriber implementing application(_:open:options:) and calling Clerk.shared.handle(url) looks like it would cover it — Clerk.shared is public and already configured by ClerkNativeBridge, and returning false would leave the URL available to React Native's Linking, with clerk-ios's own URLHandlingCoordinator deduping any double handling. There's also a com.clerk.expo.native-sdk.did-configure notification that could handle a cold launch arriving before the SDK is configured.
Workaround
A local Expo module doing exactly the above. Worth noting for anyone else hitting this: the app also needs a route for the callback URL, or expo-router renders +not-found on top of it — on Android that means a "page does not exist" screen over a session that was just created successfully.
Environment
System:
OS: macOS 26.5.2
CPU: (12) arm64 Apple M2 Pro
Binaries:
Node: 24.15.0
npm: 11.12.1
pnpm: 11.1.2
npmPackages:
@clerk/expo: ^3.7.1 => 3.7.1
@clerk/shared: 4.25.0 => 4.25.0
expo: ~57.0.12 => 57.0.12
expo-router: ~57.0.12 => 57.0.12
react: 19.2.3 => 19.2.3
react-native: 0.86.2 => 0.86.2
Also reproduced on @clerk/expo@4.2.7, and packages/expo on main has the same gap.
Email link (magic link) sign-in completes on Android but silently fails on iOS when using the prebuilt
<AuthView />from@clerk/expo/native. The app reopens on the callback URL and no session is created.Filing without a minimal reproduction — apologies. This was found and verified on a production Expo app, and the analysis below is from reading the shipped SDK source, so I wanted to get it in front of you rather than sit on it. Happy to build a standalone repro if that's what it takes to get it looked at. Publishable key omitted for the same reason (production instance).
What happens
<AuthView />in an Expo app.com.myapp.bundleid://callback?flow_id=…&approval_token=….Why (as far as I can tell)
clerk-ios completes the flow only from the callback's
flow_id+approval_token—MagicLink.swifthas no polling fallback — and its single intake path isClerk.handle(_ url:). InAuthView.swift, the only caller is SwiftUI's.onOpenURL:.onOpenURLis driven by the SwiftUI app lifecycle. Expo/React Native apps use the UIKit app delegate, so URLs arrive viaapplication(_:open:options:)and that modifier never fires for the hosted SwiftUI view — nothing ever callsClerk.handle(url).Nothing in
@clerk/expobridges the gap: searchingpackages/expoonmainreturns zero references toonOpenURL,openURL,handleURL, oronNewIntent. Android is unaffected because clerk-android registers its ownclerk://intent filter and receives the callback natively — hence the platform split.Expected behavior
@clerk/expoforwards inbound URLs to the native SDK on iOS the way clerk-android already does on Android, so email link sign-in completes on both platforms.An
ExpoAppDelegateSubscriberimplementingapplication(_:open:options:)and callingClerk.shared.handle(url)looks like it would cover it —Clerk.sharedis public and already configured byClerkNativeBridge, and returningfalsewould leave the URL available to React Native'sLinking, with clerk-ios's ownURLHandlingCoordinatordeduping any double handling. There's also acom.clerk.expo.native-sdk.did-configurenotification that could handle a cold launch arriving before the SDK is configured.Workaround
A local Expo module doing exactly the above. Worth noting for anyone else hitting this: the app also needs a route for the callback URL, or expo-router renders
+not-foundon top of it — on Android that means a "page does not exist" screen over a session that was just created successfully.Environment
Also reproduced on
@clerk/expo@4.2.7, andpackages/expoonmainhas the same gap.