react-native-nitro-auth 0.3.0 → 0.4.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 (79) hide show
  1. package/README.md +337 -39
  2. package/android/src/main/cpp/PlatformAuth+Android.cpp +5 -2
  3. package/android/src/main/java/com/auth/AuthAdapter.kt +3 -3
  4. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +18 -3
  5. package/app.plugin.js +3 -3
  6. package/cpp/HybridAuth.cpp +20 -0
  7. package/cpp/HybridAuth.hpp +1 -0
  8. package/ios/AuthAdapter.swift +16 -4
  9. package/ios/PlatformAuth+iOS.mm +6 -1
  10. package/lib/commonjs/Auth.web.js +16 -3
  11. package/lib/commonjs/Auth.web.js.map +1 -1
  12. package/lib/commonjs/service.js +130 -1
  13. package/lib/commonjs/service.js.map +1 -1
  14. package/lib/commonjs/service.web.js +40 -6
  15. package/lib/commonjs/service.web.js.map +1 -1
  16. package/lib/commonjs/ui/social-button.js +35 -7
  17. package/lib/commonjs/ui/social-button.js.map +1 -1
  18. package/lib/commonjs/ui/social-button.web.js +35 -7
  19. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  20. package/lib/commonjs/use-auth.js +27 -2
  21. package/lib/commonjs/use-auth.js.map +1 -1
  22. package/lib/module/Auth.web.js +16 -3
  23. package/lib/module/Auth.web.js.map +1 -1
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/service.js +130 -1
  26. package/lib/module/service.js.map +1 -1
  27. package/lib/module/service.web.js +40 -1
  28. package/lib/module/service.web.js.map +1 -1
  29. package/lib/module/ui/social-button.js +36 -8
  30. package/lib/module/ui/social-button.js.map +1 -1
  31. package/lib/module/ui/social-button.web.js +36 -8
  32. package/lib/module/ui/social-button.web.js.map +1 -1
  33. package/lib/module/use-auth.js +27 -2
  34. package/lib/module/use-auth.js.map +1 -1
  35. package/lib/typescript/commonjs/Auth.nitro.d.ts +3 -0
  36. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/Auth.web.d.ts +2 -1
  38. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +7 -0
  40. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/index.d.ts +1 -1
  42. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/service.d.ts +8 -1
  44. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/service.web.d.ts +29 -1
  46. package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/use-auth.d.ts +1 -0
  50. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  51. package/lib/typescript/module/Auth.nitro.d.ts +3 -0
  52. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
  53. package/lib/typescript/module/Auth.web.d.ts +2 -1
  54. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  55. package/lib/typescript/module/AuthStorage.nitro.d.ts +7 -0
  56. package/lib/typescript/module/AuthStorage.nitro.d.ts.map +1 -1
  57. package/lib/typescript/module/index.d.ts +1 -1
  58. package/lib/typescript/module/index.d.ts.map +1 -1
  59. package/lib/typescript/module/service.d.ts +8 -1
  60. package/lib/typescript/module/service.d.ts.map +1 -1
  61. package/lib/typescript/module/service.web.d.ts +29 -1
  62. package/lib/typescript/module/service.web.d.ts.map +1 -1
  63. package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
  64. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
  65. package/lib/typescript/module/use-auth.d.ts +1 -0
  66. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  67. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +1 -0
  68. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +1 -0
  69. package/nitrogen/generated/shared/c++/LoginOptions.hpp +6 -2
  70. package/package.json +3 -2
  71. package/src/Auth.nitro.ts +4 -1
  72. package/src/Auth.web.ts +38 -15
  73. package/src/AuthStorage.nitro.ts +11 -2
  74. package/src/index.ts +1 -1
  75. package/src/service.ts +167 -2
  76. package/src/service.web.ts +50 -1
  77. package/src/ui/social-button.tsx +25 -3
  78. package/src/ui/social-button.web.tsx +25 -3
  79. package/src/use-auth.ts +25 -2
