react-native-nitro-storage 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,8 @@ Every feature in this package is documented with at least one runnable example i
47
47
  - Transactions — see Transactions and Atomic Balance Transfer
48
48
  - Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
49
49
  - MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
50
+ - Raw string API (`getString`, `setString`, `deleteString`) — see Raw String API
51
+ - Keychain locked detection (`isKeychainLockedError`) — see `isKeychainLockedError(err)`
50
52
  - Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
51
53
 
52
54
  ## Requirements
@@ -280,6 +282,9 @@ import { storage, StorageScope } from "react-native-nitro-storage";
280
282
  | `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
281
283
  | `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
282
284
  | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
285
+ | `storage.getString(key, scope)` | Read a raw string value directly (bypasses serialization) |
286
+ | `storage.setString(key, value, scope)` | Write a raw string value directly (bypasses serialization) |
287
+ | `storage.deleteString(key, scope)` | Delete a raw string value by key |
283
288
  | `storage.import(data, scope)` | Bulk-load a `Record<string, string>` of raw key/value pairs into a scope |
284
289
  | `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
285
290
  | `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
@@ -509,6 +514,40 @@ const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
509
514
 
510
515
  ---
511
516
 
517
+ ### Raw String API
518
+
519
+ For cases where you want to bypass `createStorageItem` serialization entirely and work with raw key/value strings:
520
+
521
+ ```ts
522
+ import { storage, StorageScope } from "react-native-nitro-storage";
523
+
524
+ storage.setString("raw-key", "raw-value", StorageScope.Disk);
525
+ const value = storage.getString("raw-key", StorageScope.Disk); // "raw-value" | undefined
526
+ storage.deleteString("raw-key", StorageScope.Disk);
527
+ ```
528
+
529
+ These are synchronous and go directly to the native backend without any serialize/deserialize step.
530
+
531
+ ---
532
+
533
+ ### `isKeychainLockedError(err)`
534
+
535
+ Utility to detect iOS Keychain locked errors and Android key invalidation errors in secure storage operations. Returns `true` if the error was caused by a locked keychain (device locked, first unlock not yet performed, etc.) or an Android `KeyPermanentlyInvalidatedException` / `InvalidKeyException`. Always returns `false` on web.
536
+
537
+ ```ts
538
+ import { isKeychainLockedError } from "react-native-nitro-storage";
539
+
540
+ try {
541
+ secureItem.get();
542
+ } catch (err) {
543
+ if (isKeychainLockedError(err)) {
544
+ // device is locked — retry after unlock
545
+ }
546
+ }
547
+ ```
548
+
549
+ ---
550
+
512
551
  ### Enums
513
552
 
514
553
  #### `AccessControl`
@@ -14,19 +14,11 @@ def reactNativeArchitectures() {
14
14
  return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
15
15
  }
16
16
 
17
- def isNewArchitectureEnabled() {
18
- return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
- }
20
-
21
17
  apply plugin: "com.android.library"
22
18
  apply plugin: "org.jetbrains.kotlin.android"
23
19
  // Apply autolinking script for Nitro
24
20
  apply from: "../nitrogen/generated/android/NitroStorage+autolinking.gradle"
25
21
 
26
- if (isNewArchitectureEnabled()) {
27
- apply plugin: "com.facebook.react"
28
- }
29
-
30
22
  def getExtOrDefault(name) {
31
23
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroStorage_" + name]
32
24
  }
