react-native-nitro-storage 0.10.3 → 0.11.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 +85 -14
  2. package/README.md +148 -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 +48 -33
  7. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
  8. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
  9. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +22 -4
  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 +94 -17
  14. package/cpp/core/SqliteDiskStore.hpp +10 -0
  15. package/docs/api-reference.md +73 -28
  16. package/docs/benchmarks.md +41 -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 +149 -57
  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 +61 -19
  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 +61 -22
  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 +61 -12
  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 +62 -23
  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 +86 -19
  81. package/src/indexeddb-backend.ts +20 -42
  82. package/src/internal.ts +8 -0
  83. package/src/storage-core.ts +84 -33
  84. package/src/storage-runtime.ts +3 -1
  85. package/src/testing.ts +91 -18
  86. package/src/web-backend-contract.ts +6 -2
  87. package/testing/package.json +8 -0
@@ -1,5 +1,8 @@
1
1
  #include "AndroidStorageAdapterCpp.hpp"
2
2
 
3
+ #include <limits>
4
+ #include <stdexcept>
5
+
3
6
  namespace NitroStorage {
4
7
 
5
8
  using namespace facebook::jni;
@@ -7,10 +10,25 @@ using JavaStringArray = JArrayClass<jstring>;
7
10
 
8
11
  namespace {
9
12
 
13
+ local_ref<JString> toJavaString(const std::string& value) {
14
+ // fbjni's std::string overload uses c_str(), which truncates embedded NUL.
15
+ if (value.find('\0') == std::string::npos) return make_jstring(value);
16
+ if (value.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
17
+ throw std::length_error("Storage string exceeds the Java array limit");
18
+ }
19
+ const auto size = static_cast<jsize>(value.size());
20
+ auto bytes = JArrayByte::newArray(size);
21
+ bytes->setRegion(0, size, reinterpret_cast<const jbyte*>(value.data()));
22
+ static auto constructor = JString::javaClassStatic()->getConstructor<
23
+ jstring(jbyteArray, jstring)>();
24
+ auto charset = make_jstring("UTF-8");
25
+ return JString::javaClassStatic()->newObject(constructor, bytes.get(), charset.get());
26
+ }
27
+
10
28
  local_ref<JavaStringArray> toJavaStringArray(const std::vector<std::string>& values) {
11
29
  auto javaArray = JavaStringArray::newArray(static_cast<jsize>(values.size()));
12
30
  for (size_t i = 0; i < values.size(); ++i) {
13
- auto javaValue = make_jstring(values[i]);
31
+ auto javaValue = toJavaString(values[i]);
14
32
  javaArray->setElement(static_cast<jsize>(i), javaValue.get());
15
33
  }
16
34
  return javaArray;
@@ -51,35 +69,32 @@ std::vector<std::string> fromJavaStringArray(alias_ref<JavaStringArray> values)
51
69
 
52
70
  } // namespace
53
71
 
54
- AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(alias_ref<JObject> /*context*/) {
55
- // Context is validated by AndroidStorageAdapter.getContext() on the Java side.
56
- // The adapter calls static Java methods directly via fbjni.
57
- }
72
+ AndroidStorageAdapterCpp::AndroidStorageAdapterCpp() = default;
58
73
 
59
74
  AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
60
75
 
61
76
  // --- Disk ---
62
77
 
63
78
  void AndroidStorageAdapterCpp::setDisk(const std::string& key, const std::string& value) {
64
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string)>("setDisk");
65
- method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
79
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setDisk");
80
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
66
81
  }
67
82
 
68
83
  std::optional<std::string> AndroidStorageAdapterCpp::getDisk(const std::string& key) {
69
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(std::string)>("getDisk");
70
- auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
84
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getDisk");
85
+ auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
71
86
  if (!result) return std::nullopt;
72
87
  return result->toStdString();
73
88
  }
74
89
 
75
90
  void AndroidStorageAdapterCpp::deleteDisk(const std::string& key) {
76
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string)>("deleteDisk");
77
- method(AndroidStorageAdapterJava::javaClassStatic(), key);
91
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteDisk");
92
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
78
93
  }
79
94
 
80
95
  bool AndroidStorageAdapterCpp::hasDisk(const std::string& key) {
81
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(std::string)>("hasDisk");
82
- return method(AndroidStorageAdapterJava::javaClassStatic(), key);
96
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasDisk");
97
+ return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
83
98
  }
84
99
 
85
100
  std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
@@ -92,9 +107,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
92
107
 
93
108
  std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
94
109
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
95
- local_ref<JavaStringArray>(std::string)
110
+ local_ref<JavaStringArray>(alias_ref<JString>)
96
111
  >("getKeysByPrefixDisk");
97
- auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
112
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
98
113
  return fromJavaStringArray(keys);
99
114
  }
100
115
 
@@ -145,25 +160,25 @@ void AndroidStorageAdapterCpp::clearDisk() {
145
160
  // --- Secure ---
146
161
 
147
162
  void AndroidStorageAdapterCpp::setSecure(const std::string& key, const std::string& value) {
148
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string)>("setSecure");
149
- method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
163
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setSecure");
164
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
150
165
  }
151
166
 
152
167
  std::optional<std::string> AndroidStorageAdapterCpp::getSecure(const std::string& key) {
153
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(std::string)>("getSecure");
154
- auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
168
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecure");
169
+ auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
155
170
  if (!result) return std::nullopt;
156
171
  return result->toStdString();
157
172
  }
158
173
 
159
174
  void AndroidStorageAdapterCpp::deleteSecure(const std::string& key) {
160
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string)>("deleteSecure");
161
- method(AndroidStorageAdapterJava::javaClassStatic(), key);
175
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecure");
176
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
162
177
  }
163
178
 
164
179
  bool AndroidStorageAdapterCpp::hasSecure(const std::string& key) {
165
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(std::string)>("hasSecure");
166
- return method(AndroidStorageAdapterJava::javaClassStatic(), key);
180
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecure");
181
+ return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
167
182
  }
168
183
 
169
184
  std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
@@ -176,9 +191,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
176
191
 
177
192
  std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
178
193
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
179
- local_ref<JavaStringArray>(std::string)
194
+ local_ref<JavaStringArray>(alias_ref<JString>)
180
195
  >("getKeysByPrefixSecure");
181
- auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
196
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
182
197
  return fromJavaStringArray(keys);
183
198
  }
184
199
 
@@ -242,25 +257,25 @@ void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const
242
257
  }
243
258
 
244
259
  void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
245
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string, jint)>("setSecureBiometricWithLevel");
246
- method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
260
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>, jint)>("setSecureBiometricWithLevel");
261
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value), level);
247
262
  }
248
263
 
249
264
  std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
250
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(std::string)>("getSecureBiometric");
251
- auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
265
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecureBiometric");
266
+ auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
252
267
  if (!result) return std::nullopt;
253
268
  return result->toStdString();
254
269
  }
255
270
 
256
271
  void AndroidStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
257
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string)>("deleteSecureBiometric");
258
- method(AndroidStorageAdapterJava::javaClassStatic(), key);
272
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecureBiometric");
273
+ method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
259
274
  }
260
275
 
261
276
  bool AndroidStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
262
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(std::string)>("hasSecureBiometric");
263
- return method(AndroidStorageAdapterJava::javaClassStatic(), key);
277
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecureBiometric");
278
+ return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
264
279
  }
265
280
 
