react-native-nitro-auth 0.1.6 → 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 (126) hide show
  1. package/README.md +350 -38
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/cpp/PlatformAuth+Android.cpp +27 -9
  4. package/android/src/main/java/com/auth/AuthAdapter.kt +18 -18
  5. package/android/src/main/java/com/auth/GoogleSignInActivity.kt +18 -3
  6. package/app.plugin.js +3 -3
  7. package/cpp/HybridAuth.cpp +20 -0
  8. package/cpp/HybridAuth.hpp +1 -0
  9. package/ios/AuthAdapter.swift +48 -47
  10. package/ios/PlatformAuth+iOS.mm +37 -24
  11. package/lib/commonjs/Auth.web.js +27 -21
  12. package/lib/commonjs/Auth.web.js.map +1 -1
  13. package/lib/commonjs/package.json +1 -0
  14. package/lib/commonjs/service.js +130 -1
  15. package/lib/commonjs/service.js.map +1 -1
  16. package/lib/commonjs/service.web.js +40 -6
  17. package/lib/commonjs/service.web.js.map +1 -1
  18. package/lib/commonjs/ui/social-button.js +35 -7
  19. package/lib/commonjs/ui/social-button.js.map +1 -1
  20. package/lib/commonjs/ui/social-button.web.js +35 -7
  21. package/lib/commonjs/ui/social-button.web.js.map +1 -1
  22. package/lib/commonjs/use-auth.js +29 -2
  23. package/lib/commonjs/use-auth.js.map +1 -1
  24. package/lib/module/Auth.web.js +27 -21
  25. package/lib/module/Auth.web.js.map +1 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/package.json +1 -0
  28. package/lib/module/service.js +130 -1
  29. package/lib/module/service.js.map +1 -1
  30. package/lib/module/service.web.js +40 -1
  31. package/lib/module/service.web.js.map +1 -1
  32. package/lib/module/ui/social-button.js +36 -8
  33. package/lib/module/ui/social-button.js.map +1 -1
  34. package/lib/module/ui/social-button.web.js +36 -8
  35. package/lib/module/ui/social-button.web.js.map +1 -1
  36. package/lib/module/use-auth.js +29 -2
  37. package/lib/module/use-auth.js.map +1 -1
  38. package/lib/typescript/{Auth.nitro.d.ts → commonjs/Auth.nitro.d.ts} +7 -0
  39. package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -0
  40. package/lib/typescript/{Auth.web.d.ts → commonjs/Auth.web.d.ts} +2 -1
  41. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -0
  42. package/lib/typescript/{AuthStorage.nitro.d.ts → commonjs/AuthStorage.nitro.d.ts} +7 -0
  43. package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +1 -0
  44. package/lib/typescript/{index.d.ts → commonjs/index.d.ts} +1 -1
  45. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/index.web.d.ts.map +1 -0
  47. package/lib/typescript/commonjs/package.json +1 -0
  48. package/lib/typescript/commonjs/service.d.ts +10 -0
  49. package/lib/typescript/commonjs/service.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/service.web.d.ts +30 -0
  51. package/lib/typescript/commonjs/service.web.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -0
  54. package/lib/typescript/{use-auth.d.ts → commonjs/use-auth.d.ts} +1 -0
  55. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -0
  57. package/lib/typescript/module/Auth.nitro.d.ts +51 -0
  58. package/lib/typescript/module/Auth.nitro.d.ts.map +1 -0
  59. package/lib/typescript/module/Auth.web.d.ts +41 -0
  60. package/lib/typescript/module/Auth.web.d.ts.map +1 -0
  61. package/lib/typescript/module/AuthStorage.nitro.d.ts +26 -0
  62. package/lib/typescript/module/AuthStorage.nitro.d.ts.map +1 -0
  63. package/lib/typescript/module/index.d.ts +6 -0
  64. package/lib/typescript/module/index.d.ts.map +1 -0
  65. package/lib/typescript/module/index.web.d.ts +6 -0
  66. package/lib/typescript/module/index.web.d.ts.map +1 -0
  67. package/lib/typescript/module/package.json +1 -0
  68. package/lib/typescript/module/service.d.ts +10 -0
  69. package/lib/typescript/module/service.d.ts.map +1 -0
  70. package/lib/typescript/module/service.web.d.ts +30 -0
  71. package/lib/typescript/module/service.web.d.ts.map +1 -0
  72. package/lib/typescript/module/ui/social-button.d.ts +17 -0
  73. package/lib/typescript/module/ui/social-button.d.ts.map +1 -0
  74. package/lib/typescript/module/ui/social-button.web.d.ts +17 -0
  75. package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -0
  76. package/lib/typescript/module/use-auth.d.ts +16 -0
  77. package/lib/typescript/module/use-auth.d.ts.map +1 -0
  78. package/lib/typescript/module/utils/logger.d.ts +8 -0
  79. package/lib/typescript/module/utils/logger.d.ts.map +1 -0
  80. package/nitrogen/generated/android/NitroAuth+autolinking.cmake +1 -1
  81. package/nitrogen/generated/android/NitroAuth+autolinking.gradle +1 -1
  82. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +1 -1
  83. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +1 -1
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/auth/NitroAuthOnLoad.kt +1 -1
  85. package/nitrogen/generated/ios/NitroAuth+autolinking.rb +2 -2
  86. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.cpp +1 -1
  87. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Bridge.hpp +1 -1
  88. package/nitrogen/generated/ios/NitroAuth-Swift-Cxx-Umbrella.hpp +1 -1
  89. package/nitrogen/generated/ios/NitroAuthAutolinking.mm +1 -1
  90. package/nitrogen/generated/ios/NitroAuthAutolinking.swift +5 -1
  91. package/nitrogen/generated/shared/c++/AuthProvider.hpp +1 -1
  92. package/nitrogen/generated/shared/c++/AuthTokens.hpp +19 -11
  93. package/nitrogen/generated/shared/c++/AuthUser.hpp +42 -30
  94. package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +2 -1
  95. package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +2 -1
  96. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +1 -1
  97. package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +1 -1
  98. package/nitrogen/generated/shared/c++/LoginOptions.hpp +28 -12
  99. package/package.json +7 -5
  100. package/react-native-nitro-auth.podspec +1 -1
  101. package/src/Auth.nitro.ts +8 -1
  102. package/src/Auth.web.ts +50 -30
  103. package/src/AuthStorage.nitro.ts +11 -2
  104. package/src/index.ts +1 -1
  105. package/src/service.ts +167 -2
  106. package/src/service.web.ts +50 -1
  107. package/src/ui/social-button.tsx +25 -3
  108. package/src/ui/social-button.web.tsx +25 -3
  109. package/src/use-auth.ts +27 -2
  110. package/lib/typescript/Auth.nitro.d.ts.map +0 -1
  111. package/lib/typescript/Auth.web.d.ts.map +0 -1
  112. package/lib/typescript/AuthStorage.nitro.d.ts.map +0 -1
  113. package/lib/typescript/index.d.ts.map +0 -1
  114. package/lib/typescript/index.web.d.ts.map +0 -1
  115. package/lib/typescript/service.d.ts +0 -3
  116. package/lib/typescript/service.d.ts.map +0 -1
  117. package/lib/typescript/service.web.d.ts +0 -2
  118. package/lib/typescript/service.web.d.ts.map +0 -1
  119. package/lib/typescript/ui/social-button.d.ts.map +0 -1
  120. package/lib/typescript/ui/social-button.web.d.ts.map +0 -1
  121. package/lib/typescript/use-auth.d.ts.map +0 -1
  122. package/lib/typescript/utils/logger.d.ts.map +0 -1
  123. /package/lib/typescript/{index.web.d.ts → commonjs/index.web.d.ts} +0 -0
  124. /package/lib/typescript/{ui → commonjs/ui}/social-button.d.ts +0 -0
  125. /package/lib/typescript/{ui → commonjs/ui}/social-button.web.d.ts +0 -0
  126. /package/lib/typescript/{utils → commonjs/utils}/logger.d.ts +0 -0
