react-native-nitro-auth 0.5.0 → 0.5.3
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 +374 -189
- package/android/src/main/cpp/PlatformAuth+Android.cpp +4 -1
- package/android/src/main/java/com/auth/AuthAdapter.kt +99 -182
- package/android/src/main/java/com/auth/GoogleSignInActivity.kt +2 -1
- package/android/src/main/java/com/auth/NitroAuthPackage.kt +2 -0
- package/app.plugin.js +2 -9
- package/cpp/AuthCache.cpp +12 -102
- package/cpp/HybridAuth.cpp +37 -61
- package/cpp/HybridAuth.hpp +2 -4
- package/ios/AuthAdapter.swift +21 -25
- package/lib/commonjs/Auth.web.js +433 -164
- package/lib/commonjs/Auth.web.js.map +1 -1
- package/lib/commonjs/index.js +0 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +0 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/js-storage-adapter.js +2 -0
- package/lib/commonjs/js-storage-adapter.js.map +1 -0
- package/lib/commonjs/service.js +7 -84
- package/lib/commonjs/service.js.map +1 -1
- package/lib/commonjs/service.web.js +1 -5
- package/lib/commonjs/service.web.js.map +1 -1
- package/lib/commonjs/ui/social-button.js +44 -29
- package/lib/commonjs/ui/social-button.js.map +1 -1
- package/lib/commonjs/ui/social-button.web.js +44 -29
- package/lib/commonjs/ui/social-button.web.js.map +1 -1
- package/lib/commonjs/use-auth.js +8 -2
- package/lib/commonjs/use-auth.js.map +1 -1
- package/lib/commonjs/utils/logger.js +12 -4
- package/lib/commonjs/utils/logger.js.map +1 -1
- package/lib/module/Auth.web.js +433 -164
- package/lib/module/Auth.web.js.map +1 -1
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +0 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/js-storage-adapter.js +2 -0
- package/lib/module/js-storage-adapter.js.map +1 -0
- package/lib/module/service.js +7 -84
- package/lib/module/service.js.map +1 -1
- package/lib/module/service.web.js +1 -5
- package/lib/module/service.web.js.map +1 -1
- package/lib/module/ui/social-button.js +44 -29
- package/lib/module/ui/social-button.js.map +1 -1
- package/lib/module/ui/social-button.web.js +44 -29
- package/lib/module/ui/social-button.web.js.map +1 -1
- package/lib/module/use-auth.js +8 -2
- package/lib/module/use-auth.js.map +1 -1
- package/lib/module/utils/logger.js +12 -4
- package/lib/module/utils/logger.js.map +1 -1
- package/lib/typescript/commonjs/Auth.nitro.d.ts +5 -3
- package/lib/typescript/commonjs/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/commonjs/Auth.web.d.ts +18 -6
- package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +0 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/js-storage-adapter.d.ts +6 -0
- package/lib/typescript/commonjs/js-storage-adapter.d.ts.map +1 -0
- package/lib/typescript/commonjs/service.d.ts +1 -8
- package/lib/typescript/commonjs/service.d.ts.map +1 -1
- package/lib/typescript/commonjs/service.web.d.ts +1 -8
- package/lib/typescript/commonjs/service.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.d.ts +6 -6
- package/lib/typescript/commonjs/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/social-button.web.d.ts +6 -6
- package/lib/typescript/commonjs/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/use-auth.d.ts +4 -4
- package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/logger.d.ts +4 -4
- package/lib/typescript/commonjs/utils/logger.d.ts.map +1 -1
- package/lib/typescript/module/Auth.nitro.d.ts +5 -3
- package/lib/typescript/module/Auth.nitro.d.ts.map +1 -1
- package/lib/typescript/module/Auth.web.d.ts +18 -6
- package/lib/typescript/module/Auth.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +0 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/js-storage-adapter.d.ts +6 -0
- package/lib/typescript/module/js-storage-adapter.d.ts.map +1 -0
- package/lib/typescript/module/service.d.ts +1 -8
- package/lib/typescript/module/service.d.ts.map +1 -1
- package/lib/typescript/module/service.web.d.ts +1 -8
- package/lib/typescript/module/service.web.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.d.ts +6 -6
- package/lib/typescript/module/ui/social-button.d.ts.map +1 -1
- package/lib/typescript/module/ui/social-button.web.d.ts +6 -6
- package/lib/typescript/module/ui/social-button.web.d.ts.map +1 -1
- package/lib/typescript/module/use-auth.d.ts +4 -4
- package/lib/typescript/module/use-auth.d.ts.map +1 -1
- package/lib/typescript/module/utils/logger.d.ts +4 -4
- package/lib/typescript/module/utils/logger.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroAuth+autolinking.cmake +0 -1
- package/nitrogen/generated/shared/c++/AuthTokens.hpp +5 -1
- package/nitrogen/generated/shared/c++/AuthUser.hpp +5 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.cpp +0 -1
- package/nitrogen/generated/shared/c++/HybridAuthSpec.hpp +0 -5
- package/nitrogen/generated/shared/c++/LoginOptions.hpp +5 -1
- package/package.json +13 -10
- package/src/Auth.nitro.ts +6 -3
- package/src/Auth.web.ts +582 -202
- package/src/global.d.ts +0 -1
- package/src/index.ts +1 -2
- package/src/index.web.ts +0 -1
- package/src/js-storage-adapter.ts +5 -0
- package/src/service.ts +11 -104
- package/src/service.web.ts +0 -7
- package/src/ui/social-button.tsx +66 -43
- package/src/ui/social-button.web.tsx +67 -44
- package/src/use-auth.ts +18 -6
- package/src/utils/logger.ts +12 -4
- package/lib/commonjs/AuthStorage.nitro.js +0 -6
- package/lib/commonjs/AuthStorage.nitro.js.map +0 -1
- package/lib/module/AuthStorage.nitro.js +0 -4
- package/lib/module/AuthStorage.nitro.js.map +0 -1
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts +0 -26
- package/lib/typescript/commonjs/AuthStorage.nitro.d.ts.map +0 -1
- package/lib/typescript/module/AuthStorage.nitro.d.ts +0 -26
- package/lib/typescript/module/AuthStorage.nitro.d.ts.map +0 -1
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.cpp +0 -23
- package/nitrogen/generated/shared/c++/HybridAuthStorageAdapterSpec.hpp +0 -65
- package/src/AuthStorage.nitro.ts +0 -26
package/README.md
CHANGED
|
@@ -12,12 +12,13 @@ Nitro Auth is a modern authentication library for React Native built on top of [
|
|
|
12
12
|
|
|
13
13
|
Nitro Auth is designed to replace legacy modules like `@react-native-google-signin/google-signin` with a modern, high-performance architecture.
|
|
14
14
|
|
|
15
|
-
| Feature
|
|
16
|
-
|
|
|
17
|
-
| **Performance**
|
|
18
|
-
| **
|
|
19
|
-
| **Setup**
|
|
20
|
-
| **Types**
|
|
15
|
+
| Feature | Legacy Modules | Nitro Auth |
|
|
16
|
+
| :---------------- | :--------------------------- | :----------------------------------------------------------------------------- |
|
|
17
|
+
| **Performance** | Async bridge overhead (JSON) | **Direct JSI C++ (Zero-copy)** |
|
|
18
|
+
| **Storage** | Varies / Hidden defaults | **Native: in-memory only; Web: session cache (tokens memory-only by default)** |
|
|
19
|
+
| **Setup** | Manual async initialization | **Sync & declarative plugins** |
|
|
20
|
+
| **Types** | Manual / Brittle | **Fully Generated (Nitrogen)** |
|
|
21
|
+
| **Provider Data** | Varies | **Normalized auth payload** |
|
|
21
22
|
|
|
22
23
|
## Features
|
|
23
24
|
|
|
@@ -26,24 +27,56 @@ Nitro Auth is designed to replace legacy modules like `@react-native-google-sign
|
|
|
26
27
|
- **Incremental Auth**: Request additional OAuth scopes on the fly.
|
|
27
28
|
- **Expo Ready**: Comes with a powerful Config Plugin for zero-config setup.
|
|
28
29
|
- **Cross-Platform**: Unified API for iOS, Android, and Web.
|
|
29
|
-
- **
|
|
30
|
+
- **Token Lifecycle Helpers**: `getAccessToken()` refreshes near-expiry tokens; `refreshToken()` allows explicit refresh control.
|
|
30
31
|
- **Google One-Tap / Sheet**: Modern login experience on Android (Credential Manager) and iOS (Sign-In Sheet).
|
|
31
32
|
- **Error Metadata**: Detailed native error messages for easier debugging.
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
33
|
+
- **Normalized Provider Payload**: Exposes provider/user/token fields in a consistent cross-platform shape.
|
|
34
|
+
- **App-Owned Persistence**: Native is stateless by default. Web keeps a non-sensitive session snapshot; apps decide long-term persistence strategy.
|
|
35
|
+
|
|
36
|
+
## Design Philosophy
|
|
37
|
+
|
|
38
|
+
This is an **auth-only package** - on **native** (iOS/Android) it does NOT store auth data by default.
|
|
39
|
+
On **web**, Nitro Auth stores a non-sensitive auth snapshot in `sessionStorage` by default; sensitive tokens stay memory-only unless explicitly enabled.
|
|
40
|
+
|
|
41
|
+
The package provides:
|
|
42
|
+
|
|
43
|
+
- Login/logout functionality for Google, Apple, and Microsoft
|
|
44
|
+
- Token management (access token, refresh token, ID token)
|
|
45
|
+
- Scope management (request/revoke scopes)
|
|
46
|
+
- Consistent provider/user/token field exposure across iOS, Android, and Web
|
|
47
|
+
|
|
48
|
+
**Storage is the responsibility of the app using this package.** Use your own storage layer (for example [react-native-nitro-storage](https://github.com/JoaoPauloCMarra/react-native-nitro-storage)) to persist app-level auth snapshots/tokens when needed.
|
|
49
|
+
|
|
50
|
+
### Provider Data Availability
|
|
51
|
+
|
|
52
|
+
Token/data shape is normalized, but provider SDKs do not always return all fields at login time:
|
|
53
|
+
|
|
54
|
+
| Provider + Flow | `idToken` | `accessToken` | `serverAuthCode` | `expirationTime` |
|
|
55
|
+
| --------------------------- | ---------- | ---------------------------- | ---------------------------- | ----------------------------------- |
|
|
56
|
+
| Google (iOS) | Usually ✅ | Provider-dependent | Optional (if configured) | Optional |
|
|
57
|
+
| Google (Android One-Tap) | ✅ | Usually `undefined` at login | `undefined` | Derived from ID token when possible |
|
|
58
|
+
| Google (Android Legacy) | ✅ | Usually `undefined` at login | ✅ (if server client is set) | Derived from ID token when possible |
|
|
59
|
+
| Google (Web) | ✅ | ✅ | Optional (`code`) | Usually ✅ |
|
|
60
|
+
| Microsoft (iOS/Android/Web) | ✅ | Usually ✅ | `undefined` | Usually ✅ |
|
|
61
|
+
| Apple (iOS/Web) | ✅ | `undefined` | `undefined` | `undefined` |
|
|
62
|
+
|
|
63
|
+
> [!NOTE]
|
|
64
|
+
> On Apple, email/name can be limited after first consent depending on Apple policy.
|
|
34
65
|
|
|
35
66
|
## Installation
|
|
36
67
|
|
|
37
68
|
```bash
|
|
38
69
|
bun add react-native-nitro-auth react-native-nitro-modules
|
|
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
70
|
```
|
|
46
71
|
|
|
72
|
+
### Requirements
|
|
73
|
+
|
|
74
|
+
| Dependency | Version |
|
|
75
|
+
| ---------------------------- | ----------- |
|
|
76
|
+
| `react-native` | `>= 0.75.0` |
|
|
77
|
+
| `react-native-nitro-modules` | `>= 0.33.9` |
|
|
78
|
+
| `react` | `*` |
|
|
79
|
+
|
|
47
80
|
For Expo projects, rebuild native code after installation:
|
|
48
81
|
|
|
49
82
|
```bash
|
|
@@ -58,43 +91,54 @@ Fastest way to confirm the package and Microsoft login work:
|
|
|
58
91
|
In [Azure Portal](https://portal.azure.com) → **Azure Active Directory** → **App registrations** → **New registration**:
|
|
59
92
|
- Name: e.g. `Nitro Auth Example`
|
|
60
93
|
- Supported account types: **Accounts in any organizational directory and personal Microsoft accounts**
|
|
61
|
-
- Redirect URI (add after creation):
|
|
62
|
-
- **Android**: `msauth://com.auth.example/<client-id>`
|
|
94
|
+
- Redirect URI (add after creation):
|
|
95
|
+
- **Android**: `msauth://com.auth.example/<client-id>`
|
|
63
96
|
- **iOS**: `msauth.com.auth.example://auth` (use your bundle id)
|
|
64
97
|
- Under **Authentication** → **Platform configurations** → add **Mobile and desktop applications** with the Android redirect URI above and the iOS one if testing on iOS.
|
|
65
|
-
|
|
98
|
+
Copy the **Application (client) ID**.
|
|
66
99
|
|
|
67
100
|
2. **Env file**
|
|
68
101
|
From the repo root:
|
|
102
|
+
|
|
69
103
|
```bash
|
|
70
104
|
cd apps/example
|
|
71
105
|
cp .env.example .env.local
|
|
72
106
|
```
|
|
107
|
+
|
|
73
108
|
Edit `.env.local` and set at least:
|
|
109
|
+
|
|
74
110
|
```bash
|
|
75
111
|
MICROSOFT_CLIENT_ID=<your-application-client-id>
|
|
76
112
|
MICROSOFT_TENANT=common
|
|
77
113
|
```
|
|
114
|
+
|
|
78
115
|
(Google/Apple can stay placeholder if you only care about Microsoft.)
|
|
79
116
|
|
|
80
117
|
3. **Run the app**
|
|
81
118
|
From the **monorepo root**:
|
|
119
|
+
|
|
82
120
|
```bash
|
|
83
121
|
bun install
|
|
84
122
|
bun run start
|
|
85
123
|
```
|
|
124
|
+
|
|
86
125
|
In a second terminal:
|
|
126
|
+
|
|
87
127
|
```bash
|
|
88
128
|
bun run example:android
|
|
89
129
|
# or
|
|
90
130
|
bun run example:ios
|
|
91
131
|
```
|
|
132
|
+
|
|
92
133
|
Wait for the app to install and open.
|
|
93
134
|
|
|
94
135
|
4. **Test Microsoft**
|
|
95
136
|
In the app, tap **Sign in with Microsoft**. A browser or in-app tab opens; sign in with a Microsoft/personal account, then you should return to the app with the user shown (email, name, provider MICROSOFT).
|
|
96
137
|
If you see "configuration_error", check `MICROSOFT_CLIENT_ID` and that the redirect URI in Azure matches your app (e.g. `msauth://com.auth.example/<client-id>` for the example app).
|
|
97
138
|
|
|
139
|
+
> [!TIP]
|
|
140
|
+
> In the example app on Android, you can toggle **Legacy Google Sign-In** to compare Credential Manager vs legacy GoogleSignIn (and to get `serverAuthCode`).
|
|
141
|
+
|
|
98
142
|
### Expo Setup
|
|
99
143
|
|
|
100
144
|
Add the plugin to `app.json` or `app.config.js`:
|
|
@@ -294,7 +338,9 @@ Nitro Auth reads web configuration from `expo.extra`:
|
|
|
294
338
|
"microsoftClientId": "YOUR_AZURE_AD_CLIENT_ID",
|
|
295
339
|
"microsoftTenant": "common",
|
|
296
340
|
"microsoftB2cDomain": "your-tenant.b2clogin.com",
|
|
297
|
-
"appleWebClientId": "com.example.web"
|
|
341
|
+
"appleWebClientId": "com.example.web",
|
|
342
|
+
"nitroAuthWebStorage": "session",
|
|
343
|
+
"nitroAuthPersistTokensOnWeb": false
|
|
298
344
|
}
|
|
299
345
|
}
|
|
300
346
|
}
|
|
@@ -302,6 +348,9 @@ Nitro Auth reads web configuration from `expo.extra`:
|
|
|
302
348
|
|
|
303
349
|
For Apple web sign-in, `appleWebClientId` must be your Apple Service ID. For Microsoft web, make sure your Azure app includes a Web redirect URI matching your site.
|
|
304
350
|
|
|
351
|
+
- `nitroAuthWebStorage`: `"session"` (default), `"local"`, or `"memory"`.
|
|
352
|
+
- `nitroAuthPersistTokensOnWeb`: `false` by default (recommended). Set `true` only if you need cross-reload token persistence.
|
|
353
|
+
|
|
305
354
|
## Quick Start
|
|
306
355
|
|
|
307
356
|
### Using the Hook
|
|
@@ -450,7 +499,11 @@ If you previously called `GoogleSignin.configure()` at app startup, remove it. N
|
|
|
450
499
|
|
|
451
500
|
### Silent Restore
|
|
452
501
|
|
|
453
|
-
|
|
502
|
+
Attempts to restore provider SDK sessions on app startup.
|
|
503
|
+
|
|
504
|
+
- Google: restore is supported via provider SDK session state.
|
|
505
|
+
- Apple: provider credentials are re-requested by OS flow.
|
|
506
|
+
- Microsoft: no internal persistence; restore requires your app/backend session strategy.
|
|
454
507
|
|
|
455
508
|
```tsx
|
|
456
509
|
useEffect(() => {
|
|
@@ -507,56 +560,90 @@ const handleCalendar = async () => {
|
|
|
507
560
|
};
|
|
508
561
|
```
|
|
509
562
|
|
|
510
|
-
###
|
|
511
|
-
|
|
512
|
-
Nitro Auth persists the session automatically. By default, it uses secure storage on native (Keychain on iOS, EncryptedSharedPreferences on Android) and `localStorage` on web.
|
|
563
|
+
### App-Owned Persistence
|
|
513
564
|
|
|
514
|
-
|
|
565
|
+
Nitro Auth is intentionally stateless in-process. Persist only what your app needs.
|
|
515
566
|
|
|
516
|
-
|
|
567
|
+
#### Using react-native-nitro-storage (Recommended)
|
|
517
568
|
|
|
518
569
|
```ts
|
|
519
|
-
import { AuthService, type
|
|
520
|
-
import {
|
|
521
|
-
|
|
522
|
-
const storage = new MMKV();
|
|
570
|
+
import { AuthService, type AuthUser } from "react-native-nitro-auth";
|
|
571
|
+
import { createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
523
572
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
573
|
+
type AuthSnapshot = {
|
|
574
|
+
user: AuthUser | undefined;
|
|
575
|
+
scopes: string[];
|
|
576
|
+
updatedAt: number | undefined;
|
|
528
577
|
};
|
|
529
578
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
579
|
+
const authSnapshotItem = createStorageItem<AuthSnapshot>({
|
|
580
|
+
key: "auth_snapshot",
|
|
581
|
+
scope: StorageScope.Disk,
|
|
582
|
+
defaultValue: {
|
|
583
|
+
user: undefined,
|
|
584
|
+
scopes: [],
|
|
585
|
+
updatedAt: undefined,
|
|
586
|
+
},
|
|
587
|
+
});
|
|
536
588
|
|
|
537
|
-
|
|
589
|
+
// Save on auth changes (do not overwrite snapshot with empty user on app refresh)
|
|
590
|
+
AuthService.onAuthStateChanged((user) => {
|
|
591
|
+
if (!user) return;
|
|
538
592
|
|
|
539
|
-
|
|
593
|
+
authSnapshotItem.set({
|
|
594
|
+
user,
|
|
595
|
+
scopes: AuthService.grantedScopes,
|
|
596
|
+
updatedAt: Date.now(),
|
|
597
|
+
});
|
|
598
|
+
});
|
|
540
599
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
600
|
+
// Keep token/expiration fields fresh in persisted snapshot
|
|
601
|
+
AuthService.onTokensRefreshed((tokens) => {
|
|
602
|
+
authSnapshotItem.set((prev) => {
|
|
603
|
+
if (!prev.user) return prev;
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
...prev,
|
|
607
|
+
user: {
|
|
608
|
+
...prev.user,
|
|
609
|
+
accessToken: tokens.accessToken ?? prev.user.accessToken,
|
|
610
|
+
idToken: tokens.idToken ?? prev.user.idToken,
|
|
611
|
+
refreshToken: tokens.refreshToken ?? prev.user.refreshToken,
|
|
612
|
+
expirationTime: tokens.expirationTime ?? prev.user.expirationTime,
|
|
613
|
+
},
|
|
614
|
+
updatedAt: Date.now(),
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
});
|
|
545
618
|
|
|
546
|
-
|
|
619
|
+
// Clear on logout
|
|
620
|
+
function logout() {
|
|
621
|
+
AuthService.logout();
|
|
622
|
+
authSnapshotItem.set({
|
|
623
|
+
user: undefined,
|
|
624
|
+
scopes: [],
|
|
625
|
+
updatedAt: undefined,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
547
628
|
```
|
|
548
629
|
|
|
549
|
-
**Production recommendation:** If you need custom storage policies, auditability, or a different encryption model, provide your own adapter (Keychain, EncryptedSharedPreferences, or a secure JS store). See [Pluggable Storage Adapters](#pluggable-storage-adapters) above.
|
|
550
|
-
|
|
551
630
|
### Production Readiness
|
|
552
631
|
|
|
553
|
-
Nitro Auth is suitable for production use:
|
|
632
|
+
Nitro Auth is suitable for production use when configured with provider-accurate expectations:
|
|
633
|
+
|
|
634
|
+
- **Google Sign-In**: One-Tap + legacy on Android, native on iOS, popup on web.
|
|
635
|
+
- **Apple Sign-In**: iOS + web only.
|
|
636
|
+
- **Microsoft (Azure AD / B2C)**: iOS, Android, and web with PKCE/state/nonce protections.
|
|
554
637
|
|
|
555
|
-
|
|
556
|
-
- **Apple Sign-In**: Supported on iOS and Web (not available on Android).
|
|
557
|
-
- **Microsoft (Azure AD / B2C)**: Login, incremental scopes, and token refresh are supported on all platforms. Uses PKCE, state, and nonce for security.
|
|
638
|
+
Production checklist:
|
|
558
639
|
|
|
559
|
-
|
|
640
|
+
1. Configure client IDs, URL schemes, and redirect URIs per platform/provider.
|
|
641
|
+
2. Call `silentRestore()` during app startup to rehydrate provider session state when available.
|
|
642
|
+
3. Persist only app-owned snapshots/tokens (for example with `react-native-nitro-storage`), then clear them on logout.
|
|
643
|
+
4. On Android Google, use `useLegacyGoogleSignIn: true` when backend flows require `serverAuthCode`.
|
|
644
|
+
5. Treat token fields as optional and branch by provider/flow.
|
|
645
|
+
6. Keep web sensitive tokens memory-only unless you explicitly require persistent web tokens (`nitroAuthPersistTokensOnWeb: true`).
|
|
646
|
+
7. Enable logging only in development, and monitor normalized error codes in production.
|
|
560
647
|
|
|
561
648
|
### Logging & Debugging
|
|
562
649
|
|
|
@@ -568,9 +655,9 @@ import { AuthService } from "react-native-nitro-auth";
|
|
|
568
655
|
AuthService.setLoggingEnabled(true);
|
|
569
656
|
```
|
|
570
657
|
|
|
571
|
-
### Sync
|
|
658
|
+
### Sync State + Async Tokens
|
|
572
659
|
|
|
573
|
-
Nitro Auth provides synchronous access to
|
|
660
|
+
Nitro Auth provides synchronous access to in-memory state, while token retrieval remains async:
|
|
574
661
|
|
|
575
662
|
```ts
|
|
576
663
|
// Quick access to what we have in memory
|
|
@@ -614,7 +701,7 @@ try {
|
|
|
614
701
|
|
|
615
702
|
### Native Error Metadata
|
|
616
703
|
|
|
617
|
-
For more detailed debugging, Nitro Auth captures
|
|
704
|
+
For more detailed debugging, Nitro Auth captures raw provider/native details in `underlyingError` where available:
|
|
618
705
|
|
|
619
706
|
```ts
|
|
620
707
|
// From authenticated user (on success)
|
|
@@ -663,54 +750,6 @@ if (user?.serverAuthCode) {
|
|
|
663
750
|
}
|
|
664
751
|
```
|
|
665
752
|
|
|
666
|
-
### Custom Storage Adapter
|
|
667
|
-
|
|
668
|
-
By default, Nitro Auth uses secure native storage on iOS/Android and `localStorage` on web. You can provide a custom adapter for different security or storage requirements.
|
|
669
|
-
|
|
670
|
-
> [!IMPORTANT]
|
|
671
|
-
> `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.
|
|
672
|
-
|
|
673
|
-
**Example (Swift):**
|
|
674
|
-
|
|
675
|
-
```swift
|
|
676
|
-
class HybridKeychainStorage: HybridAuthStorageAdapterSpec {
|
|
677
|
-
func save(key: String, value: String) {
|
|
678
|
-
// Save to Keychain
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
func load(key: String) -> String? {
|
|
682
|
-
// Load from Keychain
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
func remove(key: String) {
|
|
686
|
-
// Remove from Keychain
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
**Usage (TypeScript):**
|
|
692
|
-
|
|
693
|
-
```ts
|
|
694
|
-
import { NitroModules } from "react-native-nitro-modules";
|
|
695
|
-
import { AuthService, AuthStorageAdapter } from "react-native-nitro-auth";
|
|
696
|
-
|
|
697
|
-
const keychainStorage =
|
|
698
|
-
NitroModules.createHybridObject<AuthStorageAdapter>("KeychainStorage");
|
|
699
|
-
AuthService.setStorageAdapter(keychainStorage);
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
### Token Refresh Listeners
|
|
703
|
-
|
|
704
|
-
Perfect for updating your API client (e.g., Axios/Fetch) whenever tokens are refreshed in the background:
|
|
705
|
-
|
|
706
|
-
```ts
|
|
707
|
-
AuthService.onTokensRefreshed((tokens) => {
|
|
708
|
-
console.log("Tokens were updated!", tokens.accessToken);
|
|
709
|
-
apiClient.defaults.headers.common["Authorization"] =
|
|
710
|
-
`Bearer ${tokens.accessToken}`;
|
|
711
|
-
});
|
|
712
|
-
```
|
|
713
|
-
|
|
714
753
|
### Google One-Tap & Sheet
|
|
715
754
|
|
|
716
755
|
Explicitly enable the modern One-Tap flow on Android or the Sign-In Sheet on iOS:
|
|
@@ -725,6 +764,15 @@ await login("google", {
|
|
|
725
764
|
> [!NOTE]
|
|
726
765
|
> One-Tap requires Google Play Services. You can check `hasPlayServices` from `useAuth()` and show a fallback UI if needed.
|
|
727
766
|
|
|
767
|
+
### Android Legacy Google Sign-In (Server Auth Code)
|
|
768
|
+
|
|
769
|
+
Credential Manager is the recommended default on Android, but it **does not return** `serverAuthCode`.
|
|
770
|
+
If your backend requires `serverAuthCode`, opt into the legacy flow:
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
await login("google", { useLegacyGoogleSignIn: true });
|
|
774
|
+
```
|
|
775
|
+
|
|
728
776
|
### Force Account Picker
|
|
729
777
|
|
|
730
778
|
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:
|
|
@@ -744,94 +792,231 @@ This is useful for scenarios where:
|
|
|
744
792
|
|
|
745
793
|
## API Reference
|
|
746
794
|
|
|
747
|
-
###
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
###
|
|
765
|
-
|
|
766
|
-
|
|
|
767
|
-
|
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
|
775
|
-
|
|
|
776
|
-
| `
|
|
777
|
-
| `
|
|
778
|
-
| `
|
|
779
|
-
| `
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
|
784
|
-
|
|
|
785
|
-
| `
|
|
786
|
-
| `
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
|
794
|
-
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
|
803
|
-
|
|
|
804
|
-
| `
|
|
805
|
-
| `
|
|
806
|
-
| `
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
|
811
|
-
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
|
820
|
-
|
|
|
795
|
+
### Package Exports
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
import {
|
|
799
|
+
AuthService,
|
|
800
|
+
SocialButton,
|
|
801
|
+
useAuth,
|
|
802
|
+
type UseAuthReturn,
|
|
803
|
+
type Auth,
|
|
804
|
+
type AuthUser,
|
|
805
|
+
type AuthTokens,
|
|
806
|
+
type AuthProvider,
|
|
807
|
+
type AuthErrorCode,
|
|
808
|
+
type LoginOptions,
|
|
809
|
+
} from "react-native-nitro-auth";
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Core Types
|
|
813
|
+
|
|
814
|
+
| Type | Definition |
|
|
815
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
816
|
+
| `AuthProvider` | `"google" \| "apple" \| "microsoft"` |
|
|
817
|
+
| `AuthErrorCode` | `"cancelled" \| "timeout" \| "popup_blocked" \| "network_error" \| "configuration_error" \| "unsupported_provider" \| "invalid_state" \| "invalid_nonce" \| "token_error" \| "no_id_token" \| "parse_error" \| "refresh_failed" \| "unknown"` |
|
|
818
|
+
| `MicrosoftPrompt` | `"login" \| "consent" \| "select_account" \| "none"` |
|
|
819
|
+
|
|
820
|
+
### `AuthUser`
|
|
821
|
+
|
|
822
|
+
| Field | Type | Description |
|
|
823
|
+
| ----------------- | ----------------------- | -------------------------------------------------------------------------- |
|
|
824
|
+
| `provider` | `AuthProvider` | Provider that authenticated the user |
|
|
825
|
+
| `email` | `string \| undefined` | User email |
|
|
826
|
+
| `name` | `string \| undefined` | Display name |
|
|
827
|
+
| `photo` | `string \| undefined` | Profile image URL (Google) |
|
|
828
|
+
| `idToken` | `string \| undefined` | OIDC ID token |
|
|
829
|
+
| `accessToken` | `string \| undefined` | OAuth access token |
|
|
830
|
+
| `refreshToken` | `string \| undefined` | OAuth refresh token |
|
|
831
|
+
| `serverAuthCode` | `string \| undefined` | Google server auth code (legacy Android flow + backend exchange scenarios) |
|
|
832
|
+
| `scopes` | `string[] \| undefined` | Granted scopes for current session |
|
|
833
|
+
| `expirationTime` | `number \| undefined` | Expiration timestamp in milliseconds since epoch |
|
|
834
|
+
| `underlyingError` | `string \| undefined` | Raw provider/native error message |
|
|
835
|
+
|
|
836
|
+
> [!NOTE]
|
|
837
|
+
> On Android Google One-Tap/Credential Manager, `idToken` is typically available immediately, while `accessToken` and `expirationTime` may be `undefined` until refresh/exchange flow provides them.
|
|
838
|
+
|
|
839
|
+
### `AuthTokens`
|
|
840
|
+
|
|
841
|
+
| Field | Type | Description |
|
|
842
|
+
| ---------------- | --------------------- | ----------------------------- |
|
|
843
|
+
| `accessToken` | `string \| undefined` | Refreshed access token |
|
|
844
|
+
| `idToken` | `string \| undefined` | Refreshed ID token |
|
|
845
|
+
| `refreshToken` | `string \| undefined` | Refresh token (if available) |
|
|
846
|
+
| `expirationTime` | `number \| undefined` | Optional expiration timestamp |
|
|
847
|
+
|
|
848
|
+
### `LoginOptions`
|
|
849
|
+
|
|
850
|
+
| Option | Type | Platform | Description |
|
|
851
|
+
| ----------------------- | ----------------- | --------- | --------------------------------------------------------------------------------- |
|
|
852
|
+
| `scopes` | `string[]` | All | Requested scopes (defaults are provider-specific) |
|
|
853
|
+
| `loginHint` | `string` | All | Prefills account identifier |
|
|
854
|
+
| `useOneTap` | `boolean` | Android | Use Credential Manager/One-Tap flow |
|
|
855
|
+
| `useSheet` | `boolean` | iOS | Use native Google Sign-In sheet |
|
|
856
|
+
| `forceAccountPicker` | `boolean` | All | Always show account chooser |
|
|
857
|
+
| `useLegacyGoogleSignIn` | `boolean` | Android | Use legacy Google Sign-In (required when you need `serverAuthCode`) |
|
|
858
|
+
| `tenant` | `string` | Microsoft | Tenant (`common`, `organizations`, `consumers`, tenant id, or full authority URL) |
|
|
859
|
+
| `prompt` | `MicrosoftPrompt` | Microsoft | Prompt behavior |
|
|
860
|
+
|
|
861
|
+
### `useAuth()`
|
|
862
|
+
|
|
863
|
+
```ts
|
|
864
|
+
declare function useAuth(): UseAuthReturn;
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
| Property | Type | Description |
|
|
868
|
+
| ----------------- | ------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
869
|
+
| `user` | `AuthUser \| undefined` | Current in-memory user |
|
|
870
|
+
| `scopes` | `string[]` | Current granted scopes |
|
|
871
|
+
| `loading` | `boolean` | `true` while an auth operation is in-flight |
|
|
872
|
+
| `error` | `Error \| undefined` | Last operation error |
|
|
873
|
+
| `hasPlayServices` | `boolean` | Android Play Services availability |
|
|
874
|
+
| `login` | `(provider: AuthProvider, options?: LoginOptions) => Promise<void>` | Starts provider login |
|
|
875
|
+
| `logout` | `() => void` | Clears current session |
|
|
876
|
+
| `requestScopes` | `(scopes: string[]) => Promise<void>` | Requests additional scopes |
|
|
877
|
+
| `revokeScopes` | `(scopes: string[]) => Promise<void>` | Revokes scopes in current session |
|
|
878
|
+
| `getAccessToken` | `() => Promise<string \| undefined>` | Returns access token, auto-refreshing when supported |
|
|
879
|
+
| `refreshToken` | `() => Promise<AuthTokens>` | Explicit refresh |
|
|
880
|
+
| `silentRestore` | `() => Promise<void>` | Restores provider SDK session (if available) |
|
|
881
|
+
|
|
882
|
+
### `AuthService`
|
|
883
|
+
|
|
884
|
+
Synchronous state + async operations (useful outside React trees).
|
|
885
|
+
|
|
886
|
+
#### Readonly state
|
|
887
|
+
|
|
888
|
+
| Property | Type | Description |
|
|
889
|
+
| ----------------- | ----------------------- | ---------------------------------- |
|
|
890
|
+
| `name` | `string` | Hybrid object name (`"Auth"`) |
|
|
891
|
+
| `currentUser` | `AuthUser \| undefined` | Current user snapshot |
|
|
892
|
+
| `grantedScopes` | `string[]` | Current scope snapshot |
|
|
893
|
+
| `hasPlayServices` | `boolean` | Android Play Services availability |
|
|
894
|
+
|
|
895
|
+
#### Methods
|
|
896
|
+
|
|
897
|
+
| Method | Signature | Description |
|
|
898
|
+
| -------------------- | -------------------------------------------------------- | -------------------------------- |
|
|
899
|
+
| `login` | `(provider, options?) => Promise<void>` | Starts login |
|
|
900
|
+
| `logout` | `() => void` | Clears session |
|
|
901
|
+
| `requestScopes` | `(scopes) => Promise<void>` | Incremental auth |
|
|
902
|
+
| `revokeScopes` | `(scopes) => Promise<void>` | Scope revoke |
|
|
903
|
+
| `getAccessToken` | `() => Promise<string \| undefined>` | Access token getter with refresh |
|
|
904
|
+
| `refreshToken` | `() => Promise<AuthTokens>` | Explicit token refresh |
|
|
905
|
+
| `silentRestore` | `() => Promise<void>` | Restore provider SDK session |
|
|
906
|
+
| `onAuthStateChanged` | `(callback: (user?: AuthUser) => void) => () => void` | Auth change listener |
|
|
907
|
+
| `onTokensRefreshed` | `(callback: (tokens: AuthTokens) => void) => () => void` | Token refresh listener |
|
|
908
|
+
| `setLoggingEnabled` | `(enabled: boolean) => void` | Debug logging toggle |
|
|
909
|
+
| `dispose` | `() => void` | Disposes hybrid object |
|
|
910
|
+
| `equals` | `(other: unknown) => boolean` | Hybrid object identity check |
|
|
911
|
+
|
|
912
|
+
### Storage Contract
|
|
913
|
+
|
|
914
|
+
Nitro Auth does not provide persistence APIs. Persist the auth data you need in your app layer (for example with `react-native-nitro-storage`, MMKV, Keychain wrappers, or backend-issued sessions).
|
|
915
|
+
There is no public `setStorageAdapter`/`setJSStorageAdapter` API in this package.
|
|
916
|
+
|
|
917
|
+
### `SocialButton`
|
|
918
|
+
|
|
919
|
+
| Prop | Type | Default | Description |
|
|
920
|
+
| -------------- | ---------------------------------------------- | ----------- | ------------------------------------------- |
|
|
921
|
+
| `provider` | `AuthProvider` | required | Provider |
|
|
922
|
+
| `variant` | `"primary" \| "outline" \| "white" \| "black"` | `"primary"` | Visual style |
|
|
923
|
+
| `borderRadius` | `number` | `8` | Border radius |
|
|
924
|
+
| `style` | `ViewStyle` | `undefined` | Container style override |
|
|
925
|
+
| `textStyle` | `TextStyle` | `undefined` | Text style override |
|
|
926
|
+
| `disabled` | `boolean` | `false` | Disabled state |
|
|
927
|
+
| `onPress` | `() => void` | `undefined` | Custom press handler (skips built-in login) |
|
|
928
|
+
| `onSuccess` | `(user: AuthUser) => void` | `undefined` | Called after successful default login |
|
|
929
|
+
| `onError` | `(error: unknown) => void` | `undefined` | Called when default login fails |
|
|
930
|
+
|
|
931
|
+
### Config Plugin API (`app.json` / `app.config.js`)
|
|
932
|
+
|
|
933
|
+
`plugins: [["react-native-nitro-auth", { ios: {...}, android: {...} }]]`
|
|
934
|
+
|
|
935
|
+
#### iOS plugin options
|
|
936
|
+
|
|
937
|
+
| Option | Type | Description |
|
|
938
|
+
| ---------------------- | --------- | -------------------------------------------- |
|
|
939
|
+
| `googleClientId` | `string` | Writes `GIDClientID` |
|
|
940
|
+
| `googleServerClientId` | `string` | Writes `GIDServerClientID` |
|
|
941
|
+
| `googleUrlScheme` | `string` | Adds Google URL scheme |
|
|
942
|
+
| `appleSignIn` | `boolean` | Enables Apple Sign-In entitlement |
|
|
943
|
+
| `microsoftClientId` | `string` | Writes `MSALClientID` + MSAL redirect scheme |
|
|
944
|
+
| `microsoftTenant` | `string` | Writes `MSALTenant` |
|
|
945
|
+
| `microsoftB2cDomain` | `string` | Writes `MSALB2cDomain` |
|
|
946
|
+
|
|
947
|
+
#### Android plugin options
|
|
948
|
+
|
|
949
|
+
| Option | Type | Description |
|
|
950
|
+
| -------------------- | -------- | ---------------------------------------------------------------- |
|
|
951
|
+
| `googleClientId` | `string` | Writes `nitro_auth_google_client_id` string |
|
|
952
|
+
| `microsoftClientId` | `string` | Writes `nitro_auth_microsoft_client_id` + redirect intent filter |
|
|
953
|
+
| `microsoftTenant` | `string` | Writes `nitro_auth_microsoft_tenant` |
|
|
954
|
+
| `microsoftB2cDomain` | `string` | Writes `nitro_auth_microsoft_b2c_domain` |
|
|
955
|
+
|
|
956
|
+
### Web runtime config (`expo.extra`)
|
|
957
|
+
|
|
958
|
+
| Key | Type | Default | Description |
|
|
959
|
+
| ----------------------------- | ---------------------------------- | ----------- | --------------------------------------------------------- |
|
|
960
|
+
| `googleWebClientId` | `string` | `undefined` | Google web OAuth client id |
|
|
961
|
+
| `microsoftClientId` | `string` | `undefined` | Microsoft app client id |
|
|
962
|
+
| `microsoftTenant` | `string` | `"common"` | Microsoft tenant/authority |
|
|
963
|
+
| `microsoftB2cDomain` | `string` | `undefined` | B2C domain when applicable |
|
|
964
|
+
| `appleWebClientId` | `string` | `undefined` | Apple Service ID |
|
|
965
|
+
| `nitroAuthWebStorage` | `"session" \| "local" \| "memory"` | `"session"` | Storage for non-sensitive web cache |
|
|
966
|
+
| `nitroAuthPersistTokensOnWeb` | `boolean` | `false` | Persist sensitive tokens on web storage instead of memory |
|
|
967
|
+
|
|
968
|
+
### Error semantics
|
|
969
|
+
|
|
970
|
+
Errors are surfaced as `Error` with `message` as a normalized code when possible, and `underlyingError` with provider/native details.
|
|
971
|
+
|
|
972
|
+
| Normalized message | Meaning |
|
|
973
|
+
| --------------------- | ---------------------------------------------- |
|
|
974
|
+
| `cancelled` | User cancelled popup/login flow |
|
|
975
|
+
| `timeout` | Provider popup did not complete before timeout |
|
|
976
|
+
| `popup_blocked` | Browser blocked popup opening |
|
|
977
|
+
| `network_error` | Network failure |
|
|
978
|
+
| `configuration_error` | Missing/invalid provider configuration |
|
|
979
|
+
|
|
980
|
+
## Quality Gates
|
|
981
|
+
|
|
982
|
+
From monorepo root:
|
|
983
|
+
|
|
984
|
+
```bash
|
|
985
|
+
bun run format:check
|
|
986
|
+
bun run lint
|
|
987
|
+
bun run typecheck
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Workspace-local commands:
|
|
991
|
+
|
|
992
|
+
```bash
|
|
993
|
+
# apps/example
|
|
994
|
+
bun run format
|
|
995
|
+
bun run lint
|
|
996
|
+
bun run typecheck
|
|
997
|
+
|
|
998
|
+
# packages/react-native-nitro-auth
|
|
999
|
+
bun run format
|
|
1000
|
+
bun run lint
|
|
1001
|
+
bun run typecheck
|
|
1002
|
+
bun run test
|
|
1003
|
+
```
|
|
821
1004
|
|
|
822
1005
|
## Platform Support
|
|
823
1006
|
|
|
824
|
-
| Feature
|
|
825
|
-
|
|
|
826
|
-
| Google Sign-In
|
|
827
|
-
| Apple Sign-In
|
|
828
|
-
| Microsoft Sign-In
|
|
829
|
-
| Custom OAuth Scopes
|
|
830
|
-
| Incremental Authorization
|
|
831
|
-
| Token Refresh
|
|
832
|
-
| Session Persistence
|
|
833
|
-
|
|
|
834
|
-
|
|
|
1007
|
+
| Feature | iOS | Android | Web |
|
|
1008
|
+
| -------------------------- | --- | ------- | --- |
|
|
1009
|
+
| Google Sign-In | ✅ | ✅ | ✅ |
|
|
1010
|
+
| Apple Sign-In | ✅ | ❌ | ✅ |
|
|
1011
|
+
| Microsoft Sign-In | ✅ | ✅ | ✅ |
|
|
1012
|
+
| Custom OAuth Scopes | ✅ | ✅ | ✅ |
|
|
1013
|
+
| Incremental Authorization | ✅ | ✅ | ✅ |
|
|
1014
|
+
| Token Refresh | ✅ | ✅ | ✅ |
|
|
1015
|
+
| Native Session Persistence | ❌ | ❌ | — |
|
|
1016
|
+
| Web Auth Snapshot Cache | — | — | ✅ |
|
|
1017
|
+
| App-Owned Persistence | ✅ | ✅ | ✅ |
|
|
1018
|
+
| Auto-Refresh | ✅ | ✅ | ✅ |
|
|
1019
|
+
| Native C++ Performance | ✅ | ✅ | — |
|
|
835
1020
|
|
|
836
1021
|
## Architecture
|
|
837
1022
|
|