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.
Files changed (40) hide show
  1. package/README.md +90 -7
  2. package/android/build.gradle +5 -5
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
  4. package/app.plugin.js +114 -9
  5. package/docs/api-reference.md +39 -36
  6. package/docs/batch-transactions-migrations.md +1 -1
  7. package/docs/recipes.md +1 -1
  8. package/docs/secure-storage.md +15 -4
  9. package/docs/web-backends.md +5 -0
  10. package/lib/commonjs/index.js +49 -9
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js +71 -11
  13. package/lib/commonjs/index.web.js.map +1 -1
  14. package/lib/commonjs/indexeddb-backend.js +28 -0
  15. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  16. package/lib/commonjs/storage-hooks.js.map +1 -1
  17. package/lib/commonjs/web-storage-backend.js.map +1 -1
  18. package/lib/module/index.js +49 -9
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/index.web.js +71 -11
  21. package/lib/module/index.web.js.map +1 -1
  22. package/lib/module/indexeddb-backend.js +28 -0
  23. package/lib/module/indexeddb-backend.js.map +1 -1
  24. package/lib/module/storage-hooks.js.map +1 -1
  25. package/lib/module/web-storage-backend.js.map +1 -1
  26. package/lib/typescript/index.d.ts +21 -9
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/index.web.d.ts +10 -3
  29. package/lib/typescript/index.web.d.ts.map +1 -1
  30. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  31. package/lib/typescript/storage-hooks.d.ts +5 -4
  32. package/lib/typescript/storage-hooks.d.ts.map +1 -1
  33. package/lib/typescript/web-storage-backend.d.ts +1 -0
  34. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  35. package/package.json +4 -4
  36. package/src/index.ts +96 -19
  37. package/src/index.web.ts +107 -11
  38. package/src/indexeddb-backend.ts +30 -0
  39. package/src/storage-hooks.ts +6 -6
  40. package/src/web-storage-backend.ts +1 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/react-native-nitro-storage)](https://www.npmjs.com/package/react-native-nitro-storage)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
6
- [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.5-black)](https://nitro.margelo.com/)
6
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.7-black)](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 Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, and initializes the Android storage adapter in `MainApplication`.
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
- export const themeItem = createStorageItem<Theme>({
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)` returns raw secret values. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
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.5`
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
  ```
@@ -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-alpha06"
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
- Log.i("NitroStorage", "Cleared corrupted storage: $name")
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 (e: Exception) {
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 { faceIDPermission, addBiometricPermissions = false } = props;
88
+ const {
89
+ faceIDPermission,
90
+ addBiometricPermissions = false,
91
+ configureAndroidBackup = true,
92
+ } = props;
12
93
 
13
94
  config = withInfoPlist(config, (config) => {
14
- if (typeof faceIDPermission === "string" && faceIDPermission.trim() !== "") {
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
+ };
@@ -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)` | Receive all change events for devtools or logging. |
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)` | Snapshot raw strings from one scope. |
109
- | `import(data, scope)` | Bulk import raw strings. |
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. Do not log `storage.export(StorageScope.Secure)` output or attach it to diagnostics, analytics, crash reports, or support bundles.
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 them or include them in diagnostics, analytics, crash reports, or support bundles.
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 events can include raw secret values, so filter them out of logs.
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