package/README.md CHANGED
@@ -27,20 +27,32 @@ Nitro Auth is designed to replace legacy modules like `@react-native-google-sign
27
27
  - **Expo Ready**: Comes with a powerful Config Plugin for zero-config setup.
28
28
  - **Cross-Platform**: Unified API for iOS, Android, and Web.
29
29
  - **Auto-Refresh**: Synchronous access to tokens with automatic silent refresh.
30
- - **Google One-Tap**: Modern login experience on Android using Credential Manager.
31
- - **Custom Storage**: Pluggable storage adapters for secure persistence (e.g., Keychain).
30
+ - **Google One-Tap / Sheet**: Modern login experience on Android (Credential Manager) and iOS (Sign-In Sheet).
31
+ - **Error Metadata**: Detailed native error messages for easier debugging.
32
+ - **Custom Storage**: Pluggable storage adapters for secure persistence (e.g. Keychain, MMKV, AsyncStorage).
32
33
  - **Refresh Interceptors**: Listen to token updates globally.
33
34
 
34
35
  ## Installation
35
36
 
36
37
  ```bash
37
38
  bun add react-native-nitro-auth react-native-nitro-modules
38
- 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
39
51
  ```
40
52
 
41
53
  ### Expo Setup
42
54
 
43
- Add the plugin to `app.json`:
55
+ Add the plugin to `app.json` or `app.config.js`:
44
56
 
45
57
  ```json
