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.
- package/README.md +337 -39
- package/android/src/main/cpp/PlatformAuth+Android.cpp +5 -2
- package/android/src/main/java/com/auth/AuthAdapter.kt +3 -3
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +18 -3
- package/app.plugin.js +3 -3
- package/cpp/HybridAuth.cpp +20 -0
- package/cpp/HybridAuth.hpp +1 -0
- package/ios/AuthAdapter.swift +16 -4
- package/ios/PlatformAuth+iOS.mm +6 -1
- package/lib/commonjs/Auth.web.js +16 -3
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/service.js +130 -1
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +40 -6
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +35 -7
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +35 -7
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +27 -2
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/module/Auth.web.js +16 -3
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/service.js +130 -1
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +40 -1
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +36 -8
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +36 -8
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js +27 -2
- package/lib/module/use-auth.js.map +1 -1
- package/lib/typescript/commonjs/Auth.nitro.d.ts +3 -0
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +2 -1
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +7 -0
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.d.ts +8 -1
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +29 -1
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +1 -0
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +3 -0
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +2 -1
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/AuthStorage.nitro.d.ts +7 -0
- package/lib/typescript/module/AuthStorage.nitro.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/service.d.ts +8 -1
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +29 -1
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +1 -0
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/LoginOptions.hpp +6 -2
- package/package.json +3 -2
- package/src/Auth.nitro.ts +4 -1
- package/src/Auth.web.ts +38 -15
- package/src/AuthStorage.nitro.ts +11 -2
- package/src/index.ts +1 -1
- package/src/service.ts +167 -2
- package/src/service.web.ts +50 -1
- package/src/ui/social-button.tsx +25 -3
- package/src/ui/social-button.web.tsx +25 -3
- 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
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
378
|
+
const error = e as Error;
|
|
379
|
+
if (error.message === "cancelled") {
|
|
143
380
|
// User closed the popup/picker
|
|
144
|
-
} else if (
|
|
381
|
+
} else if (error.message === "network_error") {
|
|
145
382
|
// Connection issues
|
|
146
383
|
}
|
|
147
384
|
}
|
|
148
385
|
```
|
|
149
386
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
| `
|
|
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
|
|
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
|
-
|
|
164
|
-
console.log(
|
|
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
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
272
|
-
|
|
|
273
|
-
| `provider`
|
|
274
|
-
| `variant`
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
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;
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
68
|
+
"0.4.0",
|
|
69
69
|
);
|
package/cpp/HybridAuth.cpp
CHANGED
|
@@ -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
|
|
package/cpp/HybridAuth.hpp
CHANGED
|
@@ -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;
|