[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:

privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

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

privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
whilelet 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

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    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:

    privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

    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

    privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
    guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
    whilelet 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

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      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:

      privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

      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

      privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
      guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
      whilelet 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

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        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:

        privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

        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

        privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
        guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
        whilelet 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

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          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:

          privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

          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

          privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
          guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
          whilelet 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

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            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:

            privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

            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

            privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
            guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
            whilelet 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

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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:

              privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

              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

              privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
              guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
              whilelet 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

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                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:

                privatefunc getPresentingViewController()->UIViewController?{guardlet scene =UIApplication.shared.connectedScenes.first as?UIWindowScene,let window = scene.windows.first,let rootVC = window.rootViewController else{returnnil}...}

                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

                privatefunc getPresentingViewController()->UIViewController?{letscenes=UIApplication.shared.connectedScenes.compactMap{ $0 as?UIWindowScene}letscene= scenes.first{ $0.activationState ==.foregroundActive }?? scenes.first { $0.activationState ==.foregroundInactive }?? scenes.first
                guardlet window = scene?.windows.first(where:{ $0.isKeyWindow })?? scene?.windows.first(where:{ !$0.isHidden && $0.rootViewController !=nil}),let rootVC = window.rootViewController else{returnnil}vartopVC= rootVC
                whilelet 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