npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

essential-google-signin

v1.0.0

Published

Native Google Sign-In for Expo using Android Credential Manager and iOS GoogleSignIn SDK

Readme

essential-google-signin

Native Google Sign-In for Expo, built on the Android Credential Manager API and the official GoogleSignIn SDK for iOS.

Both platforms return the same user object, built from the claims of the same ID token, whose audience is your web client ID — so one backend verification path covers iOS and Android.

Features

  • Android Credential Manager (the modern replacement for GoogleSignInClient)
  • Official GoogleSignIn SDK on iOS
  • Silent sign-in / session restore on both platforms
  • One set of error codes across platforms, including a distinct code for user cancellation
  • Automatic native configuration through an Expo config plugin — no AppDelegate edits
  • Fully typed, no runtime dependencies
  • Web is not supported (native SDKs only), but the module loads and degrades predictably

Requirements

| | | | --- | --- | | Expo SDK | 54 or newer (developed and tested against SDK 57) | | iOS | 15.1+ | | Android | API 24+ (Android 7.0), with Google Play Services 23.08.15 or newer | | Web | Not supported — see Web |

Install

npx expo install essential-google-signin

This module needs custom native code. It works with expo prebuild / development builds, and does not run in Expo Go.

Google Cloud setup

You need up to three OAuth 2.0 client IDs from the Google Cloud console, under APIs & Services → Credentials.

Web client ID — required on both platforms. Create a client of type Web application. Despite the name it has nothing to do with web support: Android signs in against it, and on iOS it becomes the ID token's audience so your backend has one value to verify.

iOS client ID — required for iOS. Create a client of type iOS with your bundle identifier.

Android client ID — optional. Create a client of type Android with your package name and signing certificate SHA-1:

cd android && ./gradlew signingReport   # SHA1 under "Variant: debug"

Registering that client is what lets Google match your app; the client ID string itself is never sent by Credential Manager, so passing it to the plugin is optional and only recorded for debugging.

Configuration

Add the plugin to your app config:

{
  "expo": {
    "plugins": [
      [
        "essential-google-signin",
        {
          "webClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
          "iosClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
          "androidClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
        }
      ]
    ]
  }
}

Then regenerate the native projects:

npx expo prebuild --clean

The plugin writes the client IDs into AndroidManifest.xml, writes GIDClientID and GIDServerClientID plus the reversed-client-ID URL scheme into Info.plist, and patches the Podfile with :modular_headers => true for GoogleSignIn's Objective-C dependencies.

It does not modify your AppDelegate. The OAuth callback is handled by an ExpoAppDelegateSubscriber registered by the module itself, which works with both the Swift AppDelegate used since SDK 53 and the older Objective-C one.

Plugin options

| Option | Required | Notes | | --- | --- | --- | | webClientId | Yes | Type Web application. Becomes the ID token audience on both platforms. | | iosClientId | On iOS | Type iOS. Required unless the project is Android-only. | | androidClientId | No | Type Android. Recorded in the manifest; never sent at runtime. |

The plugin fails the build on a client ID that isn't a *.apps.googleusercontent.com value, or when webClientId is accidentally set to the iOS or Android client ID — the usual cause of BAD_AUTHENTICATION at runtime.

Usage

import EssentialGoogleSignin, {
  GoogleSigninErrorCode,
  getGoogleSigninErrorCode,
  isCancelledError,
  type GoogleUser,
} from "essential-google-signin";
import { useEffect, useState } from "react";
import { Button, Platform, Text, View } from "react-native";

export default function App() {
  const [user, setUser] = useState<GoogleUser | null>(null);

  useEffect(() => {
    if (Platform.OS === "web") return;

    (async () => {
      await EssentialGoogleSignin.configure();

      try {
        // Signs a returning user back in with no UI, and refreshes their ID token.
        setUser(await EssentialGoogleSignin.signInSilently());
      } catch (error) {
        // Nobody signed in yet — the expected first-launch outcome.
        if (getGoogleSigninErrorCode(error) !== GoogleSigninErrorCode.NoCredential) {
          throw error;
        }
      }
    })();
  }, []);

  const signIn = async () => {
    try {
      setUser(await EssentialGoogleSignin.signIn());
    } catch (error) {
      // Cancellation is a normal outcome, not a failure.
      if (isCancelledError(error)) return;
      throw error;
    }
  };

  return (
    <View>
      {user ? (
        <>
          <Text>Signed in as {user.email}</Text>
          <Button
            title="Sign out"
            onPress={async () => {
              await EssentialGoogleSignin.signOut();
              setUser(null);
            }}
          />
        </>
      ) : (
        <Button title="Sign in with Google" onPress={signIn} />
      )}
    </View>
  );
}

Native button

import EssentialGoogleSignin, { GoogleSigninButton } from "essential-google-signin";

<GoogleSigninButton
  size={GoogleSigninButton.Size.Wide}
  color={GoogleSigninButton.Color.Dark}
  onPress={() => EssentialGoogleSignin.signIn()}
/>;