package/README.md CHANGED
@@ -29,19 +29,30 @@ Nitro Auth is designed to replace legacy modules like `@react-native-google-sign
29
29
  - **Auto-Refresh**: Synchronous access to tokens with automatic silent refresh.
30
30
  - **Google One-Tap / Sheet**: Modern login experience on Android (Credential Manager) and iOS (Sign-In Sheet).
31
31
  - **Error Metadata**: Detailed native error messages for easier debugging.
32
- - **Custom Storage**: Pluggable storage adapters for secure persistence (e.g., Keychain).
32
+ - **Custom Storage**: Pluggable storage adapters for secure persistence (e.g. Keychain, MMKV, AsyncStorage).
33
33
  - **Refresh Interceptors**: Listen to token updates globally.
34
34
 
35
35
  ## Installation
36
36
 
37
37
  ```bash
38
38
  bun add react-native-nitro-auth react-native-nitro-modules
39
- bun prebuild
39
+ # or
40
+ npm install react-native-nitro-auth react-native-nitro-modules
41
+ # or
42
+ yarn add react-native-nitro-auth react-native-nitro-modules
43
+ # or
44
+ pnpm add react-native-nitro-auth react-native-nitro-modules
45
+ ```
46
+
47
+ For Expo projects, rebuild native code after installation:
48
+
49
+ ```bash
50
+ bunx expo prebuild
40
51
  ```
41
52
 
42
53
  ### Expo Setup
43
54
 
44
- Add the plugin to `app.json`:
55
+ Add the plugin to `app.json` or `app.config.js`:
45
56
 
46
57
  ```json
47
58
  {
@@ -52,22 +63,70 @@ Add the plugin to `app.json`:
52
63
  {
53
64
  "ios": {
54
65
  "googleClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
55
- "googleServerClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
56
66
  "googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID",
57
67
  "appleSignIn": true
58
68
  },
59
69
  "android": {
60
- "googleClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
70
+ "googleClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
61
71
  }
62
72
  }
63
73
  ]
64
- ]
74
+ ],
75
+ "extra": {
76
+ "googleWebClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
77
+ }
65
78
  }
66
79
  }
67
80
  ```
68
81
 
69
- > [!NOTE] > `appleSignIn` on iOS is `false` by default to avoid unnecessary entitlements. Set it to `true` to enable Apple Sign-In.
70
- > `googleServerClientId` is only required if you need a `serverAuthCode` for backend integration.
82
+ **Using environment variables (recommended):**
83
+
84
+ Create a `.env.local` file:
85
+
86
+ ```bash
87
+ # iOS Client ID
88
+ GOOGLE_IOS_CLIENT_ID=your-ios-client-id.apps.googleusercontent.com
89
+ GOOGLE_IOS_URL_SCHEME=com.googleusercontent.apps.your-ios-client-id
90
+
91
+ # Web Client ID (used for Android OAuth flow)
92
+ GOOGLE_WEB_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
93
+ ```
94
+
95
+ Then reference them in `app.config.js`:
96
+
97
+ ```javascript
98
+ import "dotenv/config";
99
+
100
+ export default {
101
+ expo: {
102
+ plugins: [
103
+ [
104
+ "react-native-nitro-auth",
105
+ {
106
+ ios: {
107
+ googleClientId: process.env.GOOGLE_IOS_CLIENT_ID,
108
+ googleUrlScheme: process.env.GOOGLE_IOS_URL_SCHEME,
109
+ appleSignIn: true,
110
+ },
111
+ android: {
112
+ googleClientId: process.env.GOOGLE_WEB_CLIENT_ID,
113
+ },
114
+ },
115
+ ],
116
+ ],
117
+ extra: {
118
+ googleWebClientId: process.env.GOOGLE_WEB_CLIENT_ID,
119
+ },
120
+ },
121
+ };
122
+ ```
123
+
124
+ > [!NOTE]
125
+ >
126
+ > - `appleSignIn` on iOS is `false` by default to avoid unnecessary entitlements. Set it to `true` to enable Apple Sign-In.
127
+ > - For Android, use your **Web Client ID** (not Android Client ID) for proper OAuth flow.
128
+ > - Add `googleWebClientId` to `expo.extra` for web platform support.
129
+ > - The `serverAuthCode` is automatically included in `AuthUser` when available (requires backend integration setup in Google Cloud Console).
71
130
 
72
131
  ### Bare React Native
73
132
 
@@ -110,8 +169,96 @@ function LoginScreen() {
110
169
  }
111
170
  ```
