@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.
- package/LICENSE +20 -0
- package/README.md +316 -0
- package/ReactNativeSocialAuth.podspec +22 -0
- package/android/build.gradle +70 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/thoughtbot/reactnativesocialauth/GoogleSignInModule.kt +188 -0
- package/android/src/main/java/com/thoughtbot/reactnativesocialauth/ReactNativeSocialAuthPackage.kt +31 -0
- package/ios/GoogleSignIn.h +7 -0
- package/ios/GoogleSignIn.mm +213 -0
- package/lib/module/google/GoogleLogo.js +41 -0
- package/lib/module/google/GoogleLogo.js.map +1 -0
- package/lib/module/google/GoogleSignIn.js +124 -0
- package/lib/module/google/GoogleSignIn.js.map +1 -0
- package/lib/module/google/GoogleSignInButton.js +176 -0
- package/lib/module/google/GoogleSignInButton.js.map +1 -0
- package/lib/module/google/NativeGoogleSignIn.js +5 -0
- package/lib/module/google/NativeGoogleSignIn.js.map +1 -0
- package/lib/module/google/errors.js +69 -0
- package/lib/module/google/errors.js.map +1 -0
- package/lib/module/google/index.js +6 -0
- package/lib/module/google/index.js.map +1 -0
- package/lib/module/google/types.js +2 -0
- package/lib/module/google/types.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/google/GoogleLogo.d.ts +6 -0
- package/lib/typescript/src/google/GoogleLogo.d.ts.map +1 -0
- package/lib/typescript/src/google/GoogleSignIn.d.ts +89 -0
- package/lib/typescript/src/google/GoogleSignIn.d.ts.map +1 -0
- package/lib/typescript/src/google/GoogleSignInButton.d.ts +74 -0
- package/lib/typescript/src/google/GoogleSignInButton.d.ts.map +1 -0
- package/lib/typescript/src/google/NativeGoogleSignIn.d.ts +12 -0
- package/lib/typescript/src/google/NativeGoogleSignIn.d.ts.map +1 -0
- package/lib/typescript/src/google/errors.d.ts +57 -0
- package/lib/typescript/src/google/errors.d.ts.map +1 -0
- package/lib/typescript/src/google/index.d.ts +6 -0
- package/lib/typescript/src/google/index.d.ts.map +1 -0
- package/lib/typescript/src/google/types.d.ts +93 -0
- package/lib/typescript/src/google/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +180 -0
- package/src/google/GoogleLogo.tsx +35 -0
- package/src/google/GoogleSignIn.ts +131 -0
- package/src/google/GoogleSignInButton.tsx +224 -0
- package/src/google/NativeGoogleSignIn.ts +12 -0
- package/src/google/errors.ts +72 -0
- package/src/google/index.ts +19 -0
- package/src/google/types.ts +100 -0
- 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,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
|
+
}
|
package/android/src/main/java/com/thoughtbot/reactnativesocialauth/ReactNativeSocialAuthPackage.kt
ADDED
|
@@ -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
|
+
}
|