| Prop | Values | | --- | --- | | size | Size.Standard (230×48), Size.Wide (312×48), Size.Icon (48×48) | | color | Color.Light, Color.Dark | | disabled | boolean | | onPress | Called when the native button is tapped |

Native-only. It renders nothing useful on web.

API

configure(options?): Promise<ConfigureResult>

Reads the client IDs the plugin wrote into the native project and prepares the platform SDK. Call it once, and await it before signing in.

type ConfigureOptions = {
  /** Android only. `false` always shows the account chooser. Default `true`. */
  androidAutoSelectEnabled?: boolean;
};

type ConfigureResult = {
  webClientId: string | null;   // Android: from the manifest. iOS: the server client ID.
  androidClientId: string | null; // Android only, informational
  iosClientId: string | null;     // iOS only
};

Throws ERR_CONFIG when the client IDs are missing from the native config.

signIn(options?): Promise<GoogleUser>

Runs the interactive flow and resolves with the signed-in user.

type SignInOptions = {
  /**
   * Sent verbatim and checked against the token's `nonce` claim before resolving.
   * Generate it on your server and compare it there too. Omitted: a random nonce
   * is generated and checked locally.
   */
  nonce?: string;
};

Rejects with ERR_SIGN_IN_CANCELLED when the user backs out — check for that before treating a rejection as a failure.

signInSilently(): Promise<GoogleUser>

Signs a returning user in without any UI and refreshes their ID token. On Android it asks Credential Manager only for accounts that already authorized this app; on iOS it restores the SDK's persisted session.

Rejects with ERR_NO_CREDENTIAL when nobody is signed in — the expected first-launch outcome, not an error worth surfacing.

getCurrentUser(): Promise<GoogleUser | null>

On iOS this reads the SDK's persisted session and survives app restarts. Android's Credential Manager keeps no session, so it reports the user who signed in during this process and returns null after a restart — use signInSilently() there.

signOut(): Promise<void>

Clears the local session. Does not revoke the grant.

revokeAccess(): Promise<void>

Revokes the app's access entirely.

iOS only. Android's Credential Manager ID-token flow holds no on-device grant to revoke, so this rejects with ERR_NOT_SUPPORTED there. Revoke from your backend instead, then call signOut().

hasPlayServices(): Promise<boolean>

Whether Google Play Services is available. Always true on iOS, always false on web.

Types

type GoogleUser = {
  id: string;                 // the `sub` claim — Google's stable account ID
  email: string;
  emailVerified: boolean;     // false when the claim is absent
  name: string | null;
  givenName: string | null;
  familyName: string | null;
  pictureUrl: string | null;
  locale: string | null;      // in practice, Android only
  idToken: string;            // audience is your web client ID on both platforms
};

Fields Google omits are null, not "", so "absent" is distinguishable from "blank".

Error handling

Every rejection carries a code on error.code, identical across platforms.

import { GoogleSigninErrorCode, getGoogleSigninErrorCode } from "essential-google-signin";

switch (getGoogleSigninErrorCode(error)) {
  case GoogleSigninErrorCode.Cancelled:
    break; // user backed out
  case GoogleSigninErrorCode.NoCredential:
    break; // nobody signed in
  default:
    reportError(error);
}

| Code | Meaning | | --- | --- | | ERR_SIGN_IN_CANCELLED | The user dismissed the sheet or dialog. Not a failure. | | ERR_NO_CREDENTIAL | No eligible Google account. Expected from signInSilently(). | | ERR_NOT_CONFIGURED | signIn() was called before configure() resolved. | | ERR_CONFIG | Client IDs missing from the native config. Check the plugin and re-prebuild. | | ERR_PLAY_SERVICES_UNAVAILABLE | Android: Play Services missing or out of date. | | ERR_TOKEN | The ID token could not be decoded, or its nonce did not match. | | ERR_NO_UI_CONTEXT | No activity (Android) or view controller (iOS) to present on. | | ERR_NOT_SUPPORTED | No equivalent on this platform — revokeAccess() on Android, anything on web. | | ERR_SIGN_IN_FAILED | Anything else during sign-in. | | ERR_SIGN_OUT_FAILED | Anything else during sign-out. |

isCancelledError(error) is a shorthand for the first row.

Backend verification

signIn() gives you an ID token. Trust it only after your server verifies it. The audience is your web client ID on both platforms, so a single check covers everything:

const { OAuth2Client } = require("google-auth-library");
const client = new OAuth2Client(WEB_CLIENT_ID);

async function verify(idToken) {
  const ticket = await client.verifyIdToken({ idToken, audience: WEB_CLIENT_ID });
  const payload = ticket.getPayload();
  return { id: payload.sub, email: payload.email, name: payload.name };
}

The module decodes the token locally to populate GoogleUser, without verifying its signature and without a network call. That is deliberate: the credential arrives over IPC from Google Play Services (Android) or from the SDK (iOS), and on-device verification only added a round-trip to Google's certificate endpoint that made sign-in fail offline. Signature verification belongs on your server, where the check is meaningful.

Platform notes