112
171
 
172
+ ## Migration from @react-native-google-signin/google-signin
173
+
174
+ If you are using `@react-native-google-signin/google-signin`, the migration to Nitro Auth is mostly a drop-in at the API level, but the setup is different because Nitro Auth uses a config plugin and JSI.
175
+
176
+ ### 1) Replace the dependency
177
+
178
+ ```bash
179
+ bun remove @react-native-google-signin/google-signin
180
+ bun add react-native-nitro-auth react-native-nitro-modules
181
+ ```
182
+
183
+ ### 2) Move configuration to the Nitro Auth plugin
184
+
185
+ Nitro Auth does not use `GoogleSignin.configure(...)`. Instead, set your client IDs via the config plugin (Expo) or native config (bare).
186
+
187
+ **Expo** (recommended):
188
+
189
+ ```json
190
+ {
191
+ "expo": {
192
+ "plugins": [
193
+ [
194
+ "react-native-nitro-auth",
195
+ {
196
+ "ios": {
197
+ "googleClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
198
+ "googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID"
199
+ },
200
+ "android": {
201
+ "googleClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
202
+ }
203
+ }
204
+ ]
205
+ ],
206
+ "extra": {
207
+ "googleWebClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
208
+ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ **Bare React Native:**
214
+
215
+ - iOS: add `GIDClientID` (and optionally `GIDServerClientID`) to `Info.plist` and set the URL scheme.
216
+ - Android: add `nitro_auth_google_client_id` string resource in `res/values/strings.xml` (use your Web Client ID).
217
+
218
+ ### 3) Update API usage
219
+
220
+ | @react-native-google-signin/google-signin | Nitro Auth |
221
+ | ----------------------------------------- | --------------------------------------------------------- |
222
+ | `GoogleSignin.configure({...})` | Configure in plugin / native config |
223
+ | `GoogleSignin.signIn()` | `login("google")` or `<SocialButton provider="google" />` |
224
+ | `GoogleSignin.signOut()` | `logout()` |
225
+ | `GoogleSignin.getTokens()` | `getAccessToken()` or `refreshToken()` |
226
+ | `GoogleSignin.hasPlayServices()` | `hasPlayServices` from `useAuth()` |
227
+
228
+ **Example migration:**
229
+
230
+ ```tsx
231
+ // Before
232
+ import { GoogleSignin } from "@react-native-google-signin/google-signin";
233
+
234
+ await GoogleSignin.signIn();
235
+ const tokens = await GoogleSignin.getTokens();
236
+
237
+ // After
238
+ import { useAuth } from "react-native-nitro-auth";
239
+
240
+ const { login, getAccessToken } = useAuth();
241
+
242
+ await login("google");
243
+ const accessToken = await getAccessToken();
244
+ ```
245
+
246
+ ### 4) Remove manual init
247
+
248
+ If you previously called `GoogleSignin.configure()` at app startup, remove it. Nitro Auth loads configuration from the plugin/native settings at runtime.
249
+
113
250
  ## Advanced Features
114
251
 
252
+ ### Silent Restore
253
+
254
+ Automatically restore the user session on app startup. This is faster than a full login and works offline if the session is cached.
255
+
256
+ ```tsx
257
+ useEffect(() => {
258
+ AuthService.silentRestore();
259
+ }, []);
260
+ ```
261
+
115
262
  ### Global Auth State Listener
116
263
 
117
264
  Subscribe to authentication changes outside of React components:
@@ -131,6 +278,95 @@ const unsubscribe = AuthService.onAuthStateChanged((user) => {
131
278
  unsubscribe();
132
279
  ```
133
280
 
