-
-
Notifications
You must be signed in to change notification settings - Fork 100
bug(google-sign-in): iOS crash — NSInternalInconsistencyException in SFSafariViewController.loadView (called off main thread) #807
Description
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-packagewith the fix above