From 32b61d840248601c3d6b5fc9765f4af481754677 Mon Sep 17 00:00:00 2001 From: seanperez Date: Wed, 2 Sep 2026 20:55:45 -0400 Subject: [PATCH] fix(expo): preserve mounted native auth view --- .changeset/calm-spoons-listen.md | 5 ++ packages/expo/ios/ClerkNativeViewHost.swift | 7 +- .../ios/Tests/ClerkNativeViewHostTests.swift | 66 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .changeset/calm-spoons-listen.md create mode 100644 packages/expo/ios/Tests/ClerkNativeViewHostTests.swift diff --git a/.changeset/calm-spoons-listen.md b/.changeset/calm-spoons-listen.md new file mode 100644 index 00000000000..66ba8da049e --- /dev/null +++ b/.changeset/calm-spoons-listen.md @@ -0,0 +1,5 @@ +--- +'@clerk/expo': patch +--- + +Keep post-authentication prompts open in the native `AuthView` when the Clerk iOS SDK configuration refreshes. diff --git a/packages/expo/ios/ClerkNativeViewHost.swift b/packages/expo/ios/ClerkNativeViewHost.swift index ee00c57fce2..4e6540aceea 100644 --- a/packages/expo/ios/ClerkNativeViewHost.swift +++ b/packages/expo/ios/ClerkNativeViewHost.swift @@ -66,7 +66,8 @@ public class ClerkNativeViewHost: ExpoView { object: nil, queue: .main ) { [weak self] _ in - self?.setNeedsHostedViewUpdate() + guard let self, !hostingCoordinator.hasAttachedController else { return } + setNeedsHostedViewUpdate() } } @@ -145,6 +146,10 @@ private final class ClerkNativeHostingCoordinator { private weak var containerView: UIView? private var hostingController: UIViewController? + var hasAttachedController: Bool { + hostingController != nil + } + init(containerView: UIView) { self.containerView = containerView } diff --git a/packages/expo/ios/Tests/ClerkNativeViewHostTests.swift b/packages/expo/ios/Tests/ClerkNativeViewHostTests.swift new file mode 100644 index 00000000000..acbfed504e2 --- /dev/null +++ b/packages/expo/ios/Tests/ClerkNativeViewHostTests.swift @@ -0,0 +1,66 @@ +import UIKit +import XCTest +@testable import ClerkExpo + +final class ClerkNativeViewHostTests: XCTestCase { + @MainActor + func testConfigureNotificationDoesNotReplaceAttachedController() { + let hostView = TestClerkNativeViewHost(appContext: nil) + let controller = UIViewController() + hostView.controller = controller + let window = mountInWindow(hostView) + let callsBeforeNotification = hostView.makeHostedControllerCallCount + + NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil) + + XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeNotification) + XCTAssertTrue(controller.view.superview === hostView) + unmount(hostView, from: window) + } + + @MainActor + func testConfigureNotificationAttachesControllerWhenInitiallyUnavailable() { + let hostView = TestClerkNativeViewHost(appContext: nil) + let window = mountInWindow(hostView) + let callsBeforeConfiguration = hostView.makeHostedControllerCallCount + let controller = UIViewController() + hostView.controller = controller + + NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil) + + XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeConfiguration + 1) + XCTAssertTrue(controller.view.superview === hostView) + + NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil) + + XCTAssertEqual(hostView.makeHostedControllerCallCount, callsBeforeConfiguration + 1) + unmount(hostView, from: window) + } + + @MainActor + private func mountInWindow(_ hostView: UIView) -> UIWindow { + let window = UIWindow(frame: UIScreen.main.bounds) + let viewController = UIViewController() + window.rootViewController = viewController + viewController.view.addSubview(hostView) + window.makeKeyAndVisible() + return window + } + + @MainActor + private func unmount(_ hostView: UIView, from window: UIWindow) { + hostView.removeFromSuperview() + window.isHidden = true + } +} + +@MainActor +private final class TestClerkNativeViewHost: ClerkNativeViewHost { + var controller: UIViewController? + private(set) var makeHostedControllerCallCount = 0 + + override func makeHostedController() -> UIViewController? { + makeHostedControllerCallCount += 1 + return controller + } +}