281
+ ### Global Token Refresh Listener
282
+
283
+ Be notified whenever tokens are refreshed automatically (or manually):
284
+
285
+ ```ts
286
+ import { AuthService } from "react-native-nitro-auth";
287
+
288
+ const unsubscribe = AuthService.onTokensRefreshed((tokens) => {
289
+ console.log("New tokens:", tokens.accessToken);
290
+ // Update your API client / Apollo links
291
+ });
292
+ ```
293
+
294
+ ### Incremental Authorization
295
+
296
+ Request new scopes when you need them without logging the user out:
297
+
298
+ ```tsx
299
+ const { requestScopes, revokeScopes, scopes } = useAuth();
300
+
301
+ const handleCalendar = async () => {
302
+ try {
303
+ await requestScopes(["https://www.googleapis.com/auth/calendar.readonly"]);
304
+ console.log("Got calendar access!");
305
+ } catch (e) {
306
+ console.error("Scope request failed");
307
+ }
308
+ };
309
+ ```
310
+
311
+ ### Pluggable Storage Adapters
312
+
313
+ Nitro Auth persists the session automatically. By default, it uses simple file-based storage on native and `localStorage` on web.
314
+
315
+ #### 1) JS Storage (AsyncStorage, MMKV, etc.)
316
+
317
+ Easily swap the default storage with your preferred library from the JS layer:
318
+
319
+ ```ts
320
+ import { AuthService, type JSStorageAdapter } from "react-native-nitro-auth";
321
+ import { MMKV } from "react-native-mmkv";
322
+
323
+ const storage = new MMKV();
324
+
325
+ const mmkvAdapter: JSStorageAdapter = {
326
+ save: (key, value) => storage.set(key, value),
327
+ load: (key) => storage.getString(key),
328
+ remove: (key) => storage.delete(key),
329
+ };
330
+
331
+ // Set it once at app startup
332
+ AuthService.setJSStorageAdapter(mmkvAdapter);
333
+ ```
334
+
335
+ #### 2) Native Storage (Keychain, etc.)
336
+
337
+ For maximum security, you can implement a native HybridObject (C++, Swift, or Kotlin) and pass it to Nitro. This runs directly in memory at the C++ layer.
338
+
339
+ ```ts
340
+ import { AuthService } from "react-native-nitro-auth";
341
+ // Import your native Nitro module
342
+ import { KeychainStorage } from "./native/KeychainStorage";
343
+
344
+ AuthService.setStorageAdapter(KeychainStorage);
345
+ ```
346
+
347
+ ### Logging & Debugging
348
+
349
+ Enable verbose logging to see detailed OAuth flow information in the console:
350
+
351
+ ```ts
352
+ import { AuthService } from "react-native-nitro-auth";
353
+
354
+ AuthService.setLoggingEnabled(true);
355
+ ```
356
+
357
+ ### Sync Access to Tokens
358
+
359
+ Nitro Auth provides synchronous access to the current state, while still supporting silent refresh:
360
+
361
+ ```ts
362
+ // Quick access to what we have in memory
363
+ const user = AuthService.currentUser;
364
+ const scopes = AuthService.grantedScopes;
365
+
366
+ // Async access ensures fresh tokens (will refresh if expired)
367
+ const freshToken = await AuthService.getAccessToken();
368
+ ```
369
+
134
370
  ### Standardized Error Codes
135
371
 
136
372
  Handle failures reliably with predictable error strings:
