react-native-nitro-storage 0.5.4 → 0.5.6
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 +90 -7
- package/android/build.gradle +5 -5
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/app.plugin.js +114 -9
- package/docs/api-reference.md +39 -36
- package/docs/batch-transactions-migrations.md +1 -1
- package/docs/recipes.md +1 -1
- package/docs/secure-storage.md +15 -4
- package/docs/web-backends.md +5 -0
- package/lib/commonjs/index.js +49 -9
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +71 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +28 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-hooks.js.map +1 -1
- package/lib/commonjs/web-storage-backend.js.map +1 -1
- package/lib/module/index.js +49 -9
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +71 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +28 -0
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-hooks.js.map +1 -1
- package/lib/module/web-storage-backend.js.map +1 -1
- package/lib/typescript/index.d.ts +21 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +10 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +5 -4
- package/lib/typescript/storage-hooks.d.ts.map +1 -1
- package/lib/typescript/web-storage-backend.d.ts +1 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +96 -19
- package/src/index.web.ts +107 -11
- package/src/indexeddb-backend.ts +30 -0
- package/src/storage-hooks.ts +6 -6
- package/src/web-storage-backend.ts +1 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-native-nitro-storage)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://reactnative.dev/)
|
|
6
|
-
[](https://nitro.margelo.com/)
|
|
7
7
|
|
|
8
8
|
One storage layer for render-time state, persisted app state, and native secrets.
|
|
9
9
|
|
|
@@ -20,6 +20,7 @@ Use it when you want one storage API for React Native and web, with fast synchro
|
|
|
20
20
|
- [Quick Start](#quick-start)
|
|
21
21
|
- [Storage Scopes](#storage-scopes)
|
|
22
22
|
- [Docs](#docs)
|
|
23
|
+
- [TypeScript And IDE Safety](#typescript-and-ide-safety)
|
|
23
24
|
- [Platform Support](#platform-support)
|
|
24
25
|
- [Security Model](#security-model)
|
|
25
26
|
- [Migration Paths](#migration-paths)
|
|
@@ -83,7 +84,8 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
|
|
|
83
84
|
"react-native-nitro-storage",
|
|
84
85
|
{
|
|
85
86
|
"faceIDPermission": "Allow $(PRODUCT_NAME) to protect your secure data with Face ID",
|
|
86
|
-
"addBiometricPermissions": true
|
|
87
|
+
"addBiometricPermissions": true,
|
|
88
|
+
"configureAndroidBackup": true
|
|
87
89
|
}
|
|
88
90
|
]
|
|
89
91
|
]
|
|
@@ -95,7 +97,9 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
|
|
|
95
97
|
bunx expo prebuild
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
The
|
|
100
|
+
The example project is aligned with Expo SDK 56, React Native 0.85, and React 19.2.
|
|
101
|
+
|
|
102
|
+
The Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, initializes the Android storage adapter in `MainApplication`, and writes Android backup rules that exclude Nitro Storage secure preference files from cloud backup and device transfer. Set `configureAndroidBackup: false` only when you maintain equivalent backup rules yourself.
|
|
99
103
|
|
|
100
104
|
Bare React Native projects should install pods after adding the package:
|
|
101
105
|
|
|
@@ -122,18 +126,21 @@ Create storage items outside React render functions, then use them from anywhere
|
|
|
122
126
|
import {
|
|
123
127
|
createStorageItem,
|
|
124
128
|
StorageScope,
|
|
129
|
+
type StorageItemConfig,
|
|
125
130
|
useStorage,
|
|
126
131
|
} from "react-native-nitro-storage";
|
|
127
132
|
|
|
128
133
|
type Theme = "system" | "light" | "dark";
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
const themeConfig = {
|
|
131
136
|
key: "theme",
|
|
132
137
|
scope: StorageScope.Disk,
|
|
133
138
|
defaultValue: "system",
|
|
134
139
|
validate: (value): value is Theme =>
|
|
135
140
|
value === "system" || value === "light" || value === "dark",
|
|
136
|
-
}
|
|
141
|
+
} satisfies StorageItemConfig<Theme>;
|
|
142
|
+
|
|
143
|
+
export const themeItem = createStorageItem(themeConfig);
|
|
137
144
|
|
|
138
145
|
export function ThemeButton() {
|
|
139
146
|
const [theme, setTheme] = useStorage(themeItem);
|
|
@@ -199,7 +206,7 @@ const snapshot = storage.export(StorageScope.Disk);
|
|
|
199
206
|
storage.import(snapshot, StorageScope.Disk);
|
|
200
207
|
```
|
|
201
208
|
|
|
202
|
-
`storage.export(StorageScope.Secure)`
|
|
209
|
+
`storage.export(StorageScope.Secure)` throws unless you pass `{ includeSecureValues: true }`. Prefer `storage.exportSecureUnsafe()` only for short-lived in-memory secure migration workflows. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
|
|
203
210
|
|
|
204
211
|
Subscribe to storage changes outside React:
|
|
205
212
|
|
|
@@ -284,6 +291,60 @@ The main building blocks are:
|
|
|
284
291
|
|
|
285
292
|
See the full [API reference](docs/api-reference.md).
|
|
286
293
|
|
|
294
|
+
## TypeScript And IDE Safety
|
|
295
|
+
|
|
296
|
+
The public API is designed around typed storage items. Define the value type, default value, serializer, parser, and validator in one place so reads, writes, React hooks, batch APIs, migrations, and transactions all share the same contract.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import {
|
|
300
|
+
StorageScope,
|
|
301
|
+
createStorageItem,
|
|
302
|
+
type StorageItemConfig,
|
|
303
|
+
} from "react-native-nitro-storage";
|
|
304
|
+
|
|
305
|
+
type Preferences = {
|
|
306
|
+
theme: "system" | "light" | "dark";
|
|
307
|
+
compactMode: boolean;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function isPreferences(value: unknown): value is Preferences {
|
|
311
|
+
if (typeof value !== "object" || value === null) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const candidate = value as Partial<Preferences>;
|
|
316
|
+
return (
|
|
317
|
+
(candidate.theme === "system" ||
|
|
318
|
+
candidate.theme === "light" ||
|
|
319
|
+
candidate.theme === "dark") &&
|
|
320
|
+
typeof candidate.compactMode === "boolean"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const preferencesConfig = {
|
|
325
|
+
key: "preferences",
|
|
326
|
+
scope: StorageScope.Disk,
|
|
327
|
+
defaultValue: { theme: "system", compactMode: false },
|
|
328
|
+
validate: isPreferences,
|
|
329
|
+
} satisfies StorageItemConfig<Preferences>;
|
|
330
|
+
|
|
331
|
+
const preferencesItem = createStorageItem(preferencesConfig);
|
|
332
|
+
|
|
333
|
+
preferencesItem.set((current) => ({
|
|
334
|
+
...current,
|
|
335
|
+
compactMode: !current.compactMode,
|
|
336
|
+
}));
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
For web backend overrides, import the backend contracts instead of using loose object shapes:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import type {
|
|
343
|
+
WebDiskStorageBackend,
|
|
344
|
+
WebSecureStorageBackend,
|
|
345
|
+
} from "react-native-nitro-storage";
|
|
346
|
+
```
|
|
347
|
+
|
|
287
348
|
## Platform Support
|
|
288
349
|
|
|
289
350
|
| Platform | Status | Notes |
|
|
@@ -293,11 +354,23 @@ See the full [API reference](docs/api-reference.md).
|
|
|
293
354
|
| Expo | Supported | Add the included config plugin before prebuild. |
|
|
294
355
|
| Web | Supported | Defaults to localStorage-style backends; IndexedDB backend is available for persistent Secure storage. |
|
|
295
356
|
|
|
357
|
+
Tested release matrix:
|
|
358
|
+
|
|
359
|
+
| Surface | Version |
|
|
360
|
+
| ------------- | ------- |
|
|
361
|
+
| Expo example | SDK 56 |
|
|
362
|
+
| React Native | 0.85.3 |
|
|
363
|
+
| React | 19.2.3 |
|
|
364
|
+
| Nitro Modules | 0.35.7 |
|
|
365
|
+
| TypeScript | 6.0.3 |
|
|
366
|
+
|
|
367
|
+
The iOS example build requires Xcode 26 or newer. GitHub Actions should use the `macos-26` runner image for SDK 56 iOS build validation.
|
|
368
|
+
|
|
296
369
|
Peer dependencies:
|
|
297
370
|
|
|
298
371
|
- `react >=18.2.0`
|
|
299
372
|
- `react-native >=0.75.0`
|
|
300
|
-
- `react-native-nitro-modules >=0.35.
|
|
373
|
+
- `react-native-nitro-modules >=0.35.7`
|
|
301
374
|
|
|
302
375
|
## Security Model
|
|
303
376
|
|
|
@@ -390,6 +463,11 @@ bun run test -- --filter=react-native-nitro-storage
|
|
|
390
463
|
bun run test:coverage -- --filter=react-native-nitro-storage
|
|
391
464
|
bun run test:cpp -- --filter=react-native-nitro-storage
|
|
392
465
|
bun run test:cpp:coverage -- --filter=react-native-nitro-storage
|
|
466
|
+
bun run example:doctor
|
|
467
|
+
bun run example:typecheck
|
|
468
|
+
bun run example:prebuild:clean
|
|
469
|
+
bun run example:android:assemble
|
|
470
|
+
bun run example:ios:build
|
|
393
471
|
(cd packages/react-native-nitro-storage && bun run check:pack)
|
|
394
472
|
bun run publish-package:dry -- --yes --with-coverage
|
|
395
473
|
```
|
|
@@ -414,6 +492,11 @@ Release checks:
|
|
|
414
492
|
```sh
|
|
415
493
|
bun run build -- --filter=react-native-nitro-storage
|
|
416
494
|
bun run benchmark -- --filter=react-native-nitro-storage
|
|
495
|
+
bun run example:doctor
|
|
496
|
+
bun run example:typecheck
|
|
497
|
+
bun run example:prebuild:clean
|
|
498
|
+
bun run example:android:assemble
|
|
499
|
+
bun run example:ios:build
|
|
417
500
|
(cd packages/react-native-nitro-storage && bun run check:pack)
|
|
418
501
|
bun run publish-package:dry -- --yes
|
|
419
502
|
```
|
package/android/build.gradle
CHANGED
|
@@ -28,10 +28,10 @@ def getExtOrIntegerDefault(name) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
android {
|
|
31
|
-
namespace "com.nitrostorage"
|
|
31
|
+
namespace = "com.nitrostorage"
|
|
32
32
|
|
|
33
33
|
// Use values from root project or defaults
|
|
34
|
-
ndkVersion getExtOrDefault("ndkVersion")
|
|
34
|
+
ndkVersion = getExtOrDefault("ndkVersion")
|
|
35
35
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
36
36
|
|
|
37
37
|
defaultConfig {
|
|
@@ -55,8 +55,8 @@ android {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
buildFeatures {
|
|
58
|
-
buildConfig true
|
|
59
|
-
prefab true
|
|
58
|
+
buildConfig = true
|
|
59
|
+
prefab = true
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
compileOptions {
|
|
@@ -79,7 +79,7 @@ repositories {
|
|
|
79
79
|
dependencies {
|
|
80
80
|
//noinspection GradleDynamicVersion
|
|
81
81
|
implementation "com.facebook.react:react-native:+"
|
|
82
|
-
implementation "androidx.security:security-crypto:1.1.0
|
|
82
|
+
implementation "androidx.security:security-crypto:1.1.0"
|
|
83
83
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
|
|
84
84
|
implementation project(":react-native-nitro-modules")
|
|
85
85
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
@file:Suppress("DEPRECATION")
|
|
2
|
+
|
|
1
3
|
package com.nitrostorage
|
|
2
4
|
|
|
3
5
|
import android.content.Context
|
|
4
6
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
5
7
|
import android.security.keystore.UserNotAuthenticatedException
|
|
6
8
|
import android.content.SharedPreferences
|
|
7
|
-
import android.util.Log
|
|
8
9
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
9
10
|
import androidx.security.crypto.MasterKey
|
|
10
11
|
import java.security.InvalidKeyException
|
|
@@ -71,7 +72,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
71
72
|
.build()
|
|
72
73
|
initializeEncryptedPreferences("NitroStorageBiometric", bioKey)
|
|
73
74
|
} catch (e: Exception) {
|
|
74
|
-
Log.e("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
75
75
|
throw e.wrapStorageException(
|
|
76
76
|
"NitroStorage: Biometric storage is not available on this device. " +
|
|
77
77
|
"Ensure biometric hardware is present and credentials are enrolled.",
|
|
@@ -96,7 +96,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
96
96
|
} catch (e: Exception) {
|
|
97
97
|
when {
|
|
98
98
|
e.hasCause(AEADBadTagException::class.java) -> {
|
|
99
|
-
Log.w("NitroStorage", "Corrupted encryption keys for $name, attempting recovery...")
|
|
100
99
|
clearCorruptedStorage(name, key)
|
|
101
100
|
val freshAlias = if (name == "NitroStorageBiometric") biometricMasterKeyAlias else masterKeyAlias
|
|
102
101
|
val freshKey = MasterKey.Builder(context, freshAlias)
|
|
@@ -142,9 +141,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
142
141
|
else -> masterKeyAlias
|
|
143
142
|
}
|
|
144
143
|
keyStore.deleteEntry(alias)
|
|
145
|
-
|
|
146
|
-
} catch (e: Exception) {
|
|
147
|
-
Log.e("NitroStorage", "Failed to clear corrupted storage $name: ${e.message}", e)
|
|
144
|
+
} catch (_: Exception) {
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
|
|
@@ -153,7 +150,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
153
150
|
prefs.getString(key, null)
|
|
154
151
|
} catch (e: Exception) {
|
|
155
152
|
if (e.hasCause(AEADBadTagException::class.java)) {
|
|
156
|
-
Log.w("NitroStorage", "Corrupt entry for key '$key', removing")
|
|
157
153
|
prefs.edit().remove(key).commit()
|
|
158
154
|
null
|
|
159
155
|
} else {
|
|
@@ -198,8 +194,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
198
194
|
keys.addAll(encryptedPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
|
|
199
195
|
try {
|
|
200
196
|
keys.addAll(biometricPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
|
|
201
|
-
} catch (
|
|
202
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
197
|
+
} catch (_: Exception) {
|
|
203
198
|
}
|
|
204
199
|
val built = keys.toTypedArray()
|
|
205
200
|
secureKeysCache = built
|
|
@@ -369,8 +364,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
369
364
|
inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
|
|
370
365
|
try {
|
|
371
366
|
inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
|
|
372
|
-
} catch (
|
|
373
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
367
|
+
} catch (_: Exception) {
|
|
374
368
|
}
|
|
375
369
|
inst.invalidateSecureKeysCache()
|
|
376
370
|
}
|
|
@@ -391,8 +385,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
391
385
|
biometricEditor.remove(key)
|
|
392
386
|
}
|
|
393
387
|
inst.applySecureEditor(biometricEditor)
|
|
394
|
-
} catch (
|
|
395
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
388
|
+
} catch (_: Exception) {
|
|
396
389
|
}
|
|
397
390
|
inst.invalidateSecureKeysCache()
|
|
398
391
|
}
|
|
@@ -404,8 +397,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
404
397
|
val hasInEncrypted = inst.encryptedPreferences.contains(key)
|
|
405
398
|
val hasInBiometric = try {
|
|
406
399
|
inst.biometricPreferences.contains(key)
|
|
407
|
-
} catch (
|
|
408
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
400
|
+
} catch (_: Exception) {
|
|
409
401
|
false
|
|
410
402
|
}
|
|
411
403
|
return hasInEncrypted || hasInBiometric
|
|
@@ -433,8 +425,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
433
425
|
inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
|
|
434
426
|
try {
|
|
435
427
|
inst.applySecureEditor(inst.biometricPreferences.edit().clear())
|
|
436
|
-
} catch (
|
|
437
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
428
|
+
} catch (_: Exception) {
|
|
438
429
|
}
|
|
439
430
|
inst.invalidateSecureKeysCache()
|
|
440
431
|
}
|
|
@@ -466,8 +457,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
466
457
|
val inst = getInstanceOrThrow()
|
|
467
458
|
return try {
|
|
468
459
|
inst.getSecureSafe(inst.biometricPreferences, key)
|
|
469
|
-
} catch (
|
|
470
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
460
|
+
} catch (_: Exception) {
|
|
471
461
|
null
|
|
472
462
|
}
|
|
473
463
|
}
|
|
@@ -478,8 +468,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
478
468
|
try {
|
|
479
469
|
inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
|
|
480
470
|
inst.invalidateSecureKeysCache()
|
|
481
|
-
} catch (
|
|
482
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
471
|
+
} catch (_: Exception) {
|
|
483
472
|
}
|
|
484
473
|
}
|
|
485
474
|
|
|
@@ -487,8 +476,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
487
476
|
fun hasSecureBiometric(key: String): Boolean {
|
|
488
477
|
return try {
|
|
489
478
|
getInstanceOrThrow().biometricPreferences.contains(key)
|
|
490
|
-
} catch (
|
|
491
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
479
|
+
} catch (_: Exception) {
|
|
492
480
|
false
|
|
493
481
|
}
|
|
494
482
|
}
|
|
@@ -499,8 +487,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
499
487
|
try {
|
|
500
488
|
inst.applySecureEditor(inst.biometricPreferences.edit().clear())
|
|
501
489
|
inst.invalidateSecureKeysCache()
|
|
502
|
-
} catch (
|
|
503
|
-
Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
490
|
+
} catch (_: Exception) {
|
|
504
491
|
}
|
|
505
492
|
}
|
|
506
493
|
}
|
package/app.plugin.js
CHANGED
|
@@ -2,16 +2,100 @@ const {
|
|
|
2
2
|
withInfoPlist,
|
|
3
3
|
withAndroidManifest,
|
|
4
4
|
withMainApplication,
|
|
5
|
+
withDangerousMod,
|
|
5
6
|
createRunOncePlugin,
|
|
6
7
|
} = require("@expo/config-plugins");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const DATA_EXTRACTION_RULES_RESOURCE =
|
|
12
|
+
"@xml/nitro_storage_data_extraction_rules";
|
|
13
|
+
const FULL_BACKUP_CONTENT_RESOURCE = "@xml/nitro_storage_full_backup_content";
|
|
14
|
+
|
|
15
|
+
const secureSharedPrefs = [
|
|
16
|
+
"NitroStorageSecure.xml",
|
|
17
|
+
"NitroStorageBiometric.xml",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function sharedPrefsExcludes(indent = " ") {
|
|
21
|
+
return secureSharedPrefs
|
|
22
|
+
.map((file) => `${indent}<exclude domain="sharedpref" path="${file}" />`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dataExtractionRulesXml() {
|
|
27
|
+
const excludes = sharedPrefsExcludes(" ");
|
|
28
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
29
|
+
<data-extraction-rules>
|
|
30
|
+
<cloud-backup>
|
|
31
|
+
${excludes}
|
|
32
|
+
</cloud-backup>
|
|
33
|
+
<device-transfer>
|
|
34
|
+
${excludes}
|
|
35
|
+
</device-transfer>
|
|
36
|
+
</data-extraction-rules>
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fullBackupContentXml() {
|
|
41
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
42
|
+
<full-backup-content>
|
|
43
|
+
${sharedPrefsExcludes(" ")}
|
|
44
|
+
</full-backup-content>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureBackupAttributes(androidManifest) {
|
|
49
|
+
const application = androidManifest.manifest.application?.[0];
|
|
50
|
+
if (!application) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
application.$ = application.$ || {};
|
|
55
|
+
if (!application.$["android:dataExtractionRules"]) {
|
|
56
|
+
application.$["android:dataExtractionRules"] =
|
|
57
|
+
DATA_EXTRACTION_RULES_RESOURCE;
|
|
58
|
+
}
|
|
59
|
+
if (!application.$["android:fullBackupContent"]) {
|
|
60
|
+
application.$["android:fullBackupContent"] = FULL_BACKUP_CONTENT_RESOURCE;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeAndroidBackupFiles(projectRoot) {
|
|
65
|
+
const xmlDir = path.join(
|
|
66
|
+
projectRoot,
|
|
67
|
+
"android",
|
|
68
|
+
"app",
|
|
69
|
+
"src",
|
|
70
|
+
"main",
|
|
71
|
+
"res",
|
|
72
|
+
"xml",
|
|
73
|
+
);
|
|
74
|
+
fs.mkdirSync(xmlDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(xmlDir, "nitro_storage_data_extraction_rules.xml"),
|
|
77
|
+
dataExtractionRulesXml(),
|
|
78
|
+
);
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(xmlDir, "nitro_storage_full_backup_content.xml"),
|
|
81
|
+
fullBackupContentXml(),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
7
84
|
|
|
8
85
|
const withNitroStorage = (config, props = {}) => {
|
|
9
86
|
const defaultFaceIDPermission =
|
|
10
87
|
"Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
|
|
11
|
-
const {
|
|
88
|
+
const {
|
|
89
|
+
faceIDPermission,
|
|
90
|
+
addBiometricPermissions = false,
|
|
91
|
+
configureAndroidBackup = true,
|
|
92
|
+
} = props;
|
|
12
93
|
|
|
13
94
|
config = withInfoPlist(config, (config) => {
|
|
14
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
typeof faceIDPermission === "string" &&
|
|
97
|
+
faceIDPermission.trim() !== ""
|
|
98
|
+
) {
|
|
15
99
|
config.modResults.NSFaceIDUsageDescription = faceIDPermission;
|
|
16
100
|
} else if (!config.modResults.NSFaceIDUsageDescription) {
|
|
17
101
|
config.modResults.NSFaceIDUsageDescription = defaultFaceIDPermission;
|
|
@@ -20,6 +104,10 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
20
104
|
});
|
|
21
105
|
|
|
22
106
|
config = withAndroidManifest(config, (config) => {
|
|
107
|
+
if (configureAndroidBackup) {
|
|
108
|
+
ensureBackupAttributes(config.modResults);
|
|
109
|
+
}
|
|
110
|
+
|
|
23
111
|
if (!addBiometricPermissions) {
|
|
24
112
|
return config;
|
|
25
113
|
}
|
|
@@ -38,10 +126,10 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
38
126
|
};
|
|
39
127
|
|
|
40
128
|
const hasBiometric = permissions.some(
|
|
41
|
-
(p) => p.$?.["android:name"] === "android.permission.USE_BIOMETRIC"
|
|
129
|
+
(p) => p.$?.["android:name"] === "android.permission.USE_BIOMETRIC",
|
|
42
130
|
);
|
|
43
131
|
const hasFingerprint = permissions.some(
|
|
44
|
-
(p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT"
|
|
132
|
+
(p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT",
|
|
45
133
|
);
|
|
46
134
|
|
|
47
135
|
if (!hasBiometric) {
|
|
@@ -54,6 +142,16 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
54
142
|
return config;
|
|
55
143
|
});
|
|
56
144
|
|
|
145
|
+
if (configureAndroidBackup) {
|
|
146
|
+
config = withDangerousMod(config, [
|
|
147
|
+
"android",
|
|
148
|
+
async (config) => {
|
|
149
|
+
writeAndroidBackupFiles(config.modRequest.projectRoot);
|
|
150
|
+
return config;
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
|
|
57
155
|
config = withMainApplication(config, (config) => {
|
|
58
156
|
const { modResults } = config;
|
|
59
157
|
const { language, contents } = modResults;
|
|
@@ -67,13 +165,13 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
67
165
|
if (!contents.includes(importStatement)) {
|
|
68
166
|
modResults.contents = contents.replace(
|
|
69
167
|
/(package .*;\n)/,
|
|
70
|
-
`$1\n${importStatement}\n
|
|
168
|
+
`$1\n${importStatement}\n`,
|
|
71
169
|
);
|
|
72
170
|
}
|
|
73
171
|
|
|
74
172
|
modResults.contents = modResults.contents.replace(
|
|
75
173
|
/(super\.onCreate\(\);)/,
|
|
76
|
-
`$1\n${initStatement}
|
|
174
|
+
`$1\n${initStatement}`,
|
|
77
175
|
);
|
|
78
176
|
}
|
|
79
177
|
} else if (language === "kt") {
|
|
@@ -84,13 +182,13 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
84
182
|
if (!contents.includes(importStatement)) {
|
|
85
183
|
modResults.contents = contents.replace(
|
|
86
184
|
/(package .*\n)/,
|
|
87
|
-
`$1\n${importStatement}\n
|
|
185
|
+
`$1\n${importStatement}\n`,
|
|
88
186
|
);
|
|
89
187
|
}
|
|
90
188
|
|
|
91
189
|
modResults.contents = modResults.contents.replace(
|
|
92
190
|
/(super\.onCreate\(\))/,
|
|
93
|
-
`$1\n${initStatement}
|
|
191
|
+
`$1\n${initStatement}`,
|
|
94
192
|
);
|
|
95
193
|
}
|
|
96
194
|
}
|
|
@@ -104,5 +202,12 @@ const withNitroStorage = (config, props = {}) => {
|
|
|
104
202
|
module.exports = createRunOncePlugin(
|
|
105
203
|
withNitroStorage,
|
|
106
204
|
"react-native-nitro-storage",
|
|
107
|
-
"1.0.0"
|
|
205
|
+
"1.0.0",
|
|
108
206
|
);
|
|
207
|
+
module.exports.withNitroStorage = withNitroStorage;
|
|
208
|
+
module.exports._internal = {
|
|
209
|
+
dataExtractionRulesXml,
|
|
210
|
+
fullBackupContentXml,
|
|
211
|
+
ensureBackupAttributes,
|
|
212
|
+
writeAndroidBackupFiles,
|
|
213
|
+
};
|
package/docs/api-reference.md
CHANGED
|
@@ -72,41 +72,42 @@ See [react-hooks.md](react-hooks.md).
|
|
|
72
72
|
|
|
73
73
|
`storage` exposes raw and cross-item utilities:
|
|
74
74
|
|
|
75
|
-
| Method | Purpose
|
|
76
|
-
| ------------------------------------------------ |
|
|
77
|
-
| `clear(scope)` | Clear one scope.
|
|
78
|
-
| `clearAll()` | Clear Memory, Disk, and Secure scopes.
|
|
79
|
-
| `clearNamespace(namespace, scope)` | Remove keys under `namespace:`.
|
|
80
|
-
| `subscribe(scope, listener)` | Subscribe to raw scope-level change events.
|
|
81
|
-
| `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key.
|
|
82
|
-
| `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes.
|
|
83
|
-
| `subscribeNamespace(namespace, scope, listener)` | Subscribe to raw events for `namespace:` keys.
|
|
84
|
-
| `setEventObserver(observer)`
|
|
85
|
-
| `clearBiometric()` | Clear biometric Secure entries.
|
|
86
|
-
| `has(key, scope)` | Check for a raw key.
|
|
87
|
-
| `getAllKeys(scope)` | List raw keys.
|
|
88
|
-
| `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix.
|
|
89
|
-
| `getByPrefix(prefix, scope)` | Read raw string values by prefix.
|
|
90
|
-
| `getAll(scope)` | Read all raw string values in a scope.
|
|
91
|
-
| `size(scope)` | Return approximate scope entry count.
|
|
92
|
-
| `setAccessControl(accessControl)` | Set the default Secure access control level.
|
|
93
|
-
| `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes.
|
|
94
|
-
| `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior.
|
|
95
|
-
| `flushDiskWrites()` | Flush pending Disk writes.
|
|
96
|
-
| `flushSecureWrites()` | Flush pending Secure writes.
|
|
97
|
-
| `setKeychainAccessGroup(group)` | Configure iOS Keychain access group.
|
|
98
|
-
| `setMetricsObserver(observer)` | Receive operation timing events.
|
|
99
|
-
| `getMetricsSnapshot()` | Read aggregated metrics.
|
|
100
|
-
| `resetMetrics()` | Clear metrics counters.
|
|
101
|
-
| `getCapabilities()` | Read runtime storage capabilities.
|
|
102
|
-
| `getSecurityCapabilities()` | Read secure backend capability metadata.
|
|
103
|
-
| `getSecureMetadata(key)` | Read secure metadata for one key without returning its value.
|
|
104
|
-
| `getAllSecureMetadata()` | Read secure metadata for all secure keys without values.
|
|
105
|
-
| `getString(key, scope)` | Read a raw string.
|
|
106
|
-
| `setString(key, value, scope)` | Write a raw string.
|
|
107
|
-
| `deleteString(key, scope)` | Remove a raw key.
|
|
108
|
-
| `export(scope)`
|
|
109
|
-
| `
|
|
75
|
+
| Method | Purpose |
|
|
76
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
77
|
+
| `clear(scope)` | Clear one scope. |
|
|
78
|
+
| `clearAll()` | Clear Memory, Disk, and Secure scopes. |
|
|
79
|
+
| `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
|
|
80
|
+
| `subscribe(scope, listener)` | Subscribe to raw scope-level change events. |
|
|
81
|
+
| `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key. |
|
|
82
|
+
| `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes. |
|
|
83
|
+
| `subscribeNamespace(namespace, scope, listener)` | Subscribe to raw events for `namespace:` keys. |
|
|
84
|
+
| `setEventObserver(observer, options?)` | Receive all change events for devtools or logging. Secure values are redacted by default. |
|
|
85
|
+
| `clearBiometric()` | Clear biometric Secure entries. |
|
|
86
|
+
| `has(key, scope)` | Check for a raw key. |
|
|
87
|
+
| `getAllKeys(scope)` | List raw keys. |
|
|
88
|
+
| `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix. |
|
|
89
|
+
| `getByPrefix(prefix, scope)` | Read raw string values by prefix. |
|
|
90
|
+
| `getAll(scope)` | Read all raw string values in a scope. |
|
|
91
|
+
| `size(scope)` | Return approximate scope entry count. |
|
|
92
|
+
| `setAccessControl(accessControl)` | Set the default Secure access control level. |
|
|
93
|
+
| `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes. |
|
|
94
|
+
| `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior. |
|
|
95
|
+
| `flushDiskWrites()` | Flush pending Disk writes. |
|
|
96
|
+
| `flushSecureWrites()` | Flush pending Secure writes. |
|
|
97
|
+
| `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
|
|
98
|
+
| `setMetricsObserver(observer)` | Receive operation timing events. |
|
|
99
|
+
| `getMetricsSnapshot()` | Read aggregated metrics. |
|
|
100
|
+
| `resetMetrics()` | Clear metrics counters. |
|
|
101
|
+
| `getCapabilities()` | Read runtime storage capabilities. |
|
|
102
|
+
| `getSecurityCapabilities()` | Read secure backend capability metadata. |
|
|
103
|
+
| `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
|
|
104
|
+
| `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
|
|
105
|
+
| `getString(key, scope)` | Read a raw string. |
|
|
106
|
+
| `setString(key, value, scope)` | Write a raw string. |
|
|
107
|
+
| `deleteString(key, scope)` | Remove a raw key. |
|
|
108
|
+
| `export(scope, options?)` | Snapshot raw strings from one scope. Secure scope requires explicit unsafe opt-in. |
|
|
109
|
+
| `exportSecureUnsafe()` | Snapshot raw Secure strings for short-lived migration workflows. |
|
|
110
|
+
| `import(data, scope)` | Bulk import raw strings. |
|
|
110
111
|
|
|
111
112
|
Raw string APIs bypass item serialization and validation. Prefer `StorageItem<T>` unless you are migrating, exporting/importing, or writing a custom integration.
|
|
112
113
|
|
|
@@ -115,7 +116,7 @@ const diskSnapshot = storage.export(StorageScope.Disk);
|
|
|
115
116
|
storage.import(diskSnapshot, StorageScope.Disk);
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
Secure exports contain raw secret values.
|
|
119
|
+
Secure exports contain raw secret values. `storage.export(StorageScope.Secure)` throws unless called with `{ includeSecureValues: true }`; `storage.exportSecureUnsafe()` is the explicit equivalent. Do not log Secure exports or attach them to diagnostics, analytics, crash reports, or support bundles.
|
|
119
120
|
|
|
120
121
|
## Event Subscriptions
|
|
121
122
|
|
|
@@ -149,6 +150,8 @@ storage.setEventObserver((event) => {
|
|
|
149
150
|
});
|
|
150
151
|
```
|
|
151
152
|
|
|
153
|
+
`setEventObserver()` redacts Secure `oldValue` and `newValue` fields by default. Pass `{ redactSecureValues: false }` only for in-memory debugging paths that never persist logs. Raw `subscribe*()` APIs preserve values for state integrations.
|
|
154
|
+
|
|
152
155
|
Local batch APIs emit one `type: "batch"` envelope to scope and prefix/namespace listeners. Key subscribers receive the matching per-key change so direct key integrations do not need to unpack batch envelopes. Secure events can include raw secret values; do not log Secure event payloads in production.
|
|
153
156
|
|
|
154
157
|
## Batch Operations
|
|
@@ -82,7 +82,7 @@ storage.import(snapshot, StorageScope.Disk);
|
|
|
82
82
|
|
|
83
83
|
For Memory scope, import is atomic: all keys are written before listeners fire. For Disk and Secure, import delegates to native or web batch paths.
|
|
84
84
|
|
|
85
|
-
Secure exports contain raw secret values. Do not log
|
|
85
|
+
Secure exports contain raw secret values. `storage.export(StorageScope.Secure)` requires `{ includeSecureValues: true }`; `storage.exportSecureUnsafe()` is the explicit equivalent. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
|
|
86
86
|
|
|
87
87
|
## Transactions
|
|
88
88
|
|
package/docs/recipes.md
CHANGED
|
@@ -278,7 +278,7 @@ storage.setEventObserver((event) => {
|
|
|
278
278
|
});
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
Use `subscribePrefix()` or `subscribeNamespace()` for targeted integrations. Use `setEventObserver()` for devtools-style logging. Secure
|
|
281
|
+
Use `subscribePrefix()` or `subscribeNamespace()` for targeted integrations. Use `setEventObserver()` for devtools-style logging; it redacts Secure values by default. Raw Secure subscriptions can include secret values, so filter them out of logs.
|
|
282
282
|
|
|
283
283
|
## Capability Checks
|
|
284
284
|
|