46
58
  {
@@ -51,23 +63,70 @@ Add the plugin to `app.json`:
51
63
  {
52
64
  "ios": {
53
65
  "googleClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com",
54
- "googleServerClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com",
55
66
  "googleUrlScheme": "com.googleusercontent.apps.YOUR_IOS_CLIENT_ID",
56
67
  "appleSignIn": true
57
68
  },
58
69
  "android": {
59
- "googleClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com"
70
+ "googleClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
60
71
  }
61
72
  }
62
73
  ]
63
- ]
74
+ ],
75
+ "extra": {
76
+ "googleWebClientId": "YOUR_WEB_CLIENT_ID.apps.googleusercontent.com"
77
+ }
64
78
  }
65
79
  }
66
80
  ```
67
81
 
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
+
68
124
  > [!NOTE]
69
- > `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.
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,20 +375,42 @@ 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
- | Code | Description |
387
+ | Error Code | Description |
151
388
  | ---------------------- | ---------------------------------------------- |
152
389
  | `cancelled` | The user cancelled the sign-in flow |
153
390
  | `network_error` | A network error occurred |
154
391
  | `configuration_error` | Missing client IDs or invalid setup |
155
392
  | `unsupported_provider` | The provider is not supported on this platform |
393
+ | `unknown` | An unknown error occurred |
394
+
395
+ ### Native Error Metadata
396
+
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:
398
+
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)
407
+ try {
408
+ await login("google");
409
+ } catch (e) {
410
+ const error = e as Error & { underlyingError?: string };
411
+ console.log("Native error:", error.underlyingError);
412
+ }
413
+ ```
156
414
 
157
415
  ### Automatic Token Refresh
158
416
 
@@ -177,6 +435,9 @@ await requestScopes(["https://www.googleapis.com/auth/calendar.readonly"]);
177
435
 
178
436
  // Check granted scopes
179
437
  console.log("Granted:", scopes);
438
+
439
+ // Revoke specific scopes
440
+ await revokeScopes(["https://www.googleapis.com/auth/calendar.readonly"]);
180
441
  ```
181
442
 
182
443
  ### Offline Access (Server Auth Code)
@@ -194,24 +455,38 @@ if (user?.serverAuthCode) {
194
455
 
195
456
  ### Custom Storage Adapter
196
457
 
197
- 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):**
198
482
 
199
483
  ```ts
484
+ import { NitroModules } from "react-native-nitro-modules";
200
485
  import { AuthService, AuthStorageAdapter } from "react-native-nitro-auth";
201
486
 
202
- const myStorage: AuthStorageAdapter = {
203
- save: (key, value) => {
204
- /* Save to Keychain */
205
- },
206
- load: (key) => {
207
- /* Load from Keychain */
208
- },
209
- remove: (key) => {
210
- /* Clear from Keychain */
211
- },
212
- };
213
-
214
- AuthService.setStorageAdapter(myStorage);
487
+ const keychainStorage =
488
+ NitroModules.createHybridObject<AuthStorageAdapter>("KeychainStorage");
489
+ AuthService.setStorageAdapter(keychainStorage);
215
490
  ```
216
491
 
217
492
  ### Token Refresh Listeners
@@ -221,20 +496,39 @@ Perfect for updating your API client (e.g., Axios/Fetch) whenever tokens are ref
221
496
  ```ts
222
497
  AuthService.onTokensRefreshed((tokens) => {
223
498
  console.log("Tokens were updated!", tokens.accessToken);
224
- apiClient.defaults.headers.common[
225
- "Authorization"
226
- ] = `Bearer ${tokens.accessToken}`;
499
+ apiClient.defaults.headers.common["Authorization"] =
500
+ `Bearer ${tokens.accessToken}`;
227
501
  });
228
502
  ```
229
503
 
230
- ### Google One-Tap (Android)
504
+ ### Google One-Tap & Sheet
231
505
 
232
- Explicitly enable the modern One-Tap flow on Android:
506
+ Explicitly enable the modern One-Tap flow on Android or the Sign-In Sheet on iOS:
233
507
 
234
508
  ```ts
235
- await login("google", { useOneTap: true });
509
+ await login("google", {
510
+ useOneTap: true, // Android
511
+ useSheet: true, // iOS
512
+ });
236
513
  ```
237
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
+
238
532
  ## API Reference
239
533
 
240
534
  ### useAuth Hook
@@ -248,18 +542,36 @@ await login("google", { useOneTap: true });
248
542
  | `hasPlayServices` | `boolean` | (Android) True if Play Services available |
249
543
  | `login` | `(provider, options?) => Promise` | Start login flow |
250
544
  | `logout` | `() => void` | Clear session (synchronous) |
