expo-native-google-signin 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +300 -0
  3. package/android/build.gradle +30 -0
  4. package/android/consumer-rules.pro +4 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/expo/modules/nativegooglesignin/ErrorMapping.kt +83 -0
  7. package/android/src/main/java/expo/modules/nativegooglesignin/ExpoNativeGoogleSignInModule.kt +187 -0
  8. package/android/src/main/java/expo/modules/nativegooglesignin/SignInOptions.kt +22 -0
  9. package/app.plugin.js +1 -0
  10. package/build/CodedError.d.ts +17 -0
  11. package/build/CodedError.d.ts.map +1 -0
  12. package/build/CodedError.js +20 -0
  13. package/build/CodedError.js.map +1 -0
  14. package/build/ExpoNativeGoogleSignInModule.d.ts +9 -0
  15. package/build/ExpoNativeGoogleSignInModule.d.ts.map +1 -0
  16. package/build/ExpoNativeGoogleSignInModule.js +8 -0
  17. package/build/ExpoNativeGoogleSignInModule.js.map +1 -0
  18. package/build/index.d.ts +10 -0
  19. package/build/index.d.ts.map +1 -0
  20. package/build/index.js +52 -0
  21. package/build/index.js.map +1 -0
  22. package/build/index.web.d.ts +7 -0
  23. package/build/index.web.d.ts.map +1 -0
  24. package/build/index.web.js +16 -0
  25. package/build/index.web.js.map +1 -0
  26. package/build/types.d.ts +58 -0
  27. package/build/types.d.ts.map +1 -0
  28. package/build/types.js +22 -0
  29. package/build/types.js.map +1 -0
  30. package/expo-module.config.json +10 -0
  31. package/ios/ErrorMapping.swift +52 -0
  32. package/ios/ExpoNativeGoogleSignIn.podspec +35 -0
  33. package/ios/ExpoNativeGoogleSignInAppDelegateSubscriber.swift +14 -0
  34. package/ios/ExpoNativeGoogleSignInModule.swift +157 -0
  35. package/ios/SignInOptions.swift +13 -0
  36. package/package.json +78 -0
  37. package/plugin/build/index.d.ts +19 -0
  38. package/plugin/build/index.js +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eleftheriou Apostolos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # expo-native-google-signin