@@ -45,7 +37,6 @@ android {
45
37
  defaultConfig {
46
38
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
47
39
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
48
- buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
49
40
  consumerProguardFiles "consumer-rules.pro"
50
41
 
51
42
  externalNativeBuild {
@@ -76,9 +67,6 @@ android {
76
67
  sourceSets {
77
68
  main {
78
69
  java.srcDirs += ["src/main/java"]
79
- if (isNewArchitectureEnabled()) {
80
- java.srcDirs += ["${project.buildDir}/generated/source/codegen/java"]
81
- }
82
70
  }
83
71
  }
84
72
  }
@@ -1,4 +1,26 @@
1
- # Keep AndroidStorageAdapter and all its methods - accessed via JNI reflection
2
- -keep class com.nitrostorage.AndroidStorageAdapter { *; }
3
- -keepclassmembers class com.nitrostorage.AndroidStorageAdapter { *; }
4
- -keepclassmembers class com.nitrostorage.AndroidStorageAdapter$Companion { *; }
1
+ # NitroStorage - JNI-callable methods must survive R8/ProGuard shrinking
2
+
3
+ -keep class com.nitrostorage.AndroidStorageAdapter {
4
+ public static *** set*(...);
5
+ public static *** get*(...);
6
+ public static *** delete*(...);
7
+ public static *** has*(...);
8
+ public static *** clear*(...);
9
+ public static *** size*(...);
10
+ public static *** flush*(...);
11
+ public static void init(android.content.Context);
12
+ public static void setSecureWritesAsync(boolean);
13
+ public static void setSecureAccessControl(int);
14
+ public static void removeByPrefix(java.lang.String, int);
15
+ }
16
+ -keep class com.nitrostorage.AndroidStorageAdapter$Companion {
17
+ public <methods>;
18
+ }
19
+ -keep class com.nitrostorage.NitroStoragePackage {
20
+ <init>();
21
+ <clinit>();
22
+ *;
23
+ }
24
+ -keep class com.nitrostorage.NitroStoragePackage$Companion {
25
+ *;
26
+ }
@@ -34,26 +34,23 @@ std::vector<std::optional<std::string>> fromNullableJavaStringArray(alias_ref<Ja
34
34
  }
35
35
 
36
36
  std::vector<std::string> fromJavaStringArray(alias_ref<JavaStringArray> values) {
37
+ if (!values) return {};
38
+ const jsize size = values->size();
37
39
  std::vector<std::string> result;
38
- if (!values) return result;
39
-
40
- const jsize size = static_cast<jsize>(values->size());
41
40
  result.reserve(size);
42
41
  for (jsize i = 0; i < size; ++i) {
43
42
  auto currentValue = values->getElement(i);
44
- if (currentValue) {
45
- result.push_back(currentValue->toStdString());
46
- }
43
+ // Preserve null as empty string to maintain index alignment with caller
44
+ result.push_back(currentValue ? currentValue->toStdString() : std::string());
47
45
  }
48
46
  return result;
49
47
  }
50
48
 
51
49
  } // namespace
52
50
 
53
- AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(alias_ref<JObject> context) {
54
- if (!context) [[unlikely]] {
55
- throw std::runtime_error("NitroStorage: Android Context is null");
56
- }
51
+ AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(alias_ref<JObject> /*context*/) {
52
+ // Context is validated by AndroidStorageAdapter.getContext() on the Java side.
53
+ // The adapter calls static Java methods directly via fbjni.
57
54
  }
58
55
 
59
56
  AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
@@ -6,10 +6,6 @@
6
6
 
7
7
  namespace NitroStorage {
8
8
 
9
- struct JContext : facebook::jni::JavaClass<JContext> {
10
- static constexpr auto kJavaDescriptor = "Landroid/content/Context;";
11
- };
12
-
13
9
  struct AndroidStorageAdapterJava : facebook::jni::JavaClass<AndroidStorageAdapterJava> {
14
10
  static constexpr auto kJavaDescriptor = "Lcom/nitrostorage/AndroidStorageAdapter;";
15
11
 
@@ -8,15 +8,29 @@ import androidx.security.crypto.MasterKey
8
8
  import java.security.KeyStore
9
9
  import javax.crypto.AEADBadTagException
10
10
 
11
+ private fun Throwable.hasCause(type: Class<*>): Boolean {
12
+ var current: Throwable? = this
13
+ while (current != null) {
14
+ if (type.isInstance(current)) return true
15
+ current = current.cause
16
+ }
17
+ return false
18
+ }
19
+
11
20
  class AndroidStorageAdapter private constructor(private val context: Context) {
12
- private val sharedPreferences: SharedPreferences =
21
+ private val sharedPreferences: SharedPreferences =
13
22
  context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
14
23
 
15
24
  private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
16
- private val masterKey: MasterKey = MasterKey.Builder(context, masterKeyAlias)
17
- .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
18
- .build()
19
-
25
+
26
+ private val masterKey: MasterKey = try {
27
+ MasterKey.Builder(context, masterKeyAlias)
28
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
29
+ .build()
30
+ } catch (e: Exception) {
31
+ throw RuntimeException("NitroStorage: Cannot create encryption key. Device may not support AES256-GCM.", e)
32
+ }
33
+
20
34
  private val encryptedPreferences: SharedPreferences = initializeEncryptedPreferences("NitroStorageSecure", masterKey)
21
35
 
22
36
  private val biometricMasterKeyAlias = "${context.packageName}.nitro_storage.biometric_key"
@@ -29,8 +43,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
29
43
  .build()
30
44
  initializeEncryptedPreferences("NitroStorageBiometric", bioKey)
31
45
  } catch (e: Exception) {
32
- Log.w("NitroStorage", "Biometric storage unavailable, falling back to regular encrypted storage: ${e.message}")
33
- encryptedPreferences
46
+ Log.e("NitroStorage", "Biometric storage unavailable: ${e.message}")
47
+ throw RuntimeException("NitroStorage: Biometric storage is not available on this device. " +
48
+ "Ensure biometric hardware is present and credentials are enrolled.", e)
34
49
  }
35
50
  }
36
51
 
@@ -39,47 +54,63 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
39
54
 
40
55
  @Volatile
41
56
  private var secureKeysCache: Array<String>? = null
42
-
57
+
43
58
  private fun initializeEncryptedPreferences(name: String, key: MasterKey): SharedPreferences {
44
59
  return try {
45
60
  EncryptedSharedPreferences.create(
46
- context,
47
- name,
48
- key,
61
+ context, name, key,
49
62
  EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
50
63
  EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
51
64
  )
52
65
  } catch (e: Exception) {
53
- if (e is AEADBadTagException || e.cause is AEADBadTagException) {
54
- Log.w("NitroStorage", "Detected corrupted encryption keys for $name, clearing...")
55
- clearCorruptedStorage(name, key)
56
- EncryptedSharedPreferences.create(
57
- context,
58
- name,
59
- key,
60
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
61
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
62
- )
63
- } else {
64
- throw RuntimeException(
65
- "NitroStorage: Failed to initialize $name. " +
66
- "This may be due to corrupted encryption keys. " +
67
- "Try clearing app data or reinstalling the app.", e
68
- )
66
+ when {
67
+ e.hasCause(AEADBadTagException::class.java) -> {
68
+ Log.w("NitroStorage", "Corrupted encryption keys for $name, attempting recovery...")
69
+ clearCorruptedStorage(name, key)
70
+ val freshAlias = if (name == "NitroStorageBiometric") biometricMasterKeyAlias else masterKeyAlias
71
+ val freshKey = MasterKey.Builder(context, freshAlias)
72
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
73
+ .apply {
74
+ if (name == "NitroStorageBiometric") {
75
+ setUserAuthenticationRequired(true, 30)
76
+ }
77
+ }
78
+ .build()
79
+ try {
80
+ EncryptedSharedPreferences.create(
81
+ context, name, freshKey,
82
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
83
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
84
+ )
85
+ } catch (retryEx: Exception) {
86
+ throw RuntimeException("NitroStorage: Unrecoverable storage corruption in $name", retryEx)
87
+ }
88
+ }
89
+ else -> {
90
+ // Don't wipe on non-corruption failures (e.g., locked keystore)
91
+ throw RuntimeException(
92
+ "NitroStorage: Failed to initialize $name (${e::class.simpleName}). " +
93
+ "This may be a temporary keystore issue. If it persists, clear app data.", e
94
+ )
95
+ }
69
96
  }
70
97
  }
71
98
  }
72
-
99
+
73
100
  private fun clearCorruptedStorage(name: String, key: MasterKey) {
74
101
  try {
75
102
  context.deleteSharedPreferences(name)
76
103
  val keyStore = KeyStore.getInstance("AndroidKeyStore")
77
104
  keyStore.load(null)
78
- val alias = if (key === masterKey) masterKeyAlias else biometricMasterKeyAlias
105
+ val alias = when {
106
+ name == "NitroStorageSecure" -> masterKeyAlias
107
+ name == "NitroStorageBiometric" -> biometricMasterKeyAlias
108
+ else -> masterKeyAlias
109
+ }
79
110
  keyStore.deleteEntry(alias)
80
111
  Log.i("NitroStorage", "Cleared corrupted storage: $name")
81
112
  } catch (e: Exception) {
82
- Log.e("NitroStorage", "Failed to clear corrupted storage: $name", e)
113
+ Log.e("NitroStorage", "Failed to clear corrupted storage $name: ${e.message}", e)
83
114
  }
84
115
  }
85
116
 
@@ -87,7 +118,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
87
118
  return try {
88
119
  prefs.getString(key, null)
89
120
  } catch (e: Exception) {
90
- if (e is AEADBadTagException || e.cause is AEADBadTagException) {
121
+ if (e.hasCause(AEADBadTagException::class.java)) {
91
122
  Log.w("NitroStorage", "Corrupt entry for key '$key', removing")
92
123
  prefs.edit().remove(key).commit()
93
124
  null
@@ -98,15 +129,21 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
98
129
  }
99
130
 
100
131
  private fun applySecureEditor(editor: SharedPreferences.Editor) {
101
- if (secureWritesAsync) {
102
- editor.apply()
103
- } else {
104
- editor.commit()
132
+ try {
133
+ if (secureWritesAsync) {
134
+ editor.apply()
135
+ } else {
136
+ editor.commit()
137
+ }
138
+ } catch (e: Exception) {
139
+ throw RuntimeException("NitroStorage: Failed to write to secure storage: ${e.message}", e)
105
140
  }
106
141
  }
107
142
 
108
143
  private fun invalidateSecureKeysCache() {
109
- secureKeysCache = null
144
+ synchronized(this) {
145
+ secureKeysCache = null
146
+ }
110
147
  }
111
148
 
112
149
  private fun getSecureKeysCached(): Array<String> {
@@ -120,15 +157,20 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
120
157
  if (existing != null) {
121
158
  return existing
122
159
  }
160
+ val INTERNAL_PREFIX = "__androidx_security_crypto_encrypted_prefs_"
123
161
  val keys = linkedSetOf<String>()
124
- keys.addAll(encryptedPreferences.all.keys)
125
- keys.addAll(biometricPreferences.all.keys)
162
+ keys.addAll(encryptedPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
163
+ try {
164
+ keys.addAll(biometricPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
165
+ } catch (e: Exception) {
166
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
167
+ }
126
168
  val built = keys.toTypedArray()
127
169
  secureKeysCache = built
128
170
  return built
129
171
  }
130
172
  }
131
-
173
+
132
174
  companion object {
133
175
  @Volatile
134
176
  private var instance: AndroidStorageAdapter? = null
@@ -162,7 +204,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
162
204
  }
163
205
 
164
206
  // --- Disk ---
165
-
207
+
166
208
  @JvmStatic
167
209
  fun setDisk(key: String, value: String) {
168
210
  getInstanceOrThrow().sharedPreferences.edit().putString(key, value).apply()
@@ -177,7 +219,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
177
219
  }
178
220
  editor.apply()
179
221
  }
180
-
222
+
181
223
  @JvmStatic
182
224
  fun getDisk(key: String): String? {
183
225
  return getInstanceOrThrow().sharedPreferences.getString(key, null)
@@ -190,7 +232,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
190
232
  prefs.getString(keys[index], null)
191
233
  }
192
234
  }
193
-
235
+
194
236
  @JvmStatic
195
237
  fun deleteDisk(key: String) {
196
238
  getInstanceOrThrow().sharedPreferences.edit().remove(key).apply()
@@ -233,30 +275,35 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
233
275
  }
234
276
 
235
277
  // --- Secure (sync commit by default, async apply when enabled) ---
236
-
278
+
237
279
  @JvmStatic
238
280
  fun setSecure(key: String, value: String) {
239
281
  val inst = getInstanceOrThrow()
240
- val editor = inst.encryptedPreferences.edit().putString(key, value)
241
- inst.applySecureEditor(editor)
242
- inst.invalidateSecureKeysCache()
282
+ synchronized(inst) {
283
+ val editor = inst.encryptedPreferences.edit().putString(key, value)
284
+ inst.applySecureEditor(editor)
285
+ inst.invalidateSecureKeysCache()
286
+ }
243
287
  }
244
288
 
245
289
  @JvmStatic
246
290
  fun setSecureBatch(keys: Array<String>, values: Array<String>) {
247
291
  val inst = getInstanceOrThrow()
248
- val editor = inst.encryptedPreferences.edit()
249
- val count = minOf(keys.size, values.size)
250
- for (index in 0 until count) {
251
- editor.putString(keys[index], values[index])
292
+ synchronized(inst) {
293
+ val editor = inst.encryptedPreferences.edit()
294
+ val count = minOf(keys.size, values.size)
295
+ for (index in 0 until count) {
296
+ editor.putString(keys[index], values[index])
297
+ }
298
+ inst.applySecureEditor(editor)
299
+ inst.invalidateSecureKeysCache()
252
300
  }
253
- inst.applySecureEditor(editor)
254
- inst.invalidateSecureKeysCache()
255
301
  }
256
-
302
+
257
303
  @JvmStatic
258
304
  fun getSecure(key: String): String? {
259
- return getInstanceOrThrow().getSecureSafe(getInstanceOrThrow().encryptedPreferences, key)
305
+ val inst = getInstanceOrThrow()
306
+ return inst.getSecureSafe(inst.encryptedPreferences, key)
260
307
  }
261
308
 
262
309
  @JvmStatic
@@ -266,33 +313,54 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
266
313
  inst.getSecureSafe(inst.encryptedPreferences, keys[index])
267
314
  }
268
315
  }
269
-
316
+
270
317
  @JvmStatic
271
318
  fun deleteSecure(key: String) {
272
319
  val inst = getInstanceOrThrow()
273
- inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
274
- inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
275
- inst.invalidateSecureKeysCache()
320
+ synchronized(inst) {
321
+ inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
322
+ try {
323
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
324
+ } catch (e: Exception) {
325
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
326
+ }
327
+ inst.invalidateSecureKeysCache()
328
+ }
276
329
  }
277
330
 
278
331
  @JvmStatic
279
332
  fun deleteSecureBatch(keys: Array<String>) {
280
333
  val inst = getInstanceOrThrow()
281
- val secureEditor = inst.encryptedPreferences.edit()
282
- val biometricEditor = inst.biometricPreferences.edit()
283
- for (key in keys) {
284
- secureEditor.remove(key)
285
- biometricEditor.remove(key)
334
+ synchronized(inst) {
335
+ val editor = inst.encryptedPreferences.edit()
336
+ for (key in keys) {
337
+ editor.remove(key)
338
+ }
339
+ inst.applySecureEditor(editor)
340
+ try {
341
+ val biometricEditor = inst.biometricPreferences.edit()
342
+ for (key in keys) {
343
+ biometricEditor.remove(key)
344
+ }
345
+ inst.applySecureEditor(biometricEditor)
346
+ } catch (e: Exception) {
347
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
348
+ }
349
+ inst.invalidateSecureKeysCache()
286
350
  }
287
- inst.applySecureEditor(secureEditor)
288
- inst.applySecureEditor(biometricEditor)
289
- inst.invalidateSecureKeysCache()
290
351
  }
291
352
 
292
353
  @JvmStatic
293
354
  fun hasSecure(key: String): Boolean {
294
355
  val inst = getInstanceOrThrow()
295
- return inst.encryptedPreferences.contains(key) || inst.biometricPreferences.contains(key)
356
+ val hasInEncrypted = inst.encryptedPreferences.contains(key)
357
+ val hasInBiometric = try {
358
+ inst.biometricPreferences.contains(key)
359
+ } catch (e: Exception) {
360
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
361
+ false
362
+ }
363
+ return hasInEncrypted || hasInBiometric
296
364
  }
297
365
 
298
366
  @JvmStatic
@@ -315,7 +383,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
315
383
  fun clearSecure() {
316
384
  val inst = getInstanceOrThrow()
317
385
  inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
318
- inst.applySecureEditor(inst.biometricPreferences.edit().clear())
386
+ try {
387
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
388
+ } catch (e: Exception) {
389
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
390
+ }
319
391
  inst.invalidateSecureKeysCache()
320
392
  }
321
393
 
@@ -329,33 +401,56 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
329
401
  @JvmStatic
330
402
  fun setSecureBiometricWithLevel(key: String, value: String, @Suppress("UNUSED_PARAMETER") level: Int) {
331
403
  val inst = getInstanceOrThrow()
332
- val editor = inst.biometricPreferences.edit().putString(key, value)
333
- inst.applySecureEditor(editor)
334
- inst.invalidateSecureKeysCache()
404
+ try {
405
+ val editor = inst.biometricPreferences.edit().putString(key, value)
406
+ inst.applySecureEditor(editor)
407
+ inst.invalidateSecureKeysCache()
408
+ } catch (e: Exception) {
409
+ throw RuntimeException("NitroStorage: Biometric storage unavailable on this device", e)
410
+ }
335
411
  }
336
412
 
337
413
  @JvmStatic
338
414
  fun getSecureBiometric(key: String): String? {
339
- return getInstanceOrThrow().getSecureSafe(getInstanceOrThrow().biometricPreferences, key)
415
+ val inst = getInstanceOrThrow()
416
+ return try {
417
+ inst.getSecureSafe(inst.biometricPreferences, key)
418
+ } catch (e: Exception) {
419
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
420
+ null
421
+ }
340
422
  }
341
423
 
342
424
  @JvmStatic
343
425
  fun deleteSecureBiometric(key: String) {
344
426
  val inst = getInstanceOrThrow()
345
- inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
346
- inst.invalidateSecureKeysCache()
427
+ try {
428
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
429
+ inst.invalidateSecureKeysCache()
430
+ } catch (e: Exception) {
431
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
432
+ }
347
433
  }
348
434
 
349
435
  @JvmStatic
350
436
  fun hasSecureBiometric(key: String): Boolean {
351
- return getInstanceOrThrow().biometricPreferences.contains(key)
437
+ return try {
438
+ getInstanceOrThrow().biometricPreferences.contains(key)
439
+ } catch (e: Exception) {
440
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
441
+ false
442
+ }
352
443
  }
353
444
 
354
445
  @JvmStatic
355
446
  fun clearSecureBiometric() {
356
447
  val inst = getInstanceOrThrow()
357
- inst.applySecureEditor(inst.biometricPreferences.edit().clear())
358
- inst.invalidateSecureKeysCache()
448
+ try {
449
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
450
+ inst.invalidateSecureKeysCache()
451
+ } catch (e: Exception) {
452
+ Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
453
+ }
359
454
  }
360
455
  }
361
456
  }