Skip to content

Commit 321bea6

Browse files
authored
fix(expo): forward inbound callback URLs to the native SDK on iOS (#9470)
1 parent 9a7631d commit 321bea6

5 files changed

Lines changed: 61 additions & 1 deletion

File tree

.changeset/olive-pugs-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/expo': patch
3+
---
4+
5+
Fix email link sign-in never completing on iOS. Callback URLs opened by the "Return to App" button are now forwarded to the native SDK, including on a cold launch, so a flow started from `<AuthView />` signs the user in instead of leaving them signed out with no error.

packages/expo/expo-module.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"platforms": ["apple", "android"],
33
"apple": {
4-
"modules": ["ClerkExpoModule", "ClerkAuthViewModule", "ClerkUserProfileViewModule", "ClerkUserButtonViewModule"]
4+
"modules": ["ClerkExpoModule", "ClerkAuthViewModule", "ClerkUserProfileViewModule", "ClerkUserButtonViewModule"],
5+
"appDelegateSubscribers": ["ClerkAppDelegateSubscriber"]
56
},
67
"android": {
78
"modules": [
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// ClerkAppDelegateSubscriber - Forwards inbound URLs to the native Clerk SDK.
2+
3+
import ExpoModulesCore
4+
import UIKit
5+
6+
public class ClerkAppDelegateSubscriber: ExpoAppDelegateSubscriber {
7+
public func application(
8+
_ app: UIApplication,
9+
open url: URL,
10+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
11+
) -> Bool {
12+
ClerkNativeBridge.shared.handle(url: url)
13+
// Returning false leaves the URL available to React Native's Linking.
14+
return false
15+
}
16+
}

packages/expo/ios/ClerkExpo.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Pod::Spec.new do |s|
5252
end
5353

5454
s.source_files = "ClerkNativeBridge.swift",
55+
"ClerkAppDelegateSubscriber.swift",
5556
"ClerkExpoModule.swift",
5657
"ClerkNativeViewHost.swift",
5758
"ClerkAuthNativeView.swift",

packages/expo/ios/ClerkNativeBridge.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ final class ClerkNativeBridge {
429429
private var lastObservedClientState: ClientStateSnapshot?
430430
private var configurationDepth = 0
431431
private var jsOriginatedClientSyncDepth = 0
432+
private var pendingURL: URL?
433+
private var shouldFlushPendingURL = false
432434

433435
private init() {}
434436

@@ -459,6 +461,13 @@ final class ClerkNativeBridge {
459461
defer {
460462
lastObservedClientState = Self.clerkConfigured ? Self.clientStateSnapshot() : nil
461463
configurationDepth = max(0, configurationDepth - 1)
464+
465+
// Overlapping calls can finish out of order, so replay once the last one settles and any
466+
// of them succeeded. A batch where every call threw keeps the URL for the next attempt.
467+
if configurationDepth == 0, shouldFlushPendingURL {
468+
shouldFlushPendingURL = false
469+
flushPendingURL()
470+
}
462471
}
463472

464473
loadThemes()
@@ -472,6 +481,7 @@ final class ClerkNativeBridge {
472481
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
473482
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
474483
Self.postConfiguredNotification()
484+
shouldFlushPendingURL = true
475485
return
476486
}
477487

@@ -486,6 +496,7 @@ final class ClerkNativeBridge {
486496
_ = try await Clerk.shared.refreshClient()
487497
await Self.waitForLoadedClient()
488498
}
499+
shouldFlushPendingURL = true
489500
return
490501
}
491502

@@ -497,6 +508,32 @@ final class ClerkNativeBridge {
497508
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
498509
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
499510
Self.postConfiguredNotification()
511+
shouldFlushPendingURL = true
512+
}
513+
514+
@MainActor
515+
private func flushPendingURL() {
516+
guard let url = pendingURL else { return }
517+
pendingURL = nil
518+
handle(url: url)
519+
}
520+
521+
/// `AuthView` only reaches `Clerk.handle(_:)` from `.onOpenURL`, which never fires for a UIKit-hosted controller.
522+
@MainActor
523+
func handle(url: URL) {
524+
// A cold launch delivers the callback before, or partway through, JS calling `configure`.
525+
guard Self.clerkConfigured, configurationDepth == 0 else {
526+
pendingURL = url
527+
return
528+
}
529+
530+
Task { @MainActor in
531+
do {
532+
try await Clerk.shared.handle(url)
533+
} catch {
534+
NSLog("[Clerk] Failed to handle callback URL: \(error.localizedDescription)")
535+
}
536+
}
500537
}
501538

502539
@MainActor

0 commit comments

Comments
 (0)