2
+
3
+ Native Google sign-in for Expo apps on **Android** (Credential Manager) and **iOS** (the
4
+ `GoogleSignIn` SDK). It returns a Google ID token that any backend can verify — Supabase, Firebase,
5
+ or your own server.
6
+
7
+ **Why:** "Google Sign-In for Android" (`GoogleSignInClient` in `play-services-auth`) is deprecated
8
+ in favour of Credential Manager, and `play-services-auth` 22.0.0 already removed those classes from
9
+ the SDK. This module talks only to Google's first-party SDKs — no dependency on
10
+ `@react-native-google-signin/google-signin` or any other community wrapper. MIT-licensed.
11
+
12
+ ## Status and requirements
13
+
14
+ - **Pre-1.0 — not published yet.** The first release will be `0.1.0-beta.0`, on the npm `next` tag
15
+ (see [CHANGELOG.md](./CHANGELOG.md)); the install commands below work once it is out. Per
16
+ semver-pre-1.0, a minor bump may break the API — **pin an exact version**:
17
+
18
+ ```sh
19
+ npm install expo-native-google-signin@0.1.0-beta.0 --save-exact
20
+ ```
21
+
22
+ - **Expo SDK 57.**
23
+ - **A development build** — this module is not available in **Expo Go**.
24
+ - **Android:** `minSdk` 24.
25
+ - **iOS:** 15.1+, and **Xcode 26+** (Expo SDK 57 itself requires it — `expo-modules-jsi` needs
26
+ Swift tools 6.2).
27
+ - **`GoogleSignIn-iOS` ~> 9.2** (pinned; see `docs/ARCHITECTURE.md` for why).
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ npx expo install expo-native-google-signin
33
+ # or, pinned exactly:
34
+ npm install expo-native-google-signin@0.1.0-beta.0 --save-exact
35
+ ```
36
+
37
+ Add the config plugin in `app.json` / `app.config.*`:
38
+
39
+ ```json
40
+ {
41
+ "expo": {
42
+ "plugins": [
43
+ [
44
+ "expo-native-google-signin",
45
+ { "iosUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID" }
46
+ ]
47
+ ]
48
+ }
49
+ }
50
+ ```
51
+
52
+ Then rebuild the native projects:
53
+
54
+ ```sh
55
+ npx expo prebuild
56
+ npx expo run:android
57
+ npx expo run:ios
58
+ ```
59
+
60
+ (or an EAS build). `iosUrlScheme` is required for the sign-in sheet to hand control back to your
61
+ app on iOS — without it, the plugin only logs a warning.
62
+
63
+ ## Google Cloud setup
64
+
65
+ Create OAuth 2.0 client IDs in [Google Cloud Console](https://console.cloud.google.com/apis/credentials):
66
+
67
+ 1. **Web client** — its ID is `webClientId`. It becomes the `aud` claim of the ID token this module
68
+ returns, on **both** platforms.
69
+ 2. **iOS client** — its ID is `iosClientId`. Its reversed ID is `iosUrlScheme`
70
+ (`com.googleusercontent.apps.…`, shown as "URL scheme" on the client's page in the console).
71
+ 3. **Android client** — registered with your app's **package name** and **signing SHA-1**. There is
72
+ no `iosClientId`-equivalent to pass at runtime for Android: Google identifies the calling app by
73
+ package name + SHA-1 on this client, and the ID token's audience is the **web** client ID passed
74
+ as `webClientId`. See `docs/ARCHITECTURE.md` → "Platform mechanics" for the full rationale.
75
+
76
+ > [!WARNING]
77
+ > **Play App Signing re-signs your app**, so the Android OAuth client needs **both** the
78
+ > upload-key SHA-1 **and** the Play app-signing SHA-1. If you only register the upload-key SHA-1,
79
+ > sign-in fails only for builds installed from Google Play — usually surfacing as
80
+ > `NO_GOOGLE_ACCOUNT` even though the device has one. Also register your debug keystore's and any
81
+ > EAS build profile's SHA-1s, or sign-in won't work in development builds either.
82
+
83
+ Use placeholders like `YOUR_WEB_CLIENT_ID.apps.googleusercontent.com` when configuring — never
84
+ commit a real client ID, SHA-1 or keystore (this repo is public).
85
+
86
+ ## Usage
87
+
88
+ ```ts
89
+ import { isErrorWithCode, signIn, signOut } from "expo-native-google-signin";
90
+
91
+ async function handleSignIn() {
92
+ try {
93
+ const result = await signIn({
94
+ webClientId: "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
95
+ iosClientId: "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com", // required on iOS only
96
+ });
97
+
98
+ if (result.type === "cancelled") {
99
+ return;
100
+ }
101
+
102
+ // result.type === "success"
103
+ console.log(result.user.email, result.idToken);
104
+ // There is no `result.user.id` -- decode `result.idToken` (a JWT) and read its `sub`
105
+ // claim if you need the stable Google account identifier.
106
+ } catch (error) {
107
+ if (isErrorWithCode(error)) {
108
+ console.warn(error.code, error.message);
109
+ } else {
110
+ throw error;
111
+ }
112
+ }
113
+ }
114
+
115
+ async function handleSignOut() {
116
+ await signOut();
117
+ }
118
+ ```
119
+
120
+ ## API reference
121
+
122
+ ### `signIn(options): Promise<SignInResult>`
123
+
124
+ | Option | Type | Required | Notes |
125
+ | ------------- | -------- | ------------------ | -------------------------------------------------------------|
126
+ | `webClientId` | `string` | Yes (both platforms) | OAuth **web** client ID; becomes the token's `aud` claim. |
127
+ | `iosClientId` | `string` | iOS only | OAuth **iOS** client ID; ignored on Android. |
128
+ | `nonce` | `string` | No | Forwarded to the native SDK and embedded in the ID token. |
129
+
130
+ ```ts
131
+ type SignInResult =
132
+ | { type: "success"; idToken: string; user: GoogleUser }
133
+ | { type: "cancelled" };
134
+
135
+ type GoogleUser = {
136
+ email: string;
137
+ name?: string;
138
+ givenName?: string;
139
+ familyName?: string;
140
+ photoUrl?: string;
141
+ };
142
+ ```
143
+
144
+ There is deliberately no `user.id`: Android's `GoogleIdTokenCredential.id` is the account's email
145
+ address, not the Google subject. Decode `idToken` and read its `sub` claim instead.
146
+
147
+ A user-initiated cancel **resolves** `{ type: "cancelled" }` — it does not reject.
148
+
149
+ ### `signOut(): Promise<void>`
150
+
151
+ Clears the locally cached Google credential state on both platforms.
152
+
153
+ ### `isErrorWithCode(error): error is Error & { code: ErrorCode }`
154
+
155
+ Narrows a caught `error` to one with a known `code` from `ERROR_CODES` below.
156
+
157
+ ### `ERROR_CODES`
158
+
159
+ `signIn`/`signOut` reject with an `Error` whose `code` is one of these. The native error message is
160
+ always preserved on `error.message` for logging.
161
+
162
+ | Code | When | Platform(s) |
163
+ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------| ----------- |
164
+ | `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 |
165
+ | `NO_GOOGLE_ACCOUNT` | No Google account is available to choose from (Credential Manager's `NoCredentialException`) | Android only |
166
+ | `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 |
167
+ | `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 |
168
+ | `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 |
169
+ | `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 |
170
+
171
+ ## Backends
172
+
173
+ ### Supabase — `signInWithIdToken` with a nonce
174
+
175
+ Verifying the nonce ties the ID token to this specific sign-in attempt. Generate a raw nonce,
176
+ SHA-256-hash it, pass the **hashed** nonce into `signIn`, and pass the **raw** nonce to Supabase (it
177
+ hashes it again internally to compare against the token's `nonce` claim):
178
+
179
+ ```ts
180
+ import * as Crypto from "expo-crypto";
181
+ import { signIn } from "expo-native-google-signin";
182
+ import { createClient } from "@supabase/supabase-js";
183
+
184
+ const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
185
+
186
+ async function signInWithGoogle() {
187
+ const rawNonce = Crypto.randomUUID();
188
+ const hashedNonce = await Crypto.digestStringAsync(
189
+ Crypto.CryptoDigestAlgorithm.SHA256,
190
+ rawNonce,
191
+ );
192
+
193
+ const result = await signIn({
194
+ webClientId: "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
195
+ iosClientId: "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
196
+ nonce: hashedNonce,
197
+ });
198
+
199
+ if (result.type === "cancelled") {
200
+ return;
201
+ }
202
+
203
+ const { data, error } = await supabase.auth.signInWithIdToken({
204
+ provider: "google",
205
+ token: result.idToken,
206
+ nonce: rawNonce,
207
+ });
208
+ }
209
+ ```
210
+
211
+ (`expo-crypto` and `@supabase/supabase-js` are not dependencies of this module — install them
212
+ yourself if you use this pattern.)
213
+
214
+ ### Firebase
215
+
216
+ ```ts
217
+ import { getAuth, GoogleAuthProvider, signInWithCredential } from "firebase/auth";
218
+
219
+ const credential = GoogleAuthProvider.credential(result.idToken);
220
+ await signInWithCredential(getAuth(), credential);
221
+ ```
222
+
223
+ ### A custom server
224
+
225
+ Verify, on the server: the ID token's signature (against Google's published JWKS), `aud` (must
226
+ equal your **web** client ID), `iss` (`accounts.google.com` / `https://accounts.google.com`),
227
+ `exp`, and — if you passed one to `signIn` — the (hashed) `nonce` claim.
228
+
229
+ ## Migrating from `@react-native-google-signin/google-signin`
230
+
231
+ | `@react-native-google-signin/google-signin` | `expo-native-google-signin` |
232
+ | ---------------------------------------------------------------------| ----------------------------------------------------------------------------------------------|
233
+ | `GoogleSignin.configure({ webClientId, iosClientId })` then `GoogleSignin.signIn()` | One call: `signIn({ webClientId, iosClientId, nonce? })` — no separate configure step |
234
+ | Result `{ type: "success" \| "cancelled", ... }` | Same `type` values and meaning — a cancel resolves, it doesn't throw |
235
+ | `user.id` (Google subject) | No `id` — decode `idToken` and read its `sub` claim instead |
236
+ | `androidClientId` config option | Removed — not needed; Google matches by package name + SHA-1, and `webClientId` is the token audience on both platforms |
237
+ | `GoogleSignin.signOut()` | `signOut()` |
238
+ | Config plugin option `iosUrlScheme` | Same option name, same purpose |
239
+
240
+ This module exists because the free version of `@react-native-google-signin/google-signin` stays on
241
+ the legacy, deprecated Android sign-in API — its Credential Manager support is part of a paid
242
+ product (see `docs/ARCHITECTURE.md` → "Why this project exists").
243
+
244
+ ## Troubleshooting
245
+
246
+ - **`NO_GOOGLE_ACCOUNT` only on builds installed from Google Play:** the Android OAuth client is
247
+ missing the Play app-signing SHA-1 (see the warning above) — register both SHA-1s.
248
+ - **`SIGN_IN_FAILED` with `[16] Account reauth failed` on Android:** the Android OAuth client is
249
+ missing this build's signing SHA-1 (debug, upload or Play app-signing). An Android OAuth client
250
+ holds one SHA-1, so register one client per key, with the same package name, in the same Google
251
+ Cloud project as `webClientId`.
252
+ - **No accounts on an emulator:** use an emulator image that includes the Play Store, and sign into
253
+ a Google account on it first.
254
+ - **`CONFIGURATION_ERROR` in Expo Go:** this module requires a development build; Expo Go can't load
255
+ it.
256
+ - **iOS never returns to the app after choosing an account:** the config plugin's `iosUrlScheme` is
257
+ missing or wrong — check `Info.plist`'s `CFBundleURLTypes` after `expo prebuild`.
258
+ - **iOS build fails to resolve packages / Xcode errors:** this module (via Expo SDK 57) requires
259
+ **Xcode 26+**.
260
+
261
+ ## Example app
262
+
263
+ ```sh
264
+ cd example
265
+ cp .env.example .env
266
+ # fill in EXPO_PUBLIC_WEB_CLIENT_ID, EXPO_PUBLIC_IOS_CLIENT_ID, EXPO_PUBLIC_IOS_URL_SCHEME
267
+ npm install
268
+ npx expo run:android # or: npx expo run:ios
269
+ ```
270
+
271
+ ## Contributing / releasing (maintainers)
272
+
273
+ See `CLAUDE.md` for the full command reference (`npm run build`, `lint`, `typecheck`, `test`,
274
+ `build:plugin`, `test:plugin`, etc.).
275
+
276
+ **Release:**
277
+
278
+ 1. Bump the version in `package.json` (and `android/build.gradle`'s `version`/`versionName`, which
279
+ is not read from `package.json`), and add a `CHANGELOG.md` entry.
280
+ 2. Merge to `main`.
281
+ 3. Tag `vX.Y.Z` from `main` and push the tag — `release.yml` publishes to npm with provenance
282
+ (prereleases go to the `next` dist-tag, stable versions to `latest`), but only if the tag matches
283
+ `package.json`'s version and the tagged commit is on `main`.
284
+
285
+ **First publish:** npm trusted publishing needs the package to already exist, so the very first
286
+ version is published manually (`npm publish --access public --tag next`), or via an `NPM_TOKEN`
287
+ repository secret.
288
+
289
+ **After that**, configure npm trusted publishing so `release.yml` doesn't need a token: on
290
+ npmjs.com, package settings → **Trusted Publisher** → GitHub Actions, repo
291
+ `ele-ap/expo-native-google-signin`, workflow `release.yml`, environment `npm`. Then delete the
292
+ `NPM_TOKEN` secret.
293
+
294
+ ## Disclaimer
295
+
296
+ This project is not affiliated with or endorsed by Google.
297
+
298
+ ## License
299
+
300
+ [MIT](./LICENSE)
@@ -0,0 +1,30 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'expo.modules.nativegooglesignin'
7
+ version = '0.1.0-beta.0'
8
+
9
+ android {
10
+ namespace "expo.modules.nativegooglesignin"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName "0.1.0-beta.0"
14
+ consumerProguardFiles 'consumer-rules.pro'
15
+ }
16
+ lintOptions {
17
+ abortOnError false
18
+ }
19
+ testOptions {
20
+ unitTests.returnDefaultValues = true
21
+ }
22
+ }
23
+
24
+ dependencies {
25
+ implementation 'androidx.credentials:credentials:1.6.0'
26
+ implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
27
+ implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.0'
28
+
29
+ testImplementation 'junit:junit:4.13.2'
30
+ }
@@ -0,0 +1,4 @@
1
+ -if class androidx.credentials.CredentialManager
2
+ -keep class androidx.credentials.playservices.** {
3
+ *;
4
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,83 @@
1
+ package expo.modules.nativegooglesignin
2
+
3
+ import androidx.credentials.exceptions.GetCredentialCancellationException
4
+ import androidx.credentials.exceptions.GetCredentialException
5
+ import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
6
+ import androidx.credentials.exceptions.GetCredentialUnsupportedException
7
+ import androidx.credentials.exceptions.NoCredentialException
8
+
9
+ /**
10
+ * Error codes `signIn`/`signOut` reject with. Must match `ERROR_CODES` in `src/types.ts` exactly
11
+ * -- see docs/ARCHITECTURE.md -> "Public API shape".
12
+ */
13
+ object ErrorCodes {
14
+ const val CONFIGURATION_ERROR = "CONFIGURATION_ERROR"
15
+ const val NO_GOOGLE_ACCOUNT = "NO_GOOGLE_ACCOUNT"
16
+ const val PROVIDER_UNAVAILABLE = "PROVIDER_UNAVAILABLE"
17
+ const val NO_PRESENTER = "NO_PRESENTER"
18
+ const val UNEXPECTED_CREDENTIAL = "UNEXPECTED_CREDENTIAL"
19
+ const val SIGN_IN_FAILED = "SIGN_IN_FAILED"
20
+ }
21
+
22
+ /**
23
+ * The outcome of mapping a Credential Manager [GetCredentialException] for `signIn`.
24
+ *
25
+ * Kept free of Android framework calls (pure logic over the exception's own properties) so it can
26
+ * be covered by a plain JVM unit test -- see `ErrorMappingTest`.
27
+ */
28
+ sealed interface SignInFailure {
29
+ /**
30
+ * The user dismissed the account chooser; `signIn` should resolve `{ type: 'cancelled' }`.
31
+ * Not every [GetCredentialCancellationException] is a dismissal: `[16] Account reauth failed`
32
+ * (matched on `[16]` or "reauth failed") is a misconfiguration and maps to [Coded] -- see [mapGetCredentialException].
33
+ */
34
+ object Cancelled : SignInFailure
35
+
36
+ /** `signIn` should reject with this coded error. */
37
+ data class Coded(val code: String, val message: String) : SignInFailure
38
+ }
39
+
40
+ private const val REAUTH_FAILED_HINT =
41
+ " -- this usually means the app's package name + signing SHA-1 aren't registered on an " +
42
+ "Android OAuth client in the same Google Cloud project as webClientId (register debug, " +
43
+ "upload and Play app-signing SHA-1s; see README Troubleshooting)."
44
+
45
+ private fun isReauthFailed(msg: String): Boolean =
46
+ msg.contains("[16]") || msg.contains("reauth failed", ignoreCase = true)
47
+
48
+ /**
49
+ * Maps a [GetCredentialException] thrown by `CredentialManager.getCredentialAsync` to a
50
+ * [SignInFailure] -- see docs/ARCHITECTURE.md -> "Public API shape" and the plan's "Native
51
+ * behaviour -> Android -> Error mapping" for the rationale behind each mapping.
52
+ *
53
+ * Exception: Google Play services reports `[16] Account reauth failed` -- usually an unregistered
54
+ * package name + signing SHA-1 -- as a [GetCredentialCancellationException]. That is a real
55
+ * failure, not a user dismissal, so a cancellation whose message contains `[16]` or "reauth
56
+ * failed" (case-insensitive, in case a Play services version formats the code differently) maps
57
+ * to `SIGN_IN_FAILED` (with a hint) instead of [SignInFailure.Cancelled]. Any other cancellation
58
+ * stays [SignInFailure.Cancelled].
59
+ */
60
+ fun mapGetCredentialException(e: GetCredentialException): SignInFailure = when (e) {
61
+ is GetCredentialCancellationException ->
62
+ if (isReauthFailed(e.message.orEmpty())) {
63
+ SignInFailure.Coded(ErrorCodes.SIGN_IN_FAILED, nativeMessage(e) + REAUTH_FAILED_HINT)
64
+ } else {
65
+ SignInFailure.Cancelled
66
+ }
67
+ is NoCredentialException -> SignInFailure.Coded(ErrorCodes.NO_GOOGLE_ACCOUNT, nativeMessage(e))
68
+ is GetCredentialProviderConfigurationException,
69
+ is GetCredentialUnsupportedException ->
70
+ SignInFailure.Coded(ErrorCodes.PROVIDER_UNAVAILABLE, nativeMessage(e))
71
+ else -> SignInFailure.Coded(ErrorCodes.SIGN_IN_FAILED, nativeMessage(e))
72
+ }
73
+
74
+ /**
75
+ * The native message: [Throwable.message] (which androidx sets from the exception's error
76
+ * message; `errorMessage` itself is a restricted API), falling back to
77
+ * [GetCredentialException.type]. Prefixed with the exception's simple class name, since several
78
+ * exception types share a single error code above.
79
+ */
80
+ private fun nativeMessage(e: GetCredentialException): String {
81
+ val message = e.message ?: e.type
82
+ return "${e.javaClass.simpleName}: $message"
83
+ }
@@ -0,0 +1,187 @@
1
+ package expo.modules.nativegooglesignin
2
+
3
+ import android.os.CancellationSignal
4
+ import androidx.core.content.ContextCompat
5
+ import androidx.credentials.ClearCredentialStateRequest
6
+ import androidx.credentials.CredentialManager
7
+ import androidx.credentials.CredentialManagerCallback
8
+ import androidx.credentials.CustomCredential
9
+ import androidx.credentials.GetCredentialRequest
10
+ import androidx.credentials.GetCredentialResponse
11
+ import androidx.credentials.exceptions.ClearCredentialException
12
+ import androidx.credentials.exceptions.GetCredentialException
13
+ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
14
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
15
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
16
+ import expo.modules.kotlin.Promise
17
+ import expo.modules.kotlin.exception.CodedException
18
+ import expo.modules.kotlin.modules.Module
19
+ import expo.modules.kotlin.modules.ModuleDefinition
20
+
21
+ class ExpoNativeGoogleSignInModule : Module() {
22
+ override fun definition() = ModuleDefinition {
23
+ Name("ExpoNativeGoogleSignIn")
24
+
25
+ // Promise-based overloads: a lambda that only throws has type `Nothing`, which Expo's reified
26
+ // AsyncFunction can't accept (see CLAUDE.md -> Conventions).
27
+ AsyncFunction("signIn") { options: SignInOptions, promise: Promise ->
28
+ signIn(options, promise)
29
+ }
30
+
31
+ AsyncFunction("signOut") { promise: Promise ->
32
+ signOut(promise)
33
+ }
34
+ }
35
+
36
+ private fun signIn(options: SignInOptions, promise: Promise) {
37
+ val webClientId = options.webClientId.trim()
38
+ if (webClientId.isEmpty()) {
39
+ promise.reject(
40
+ CodedException(
41
+ ErrorCodes.CONFIGURATION_ERROR,
42
+ "expo-native-google-signin: `webClientId` is required on Android (pass the OAuth " +
43
+ "web client ID as `serverClientId` -- see docs/ARCHITECTURE.md).",
44
+ null
45
+ )
46
+ )
47
+ return
48
+ }
49
+
50
+ val activity = appContext.currentActivity
51
+ if (activity == null) {
52
+ promise.reject(
53
+ CodedException(
54
+ ErrorCodes.NO_PRESENTER,
55
+ "expo-native-google-signin: no current Activity to present the sign-in UI from.",
56
+ null
57
+ )
58
+ )
59
+ return
60
+ }
61
+
62
+ try {
63
+ // Treat a blank nonce as absent.
64
+ val nonce = options.nonce?.trim()?.takeUnless { it.isEmpty() }
65
+ val signInOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId)
66
+ .apply { nonce?.let { setNonce(it) } }
67
+ .build()
68
+ val request = GetCredentialRequest.Builder()
69
+ .addCredentialOption(signInOption)
70
+ .build()
71
+
72
+ CredentialManager.create(activity).getCredentialAsync(
73
+ activity,
74
+ request,
75
+ CancellationSignal(),
76
+ ContextCompat.getMainExecutor(activity),
77
+ object : CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
78
+ override fun onResult(result: GetCredentialResponse) = resolveSignIn(result, promise)
79
+
80
+ override fun onError(e: GetCredentialException) = rejectSignIn(e, promise)
81
+ }
82
+ )
83
+ } catch (t: Throwable) {
84
+ promise.reject(CodedException(ErrorCodes.SIGN_IN_FAILED, t.message ?: t.toString(), t))
85
+ }
86
+ }
87
+
88
+ private fun resolveSignIn(response: GetCredentialResponse, promise: Promise) {
89
+ try {
90
+ // 1.6.0 exposes only `credential` (singular); the `credentials` list arrived in 1.7.0-alpha03.
91
+ val credential = response.credential
92
+ if (
93
+ credential !is CustomCredential ||
94
+ credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
95
+ ) {
96
+ promise.reject(
97
+ CodedException(
98
+ ErrorCodes.UNEXPECTED_CREDENTIAL,
99
+ "expo-native-google-signin: received an unexpected credential of type " +
100
+ "'${credential.type}'.",
101
+ null
102
+ )
103
+ )
104
+ return
105
+ }
106
+
107
+ val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
108
+
109
+ // Omit null user fields rather than sending null, since the TS types are optional.
110
+ val user = mutableMapOf<String, Any?>("email" to googleIdTokenCredential.id)
111
+ googleIdTokenCredential.displayName?.let { user["name"] = it }
112
+ googleIdTokenCredential.givenName?.let { user["givenName"] = it }
113
+ googleIdTokenCredential.familyName?.let { user["familyName"] = it }
114
+ googleIdTokenCredential.profilePictureUri?.let { user["photoUrl"] = it.toString() }
115
+
116
+ promise.resolve(
117
+ mapOf(
118
+ "type" to "success",
119
+ "idToken" to googleIdTokenCredential.idToken,
120
+ "user" to user
121
+ )
122
+ )
123
+ } catch (e: GoogleIdTokenParsingException) {
124
+ promise.reject(
125
+ CodedException(
126
+ ErrorCodes.UNEXPECTED_CREDENTIAL,
127
+ "expo-native-google-signin: failed to parse the Google ID token credential " +
128
+ "(${e.message ?: e.toString()}).",
129
+ e
130
+ )
131
+ )
132
+ } catch (t: Throwable) {
133
+ promise.reject(CodedException(ErrorCodes.SIGN_IN_FAILED, t.message ?: t.toString(), t))
134
+ }
135
+ }
136
+
137
+ private fun rejectSignIn(e: GetCredentialException, promise: Promise) {
138
+ try {
139
+ when (val failure = mapGetCredentialException(e)) {
140
+ is SignInFailure.Cancelled -> promise.resolve(mapOf("type" to "cancelled"))
141
+ is SignInFailure.Coded ->
142
+ promise.reject(CodedException(failure.code, failure.message, e))
143
+ }
144
+ } catch (t: Throwable) {
145
+ promise.reject(CodedException(ErrorCodes.SIGN_IN_FAILED, t.message ?: t.toString(), t))
146
+ }
147
+ }
148
+
149
+ private fun signOut(promise: Promise) {
150
+ val context = appContext.reactContext
151
+ if (context == null) {
152
+ promise.reject(
153
+ CodedException(
154
+ ErrorCodes.SIGN_IN_FAILED,
155
+ "expo-native-google-signin: no application context available to sign out.",
156
+ null
157
+ )
158
+ )
159
+ return
160
+ }
161
+
162
+ try {
163
+ CredentialManager.create(context).clearCredentialStateAsync(
164
+ ClearCredentialStateRequest(),
165
+ CancellationSignal(),
166
+ ContextCompat.getMainExecutor(context),
167
+ object : CredentialManagerCallback<Void?, ClearCredentialException> {
168
+ override fun onResult(result: Void?) {
169
+ promise.resolve(null)
170
+ }
171
+
172
+ override fun onError(e: ClearCredentialException) {
173
+ promise.reject(
174
+ CodedException(
175
+ ErrorCodes.SIGN_IN_FAILED,
176
+ e.message ?: e.type,
177
+ e
178
+ )
179
+ )
180
+ }
181
+ }
182
+ )
183
+ } catch (t: Throwable) {
184
+ promise.reject(CodedException(ErrorCodes.SIGN_IN_FAILED, t.message ?: t.toString(), t))
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,22 @@
1
+ package expo.modules.nativegooglesignin
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+ import expo.modules.kotlin.types.OptimizedRecord
6
+
7
+ /**
8
+ * Options for `signIn`. Field names and types must match `SignInOptions` in `src/types.ts`
9
+ * exactly, since `src/index.ts` passes the JS object straight through.
10
+ *
11
+ * `iosClientId` is accepted (so the JS object doesn't need platform-specific stripping) but
12
+ * ignored on Android -- see docs/ARCHITECTURE.md -> "No `androidClientId` option".
13
+ */
14
+ @OptimizedRecord
15
+ data class SignInOptions(
16
+ @Field
17
+ val webClientId: String = "",
18
+ @Field
19
+ val iosClientId: String? = null,
20
+ @Field
21
+ val nonce: String? = null
22
+ ) : Record
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build');
@@ -0,0 +1,17 @@
1
+ import type { ErrorCode } from "./types";
2
+ /**
3
+ * A minimal stand-in for `expo-modules-core`'s `CodedError` -- same shape (an `Error` subclass
4
+ * with a `code` string) -- shared by `index.ts` and `index.web.ts` so `isErrorWithCode` treats
5
+ * JS-thrown errors (from either platform) and native-thrown errors identically.
6
+ *
7
+ * Deliberately not a dependency on `expo-modules-core` itself: this package only declares a peer
8
+ * dependency on `expo`, which doesn't re-export `CodedError`, and depending on
9
+ * `expo-modules-core` directly would break for consumers whose package manager doesn't hoist it
10
+ * (pnpm, Yarn PnP). Not exported from the package's public surface (`src/index.ts`) -- it's an
11
+ * internal implementation detail, not part of the public API.
12
+ */
13
+ export declare class CodedError extends Error {
14
+ code: ErrorCode;
15
+ constructor(code: ErrorCode, message: string);
16
+ }
17
+ //# sourceMappingURL=CodedError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodedError.d.ts","sourceRoot":"","sources":["../src/CodedError.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;;;GAUG;AACH,qBAAa,UAAW,SAAQ,KAAK;IACnC,IAAI,EAAE,SAAS,CAAC;gBAEJ,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM;CAK7C"}