Capacitor GoogleSignIn iOS 8.0.0: signIn() never opens consent sheet, iosClientIdMissing error
iOS Capacitor app using @capawesome/capacitor-google-sign-in 0.1.2 (GoogleSignIn pod 8.0.0): GoogleSignIn.signIn() never opens the consent sheet. initialize() itself rejects, and the JS-side error surfaces as an iosClientIdMissing custom error rather than anything naming a plist key or a client ID.
The same JS worked unchanged on Android via Credential Manager, so the shared TypeScript looked correct:
const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
await GoogleSignIn.initialize({ clientId });
const result = await GoogleSignIn.signIn();Things tried, all wrong:
- Assumed
initialize({ clientId })takes the per-platform client ID, so swapped in the iOS-type OAuth client ID on iOS builds behind a platform check. Still iosClientIdMissing, because nothing the JS passes is read for this. - Assumed the plugin only needed the reversed client ID registered as a CFBundleURLTypes / CFBundleURLSchemes entry in ios/App/App/Info.plist (what most Capacitor Google Sign-In write-ups and older plugin guides show). Adding only the URL scheme does not fix it.
- Assumed a downloaded GoogleService-Info.plist dropped into ios/App/App/ would supply the missing configuration the way it does for Firebase-based sign-in. It does not; this plugin is not Firebase-backed and does not read that file.
- Verified the pod was installed and linked (Podfile.lock resolved CapawesomeCapacitorGoogleSignIn 0.1.2 depending on GoogleSignIn ~> 8.0) and re-ran
npx cap sync ios. Both fine; not a linkage problem.
No error text names the missing key, and typechecking passes throughout because the missing piece is not in the TypeScript surface at all.
iOS needs two different OAuth client IDs from two different client types, and only one of them travels through JavaScript. initialize({ clientId }) is the server/web client ID on every platform; the iOS client ID is read natively out of Info.plist under a GIDClientID key your JS never touches.
The plugin's iOS implementation makes this explicit:
@objc public func initialize(_ options: InitializeOptions, completion: @escaping (_ error: Error?) -> Void) {
guard let iosClientId = Bundle.main.object(forInfoDictionaryKey: "GIDClientID") as? String else {
completion(CustomError.iosClientIdMissing)
return
}
let configuration = GIDConfiguration(clientID: iosClientId, serverClientID: options.clientId)
GIDSignIn.sharedInstance.configuration = configuration
...
}GIDConfiguration(clientID:serverClientID:) takes the iOS client from the bundle and the value you passed from JS as serverClientID. So initialize() fails before touching your argument when GIDClientID is absent — which is why substituting the iOS client ID into clientId changes nothing.
The plugin's own API docs confirm the JS side: clientId is documented as "The web client ID from Google Cloud Console... This must be a web client ID on all platforms, even on Android and iOS." On Android it is passed as the Credential Manager server client ID; on iOS as serverClientID.
Fix
Create an iOS-type OAuth 2.0 client in Google Cloud Console for your bundle ID (a separate client from the Web-type one you pass as clientId), then add both of these to ios/App/App/Info.plist inside the top-level <dict>:
<key>GIDClientID</key>
<string>0123456789.apps.googleusercontent.com</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.0123456789</string>
</array>
</dict>
</array>GIDClientID is the plain iOS client ID; the URL scheme is the reversed form of that same iOS client ID. They are not interchangeable and you need both: GIDClientID gets you past initialize(), and the URL scheme gets the browser redirect back into your app.
Leave your JS untouched, still passing the Web client ID. No platform branch, no second env variable.
Why this is easy to get wrong
Everywhere else you configure Google Sign-In wants the platform-native client ID, so initialize({ clientId }) reads like it should too. The error name (iosClientIdMissing) sounds like "the iOS client ID you gave me is blank", which sends you back to your JS, when it actually means "there is no GIDClientID in the app bundle". Nothing type-checks or lints for it, because the required value lives in a plist rather than in code.
One knock-on for store builds: Info.plist is a single committed file holding one GIDClientID, so if your development and production Google clients live in different GCP projects, that plist pins the app to one of them. A client ID's leading numeric segment is the GCP project number, so comparing those prefixes tells you whether two clients share a project. If they differ, put the production iOS client in the plist and override the value for debug builds with an xcodebuild build setting (the INFOPLIST_KEY_ prefix followed by the plist key name) instead of maintaining a second plist.