266
281
  void AndroidStorageAdapterCpp::clearSecureBiometric() {
@@ -9,16 +9,16 @@ namespace NitroStorage {
9
9
  struct AndroidStorageAdapterJava : facebook::jni::JavaClass<AndroidStorageAdapterJava> {
10
10
  static constexpr auto kJavaDescriptor = "Lcom/nitrostorage/AndroidStorageAdapter;";
11
11
 
12
- static facebook::jni::alias_ref<facebook::jni::JObject> getContext() {
12
+ static void ensureInitialized() {
13
13
  static auto method = javaClassStatic()->getStaticMethod<facebook::jni::JObject()>("getContext", "()Landroid/content/Context;");
14
- return method(javaClassStatic());
14
+ method(javaClassStatic());
15
15
  }
16
16
 
17
17
  };
18
18
 
19
19
  class AndroidStorageAdapterCpp : public NativeStorageAdapter {
20
20
  public:
21
- explicit AndroidStorageAdapterCpp(facebook::jni::alias_ref<facebook::jni::JObject> context);
21
+ AndroidStorageAdapterCpp();
22
22
  ~AndroidStorageAdapterCpp() override;
23
23
 
24
24
  void setDisk(const std::string& key, const std::string& value) override;
@@ -65,20 +65,44 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
65
65
 
66
66
  private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
67
67
 
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)
68
+ private val masterKey: MasterKey by lazy {
69
+ try {
70
+ createDefaultMasterKey()
71
+ } catch (e: Exception) {
72
+ throw permanentSecureStoreError(
73
+ "NitroStorage: Cannot create the Secure encryption key.",
74
+ e,
75
+ )
76
+ }
74
77
  }
75
78
 
76
- private val encryptedPreferences: SharedPreferences = initializeEncryptedPreferences(
77
- "NitroStorageSecure",
78
- masterKey,
79
- masterKeyAlias,
80
- ::createDefaultMasterKey,
81
- )
79
+ private val encryptedPreferences: SharedPreferences by lazy {
80
+ try {
81
+ initializeEncryptedPreferences(
82
+ "NitroStorageSecure",
83
+ masterKey,
84
+ masterKeyAlias,
85
+ ::createDefaultMasterKey,
86
+ )
87
+ } catch (e: Exception) {
88
+ throw permanentSecureStoreError(
89
+ "NitroStorage: Cannot open Secure storage.",
90
+ e,
91
+ )
92
+ }
93
+ }
94
+
95
+ private fun permanentSecureStoreError(message: String, cause: Exception): RuntimeException {
96
+ if (cause.message?.contains("[nitro-error:storage_corruption]") == true &&
97
+ cause is RuntimeException
98
+ ) {
99
+ return cause
100
+ }
101
+ return RuntimeException(
102
+ "[nitro-error:storage_corruption] $message ${cause.message ?: ""}".trimEnd(),
103
+ cause,
104
+ )
105
+ }
82
106
 
83
107
  private val biometricMasterKeyAlias = "${context.packageName}.nitro_storage.biometric_key"
84
108
  private val biometricOrPasscodeMasterKeyAlias =
@@ -273,25 +297,55 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
273
297
 
274
298
  private fun existingBiometricPreferences(): List<SharedPreferences> {
275
299
  val stores = mutableListOf<SharedPreferences>()
300
+ if (
301
+ biometricOrPasscodeInitialized ||
302
+ preferencesFileExists("NitroStorageBiometricOrPasscode")
303
+ ) {
304
+ stores.add(biometricOrPasscodePreferences)
305
+ }
306
+ if (
307
+ legacyBiometricInitialized ||
308
+ preferencesFileExists("NitroStorageBiometric")
309
+ ) {
310
+ stores.add(legacyBiometricPreferences)
311
+ }
276
312
  if (
277
313
  biometricOnlyInitialized ||
278
314
  preferencesFileExists("NitroStorageBiometricOnly")
279
315
  ) {
280
316
  stores.add(biometricOnlyPreferences)
281
317
  }
318
+ return stores
319
+ }
320
+
321
+ private fun <T : Any> firstInExistingBiometricPreferences(
322
+ lookup: (SharedPreferences) -> T?,
323
+ ): T? {
282
324
  if (
283
325
  biometricOrPasscodeInitialized ||
284
326
  preferencesFileExists("NitroStorageBiometricOrPasscode")
285
327
  ) {
286
- stores.add(biometricOrPasscodePreferences)
328
+ lookup(biometricOrPasscodePreferences)?.let { return it }
287
329
  }
288
330
  if (
289
331
  legacyBiometricInitialized ||
290
332
  preferencesFileExists("NitroStorageBiometric")
291
333
  ) {
292
- stores.add(legacyBiometricPreferences)
334
+ lookup(legacyBiometricPreferences)?.let { return it }
293
335
  }
294
- return stores
336
+ if (
337
+ biometricOnlyInitialized ||
338
+ preferencesFileExists("NitroStorageBiometricOnly")
339
+ ) {
340
+ lookup(biometricOnlyPreferences)?.let { return it }
341
+ }
342
+ return null
343
+ }
344
+
345
+ private fun biometricPreferencesContain(key: String): Boolean {
346
+ return firstInExistingBiometricPreferences { preferences ->
347
+ if (preferences.contains(key)) true else null
348
+ } ?: false
295
349
  }
296
350
 
297
351
  private fun biometricPreferencesForLevel(level: Int): SharedPreferences {
@@ -663,7 +717,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
663
717
  if (inst.encryptedPreferences.contains(key)) {
664
718
  return true
665
719
  }
666
- return inst.existingBiometricPreferences().any { it.contains(key) }
720
+ return inst.biometricPreferencesContain(key)
667
721
  }
668
722
 
669
723
  @JvmStatic
@@ -776,13 +830,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
776
830
  fun getSecureBiometric(key: String): String? {
777
831
  val inst = getInstanceOrThrow()
778
832
  return try {
779
- for (preferences in inst.existingBiometricPreferences()) {
780
- val value = inst.getSecureSafe(preferences, key)
781
- if (value != null) {
782
- return value
783
- }
833
+ inst.firstInExistingBiometricPreferences { preferences ->
834
+ inst.getSecureSafe(preferences, key)
784
835
  }
785
- null
786
836
  } catch (e: Exception) {
787
837
  throw e.wrapStorageException(
788
838
  "NitroStorage: Failed to read biometric storage: ${e.message}",
@@ -809,7 +859,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
809
859
  fun hasSecureBiometric(key: String): Boolean {
810
860
  val inst = getInstanceOrThrow()
811
861
  return try {
812
- inst.existingBiometricPreferences().any { it.contains(key) }
862
+ inst.biometricPreferencesContain(key)
813
863
  } catch (e: Exception) {
814
864
  throw e.wrapStorageException(
815
865
  "NitroStorage: Failed to inspect biometric storage: ${e.message}",
@@ -44,6 +44,7 @@ internal class DiskSqliteStore(
44
44
  @Synchronized
45
45
  fun remove(key: String) {
46
46
  db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
47
+ removeLegacyKeys(listOf(key))
47
48
  }
48
49
 
49
50
  @Synchronized
@@ -86,6 +87,7 @@ internal class DiskSqliteStore(
86
87
  } finally {
87
88
  db.endTransaction()
88
89
  }
90
+ removeLegacyKeys(keys.asIterable())
89
91
  }
90
92
 
91
93
  @Synchronized
@@ -103,9 +105,9 @@ internal class DiskSqliteStore(
103
105
  fun getKeysByPrefix(prefix: String): Array<String> {
104
106
  val pattern = buildString {
105
107
  for (character in prefix) {
106
- if (character == '%' || character == '_' || character == '\\') {
107
- append('\\')
108
- }
108
+ // LIKE stops at NUL; startsWith below checks the complete prefix.
109
+ if (character == '\u0000') break
110
+ if (character == '%' || character == '_' || character == '\\') append('\\')
109
111
  append(character)
110
112
  }
111
113
  append('%')
@@ -116,7 +118,10 @@ internal class DiskSqliteStore(
116
118
  ).use { cursor ->
117
119
  val keys = ArrayList<String>(cursor.count)
118
120
  while (cursor.moveToNext()) {
119
- keys.add(cursor.getString(0))
121
+ val key = cursor.getString(0)
122
+ if (key.startsWith(prefix)) {
123
+ keys.add(key)
124
+ }
120
125
  }
121
126
  return keys.toTypedArray()
122
127
  }
@@ -132,6 +137,9 @@ internal class DiskSqliteStore(
132
137
  @Synchronized
133
138
  fun clear() {
134
139
  db.execSQL("DELETE FROM kv")
140
+ if (legacyPreferences.all.isNotEmpty()) {
141
+ legacyPreferences.edit().clear().apply()
142
+ }
135
143
  }
136
144
 
137
145
  private fun migrateLegacyPreferences() {
@@ -164,6 +172,16 @@ internal class DiskSqliteStore(
164
172
  }
165
173
  }
166
174
 
175
+ private fun removeLegacyKeys(keys: Iterable<String>) {
176
+ val present = keys.filter { legacyPreferences.contains(it) }
177
+ if (present.isEmpty()) {
178
+ return
179
+ }
180
+ val editor = legacyPreferences.edit()
181
+ present.forEach { editor.remove(it) }
182
+ editor.apply()
183
+ }
184
+
167
185
  private companion object {
168
186
  const val DATABASE_NAME = "nitro-storage-disk.sqlite"
169
187
  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;