Skip to content

[expo] iOS native Google sign-in fails with "Unable to open Safari." — getPresentingViewController picks an arbitrary scene and window #9504

Description

@Fastdrecad

Package

@clerk/expo@3.6.5 (useSignInWithGoogle, iOS). The same code is present verbatim in
@clerk/expo-google-signin@1.0.1, so upgrading does not resolve it.

What happens

On a release iOS build, tapping "Continue with Google" never reaches Google. The promise
rejects with one of:

  • Unable to open Safari.
  • The authorization attempt failed for an unknown reason
  • No presenting view controller available

Retrying reproduces it every time within the same session. Sign in with Apple on the same
build is unaffected.

Cause

ios/ClerkGoogleSignInModule.swift resolves the presenting controller like this:

private func getPresentingViewController() -> UIViewController? {
  guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
        let window = scene.windows.first,
        let rootVC = window.rootViewController else {
    return nil
  }
  ...
}

connectedScenes is a Set and UIWindowScene.windows is unordered, so .first returns an
arbitrary element in both cases — neither checks activationState nor isKeyWindow. An app
with more than one window (splash, keyboard, an overlay from another module) can therefore
hand GIDSignIn a controller that is not in the visible hierarchy.

That produces the two observed outcomes:

  • resolution returns nil → the module's own "No presenting view controller available"
  • resolution returns an off-screen window → GIDSignIn cannot present its authorization
    session and AppAuth reports OIDErrorCodeSafariOpenError, surfaced through
    handleSignInResult as error.localizedDescription"Unable to open Safari."

Suggested fix

private func getPresentingViewController() -> UIViewController? {
  let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
  let scene = scenes.first { $0.activationState == .foregroundActive }
    ?? scenes.first { $0.activationState == .foregroundInactive }
    ?? scenes.first

  guard let window = scene?.windows.first(where: { $0.isKeyWindow })
          ?? scene?.windows.first(where: { !$0.isHidden && $0.rootViewController != nil }),
        let rootVC = window.rootViewController else {
    return nil
  }

  var topVC = rootVC
  while let presentedVC = topVC.presentedViewController, !presentedVC.isBeingDismissed {
    topVC = presentedVC
  }
  return topVC
}

The isBeingDismissed guard covers a related case: a controller mid-dismissal is still
reachable through presentedViewController but can no longer present.

Environment

  • Expo SDK 55, React Native 0.83.2, Hermes, Fabric
  • iOS 26.5 / 26.6, physical devices and simulator
  • GoogleSignIn ~> 9.0 as declared by ClerkGoogleSignIn.podspec

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions