react-native-nitro-storage 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +86 -14
  2. package/README.md +149 -41
  3. package/SECURITY.md +16 -6
  4. package/android/build.gradle +0 -1
  5. package/android/consumer-rules.pro +0 -3
  6. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +1 -4
  7. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
  8. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +107 -36
  9. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +32 -13
  10. package/app.plugin.js +51 -51
  11. package/cpp/bindings/HybridStorage.cpp +25 -6
  12. package/cpp/core/NativeStorageAdapter.hpp +14 -1
  13. package/cpp/core/SqliteDiskStore.cpp +105 -20
  14. package/cpp/core/SqliteDiskStore.hpp +11 -0
  15. package/docs/api-reference.md +74 -28
  16. package/docs/benchmarks.md +4 -12
  17. package/docs/mmkv-migration.md +3 -1
  18. package/docs/native-libraries.md +10 -4
  19. package/docs/secure-storage.md +26 -12
  20. package/docs/web-backends.md +6 -2
  21. package/indexeddb-backend/package.json +6 -0
  22. package/ios/IOSStorageAdapterCpp.hpp +4 -0
  23. package/ios/IOSStorageAdapterCpp.mm +96 -18
  24. package/lib/commonjs/capabilities.js +3 -8
  25. package/lib/commonjs/capabilities.js.map +1 -1
  26. package/lib/commonjs/index.js +23 -8
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/index.web.js +63 -20
  29. package/lib/commonjs/index.web.js.map +1 -1
  30. package/lib/commonjs/indexeddb-backend.js +21 -37
  31. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  32. package/lib/commonjs/internal.js +6 -0
  33. package/lib/commonjs/internal.js.map +1 -1
  34. package/lib/commonjs/storage-core.js +35 -11
  35. package/lib/commonjs/storage-core.js.map +1 -1
  36. package/lib/commonjs/storage-runtime.js +1 -1
  37. package/lib/commonjs/storage-runtime.js.map +1 -1
  38. package/lib/commonjs/testing.js +68 -23
  39. package/lib/commonjs/testing.js.map +1 -1
  40. package/lib/commonjs/web-backend-contract.js +6 -2
  41. package/lib/commonjs/web-backend-contract.js.map +1 -1
  42. package/lib/module/capabilities.js +3 -8
  43. package/lib/module/capabilities.js.map +1 -1
  44. package/lib/module/index.js +14 -5
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.web.js +63 -13
  47. package/lib/module/index.web.js.map +1 -1
  48. package/lib/module/indexeddb-backend.js +21 -37
  49. package/lib/module/indexeddb-backend.js.map +1 -1
  50. package/lib/module/internal.js +5 -0
  51. package/lib/module/internal.js.map +1 -1
  52. package/lib/module/storage-core.js +36 -12
  53. package/lib/module/storage-core.js.map +1 -1
  54. package/lib/module/storage-runtime.js +1 -1
  55. package/lib/module/storage-runtime.js.map +1 -1
  56. package/lib/module/testing.js +59 -16
  57. package/lib/module/testing.js.map +1 -1
  58. package/lib/module/web-backend-contract.js +5 -2
  59. package/lib/module/web-backend-contract.js.map +1 -1
  60. package/lib/typescript/capabilities.d.ts.map +1 -1
  61. package/lib/typescript/index.d.ts +8 -1
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/index.web.d.ts +7 -1
  64. package/lib/typescript/index.web.d.ts.map +1 -1
  65. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  66. package/lib/typescript/internal.d.ts +1 -0
  67. package/lib/typescript/internal.d.ts.map +1 -1
  68. package/lib/typescript/storage-core.d.ts +1 -1
  69. package/lib/typescript/storage-core.d.ts.map +1 -1
  70. package/lib/typescript/storage-runtime.d.ts +1 -1
  71. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  72. package/lib/typescript/testing.d.ts +15 -2
  73. package/lib/typescript/testing.d.ts.map +1 -1
  74. package/lib/typescript/web-backend-contract.d.ts +1 -0
  75. package/lib/typescript/web-backend-contract.d.ts.map +1 -1
  76. package/package.json +14 -5
  77. package/react-native-nitro-storage.podspec +4 -2
  78. package/src/capabilities.ts +4 -9
  79. package/src/index.ts +19 -5
  80. package/src/index.web.ts +91 -20
  81. package/src/indexeddb-backend.ts +20 -42
  82. package/src/internal.ts +8 -0
  83. package/src/storage-core.ts +52 -19
  84. package/src/storage-runtime.ts +5 -1
  85. package/src/testing.ts +91 -18
  86. package/src/web-backend-contract.ts +6 -2
  87. package/testing/package.json +8 -0
@@ -3,6 +3,9 @@
3
3
  package com.nitrostorage
4
4
 
5
5
  import android.content.Context
6
+ import android.database.sqlite.SQLiteDatabaseCorruptException
7
+ import android.database.sqlite.SQLiteException
8
+ import android.database.sqlite.SQLiteFullException
6
9
  import android.os.Build
7
10
  import android.security.keystore.KeyGenParameterSpec
8
11
  import android.security.keystore.KeyPermanentlyInvalidatedException
@@ -33,7 +36,9 @@ private fun Throwable.storageErrorCode(): String? {
33
36
  return taggedCode
34
37
  }
35
38
  return when {
36
- hasCause(AEADBadTagException::class.java) -> "storage_corruption"
39
+ hasCause(SQLiteFullException::class.java) -> "storage_full"
40
+ hasCause(SQLiteDatabaseCorruptException::class.java) ||
41
+ hasCause(AEADBadTagException::class.java) -> "storage_corruption"
37
42
  hasCause(UserNotAuthenticatedException::class.java) ||
38
43
  hasCause(KeyStoreException::class.java) -> "authentication_required"
39
44
  hasCause(KeyPermanentlyInvalidatedException::class.java) ||
@@ -61,24 +66,50 @@ private fun Throwable.wrapStorageException(
61
66
  class AndroidStorageAdapter private constructor(private val context: Context) {
62
67
  private val sharedPreferences: SharedPreferences =
63
68
  context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
64
- private val diskStore: DiskSqliteStore = DiskSqliteStore(context, sharedPreferences)
69
+ private val diskStore: DiskSqliteStore by lazy {
70
+ DiskSqliteStore(context, sharedPreferences)
71
+ }
65
72
 
66
73
  private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
67
74
 
68
- private val masterKey: MasterKey = try {
69
- MasterKey.Builder(context, masterKeyAlias)
70
- .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
71
- .build()
72
- } catch (e: Exception) {
73
- throw RuntimeException("NitroStorage: Cannot create encryption key. Device may not support AES256-GCM.", e)
75
+ private val masterKey: MasterKey by lazy {
76
+ try {
77
+ createDefaultMasterKey()
78
+ } catch (e: Exception) {
79
+ throw permanentSecureStoreError(
80
+ "NitroStorage: Cannot create the Secure encryption key.",
81
+ e,
82
+ )
83
+ }
74
84
  }
75
85
 
76
- private val encryptedPreferences: SharedPreferences = initializeEncryptedPreferences(
77
- "NitroStorageSecure",
78
- masterKey,
79
- masterKeyAlias,
80
- ::createDefaultMasterKey,
81
- )
86
+ private val encryptedPreferences: SharedPreferences by lazy {
87
+ try {
88
+ initializeEncryptedPreferences(
89
+ "NitroStorageSecure",
90
+ masterKey,
91
+ masterKeyAlias,
92
+ ::createDefaultMasterKey,
93
+ )
94
+ } catch (e: Exception) {
95
+ throw permanentSecureStoreError(
96
+ "NitroStorage: Cannot open Secure storage.",
97
+ e,
98
+ )
99
+ }
100
+ }
101
+
102
+ private fun permanentSecureStoreError(message: String, cause: Exception): RuntimeException {
103
+ if (cause.message?.contains("[nitro-error:storage_corruption]") == true &&
104
+ cause is RuntimeException
105
+ ) {
106
+ return cause
107
+ }
108
+ return RuntimeException(
109
+ "[nitro-error:storage_corruption] $message ${cause.message ?: ""}".trimEnd(),
110
+ cause,
111
+ )
112
+ }
82
113
 
83
114
  private val biometricMasterKeyAlias = "${context.packageName}.nitro_storage.biometric_key"
84
115
  private val biometricOrPasscodeMasterKeyAlias =
@@ -273,25 +304,55 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
273
304
 
274
305
  private fun existingBiometricPreferences(): List<SharedPreferences> {
275
306
  val stores = mutableListOf<SharedPreferences>()
307
+ if (
308
+ biometricOrPasscodeInitialized ||
309
+ preferencesFileExists("NitroStorageBiometricOrPasscode")
310
+ ) {
311
+ stores.add(biometricOrPasscodePreferences)
312
+ }
313
+ if (
314
+ legacyBiometricInitialized ||
315
+ preferencesFileExists("NitroStorageBiometric")
316
+ ) {
317
+ stores.add(legacyBiometricPreferences)
318
+ }
276
319
  if (
277
320
  biometricOnlyInitialized ||
278
321
  preferencesFileExists("NitroStorageBiometricOnly")
279
322
  ) {
280
323
  stores.add(biometricOnlyPreferences)
281
324
  }
325
+ return stores
326
+ }
327
+
328
+ private fun <T : Any> firstInExistingBiometricPreferences(
329
+ lookup: (SharedPreferences) -> T?,
330
+ ): T? {
282
331
  if (
283
332
  biometricOrPasscodeInitialized ||
284
333
  preferencesFileExists("NitroStorageBiometricOrPasscode")
285
334
  ) {
286
- stores.add(biometricOrPasscodePreferences)
335
+ lookup(biometricOrPasscodePreferences)?.let { return it }
287
336
  }
288
337
  if (
289
338
  legacyBiometricInitialized ||
290
339
  preferencesFileExists("NitroStorageBiometric")
291
340
  ) {
292
- stores.add(legacyBiometricPreferences)
341
+ lookup(legacyBiometricPreferences)?.let { return it }
293
342
  }
294
- return stores
343
+ if (
344
+ biometricOnlyInitialized ||
345
+ preferencesFileExists("NitroStorageBiometricOnly")
346
+ ) {
347
+ lookup(biometricOnlyPreferences)?.let { return it }
348
+ }
349
+ return null
350
+ }
351
+
352
+ private fun biometricPreferencesContain(key: String): Boolean {
353
+ return firstInExistingBiometricPreferences { preferences ->
354
+ if (preferences.contains(key)) true else null
355
+ } ?: false
295
356
  }
296
357
 
297
358
  private fun biometricPreferencesForLevel(level: Int): SharedPreferences {
@@ -518,59 +579,73 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
518
579
 
519
580
  // --- Disk ---
520
581
 
582
+ private inline fun <T> diskOperation(
583
+ operation: String,
584
+ block: DiskSqliteStore.() -> T,
585
+ ): T {
586
+ val instance = getInstanceOrThrow()
587
+ try {
588
+ return instance.diskStore.block()
589
+ } catch (e: SQLiteException) {
590
+ throw e.wrapStorageException(
591
+ "NitroStorage: Disk SQLite $operation failed: ${e.message}",
592
+ )
593
+ }
594
+ }
595
+
521
596
  @JvmStatic
522
597
  fun setDisk(key: String, value: String) {
523
- getInstanceOrThrow().diskStore.set(key, value)
598
+ diskOperation("set") { set(key, value) }
524
599
  }
525
600
 
526
601
  @JvmStatic
527
602
  fun setDiskBatch(keys: Array<String>, values: Array<String>) {
528
- getInstanceOrThrow().diskStore.setBatch(keys, values)
603
+ diskOperation("set") { setBatch(keys, values) }
529
604
  }
530
605
 
531
606
  @JvmStatic
532
607
  fun getDisk(key: String): String? {
533
- return getInstanceOrThrow().diskStore.get(key)
608
+ return diskOperation("get") { get(key) }
534
609
  }
535
610
 
536
611
  @JvmStatic
537
612
  fun getDiskBatch(keys: Array<String>): Array<String?> {
538
- return getInstanceOrThrow().diskStore.getBatch(keys)
613
+ return diskOperation("get") { getBatch(keys) }
539
614
  }
540
615
 
541
616
  @JvmStatic
542
617
  fun deleteDisk(key: String) {
543
- getInstanceOrThrow().diskStore.remove(key)
618
+ diskOperation("remove") { remove(key) }
544
619
  }
545
620
 
546
621
  @JvmStatic
547
622
  fun deleteDiskBatch(keys: Array<String>) {
548
- getInstanceOrThrow().diskStore.removeBatch(keys)
623
+ diskOperation("remove") { removeBatch(keys) }
549
624
  }
550
625
 
551
626
  @JvmStatic
552
627
  fun hasDisk(key: String): Boolean {
553
- return getInstanceOrThrow().diskStore.has(key)
628
+ return diskOperation("has") { has(key) }
554
629
  }
555
630
 
556
631
  @JvmStatic
557
632
  fun getAllKeysDisk(): Array<String> {
558
- return getInstanceOrThrow().diskStore.getAllKeys()
633
+ return diskOperation("getAllKeys") { getAllKeys() }
559
634
  }
560
635
 
561
636
  @JvmStatic
562
637
  fun getKeysByPrefixDisk(prefix: String): Array<String> {
563
- return getInstanceOrThrow().diskStore.getKeysByPrefix(prefix)
638
+ return diskOperation("getKeysByPrefix") { getKeysByPrefix(prefix) }
564
639
  }
565
640
 
566
641
  @JvmStatic
567
642
  fun sizeDisk(): Int {
568
- return getInstanceOrThrow().diskStore.size()
643
+ return diskOperation("size") { size() }
569
644
  }
570
645
 
571
646
  @JvmStatic
572
647
  fun clearDisk() {
573
- getInstanceOrThrow().diskStore.clear()
648
+ diskOperation("clear") { clear() }
574
649
  }
575
650
 
576
651
  // --- Secure (async apply by default, sync commit when requested) ---
@@ -663,7 +738,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
663
738
  if (inst.encryptedPreferences.contains(key)) {
664
739
  return true
665
740
  }
666
- return inst.existingBiometricPreferences().any { it.contains(key) }
741
+ return inst.biometricPreferencesContain(key)
667
742
  }
668
743
 
669
744
  @JvmStatic
@@ -776,13 +851,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
776
851
  fun getSecureBiometric(key: String): String? {
777
852
  val inst = getInstanceOrThrow()
778
853
  return try {
779
- for (preferences in inst.existingBiometricPreferences()) {
780
- val value = inst.getSecureSafe(preferences, key)
781
- if (value != null) {
782
- return value
783
- }
854
+ inst.firstInExistingBiometricPreferences { preferences ->
855
+ inst.getSecureSafe(preferences, key)
784
856
  }
785
- null
786
857
  } catch (e: Exception) {
787
858
  throw e.wrapStorageException(
788
859
  "NitroStorage: Failed to read biometric storage: ${e.message}",
@@ -809,7 +880,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
809
880
  fun hasSecureBiometric(key: String): Boolean {
810
881
  val inst = getInstanceOrThrow()
811
882
  return try {
812
- inst.existingBiometricPreferences().any { it.contains(key) }
883
+ inst.biometricPreferencesContain(key)
813
884
  } catch (e: Exception) {
814
885
  throw e.wrapStorageException(
815
886
  "NitroStorage: Failed to inspect biometric storage: ${e.message}",
@@ -9,21 +9,25 @@ internal class DiskSqliteStore(
9
9
  context: Context,
10
10
  private val legacyPreferences: SharedPreferences,
11
11
  ) {
12
- private val db: SQLiteDatabase
12
+ private val db: SQLiteDatabase =
13
+ SQLiteDatabase.openOrCreateDatabase(File(context.filesDir, DATABASE_NAME), null)
13
14
 
14
15
  init {
15
- val file = File(context.filesDir, DATABASE_NAME)
16
- db = SQLiteDatabase.openOrCreateDatabase(file, null)
17
- // PRAGMA journal_mode returns a row; Android forbids result-bearing SQL on execSQL.
18
- db.rawQuery("PRAGMA journal_mode=WAL", null).close()
19
- db.execSQL("PRAGMA synchronous=NORMAL")
20
- db.execSQL(
21
- "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
22
- )
23
- db.execSQL(
24
- "CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
25
- )
26
- migrateLegacyPreferences()
16
+ try {
17
+ // PRAGMA journal_mode returns a row; Android forbids result-bearing SQL on execSQL.
18
+ db.rawQuery("PRAGMA journal_mode=WAL", null).close()
19
+ db.execSQL("PRAGMA synchronous=NORMAL")
20
+ db.execSQL(
21
+ "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
22
+ )
23
+ db.execSQL(
24
+ "CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
25
+ )
26
+ migrateLegacyPreferences()
27
+ } catch (e: Exception) {
28
+ db.close()
29
+ throw e
30
+ }
27
31
  }
28
32
 
29
33
  @Synchronized
@@ -44,6 +48,7 @@ internal class DiskSqliteStore(
44
48
  @Synchronized
45
49
  fun remove(key: String) {
46
50
  db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
51
+ removeLegacyKeys(listOf(key))
47
52
  }
48
53
 
49
54
  @Synchronized
@@ -86,6 +91,7 @@ internal class DiskSqliteStore(
86
91
  } finally {
87
92
  db.endTransaction()
88
93
  }
94
+ removeLegacyKeys(keys.asIterable())
89
95
  }
90
96
 
91
97
  @Synchronized
@@ -135,6 +141,9 @@ internal class DiskSqliteStore(
135
141
  @Synchronized
136
142
  fun clear() {
137
143
  db.execSQL("DELETE FROM kv")
144
+ if (legacyPreferences.all.isNotEmpty()) {
145
+ legacyPreferences.edit().clear().apply()
146
+ }
138
147
  }
139
148
 
140
149
  private fun migrateLegacyPreferences() {
@@ -167,6 +176,16 @@ internal class DiskSqliteStore(
167
176
  }
168
177
  }
169
178
 
179
+ private fun removeLegacyKeys(keys: Iterable<String>) {
180
+ val present = keys.filter { legacyPreferences.contains(it) }
181
+ if (present.isEmpty()) {
182
+ return
183
+ }
184
+ val editor = legacyPreferences.edit()
185
+ present.forEach { editor.remove(it) }
186
+ editor.apply()
187
+ }
188
+
170
189
  private companion object {
171
190
  const val DATABASE_NAME = "nitro-storage-disk.sqlite"
172
191
  const val PREFS_MIGRATION_KEY = "prefs_v1"
package/app.plugin.js CHANGED
@@ -1,11 +1,11 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
1
3
  const {
2
4
  withInfoPlist,
3
5
  withAndroidManifest,
4
6
  withDangerousMod,
5
7
  createRunOncePlugin,
6
- } = require("@expo/config-plugins");
7
- const fs = require("fs");
8
- const path = require("path");
8
+ } = require("expo/config-plugins");
9
9
  const pkg = require("./package.json");
10
10
 
11
11
  const DATA_EXTRACTION_RULES_RESOURCE =
@@ -84,63 +84,61 @@ function writeAndroidBackupFiles(projectRoot) {
84
84
  );
85
85
  }
86
86
 
87
- const withNitroStorage = (config, props = {}) => {
88
- const defaultFaceIDPermission =
89
- "Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
90
- const {
91
- faceIDPermission,
92
- addBiometricPermissions = false,
93
- configureAndroidBackup = true,
94
- } = props;
95
-
96
- config = withInfoPlist(config, (config) => {
97
- if (
98
- typeof faceIDPermission === "string" &&
99
- faceIDPermission.trim() !== ""
100
- ) {
101
- config.modResults.NSFaceIDUsageDescription = faceIDPermission;
102
- } else if (!config.modResults.NSFaceIDUsageDescription) {
103
- config.modResults.NSFaceIDUsageDescription = defaultFaceIDPermission;
104
- }
105
- return config;
106
- });
87
+ const DEFAULT_FACE_ID_PERMISSION =
88
+ "Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
107
89
 
108
- config = withAndroidManifest(config, (config) => {
109
- if (configureAndroidBackup) {
110
- ensureBackupAttributes(config.modResults);
111
- }
90
+ const BIOMETRIC_PERMISSIONS = [
91
+ "android.permission.USE_BIOMETRIC",
92
+ "android.permission.USE_FINGERPRINT",
93
+ ];
112
94
 
113
- if (!addBiometricPermissions) {
114
- return config;
115
- }
95
+ function applyInfoPlist(infoPlist, props = {}) {
96
+ const { faceIDPermission } = props;
97
+ if (typeof faceIDPermission === "string" && faceIDPermission.trim() !== "") {
98
+ infoPlist.NSFaceIDUsageDescription = faceIDPermission;
99
+ } else if (!infoPlist.NSFaceIDUsageDescription) {
100
+ infoPlist.NSFaceIDUsageDescription = DEFAULT_FACE_ID_PERMISSION;
101
+ }
102
+ return infoPlist;
103
+ }
116
104
 
117
- if (!config.modResults.manifest["uses-permission"]) {
118
- config.modResults.manifest["uses-permission"] = [];
119
- }
105
+ function applyAndroidManifest(androidManifest, props = {}) {
106
+ const { addBiometricPermissions = false, configureAndroidBackup = true } =
107
+ props;
120
108
 
121
- const permissions = config.modResults.manifest["uses-permission"];
109
+ if (configureAndroidBackup) {
110
+ ensureBackupAttributes(androidManifest);
111
+ }
122
112
 
123
- const biometricPermission = {
124
- $: { "android:name": "android.permission.USE_BIOMETRIC" },
125
- };
126
- const fingerprintPermission = {
127
- $: { "android:name": "android.permission.USE_FINGERPRINT" },
128
- };
113
+ if (!addBiometricPermissions) {
114
+ return androidManifest;
115
+ }
129
116
 
130
- const hasBiometric = permissions.some(
131
- (p) => p.$?.["android:name"] === "android.permission.USE_BIOMETRIC",
132
- );
133
- const hasFingerprint = permissions.some(
134
- (p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT",
135
- );
117
+ if (!androidManifest.manifest["uses-permission"]) {
118
+ androidManifest.manifest["uses-permission"] = [];
119
+ }
136
120
 
137
- if (!hasBiometric) {
138
- permissions.push(biometricPermission);
139
- }
140
- if (!hasFingerprint) {
141
- permissions.push(fingerprintPermission);
121
+ const permissions = androidManifest.manifest["uses-permission"];
122
+ for (const name of BIOMETRIC_PERMISSIONS) {
123
+ const present = permissions.some((p) => p.$?.["android:name"] === name);
124
+ if (!present) {
125
+ permissions.push({ $: { "android:name": name } });
142
126
  }
127
+ }
128
+
129
+ return androidManifest;
130
+ }
131
+
132
+ const withNitroStorage = (config, props = {}) => {
133
+ const { configureAndroidBackup = true } = props;
134
+
135
+ config = withInfoPlist(config, (config) => {
136
+ applyInfoPlist(config.modResults, props);
137
+ return config;
138
+ });
143
139
 
140
+ config = withAndroidManifest(config, (config) => {
141
+ applyAndroidManifest(config.modResults, props);
144
142
  return config;
145
143
  });
146
144
 
@@ -164,6 +162,8 @@ module.exports = createRunOncePlugin(
164
162
  );
165
163
  module.exports.withNitroStorage = withNitroStorage;
166
164
  module.exports._internal = {
165
+ applyInfoPlist,
166
+ applyAndroidManifest,
167
167
  dataExtractionRulesXml,
168
168
  fullBackupContentXml,
169
169
  ensureBackupAttributes,
@@ -1,4 +1,5 @@
1
1
  #include "HybridStorage.hpp"
2
+ #include <algorithm>
2
3
  #include <cmath>
3
4
  #include <stdexcept>
4
5
 
@@ -32,8 +33,8 @@ HybridStorage::HybridStorage()
32
33
  #if __APPLE__
33
34
  nativeAdapter_ = std::make_shared<::NitroStorage::IOSStorageAdapterCpp>();
34
35
  #elif __ANDROID__
35
- auto context = ::NitroStorage::AndroidStorageAdapterJava::getContext();
36
- nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>(context);
36
+ ::NitroStorage::AndroidStorageAdapterJava::ensureInitialized();
37
+ nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>();
37
38
  #endif
38
39
  #endif
39
40
  }
@@ -338,8 +339,17 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
338
339
  break;
339
340
  case Scope::Secure:
340
341
  ensureAdapter();
341
- runAdapterOperation(
342
- [&] { nativeAdapter_->setSecureBatch(keys, values); }, "Secure setBatch");
342
+ try {
343
+ runAdapterOperation(
344
+ [&] { nativeAdapter_->setSecureBatch(keys, values); }, "Secure setBatch");
345
+ } catch (const ::NitroStorage::PartialBatchError& error) {
346
+ const auto listeners = copyListenersForScope(static_cast<int>(s));
347
+ const auto applied = std::min(error.appliedCount(), keys.size());
348
+ for (size_t i = 0; i < applied; ++i) {
349
+ notifyListeners(listeners, keys[i], values[i]);
350
+ }
351
+ throw;
352
+ }
343
353
  break;
344
354
  }
345
355
 
@@ -402,8 +412,17 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
402
412
  break;
403
413
  case Scope::Secure:
404
414
  ensureAdapter();
405
- runAdapterOperation(
406
- [&] { nativeAdapter_->deleteSecureBatch(keys); }, "Secure removeBatch");
415
+ try {
416
+ runAdapterOperation(
417
+ [&] { nativeAdapter_->deleteSecureBatch(keys); }, "Secure removeBatch");
418
+ } catch (const ::NitroStorage::PartialBatchError& error) {
419
+ const auto listeners = copyListenersForScope(static_cast<int>(s));
420
+ const auto applied = std::min(error.appliedCount(), keys.size());
421
+ for (size_t i = 0; i < applied; ++i) {
422
+ notifyListeners(listeners, keys[i], std::nullopt);
423
+ }
424
+ throw;
425
+ }
407
426
  break;
408
427
  }
409
428
 
@@ -1,11 +1,24 @@
1
1
  #pragma once
2
2
 
3
- #include <string>
3
+ #include <cstddef>
4
4
  #include <optional>
5
+ #include <stdexcept>
6
+ #include <string>
5
7
  #include <vector>
6
8
 
7
9
  namespace NitroStorage {
8
10
 
11
+ class PartialBatchError : public std::runtime_error {
12
+ public:
13
+ PartialBatchError(size_t appliedCount, const std::string& message)
14
+ : std::runtime_error(message), appliedCount_(appliedCount) {}
15
+
16
+ size_t appliedCount() const noexcept { return appliedCount_; }
17
+
18
+ private:
19
+ size_t appliedCount_;
20
+ };
21
+
9
22
  class NativeStorageAdapter {
10
23
  public:
11
24
  virtual ~NativeStorageAdapter() = default;