@@ -139,29 +375,40 @@ Handle failures reliably with predictable error strings:
139
375
  try {
140
376
  await login("google");
141
377
  } catch (e) {
142
- if (e.message === "cancelled") {
378
+ const error = e as Error;
379
+ if (error.message === "cancelled") {
143
380
  // User closed the popup/picker
144
- } else if (e.message === "network_error") {
381
+ } else if (error.message === "network_error") {
145
382
  // Connection issues
146
383
  }
147
384
  }
148
385
  ```
149
386
 
150
- | `cancelled` | The user cancelled the sign-in flow |
151
- | `network_error` | A network error occurred |
152
- | `configuration_error` | Missing client IDs or invalid setup |
387
+ | Error Code | Description |
388
+ | ---------------------- | ---------------------------------------------- |
389
+ | `cancelled` | The user cancelled the sign-in flow |
390
+ | `network_error` | A network error occurred |
391
+ | `configuration_error` | Missing client IDs or invalid setup |
153
392
  | `unsupported_provider` | The provider is not supported on this platform |
393
+ | `unknown` | An unknown error occurred |
154
394
 
155
395
  ### Native Error Metadata
156
396
 
157
- For more detailed debugging, Nitro Auth captures the raw native error message in the `underlyingError` property of the `AuthUser` (on success) or surfaces it in the caught `Error` object:
397
+ For more detailed debugging, Nitro Auth captures the raw native error message. You can access it from the authenticated user or cast the error:
158
398
 
159
399
  ```ts
400
+ // From authenticated user (on success)
401
+ const { user } = useAuth();
402
+ if (user?.underlyingError) {
403
+ console.warn("Auth warning:", user.underlyingError);
404
+ }
405
+
406
+ // From error (on failure)
160
407
  try {
161
408
  await login("google");
162
409
  } catch (e) {
163
- // Access raw native error message
164
- console.log(e.underlyingError);
410
+ const error = e as Error & { underlyingError?: string };
411
+ console.log("Native error:", error.underlyingError);
165
412
  }
166
413
  ```
167
414
 
@@ -188,6 +435,9 @@ await requestScopes(["https://www.googleapis.com/auth/calendar.readonly"]);
188
435
 
189
436
  // Check granted scopes
190
437
  console.log("Granted:", scopes);
438
+
439
+ // Revoke specific scopes
440
+ await revokeScopes(["https://www.googleapis.com/auth/calendar.readonly"]);
191
441
  ```
192
442
 
193
443
  ### Offline Access (Server Auth Code)
@@ -205,24 +455,38 @@ if (user?.serverAuthCode) {
205
455
 
206
456
  ### Custom Storage Adapter
207
457
 
208
- By default, Nitro Auth uses standard local storage. You can provide a custom adapter for better security (e.g., using `react-native-keychain`):
458
+ By default, Nitro Auth uses standard local storage. You can provide a custom adapter for better security.
459
+
460
+ > [!IMPORTANT]
461
+ > `AuthStorageAdapter` must be implemented as a **native Nitro HybridObject** in C++, Swift, or Kotlin. Plain JavaScript objects are not supported due to Nitro's type system. See [Nitro Hybrid Objects documentation](https://nitro.margelo.com/docs/hybrid-objects) for implementation details.
462
+
463
+ **Example (Swift):**
464
+
465
+ ```swift
466
+ class HybridKeychainStorage: HybridAuthStorageAdapterSpec {
467
+ func save(key: String, value: String) {
468
+ // Save to Keychain
469
+ }
470
+
471
+ func load(key: String) -> String? {
472
+ // Load from Keychain
473
+ }
474
+
475
+ func remove(key: String) {
476
+ // Remove from Keychain
477
+ }
478
+ }
479
+ ```
480
+
481
+ **Usage (TypeScript):**
209
482
 
210
483
  ```ts
484
+ import { NitroModules } from "react-native-nitro-modules";
211
485
  import { AuthService, AuthStorageAdapter } from "react-native-nitro-auth";
212
486
 
213
- const myStorage: AuthStorageAdapter = {
214
- save: (key, value) => {
215
- /* Save to Keychain */
216
- },
217
- load: (key) => {
218
- /* Load from Keychain */
219
- },
220
- remove: (key) => {
221
- /* Clear from Keychain */
222
- },
223
- };
224
-
225
- AuthService.setStorageAdapter(myStorage);
487
+ const keychainStorage =
488
+ NitroModules.createHybridObject<AuthStorageAdapter>("KeychainStorage");
489
+ AuthService.setStorageAdapter(keychainStorage);
226
490
  ```
227
491
 
228
492
  ### Token Refresh Listeners
@@ -232,9 +496,8 @@ Perfect for updating your API client (e.g., Axios/Fetch) whenever tokens are ref
232
496
  ```ts
233
497
  AuthService.onTokensRefreshed((tokens) => {
234
498
  console.log("Tokens were updated!", tokens.accessToken);
235
- apiClient.defaults.headers.common[
236
- "Authorization"
237
- ] = `Bearer ${tokens.accessToken}`;
499
+ apiClient.defaults.headers.common["Authorization"] =
500
+ `Bearer ${tokens.accessToken}`;
238
501
  });
239
502
  ```
240
503
 
@@ -249,6 +512,23 @@ await login("google", {
249
512
  });
250
513
  ```
251
514
 
515
+ ### Force Account Picker
516
+
517
+ When connecting additional services (like Google Calendar), you may want to let users pick a different account than the one they signed in with. Use `forceAccountPicker` to clear any cached session and show the account picker:
518
+
519
+ ```ts
520
+ await login("google", {
521
+ scopes: ["https://www.googleapis.com/auth/calendar.readonly"],
522
+ forceAccountPicker: true, // Always show account picker
523
+ });
524
+ ```
525
+
526
+ This is useful for scenarios where:
527
+
528
+ - Users want to connect a different Google account for calendar integration
529
+ - You need to ensure the user can select any account they've added to their device
530
+ - The cached session is interfering with the expected account selection UX
531
+
252
532
  ## API Reference
253
533
 
254
534
  ### useAuth Hook
@@ -262,18 +542,36 @@ await login("google", {
262
542
  | `hasPlayServices` | `boolean` | (Android) True if Play Services available |
263
543
  | `login` | `(provider, options?) => Promise` | Start login flow |
264
544
  | `logout` | `() => void` | Clear session (synchronous) |
545
+ | `silentRestore` | `() => Promise<void>` | Restore session automatically on startup |
265
546
  | `requestScopes` | `(scopes) => Promise` | Request additional OAuth scopes |
547
+ | `revokeScopes` | `(scopes) => Promise` | Revoke previously granted scopes |
266
548
  | `getAccessToken` | `() => Promise<string?>` | Get current access token (auto-refreshes) |
267
549
  | `refreshToken` | `() => Promise<AuthTokens>` | Explicitly refresh and return new tokens |
268
550
 
551
+ ### LoginOptions
552
+
553
+ | Option | Type | Platform | Description |
554
+ | -------------------- | ---------- | -------- | ----------------------------------------------- |
555
+ | `scopes` | `string[]` | All | Required OAuth scopes (default: email, profile) |
556
+ | `loginHint` | `string` | All | Pre-fill email address in the login picker |
557
+ | `useOneTap` | `boolean` | Android | Enable Google One-Tap (Credential Manager) |
558
+ | `useSheet` | `boolean` | iOS | Enable iOS Google Sign-In Sheet |
559
+ | `forceAccountPicker` | `boolean` | All | Always show the account selection screen |
560
+ | `webClientId` | `string` | Web | Override the default Google Web Client ID |
561
+
269
562
  ### SocialButton Props
270
563
 
271
- | Prop | Type | Default | Description |
272
- | ----------- | ---------------------------------------------- | ----------- | -------------------------------- |
273
- | `provider` | `"google" \| "apple"` | required | Authentication provider |
274
- | `variant` | `"primary" \| "outline" \| "white" \| "black"` | `"primary"` | Button style variant |
275
- | `onSuccess` | `(user: AuthUser) => void` | — | Called with user data on success |
276
- | `onError` | `(error: Error) => void` | — | Called on failure |
564
+ | Prop | Type | Default | Description |
565
+ | -------------- | ---------------------------------------------- | ----------- | --------------------------------------------- |
566
+ | `provider` | `"google" \| "apple"` | required | Authentication provider |
567
+ | `variant` | `"primary" \| "outline" \| "white" \| "black"` | `"primary"` | Button style variant |
568
+ | `onPress` | `() => void` | — | Custom handler (disables default login) |
569
+ | `onSuccess` | `(user: AuthUser) => void` | — | Called with user data on success (auto-login) |
570
+ | `onError` | `(error: unknown) => void` | — | Called on failure (auto-login) |
571
+ | `disabled` | `boolean` | `false` | Disable button interaction |
572
+ | `style` | `ViewStyle` | — | Custom container styles |
573
+ | `textStyle` | `TextStyle` | — | Custom text styles |
574
+ | `borderRadius` | `number` | `8` | Button border radius |
277
575
 
278
576
  ## Platform Support
279
577
 
@@ -41,11 +41,13 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
41
41
  std::vector<std::string> scopes = {"email", "profile"};
42
42
  std::optional<std::string> loginHint;
43
43
  bool useOneTap = false;
44
+ bool forceAccountPicker = false;
44
45
 
45
46
  if (options) {
46
47
  if (options->scopes) scopes = *options->scopes;
47
48
  loginHint = options->loginHint;
48
49
  useOneTap = options->useOneTap.value_or(false);
50
+ forceAccountPicker = options->forceAccountPicker.value_or(false);
49
51
  }
50
52
 
51
53
  JNIEnv* env = Environment::current();
@@ -58,14 +60,15 @@ std::shared_ptr<Promise<AuthUser>> PlatformAuth::login(AuthProvider provider, co
58
60
  jstring jLoginHint = loginHint.has_value() ? make_jstring(loginHint.value()).get() : nullptr;
59
61
  jclass adapterClass = env->FindClass("com/auth/AuthAdapter");
60
62
  jmethodID loginMethod = env->GetStaticMethodID(adapterClass, "loginSync",
61
- "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Z)V");
63
+ "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;ZZ)V");
62
64
  env->CallStaticVoidMethod(adapterClass, loginMethod,
63
65
  contextPtr,
64
66
  make_jstring(providerStr).get(),
65
67
  nullptr,
66
68
  jScopes,
67
69
  jLoginHint,
68
- (jboolean)useOneTap);
70
+ (jboolean)useOneTap,
71
+ (jboolean)forceAccountPicker);
69
72
 
70
73
  return promise;
71
74
  }
@@ -96,7 +96,7 @@ object AuthAdapter {
96
96
  }
97
97
 
98
98
  @JvmStatic
99
- fun loginSync(context: Context, provider: String, googleClientId: String?, scopes: Array<String>?, loginHint: String?, useOneTap: Boolean) {
99
+ fun loginSync(context: Context, provider: String, googleClientId: String?, scopes: Array<String>?, loginHint: String?, useOneTap: Boolean, forceAccountPicker: Boolean = false) {
100
100
  if (provider == "apple") {
101
101
  nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
102
102
  return
@@ -117,10 +117,10 @@ object AuthAdapter {
117
117
  val requestedScopes = scopes?.toList() ?: listOf("email", "profile")
118
118
  pendingScopes = requestedScopes
119
119
 
120
- if (useOneTap) {
120
+ if (useOneTap && !forceAccountPicker) {
121
121
  loginOneTap(context, clientId, requestedScopes)
122
122
  } else {
123
- val intent = GoogleSignInActivity.createIntent(ctx, clientId, requestedScopes.toTypedArray(), loginHint)
123
+ val intent = GoogleSignInActivity.createIntent(ctx, clientId, requestedScopes.toTypedArray(), loginHint, forceAccountPicker)
124
124
  intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
125
125
  ctx.startActivity(intent)
126
126
  }
@@ -18,12 +18,14 @@ class GoogleSignInActivity : ComponentActivity() {
18
18
  private const val EXTRA_CLIENT_ID = "client_id"
19
19
  private const val EXTRA_SCOPES = "scopes"
20
20
  private const val EXTRA_LOGIN_HINT = "login_hint"
21
+ private const val EXTRA_FORCE_PICKER = "force_picker"
21
22
 
22
- fun createIntent(context: Context, clientId: String, scopes: Array<String>, loginHint: String?): Intent {
23
+ fun createIntent(context: Context, clientId: String, scopes: Array<String>, loginHint: String?, forcePicker: Boolean = false): Intent {
23
24
  return Intent(context, GoogleSignInActivity::class.java).apply {
24
25
  putExtra(EXTRA_CLIENT_ID, clientId)
25
26
  putExtra(EXTRA_SCOPES, scopes)
26
27
  putExtra(EXTRA_LOGIN_HINT, loginHint)
28
+ putExtra(EXTRA_FORCE_PICKER, forcePicker)
27
29
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
28
30
  }
29
31
  }
@@ -48,6 +50,7 @@ class GoogleSignInActivity : ComponentActivity() {
48
50
  val clientId = intent.getStringExtra(EXTRA_CLIENT_ID)
49
51
  val scopes = intent.getStringArrayExtra(EXTRA_SCOPES) ?: arrayOf("email", "profile")
50
52
  val loginHint = intent.getStringExtra(EXTRA_LOGIN_HINT)
53
+ val forcePicker = intent.getBooleanExtra(EXTRA_FORCE_PICKER, false)
51
54
 
52
55
  if (clientId == null) {
53
56
  AuthAdapter.onSignInError(8, "Missing client ID")
@@ -66,9 +69,21 @@ class GoogleSignInActivity : ComponentActivity() {
66
69
  }
67
70
  }
68
71
 
69
- if (loginHint != null) gsoBuilder.setAccountName(loginHint)
72
+ // Only set account name if not forcing picker
73
+ if (!forcePicker && loginHint != null) {
74
+ gsoBuilder.setAccountName(loginHint)
75
+ }
70
76
 
71
77
  val client = GoogleSignIn.getClient(this, gsoBuilder.build())
72
- signInLauncher.launch(client.signInIntent)
78
+
79
+ if (forcePicker) {
80
+ // Sign out first to ensure account picker shows all accounts
81
+ client.signOut().addOnCompleteListener {
82
+ signInLauncher.launch(client.signInIntent)
83
+ }
84
+ } else {
85
+ signInLauncher.launch(client.signInIntent)
86
+ }
73
87
  }
74
88
  }
89
+
package/app.plugin.js CHANGED
@@ -21,7 +21,7 @@ const withNitroAuth = (config, props = {}) => {
21
21
  const existingSchemes = config.modResults.CFBundleURLTypes || [];
22
22
  if (
23
23
  !existingSchemes.some((scheme) =>
24
- scheme.CFBundleURLSchemes.includes(ios.googleUrlScheme)
24
+ scheme.CFBundleURLSchemes.includes(ios.googleUrlScheme),
25
25
  )
26
26
  ) {
27
27
  config.modResults.CFBundleURLTypes = [
@@ -53,7 +53,7 @@ const withNitroAuth = (config, props = {}) => {
53
53
  _: android.googleClientId,
54
54
  },
55
55
  ],
56
- config.modResults
56
+ config.modResults,
57
57
  );
58
58
  }
59
59
  return config;
@@ -65,5 +65,5 @@ const withNitroAuth = (config, props = {}) => {
65
65
  module.exports = createRunOncePlugin(
66
66
  withNitroAuth,
67
67
  "react-native-nitro-auth",
68
- "0.1.6"
68
+ "0.4.0",
69
69
  );
@@ -110,6 +110,26 @@ void HybridAuth::logout() {
110
110
  notifyAuthStateChanged();
111
111
  }
112
112
 
113
+ std::shared_ptr<Promise<void>> HybridAuth::silentRestore() {
114
+ auto promise = Promise<void>::create();
115
+ auto silentPromise = PlatformAuth::silentRestore();
116
+ silentPromise->addOnResolvedListener([this, promise](const std::optional<AuthUser>& user) {
117
+ if (user) {
118
+ std::lock_guard<std::mutex> lock(_mutex);
119
+ _currentUser = user;
120
+ if (user->scopes) _grantedScopes = *user->scopes;
121
+ saveToCache(_currentUser);
122
+ }
123
+ notifyAuthStateChanged();
124
+ promise->resolve();
125
+ });
126
+
127
+ silentPromise->addOnRejectedListener([promise](const std::exception_ptr& error) {
128
+ promise->reject(error);
129
+ });
130
+ return promise;
131
+ }
132
+
113
133
  std::shared_ptr<Promise<void>> HybridAuth::login(AuthProvider provider, const std::optional<LoginOptions>& options) {
114
134
  auto promise = Promise<void>::create();
115
135
 
@@ -27,6 +27,7 @@ public:
27
27
  std::shared_ptr<Promise<AuthTokens>> refreshToken() override;
28
28
 
29
29
  void logout() override;
30
+ std::shared_ptr<Promise<void>> silentRestore() override;
30
31
  std::function<void()> onAuthStateChanged(const std::function<void(const std::optional<AuthUser>&)>& callback) override;
31
32
  std::function<void()> onTokensRefreshed(const std::function<void(const AuthTokens&)>& callback) override;
32
33
  void setLoggingEnabled(bool enabled) override;