545
+ | `silentRestore` | `() => Promise<void>` | Restore session automatically on startup |
251
546
  | `requestScopes` | `(scopes) => Promise` | Request additional OAuth scopes |
547
+ | `revokeScopes` | `(scopes) => Promise` | Revoke previously granted scopes |
252
548
  | `getAccessToken` | `() => Promise<string?>` | Get current access token (auto-refreshes) |
253
549
  | `refreshToken` | `() => Promise<AuthTokens>` | Explicitly refresh and return new tokens |
254
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
+
255
562
  ### SocialButton Props
256
563
 
257
- | Prop | Type | Default | Description |
258
- | ----------- | ---------------------------------------------- | ----------- | -------------------------------- |
259
- | `provider` | `"google" \| "apple"` | required | Authentication provider |
260
- | `variant` | `"primary" \| "outline" \| "white" \| "black"` | `"primary"` | Button style variant |
261
- | `onSuccess` | `(user: AuthUser) => void` | — | Called with user data on success |
262
- | `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 |
263
575
 
264
576
  ## Platform Support
265
577
 
@@ -93,7 +93,7 @@ dependencies {
93
93
  implementation project(":react-native-nitro-modules")
94
94
 
95
95
  // Google Sign-In SDK (full scope support)
96
- implementation "com.google.android.gms:play-services-auth:21.2.0"
96
+ implementation "com.google.android.gms:play-services-auth:21.5.0"
97
97
 
98
98
  // Activity result APIs
99
99
  implementation "androidx.activity:activity-ktx:1.9.3"
@@ -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
  }
@@ -225,13 +228,14 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginSuccess
225
228
  jmethodID longValueMethod = env->GetMethodID(longClass, "longValue", "()J");
226
229
  user.expirationTime = (double)env->CallLongMethod(expirationTime, longValueMethod);
227
230
  }
231
+
228
232
  if (loginPromise) loginPromise->resolve(user);
229
233
  if (scopesPromise) scopesPromise->resolve(user);
230
234
  if (silentPromise) silentPromise->resolve(user);
231
235
  }
232
236
 
233
237
  extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginError(
234
- JNIEnv* env, jclass, jstring error) {
238
+ JNIEnv* env, jclass, jstring error, jstring underlyingError) {
235
239
 
236
240
  std::shared_ptr<Promise<AuthUser>> loginPromise;
237
241
  std::shared_ptr<Promise<AuthUser>> scopesPromise;
@@ -249,12 +253,19 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnLoginError(
249
253
  const char* errorCStr = env->GetStringUTFChars(error, nullptr);
250
254
  std::string errorStr(errorCStr);
251
255
  env->ReleaseStringUTFChars(error, errorCStr);
256
+
257
+ std::string finalError = errorStr;
258
+ if (underlyingError) {
259
+ const char* uCStr = env->GetStringUTFChars(underlyingError, nullptr);
260
+ finalError = std::string(uCStr);
261
+ env->ReleaseStringUTFChars(underlyingError, uCStr);
262
+ }
252
263
 
253
- if (loginPromise) loginPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
254
- if (scopesPromise) scopesPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
264
+ if (loginPromise) loginPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
265
+ if (scopesPromise) scopesPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
255
266
  if (silentPromise) {
256
267
  if (errorStr == "No session") silentPromise->resolve(std::nullopt);
257
- else silentPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
268
+ else silentPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
258
269
  }
259
270
  }
260
271
 
@@ -290,7 +301,7 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshSucce
290
301
  }
291
302
 
292
303
  extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshError(
293
- JNIEnv* env, jclass, jstring error) {
304
+ JNIEnv* env, jclass, jstring error, jstring underlyingError) {
294
305
 
295
306
  std::shared_ptr<Promise<AuthTokens>> refreshPromise;
296
307
  {
@@ -299,10 +310,17 @@ extern "C" JNIEXPORT void JNICALL Java_com_auth_AuthAdapter_nativeOnRefreshError
299
310
  gRefreshPromise = nullptr;
300
311
  }
301
312
  if (refreshPromise) {
313
+ std::string finalError;
302
314
  const char* errorCStr = env->GetStringUTFChars(error, nullptr);
303
- std::string errorStr(errorCStr);
315
+ finalError = std::string(errorCStr);
304
316
  env->ReleaseStringUTFChars(error, errorCStr);
305
- refreshPromise->reject(std::make_exception_ptr(std::runtime_error(errorStr)));
317
+
318
+ if (underlyingError) {
319
+ const char* uCStr = env->GetStringUTFChars(underlyingError, nullptr);
320
+ finalError = std::string(uCStr);
321
+ env->ReleaseStringUTFChars(underlyingError, uCStr);
322
+ }
323
+ refreshPromise->reject(std::make_exception_ptr(std::runtime_error(finalError)));
306
324
  }
307
325
  }
308
326