| | Android | iOS | | --- | --- | --- | | Implementation | Credential Manager | GoogleSignIn SDK | | Session persists across restarts | No — use signInSilently() | Yes | | getCurrentUser() after a restart | null | The stored user | | revokeAccess() | ERR_NOT_SUPPORTED | Supported | | locale claim | Usually present | Usually absent | | androidAutoSelectEnabled | Applies | Ignored |

Web

Web is not supported — this wraps two native SDKs. The module still imports cleanly so your bundle builds: hasPlayServices() resolves false, getCurrentUser() resolves null, and the sign-in methods reject with ERR_NOT_SUPPORTED. Gate on Platform.OS, or integrate Google Identity Services for the web separately.

Troubleshooting

ERR_CONFIG / "No web client ID in AndroidManifest.xml" — the plugin didn't run, or the native project is stale. Run npx expo prebuild --clean. Note that 1.0.0 changed the manifest metadata keys, so a project prebuilt with 0.x must be regenerated.

Android: ERR_PLAY_SERVICES_UNAVAILABLE, or "getCredentialAsync no provider dependencies found" — Google Play Services on the device is older than the 23.08.15 that Credential Manager's Google provider requires, so it declines to act as a provider. Despite the wording, the underlying message is not about a missing Gradle dependency.

This bites most often on emulators: many system images ship a Play Services from the year the image was cut, and it is never updated. Check with adb shell dumpsys package com.google.android.gms | grep versionName, and use a recent Google Play system image or a real device. hasPlayServices() checks against this same minimum, so it returns false on such a device rather than a misleading true.

Android: "Cannot find a matching credential" — the SHA-1 in the Google Cloud console doesn't match the certificate the app was signed with. Get it from ./gradlew signingReport and add it to your Android OAuth client. Remember that debug, release, and Play App Signing all use different certificates.

Android: BAD_AUTHENTICATION / "Long live credential not available" — webClientId is not actually a Web application client. The plugin now rejects the obvious cases at build time.

iOS: "Your app is missing support for the following URL schemes" — the reversed client ID isn't in Info.plist. Run npx expo prebuild --clean.

iOS: "package 'apple' is using Swift tools version 6.2.0 but the installed version is 6.1.0" — Expo SDK 57 ships Swift packages requiring Swift tools 6.2, so it needs Xcode 26 or newer. Update Xcode, or on CI use a runner image that provides it.

iOS: "cannot inherit from class 'SharedObject' (compiled with Swift 5.10)" — a stale ios/Pods left over from an earlier Expo SDK. Run npx expo prebuild --platform ios --clean.

iOS: "could not build Objective-C module 'GoogleUtilities'" — GoogleSignIn 8.0+ pulls in AppCheckCore, whose Objective-C dependencies ship without module maps. The plugin patches the Podfile for this automatically; if you manage your Podfile by hand, add after use_native_modules!:

pod 'GoogleUtilities', :modular_headers => true
pod 'RecaptchaInterop', :modular_headers => true

A backend rejects iOS tokens but accepts Android ones — that was the pre-1.0 behavior, where iOS tokens were minted for the iOS client ID. Upgrade and re-prebuild so GIDServerClientID is written.

Migrating from 0.5.x

signIn() used to resolve { success: true, data: {...} }. It now resolves the user directly:

- const result = await EssentialGoogleSignin.signIn();
- setUser(result.data);
+ const user = await EssentialGoogleSignin.signIn();
+ setUser(user);

Also changed:

  • signOut() resolves undefined instead of { success: true }.
  • configure() resolves the actual client IDs instead of { success: true } — it previously contradicted its own type.
  • GoogleUserData is now GoogleUser; the old name remains as a deprecated alias.
  • Optional profile fields are null instead of "".
  • Error codes changed: SIGN_IN_ERROR became ERR_SIGN_IN_FAILED, and cancellation now has its own ERR_SIGN_IN_CANCELLED rather than being indistinguishable from a real failure.
  • iOS ID tokens now carry the web client ID as their audience. If your backend was verifying iOS tokens against the iOS client ID as a workaround, switch it to the web client ID.
  • The Android manifest metadata moved out of the Play Games namespace, so npx expo prebuild --clean is required.
  • androidClientId is now optional in the plugin config.
  • New: signInSilently(), getCurrentUser(), revokeAccess(), signIn({ nonce }), GoogleSigninErrorCode, getGoogleSigninErrorCode(), isCancelledError().

Development

npm install
npm run build
npm run lint
npm test

cd example
npm install
npx expo prebuild --clean
npx expo run:ios      # or run:android

The example app doubles as a test harness. Run checks exercises the whole API and asserts the contract — that configure() returns the client IDs, that the user object's optional fields are null rather than "", that the ID token's audience is the web client ID, and that revokeAccess() rejects with ERR_NOT_SUPPORTED on Android. The ID token card decodes the token in place so you can read aud, nonce and expiry without leaving the app, and the two interactive checks cover the paths that need a human: dismissing the sheet should give ERR_SIGN_IN_CANCELLED, and a supplied nonce should come back in the token.

License

MIT © Kevin Jossendal