expo-native-google-signin
v0.1.0
Published
Native Google Sign-In for Expo apps using Android Credential Manager and the iOS GoogleSignIn SDK — no legacy play-services-auth, no @react-native-google-signin/google-signin dependency.
Maintainers
Readme
expo-native-google-signin
Native Google sign-in for Expo apps on Android (Credential Manager) and iOS (the
GoogleSignIn SDK). It returns a Google ID token that any backend can verify — Supabase, Firebase,
or your own server.
Why: "Google Sign-In for Android" (GoogleSignInClient in play-services-auth) is deprecated
in favour of Credential Manager, and play-services-auth 22.0.0 already removed those classes from
the SDK. This module talks only to Google's first-party SDKs — no dependency on
@react-native-google-signin/google-signin or any other community wrapper. MIT-licensed.
Status and requirements
Pre-1.0.
0.1.0is on npm (latest; see CHANGELOG.md). Still pre-1.0, so a minor bump may break the API — pin an exact version:npm install [email protected] --save-exactExpo SDK 57.
A development build — this module is not available in Expo Go.
Android:
minSdk24.iOS: 15.1+, and Xcode 26+ (Expo SDK 57 itself requires it —
expo-modules-jsineeds Swift tools 6.2).GoogleSignIn-iOS~> 9.2 (pinned; seedocs/ARCHITECTURE.mdfor why).
Install
npx expo install expo-native-google-signin
# or, pinned exactly:
npm install [email protected] --save-exactAdd the config plugin in app.json / app.config.*:
{
"expo": {
"plugins": [
[
"expo-native-google-signin",
{ "iosUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID" }
]
]
}
}Then rebuild the native projects:
npx expo prebuild
npx expo run:android
npx expo run:ios(or an EAS build). iosUrlScheme is required for the sign-in sheet to hand control back to your
app on iOS — without it, the plugin only logs a warning.
Google Cloud setup
Create OAuth 2.0 client IDs in Google Cloud Console:
- Web client — its ID is
webClientId. It becomes theaudclaim of the ID token this module returns, on both platforms. - iOS client — its ID is
iosClientId. Its reversed ID isiosUrlScheme(com.googleusercontent.apps.…, shown as "URL scheme" on the client's page in the console). - Android client — registered with your app's package name and signing SHA-1. There is
no
iosClientId-equivalent to pass at runtime for Android: Google identifies the calling app by package name + SHA-1 on this client, and the ID token's audience is the web client ID passed aswebClientId. Seedocs/ARCHITECTURE.md→ "Platform mechanics" for the full rationale.
[!WARNING] Play App Signing re-signs your app, so the Android OAuth client needs both the upload-key SHA-1 and the Play app-signing SHA-1. If you only register the upload-key SHA-1, sign-in fails only for builds installed from Google Play — usually surfacing as
NO_GOOGLE_ACCOUNTeven though the device has one. Also register your debug keystore's and any EAS build profile's SHA-1s, or sign-in won't work in development builds either.
Use placeholders like YOUR_WEB_CLIENT_ID.apps.googleusercontent.com when configuring — never
commit a real client ID, SHA-1 or keystore (this repo is public).
Usage
import { isErrorWithCode, signIn, signOut } from "expo-native-google-signin";
async function handleSignIn() {
try {
const result = await signIn({
webClientId: "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
iosClientId: "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com", // required on iOS only
});
if (result.type === "cancelled") {
return;
}
// result.type === "success"
console.log(result.user.email, result.idToken);
// There is no `result.user.id` -- decode `result.idToken` (a JWT) and read its `sub`
// claim if you need the stable Google account identifier.
} catch (error) {
if (isErrorWithCode(error)) {
console.warn(error.code, error.message);
} else {
throw error;
}
}
}
async function handleSignOut() {
await signOut();
}API reference
signIn(options): Promise<SignInResult>
| Option | Type | Required | Notes |
| ------------- | -------- | ------------------ | -------------------------------------------------------------|
| webClientId | string | Yes (both platforms) | OAuth web client ID; becomes the token's aud claim. |
| iosClientId | string | iOS only | OAuth iOS client ID; ignored on Android. |
| nonce | string | No | Forwarded to the native SDK and embedded in the ID token. |
type SignInResult =
| { type: "success"; idToken: string; user: GoogleUser }
| { type: "cancelled" };
type GoogleUser = {
email: string;
name?: string;
givenName?: string;
familyName?: string;
photoUrl?: string;
};There is deliberately no user.id: Android's GoogleIdTokenCredential.id is the account's email
address, not the Google subject. Decode idToken and read its sub claim instead.
A user-initiated cancel resolves { type: "cancelled" } — it does not reject.
signOut(): Promise<void>
Clears the locally cached Google credential state on both platforms.
isErrorWithCode(error): error is Error & { code: ErrorCode }
Narrows a caught error to one with a known code from ERROR_CODES below.
ERROR_CODES
signIn/signOut reject with an Error whose code is one of these. The native error message is
always preserved on error.message for logging.
| Code | When | Platform(s) |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------| ----------- |
| CONFIGURATION_ERROR | Missing/blank webClientId; missing/blank iosClientId on iOS; an invalid nonce type; or the module is unavailable (e.g. running in Expo Go instead of a development build) | Both |
| NO_GOOGLE_ACCOUNT | No Google account is available to choose from (Credential Manager's NoCredentialException) | Android only |
| PROVIDER_UNAVAILABLE | The credential provider isn't configured or supported on the device (e.g. no Play services, or an emulator without a Play Store image) | Android only |
| NO_PRESENTER | No current Activity (Android) / view controller (iOS) to present the sign-in UI from — e.g. the app was backgrounded mid-tap | Both |
| UNEXPECTED_CREDENTIAL | The returned credential isn't a parseable Google ID token credential, or is missing a required field (e.g. no email/profile on iOS) | Both |
| SIGN_IN_FAILED | Any other failure. On iOS, every non-cancel failure uses this one code — GoogleSignIn has a single flat error domain that doesn't distinguish "no account" or "provider unavailable" the way Android's Credential Manager does. The native domain, code and message are always kept in error.message. On Android, [16] Account reauth failed (an unregistered package + signing SHA-1) rejects with SIGN_IN_FAILED rather than resolving as cancelled. | Both |
Backends
Supabase — signInWithIdToken with a nonce
Verifying the nonce ties the ID token to this specific sign-in attempt. Generate a raw nonce,
SHA-256-hash it, pass the hashed nonce into signIn, and pass the raw nonce to Supabase (it
hashes it again internally to compare against the token's nonce claim):
import * as Crypto from "expo-crypto";
import { signIn } from "expo-native-google-signin";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
async function signInWithGoogle() {
const rawNonce = Crypto.randomUUID();
const hashedNonce = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
rawNonce,
);
const result = await signIn({
webClientId: "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
iosClientId: "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
nonce: hashedNonce,
});
if (result.type === "cancelled") {
return;
}
const { data, error } = await supabase.auth.signInWithIdToken({
provider: "google",
token: result.idToken,
nonce: rawNonce,
});
}(expo-crypto and @supabase/supabase-js are not dependencies of this module — install them
yourself if you use this pattern.)
Firebase
import { getAuth, GoogleAuthProvider, signInWithCredential } from "firebase/auth";
const credential = GoogleAuthProvider.credential(result.idToken);
await signInWithCredential(getAuth(), credential);A custom server
Verify, on the server: the ID token's signature (against Google's published JWKS), aud (must
equal your web client ID), iss (accounts.google.com / https://accounts.google.com),
exp, and — if you passed one to signIn — the (hashed) nonce claim.
Migrating from @react-native-google-signin/google-signin
| @react-native-google-signin/google-signin | expo-native-google-signin |
| ---------------------------------------------------------------------| ----------------------------------------------------------------------------------------------|
| GoogleSignin.configure({ webClientId, iosClientId }) then GoogleSignin.signIn() | One call: signIn({ webClientId, iosClientId, nonce? }) — no separate configure step |
| Result { type: "success" \| "cancelled", ... } | Same type values and meaning — a cancel resolves, it doesn't throw |
| user.id (Google subject) | No id — decode idToken and read its sub claim instead |
| androidClientId config option | Removed — not needed; Google matches by package name + SHA-1, and webClientId is the token audience on both platforms |
| GoogleSignin.signOut() | signOut() |
| Config plugin option iosUrlScheme | Same option name, same purpose |
This module exists because the free version of @react-native-google-signin/google-signin stays on
the legacy, deprecated Android sign-in API — its Credential Manager support is part of a paid
product (see docs/ARCHITECTURE.md → "Why this project exists").
Troubleshooting
NO_GOOGLE_ACCOUNTonly on builds installed from Google Play: the Android OAuth client is missing the Play app-signing SHA-1 (see the warning above) — register both SHA-1s.SIGN_IN_FAILEDwith[16] Account reauth failedon Android: the Android OAuth client is missing this build's signing SHA-1 (debug, upload or Play app-signing). An Android OAuth client holds one SHA-1, so register one client per key, with the same package name, in the same Google Cloud project aswebClientId.- No accounts on an emulator: use an emulator image that includes the Play Store, and sign into a Google account on it first.
CONFIGURATION_ERRORin Expo Go: this module requires a development build; Expo Go can't load it.- iOS never returns to the app after choosing an account: the config plugin's
iosUrlSchemeis missing or wrong — checkInfo.plist'sCFBundleURLTypesafterexpo prebuild. - iOS build fails to resolve packages / Xcode errors: this module (via Expo SDK 57) requires Xcode 26+.
Example app
cd example
cp .env.example .env
# fill in EXPO_PUBLIC_WEB_CLIENT_ID, EXPO_PUBLIC_IOS_CLIENT_ID, EXPO_PUBLIC_IOS_URL_SCHEME
npm install
npx expo run:android # or: npx expo run:iosContributing / releasing (maintainers)
See CLAUDE.md for the full command reference (npm run build, lint, typecheck, test,
build:plugin, test:plugin, etc.).
Release:
- Bump the version in
package.json(andandroid/build.gradle'sversion/versionName, which is not read frompackage.json), and add aCHANGELOG.mdentry. - Merge to
main. - Tag
vX.Y.Zfrommainand push the tag —release.ymlpublishes to npm with provenance (prereleases go to thenextdist-tag, stable versions tolatest), but only if the tag matchespackage.json's version and the tagged commit is onmain.
Auth: release.yml publishes only through npm trusted publishing (OIDC), configured on
npmjs.com: package settings → Trusted Publisher → GitHub Actions, repo
ele-ap/expo-native-google-signin, workflow release.yml, environment npm. There is no npm token
or NPM_TOKEN secret, and you shouldn't add one. If you rename the workflow file or the environment,
update the npmjs.com config too, or publishing fails with ENEEDAUTH. The first version
(0.1.0-beta.0) was published manually with 2FA, because npm only lets a 2FA-bypassing token
publish to a staging area for a package that doesn't exist yet (E_STAGE_REQUIRED).
Disclaimer
This project is not affiliated with or endorsed by Google.
