@thoughtbot/react-native-social-auth 0.1.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 (52) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +316 -0
  3. package/ReactNativeSocialAuth.podspec +22 -0
  4. package/android/build.gradle +70 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt +188 -0
  7. package/android/src/main/java/com/thoughtbot/reactnativesocialauth/ReactNativeSocialAuthPackage.kt +31 -0
  8. package/ios/GoogleSignIn.h +7 -0
  9. package/ios/GoogleSignIn.mm +213 -0
  10. package/lib/module/google/GoogleLogo.js +41 -0
  11. package/lib/module/google/GoogleLogo.js.map +1 -0
  12. package/lib/module/google/GoogleSignIn.js +124 -0
  13. package/lib/module/google/GoogleSignIn.js.map +1 -0
  14. package/lib/module/google/GoogleSignInButton.js +176 -0
  15. package/lib/module/google/GoogleSignInButton.js.map +1 -0
  16. package/lib/module/google/NativeGoogleSignIn.js +5 -0
  17. package/lib/module/google/NativeGoogleSignIn.js.map +1 -0
  18. package/lib/module/google/errors.js +69 -0
  19. package/lib/module/google/errors.js.map +1 -0
  20. package/lib/module/google/index.js +6 -0
  21. package/lib/module/google/index.js.map +1 -0
  22. package/lib/module/google/types.js +2 -0
  23. package/lib/module/google/types.js.map +1 -0
  24. package/lib/module/index.js +4 -0
  25. package/lib/module/index.js.map +1 -0
  26. package/lib/module/package.json +1 -0
  27. package/lib/typescript/package.json +1 -0
  28. package/lib/typescript/src/google/GoogleLogo.d.ts +6 -0
  29. package/lib/typescript/src/google/GoogleLogo.d.ts.map +1 -0
  30. package/lib/typescript/src/google/GoogleSignIn.d.ts +89 -0
  31. package/lib/typescript/src/google/GoogleSignIn.d.ts.map +1 -0
  32. package/lib/typescript/src/google/GoogleSignInButton.d.ts +74 -0
  33. package/lib/typescript/src/google/GoogleSignInButton.d.ts.map +1 -0
  34. package/lib/typescript/src/google/NativeGoogleSignIn.d.ts +12 -0
  35. package/lib/typescript/src/google/NativeGoogleSignIn.d.ts.map +1 -0
  36. package/lib/typescript/src/google/errors.d.ts +57 -0
  37. package/lib/typescript/src/google/errors.d.ts.map +1 -0
  38. package/lib/typescript/src/google/index.d.ts +6 -0
  39. package/lib/typescript/src/google/index.d.ts.map +1 -0
  40. package/lib/typescript/src/google/types.d.ts +93 -0
  41. package/lib/typescript/src/google/types.d.ts.map +1 -0
  42. package/lib/typescript/src/index.d.ts +3 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -0
  44. package/package.json +180 -0
  45. package/src/google/GoogleLogo.tsx +35 -0
  46. package/src/google/GoogleSignIn.ts +131 -0
  47. package/src/google/GoogleSignInButton.tsx +224 -0
  48. package/src/google/NativeGoogleSignIn.ts +12 -0
  49. package/src/google/errors.ts +72 -0
  50. package/src/google/index.ts +19 -0
  51. package/src/google/types.ts +100 -0
  52. package/src/index.tsx +18 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomi Alu
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # @thoughtbot/react-native-social-auth
2
+
3
+ Drop-in Google Sign-In for React Native using Android's Credential Manager and the official Google branding.
4
+
5
+ **Platform support:** ✅ Android · ✅ iOS
6
+
7
+ ## Features
8
+
9
+ - Android **Credential Manager** and iOS **GoogleSignIn-iOS SDK** integration, both with auto-sign-in + interactive fallback
10
+ - Google-branding-compliant **`GoogleSignInButton`** (3 themes, 2 shapes, 3 text variants, icon-only)
11
+ - TypeScript-first, ships as a **Turbo Module** (new architecture)
12
+ - Typed errors via `GoogleSignInError` and `GoogleSignInErrorCode` for clean UX-level handling
13
+
14
+ ## Requirements
15
+
16
+ - React Native `>=0.74` with the new architecture enabled
17
+ - Android `minSdkVersion` 24
18
+ - A Google Cloud project with OAuth 2.0 credentials (see [setup](#google-cloud-console-setup))
19
+ - [`react-native-svg`](https://github.com/software-mansion/react-native-svg) `>=13.0.0` (peer dependency — used to render the Google "G" logo)
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ yarn add @thoughtbot/react-native-social-auth react-native-svg
25
+ # or
26
+ npm install @thoughtbot/react-native-social-auth react-native-svg
27
+ ```
28
+
29
+ > `react-native-svg` is a peer dependency. If it's not already in your app, install it explicitly — otherwise you'll see runtime errors like `Tried to register two views with the same name RNSVGRect`.
30
+
31
+ After installing, rebuild the native app:
32
+
33
+ ```sh
34
+ # Android
35
+ yarn android
36
+
37
+ # iOS (when supported)
38
+ cd ios && pod install && cd ..
39
+ yarn ios
40
+ ```
41
+
42
+ ## Google Cloud Console setup
43
+
44
+ Most "sign-in failed" issues come from misconfigured credentials. Follow these steps once per project.
45
+
46
+ 1. Create a project at [console.cloud.google.com](https://console.cloud.google.com/).
47
+ 2. Configure the **OAuth consent screen**:
48
+ - User type: **External**
49
+ - Publishing status: **Testing** is fine for development
50
+ - Add your Google account under **Test users**
51
+ 3. Create a **Web application** OAuth client ID under **APIs & Services → Credentials**. Copy the Client ID — this is the value you'll pass as `webClientId`.
52
+ 4. Create an **Android** OAuth client ID in the **same project**:
53
+ - **Package name**: your app's `applicationId` (e.g. `com.example.myapp`)
54
+ - **SHA-1 certificate fingerprint**: get it with
55
+ ```sh
56
+ cd android && ./gradlew signingReport
57
+ ```
58
+ and copy the `SHA1` line under `Variant: debug`.
59
+ 5. **Repeat step 4 for production.** The debug client only authorizes your debug-signed APK. Before shipping a signed release build (Play Store, internal testing tracks, or any release-signed APK), create a **second Android OAuth client** in the same GCP project using the same package name and the **release SHA-1** of your upload/signing key. If you use **Play App Signing**, use the **App signing key certificate** SHA-1 from the Play Console (Setup → App integrity), not your upload key. Without this, production users will hit `[28444] Developer console is not set up correctly`.
60
+ 6. **For iOS**, also create an **iOS** OAuth client ID in the same GCP project with your app's **Bundle Identifier**. Copy the Client ID — pass it as `iosClientId`. Copy the **iOS URL scheme** Google shows you (it's the reversed iOS Client ID, e.g. `com.googleusercontent.apps.123456-abcdef`) — you'll add it to `Info.plist` in the next section.
61
+
62
+ The Web client ID is what your code references for the ID-token audience; each platform-specific client (Android debug, Android release, iOS) is an invisible passport that authorizes a specific build to use it. **All clients must live in the same GCP project.**
63
+
64
+ ## iOS setup
65
+
66
+ In addition to the Cloud Console step above, the host app needs two iOS-specific changes.
67
+
68
+ ### 1. Register the OAuth URL scheme
69
+
70
+ Google routes the sign-in callback back into your app via a custom URL scheme. Add the reversed iOS Client ID to `Info.plist`:
71
+
72
+ ```xml
73
+ <key>CFBundleURLTypes</key>
74
+ <array>
75
+ <dict>
76
+ <key>CFBundleURLSchemes</key>
77
+ <array>
78
+ <string>com.googleusercontent.apps.YOUR-REVERSED-IOS-CLIENT-ID</string>
79
+ </array>
80
+ </dict>
81
+ </array>
82
+ ```
83
+
84
+ If you use **Expo**, declare it in `app.json` under `expo.ios.infoPlist.CFBundleURLTypes` and rerun `npx expo prebuild --platform ios --clean`.
85
+
86
+ ### 2. Forward incoming URLs to the SDK
87
+
88
+ In your `AppDelegate`, forward `application(_:open:options:)` to `GoogleSignIn.handleURL(_:)`.
89
+
90
+ **Swift:**
91
+ ```swift
92
+ import react_native_social_auth
93
+
94
+ func application(
95
+ _ app: UIApplication,
96
+ open url: URL,
97
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
98
+ ) -> Bool {
99
+ return GoogleSignIn.handleURL(url)
100
+ }
101
+ ```
102
+
103
+ **Objective-C:**
104
+ ```objc
105
+ #import <react_native_social_auth/GoogleSignIn.h>
106
+
107
+ - (BOOL)application:(UIApplication *)app
108
+ openURL:(NSURL *)url
109
+ options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
110
+ return [GoogleSignIn handleURL:url];
111
+ }
112
+ ```
113
+
114
+ ### 3. Configure with both client IDs
115
+
116
+ Pass both the Web client ID (token audience) and iOS client ID (caller identity) to `configure`:
117
+
118
+ ```ts
119
+ GoogleSignIn.configure({
120
+ webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
121
+ iosClientId: 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com',
122
+ });
123
+ ```
124
+
125
+ Finally, run `cd ios && pod install` after installing the package.
126
+
127
+ ## Quick start
128
+
129
+ ```tsx
130
+ import { useState } from 'react';
131
+ import {
132
+ GoogleSignIn,
133
+ GoogleSignInButton,
134
+ isGoogleSignInError,
135
+ type GoogleUser,
136
+ } from '@thoughtbot/react-native-social-auth';
137
+
138
+ GoogleSignIn.configure({
139
+ webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
140
+ });
141
+
142
+ export function SignInScreen() {
143
+ const [user, setUser] = useState<GoogleUser | null>(null);
144
+
145
+ const handleSignIn = async () => {
146
+ try {
147
+ const credential = await GoogleSignIn.signIn();
148
+ setUser(credential.user);
149
+ // Send credential.idToken to your backend for verification.
150
+ } catch (error) {
151
+ if (isGoogleSignInError(error)) {
152
+ console.warn(error.code, error.message);
153
+ }
154
+ }
155
+ };
156
+
157
+ return <GoogleSignInButton onPress={handleSignIn} />;
158
+ }
159
+ ```
160
+
161
+ ## API reference
162
+
163
+ All members are named exports from `@thoughtbot/react-native-social-auth`.
164
+
165
+ ### `GoogleSignIn`
166
+
167
+ A singleton with the runtime sign-in API. **You must call `configure()` once before any other method**, or they'll throw `GoogleSignInError` with code `NOT_CONFIGURED`.
168
+
169
+ #### `configure(config: GoogleSignInConfig): void`
170
+
171
+ Stores credentials and options used by subsequent calls.
172
+
173
+ | Field | Type | Required | Description |
174
+ | ---------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------- |
175
+ | `webClientId` | `string` | Yes | The **Web application** OAuth Client ID. This is the audience of the issued ID token. |
176
+ | `iosClientId` | `string` | No | iOS OAuth Client ID (used once iOS support lands). |
177
+ | `offlineAccess` | `boolean` | No | Request a server auth code in addition to the ID token. Default `false`. |
178
+ | `scopes` | `string[]` | No | Additional OAuth scopes beyond the default profile/email. |
179
+ | `hostedDomain` | `string` | No | Restrict sign-in to a Google Workspace domain. |
180
+ | `autoSelect` | `boolean` | No | If `true`, returning users sign in silently when possible. Default `false`. |
181
+ | `nonce` | `string` | No | A unique value bound into the ID token; recommended when verifying tokens on a backend. |
182
+
183
+ #### `signIn(): Promise<GoogleAuthCredential>`
184
+
185
+ Launches the sign-in flow. On Android, this tries an **auto sign-in** first (silently re-uses a previously authorized account) and falls back to the **bottom sheet** when no authorized account is found.
186
+
187
+ Resolves to a `GoogleAuthCredential`:
188
+
189
+ | Field | Type | Description |
190
+ | ---------------- | -------------------------- | ---------------------------------------------------------------------------- |
191
+ | `idToken` | `string` | The Google ID token. Verify this on your backend. |
192
+ | `accessToken` | `string \| null` | OAuth access token (null when not requested). |
193
+ | `serverAuthCode` | `string \| null` | One-time code for backend exchange (requires `offlineAccess: true`). |
194
+ | `user` | [`GoogleUser`](#googleuser) | The authenticated user's profile. |
195
+
196
+ Rejects with a [`GoogleSignInError`](#error-handling).
197
+
198
+ #### `signOut(): Promise<void>`
199
+
200
+ Clears the local credential state. The user remains signed into Google itself.
201
+
202
+ #### `getCurrentUser(): Promise<GoogleUser | null>`
203
+
204
+ Returns the in-memory authenticated user, or `null` if no one has signed in since app launch.
205
+
206
+ #### `revokeAccess(): Promise<void>`
207
+
208
+ Revokes the app's access to the user's Google account.
209
+
210
+ #### `isSignedIn(): boolean`
211
+
212
+ Synchronous check for whether a user is currently signed in (in memory).
213
+
214
+ ### Types
215
+
216
+ #### `GoogleUser`
217
+
218
+ | Field | Type |
219
+ | ------------- | ---------------- |
220
+ | `id` | `string` |
221
+ | `email` | `string` |
222
+ | `displayName` | `string \| null` |
223
+ | `givenName` | `string \| null` |
224
+ | `familyName` | `string \| null` |
225
+ | `photoUrl` | `string \| null` |
226
+
227
+ #### `GoogleAuthCredential`
228
+
229
+ See [`signIn`](#signin-promisegoogleauthcredential) above.
230
+
231
+ ### `<GoogleSignInButton />`
232
+
233
+ A pre-built button that conforms to the [official Google branding guidelines](https://developers.google.com/identity/branding-guidelines). The button renders the Google "G" via `react-native-svg`, so it stays crisp at any density without bundling raster assets.
234
+
235
+ | Prop | Type | Default | Description |
236
+ | ---------- | ------------------------------------------ | ------------ | -------------------------------------------------------------------- |
237
+ | `theme` | `'light' \| 'dark' \| 'neutral'` | `'light'` | Visual theme. Picks the right background, border, and text color. |
238
+ | `shape` | `'rounded' \| 'square'` | `'rounded'` | Pill (`borderRadius: 20`) or square (`borderRadius: 4`). |
239
+ | `text` | `'signin' \| 'signup' \| 'continue'` | `'signin'` | One of the three call-to-actions Google permits. |
240
+ | `size` | `'standard' \| 'icon'` | `'standard'` | Full-width button with text, or 40×40 icon-only. |
241
+ | `onPress` | `() => void` | — | Tap handler — wire this to `GoogleSignIn.signIn()`. |
242
+ | `disabled` | `boolean` | `false` | Renders at 0.38 opacity and disables taps. |
243
+ | `style` | `StyleProp<ViewStyle>` | — | Additional container styles (margin, alignment, etc.). |
244
+ | `testID` | `string` | — | Testing identifier. |
245
+
246
+ > **Do not restyle** the logo, text, or theme colors — Google's brand review will reject apps that do.
247
+
248
+ ## Error handling
249
+
250
+ Every error from `GoogleSignIn` is a `GoogleSignInError` with a `code` from `GoogleSignInErrorCode`. Use `isGoogleSignInError` to narrow:
251
+
252
+ ```tsx
253
+ import {
254
+ GoogleSignIn,
255
+ isGoogleSignInError,
256
+ GoogleSignInErrorCode,
257
+ } from '@thoughtbot/react-native-social-auth';
258
+
259
+ try {
260
+ await GoogleSignIn.signIn();
261
+ } catch (error) {
262
+ if (isGoogleSignInError(error)) {
263
+ switch (error.code) {
264
+ case GoogleSignInErrorCode.SIGN_IN_CANCELLED:
265
+ // User dismissed the bottom sheet — no UI needed.
266
+ break;
267
+ case GoogleSignInErrorCode.NO_CREDENTIALS:
268
+ showAlert('No Google accounts on this device.');
269
+ break;
270
+ case GoogleSignInErrorCode.PLAY_SERVICES_NOT_AVAILABLE:
271
+ showAlert('Google Play Services is missing or out of date.');
272
+ break;
273
+ default:
274
+ showAlert(`Sign-in failed: ${error.message}`);
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ | Code | Meaning |
281
+ | ------------------------------ | ---------------------------------------------------------------------------------------- |
282
+ | `SIGN_IN_CANCELLED` | The user dismissed the bottom sheet. Don't show an error. |
283
+ | `SIGN_IN_FAILED` | Generic failure from Credential Manager — the `message` has details. |
284
+ | `NO_CREDENTIALS` | No Google accounts on the device, or no authorized accounts when auto sign-in was tried. |
285
+ | `PLAY_SERVICES_NOT_AVAILABLE` | Device is missing Google Play Services (common on bare emulators). |
286
+ | `NETWORK_ERROR` | The device couldn't reach Google's auth servers. |
287
+ | `NOT_CONFIGURED` | A method was called before `GoogleSignIn.configure()`. |
288
+
289
+ ## Example app
290
+
291
+ A runnable example lives in [`/example`](example/). To try it:
292
+
293
+ ```sh
294
+ yarn install
295
+ cp example/.env.example example/.env
296
+ # Edit example/.env and set EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID (and EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID for iOS)
297
+ yarn workspace @thoughtbot/react-native-social-auth-example start --clear
298
+ yarn workspace @thoughtbot/react-native-social-auth-example android
299
+ # or for iOS:
300
+ yarn workspace @thoughtbot/react-native-social-auth-example ios
301
+ ```
302
+
303
+ For iOS, edit `example/app.json` and replace `REPLACE_WITH_REVERSED_IOS_CLIENT_ID` under `expo.ios.infoPlist.CFBundleURLTypes` with your reversed iOS Client ID, then run `npx expo prebuild --platform ios --clean` before the `yarn ios` command.
304
+
305
+ The example showcases every variant of `GoogleSignInButton` and exercises the full public API.
306
+
307
+ ## Contributing
308
+
309
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
310
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
311
+ - [Code of conduct](CODE_OF_CONDUCT.md)
312
+ - [Roadmap](ROADMAP.md)
313
+
314
+ ## License
315
+
316
+ MIT
@@ -0,0 +1,22 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ReactNativeSocialAuth"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/thoughtbot/react-native-social-auth.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+
21
+ s.dependency "GoogleSignIn", "~> 7.1"
22
+ end
@@ -0,0 +1,70 @@
1
+ buildscript {
2
+ ext.ReactNativeSocialAuth = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return ReactNativeSocialAuth[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.thoughtbot.reactnativesocialauth"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+ implementation "androidx.credentials:credentials:1.5.0"
68
+ implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
69
+ implementation "com.google.android.libraries.identity.googleid:googleid:1.1.1"
70
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,188 @@
1
+ package com.thoughtbot.reactnativesocialauth
2
+
3
+ import androidx.credentials.ClearCredentialStateRequest
4
+ import androidx.credentials.CredentialManager
5
+ import androidx.credentials.CustomCredential
6
+ import androidx.credentials.GetCredentialRequest
7
+ import androidx.credentials.GetCredentialResponse
8
+ import androidx.credentials.exceptions.GetCredentialCancellationException
9
+ import androidx.credentials.exceptions.GetCredentialException
10
+ import androidx.credentials.exceptions.NoCredentialException
11
+ import com.facebook.react.bridge.Arguments
12
+ import com.facebook.react.bridge.Promise
13
+ import com.facebook.react.bridge.ReactApplicationContext
14
+ import com.facebook.react.bridge.ReadableMap
15
+ import com.google.android.libraries.identity.googleid.GetGoogleIdOption
16
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
17
+ import kotlinx.coroutines.CoroutineScope
18
+ import kotlinx.coroutines.Dispatchers
19
+ import kotlinx.coroutines.SupervisorJob
20
+ import kotlinx.coroutines.cancel
21
+ import kotlinx.coroutines.launch
22
+
23
+ class GoogleSignInModule(reactContext: ReactApplicationContext) :
24
+ NativeGoogleSignInSpec(reactContext) {
25
+
26
+ private val credentialManager: CredentialManager =
27
+ CredentialManager.create(reactContext)
28
+
29
+ private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
30
+
31
+ private var webClientId: String? = null
32
+ private var nonce: String? = null
33
+ private var autoSelect: Boolean = false
34
+ private var currentUser: GoogleIdTokenCredential? = null
35
+
36
+ override fun configure(config: ReadableMap) {
37
+ webClientId = config.getString("webClientId")
38
+ nonce = if (config.hasKey("nonce")) config.getString("nonce") else null
39
+ autoSelect = if (config.hasKey("autoSelect")) config.getBoolean("autoSelect") else false
40
+ }
41
+
42
+ override fun signIn(promise: Promise) {
43
+ val clientId = webClientId
44
+ if (clientId == null) {
45
+ promise.reject("ERR_NOT_CONFIGURED", "GoogleSignIn.configure() must be called before signIn()")
46
+ return
47
+ }
48
+
49
+ val activity = currentActivity
50
+ if (activity == null) {
51
+ promise.reject("ERR_NO_ACTIVITY", "No current activity available")
52
+ return
53
+ }
54
+
55
+ scope.launch {
56
+ try {
57
+ val result = getCredentialWithAutoSignIn(clientId, activity)
58
+ handleSignInResult(result, promise)
59
+ } catch (e: GetCredentialCancellationException) {
60
+ promise.reject("SIGN_IN_CANCELLED", "User cancelled the sign-in flow", e)
61
+ } catch (e: NoCredentialException) {
62
+ promise.reject("NO_CREDENTIALS", "No credentials available on this device", e)
63
+ } catch (e: GetCredentialException) {
64
+ promise.reject("SIGN_IN_FAILED", e.message, e)
65
+ }
66
+ }
67
+ }
68
+
69
+ private suspend fun getCredentialWithAutoSignIn(
70
+ clientId: String,
71
+ activity: android.app.Activity
72
+ ): GetCredentialResponse {
73
+ val autoSignInOption = GetGoogleIdOption.Builder()
74
+ .setFilterByAuthorizedAccounts(true)
75
+ .setServerClientId(clientId)
76
+ .setAutoSelectEnabled(true)
77
+ .apply { nonce?.let { setNonce(it) } }
78
+ .build()
79
+
80
+ val autoRequest = GetCredentialRequest.Builder()
81
+ .addCredentialOption(autoSignInOption)
82
+ .build()
83
+
84
+ return try {
85
+ credentialManager.getCredential(activity, autoRequest)
86
+ } catch (e: NoCredentialException) {
87
+ val fallbackOption = GetGoogleIdOption.Builder()
88
+ .setFilterByAuthorizedAccounts(false)
89
+ .setServerClientId(clientId)
90
+ .setAutoSelectEnabled(false)
91
+ .apply { nonce?.let { setNonce(it) } }
92
+ .build()
93
+
94
+ val fallbackRequest = GetCredentialRequest.Builder()
95
+ .addCredentialOption(fallbackOption)
96
+ .build()
97
+
98
+ credentialManager.getCredential(activity, fallbackRequest)
99
+ }
100
+ }
101
+
102
+ private fun handleSignInResult(result: GetCredentialResponse, promise: Promise) {
103
+ val credential = result.credential
104
+
105
+ if (credential !is CustomCredential ||
106
+ credential.type != GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
107
+ promise.reject("SIGN_IN_FAILED", "Unexpected credential type: ${credential.type}")
108
+ return
109
+ }
110
+
111
+ val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
112
+ currentUser = googleIdTokenCredential
113
+
114
+ val userMap = Arguments.createMap().apply {
115
+ putString("id", googleIdTokenCredential.id)
116
+ putString("email", googleIdTokenCredential.id)
117
+ putString("displayName", googleIdTokenCredential.displayName)
118
+ putString("givenName", googleIdTokenCredential.givenName)
119
+ putString("familyName", googleIdTokenCredential.familyName)
120
+ putString("photoUrl", googleIdTokenCredential.profilePictureUri?.toString())
121
+ }
122
+
123
+ val resultMap = Arguments.createMap().apply {
124
+ putString("idToken", googleIdTokenCredential.idToken)
125
+ putNull("accessToken")
126
+ putNull("serverAuthCode")
127
+ putMap("user", userMap)
128
+ }
129
+
130
+ promise.resolve(resultMap)
131
+ }
132
+
133
+ override fun signOut(promise: Promise) {
134
+ scope.launch {
135
+ try {
136
+ credentialManager.clearCredentialState(ClearCredentialStateRequest())
137
+ currentUser = null
138
+ promise.resolve(null)
139
+ } catch (e: Exception) {
140
+ promise.reject("SIGN_OUT_FAILED", e.message, e)
141
+ }
142
+ }
143
+ }
144
+
145
+ override fun getCurrentUser(promise: Promise) {
146
+ val user = currentUser
147
+ if (user == null) {
148
+ promise.resolve(null)
149
+ return
150
+ }
151
+
152
+ val userMap = Arguments.createMap().apply {
153
+ putString("id", user.id)
154
+ putString("email", user.id)
155
+ putString("displayName", user.displayName)
156
+ putString("givenName", user.givenName)
157
+ putString("familyName", user.familyName)
158
+ putString("photoUrl", user.profilePictureUri?.toString())
159
+ }
160
+
161
+ promise.resolve(userMap)
162
+ }
163
+
164
+ override fun revokeAccess(promise: Promise) {
165
+ scope.launch {
166
+ try {
167
+ credentialManager.clearCredentialState(ClearCredentialStateRequest())
168
+ currentUser = null
169
+ promise.resolve(null)
170
+ } catch (e: Exception) {
171
+ promise.reject("REVOKE_FAILED", e.message, e)
172
+ }
173
+ }
174
+ }
175
+
176
+ override fun isSignedIn(): Boolean {
177
+ return currentUser != null
178
+ }
179
+
180
+ override fun invalidate() {
181
+ scope.cancel()
182
+ super.invalidate()
183
+ }
184
+
185
+ companion object {
186
+ const val NAME = NativeGoogleSignInSpec.NAME
187
+ }
188
+ }
@@ -0,0 +1,31 @@
1
+ package com.thoughtbot.reactnativesocialauth
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class ReactNativeSocialAuthPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == GoogleSignInModule.NAME) {
13
+ GoogleSignInModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ GoogleSignInModule.NAME to ReactModuleInfo(
22
+ name = GoogleSignInModule.NAME,
23
+ className = GoogleSignInModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ #import <ReactNativeSocialAuthSpec/ReactNativeSocialAuthSpec.h>
2
+
3
+ @interface GoogleSignIn : NSObject <NativeGoogleSignInSpec>
4
+
5
+ + (BOOL)handleURL:(NSURL *)url;
6
+
7
+ @end