Skip to content

bug(google-sign-in): iOS crash — NSInternalInconsistencyException in SFSafariViewController.loadView (called off main thread) #807

@Silkjaer

Description

@Silkjaer

Plugin(s)

@capawesome/capacitor-google-sign-in

Platform(s)

iOS

Current behavior

Calling GoogleSignIn.signIn() intermittently crashes with:

Fatal Exception: NSInternalInconsistencyException
-[UIView(UIViewBoundingPathSupport) _addBoundingPathChangeObserver:]

Full stack trace:

0  CoreFoundation        __exceptionPreprocess
1  libobjc.A.dylib       objc_exception_throw
2  Foundation            _userInfoForFileAndLine
3  UIKitCore             -[UIView(UIViewBoundingPathSupport) _addBoundingPathChangeObserver:]
4  SafariServices        -[SFSafariViewController loadView]
5  UIKitCore             -[UIViewController loadViewIfRequired]
…
17 App                   -[OIDExternalUserAgentIOS presentExternalUserAgentRequest:session:]
…
27 App                   specialized GoogleSignInImpl.signIn(completion:)
28 App                   @objc GoogleSignInPlugin.signIn(_:)
29 Capacitor             closure #2 in CapacitorBridge.handleJSCall(call:)
30 Capacitor             <deduplicated_symbol>
31 libdispatch.dylib     _dispatch_call_block_and_release
32 libdispatch.dylib     _dispatch_client_callout
33 libdispatch.dylib     _dispatch_lane_serial_drain

Expected behavior

signIn() should present the OAuth Safari View Controller without crashing.

Root cause

Capacitor dispatches plugin calls (handleJSCall) on a background serial dispatch queue. GoogleSignInImpl.signIn() calls GIDSignIn.sharedInstance.signIn(withPresenting:) directly on that queue. Internally, Google Sign-In creates and presents an SFSafariViewController, which requires the main thread for all UIKit operations. When the timing is unfavorable (device under load, App Store Review, slower hardware), UIKit throws NSInternalInconsistencyException.

This is a race condition — it does not reproduce reliably, but it does crash real users and App Store Review.

Suggested fix

Wrap the signIn(withPresenting:) calls in DispatchQueue.main.async in GoogleSignIn.swift:

// Before (line ~79 in GoogleSignIn.swift)
if let scopes = self.scopes, !scopes.isEmpty {
    GIDSignIn.sharedInstance.signIn(withPresenting: viewController, hint: nil, additionalScopes: scopes, completion: signInCompletion)
} else {
    GIDSignIn.sharedInstance.signIn(withPresenting: viewController, completion: signInCompletion)
}

// After
DispatchQueue.main.async {
    if let scopes = self.scopes, !scopes.isEmpty {
        GIDSignIn.sharedInstance.signIn(withPresenting: viewController, hint: nil, additionalScopes: scopes, completion: signInCompletion)
    } else {
        GIDSignIn.sharedInstance.signIn(withPresenting: viewController, completion: signInCompletion)
    }
}

Reproduction steps

Not reliably reproducible — it is a threading race condition. Observed in production via Firebase Crashlytics on iOS (en-US locale devices). Likely triggered during Apple's App Store Review automated testing.

Other information

  • Plugin version: 0.1.1
  • Capacitor version: 8.2.0
  • iOS version: iOS 18.x
  • Workaround: Using patch-package with the fix above

Metadata

Metadata

Assignees

No one assigned

    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