react-native-nitro-storage 0.3.1 → 0.4.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 (44) hide show
  1. package/README.md +334 -34
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
  6. package/cpp/bindings/HybridStorage.cpp +214 -23
  7. package/cpp/bindings/HybridStorage.hpp +31 -3
  8. package/cpp/core/NativeStorageAdapter.hpp +4 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +17 -0
  10. package/ios/IOSStorageAdapterCpp.mm +140 -10
  11. package/lib/commonjs/index.js +555 -288
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +750 -309
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/internal.js +25 -0
  16. package/lib/commonjs/internal.js.map +1 -1
  17. package/lib/commonjs/storage-hooks.js +36 -0
  18. package/lib/commonjs/storage-hooks.js.map +1 -0
  19. package/lib/module/index.js +537 -287
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/index.web.js +732 -308
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/internal.js +24 -0
  24. package/lib/module/internal.js.map +1 -1
  25. package/lib/module/storage-hooks.js +30 -0
  26. package/lib/module/storage-hooks.js.map +1 -0
  27. package/lib/typescript/Storage.nitro.d.ts +4 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +41 -4
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +45 -4
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/internal.d.ts +1 -0
  34. package/lib/typescript/internal.d.ts.map +1 -1
  35. package/lib/typescript/storage-hooks.d.ts +10 -0
  36. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  37. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
  38. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
  39. package/package.json +5 -3
  40. package/src/Storage.nitro.ts +4 -0
  41. package/src/index.ts +704 -324
  42. package/src/index.web.ts +929 -346
  43. package/src/internal.ts +28 -0
  44. package/src/storage-hooks.ts +48 -0
package/README.md CHANGED
@@ -14,11 +14,37 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
14
14
  - **Biometric storage** — hardware-backed biometric protection on iOS & Android
15
15
  - **Auth storage factory** — `createSecureAuthStorage` for multi-token auth flows
16
16
  - **Batch operations** — atomic multi-key get/set/remove via native batch APIs
17
+ - **Prefix queries** — fast key/value scans with `storage.getKeysByPrefix` and `storage.getByPrefix`
18
+ - **Versioned writes** — optimistic concurrency with `item.getWithVersion()` and `item.setIfVersion(...)`
19
+ - **Performance metrics** — observe operation timings and aggregate snapshots
20
+ - **Web secure backend override** — plug custom secure storage backend on web
17
21
  - **Transactions** — grouped writes with automatic rollback on error
18
22
  - **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
19
23
  - **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
20
24
  - **Cross-platform** — iOS, Android, and web (`localStorage` fallback)
21
25
 
26
+ ## Feature Coverage
27
+
28
+ Every feature in this package is documented with at least one runnable example in this README:
29
+
30
+ - Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
31
+ - Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
32
+ - Scopes (`Memory`, `Disk`, `Secure`) — see Storage Scopes and multiple use cases
33
+ - Namespaces — see Multi-Tenant / Namespaced Storage
34
+ - TTL expiration + callbacks — see OTP / Temporary Codes
35
+ - Validation + recovery — see Feature Flags with Validation
36
+ - Biometric + access control — see Biometric-protected Secrets
37
+ - Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
38
+ - Prefix utilities (`getKeysByPrefix`, `getByPrefix`) — see Prefix Queries and Namespace Inspection
39
+ - Versioned item API (`getWithVersion`, `setIfVersion`) — see Optimistic Versioned Writes
40
+ - Metrics API (`setMetricsObserver`, `getMetricsSnapshot`, `resetMetrics`) — see Storage Metrics Instrumentation
41
+ - Web secure backend override (`setWebSecureStorageBackend`, `getWebSecureStorageBackend`) — see Custom Web Secure Backend
42
+ - Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
43
+ - Transactions — see Transactions and Atomic Balance Transfer
44
+ - Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
45
+ - MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
46
+ - Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
47
+
22
48
  ## Requirements
23
49
 
24
50
  | Dependency | Version |
@@ -167,21 +193,35 @@ function createStorageItem<T = undefined>(
167
193
  | `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
168
194
  | `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
169
195
  | `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
196
+ | `biometricLevel` | `BiometricLevel` | `None` | Biometric policy (`BiometryOrPasscode` / `BiometryOnly`) |
170
197
  | `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
171
198
 
172
199
  **Returned `StorageItem<T>`:**
173
200
 
174
- | Method / Property | Type | Description |
175
- | ----------------- | ---------------------------------------- | -------------------------------------------------- |
176
- | `get()` | `() => T` | Read current value (synchronous) |
177
- | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
178
- | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
179
- | `has()` | `() => boolean` | Check if a value exists in storage |
180
- | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
181
- | `serialize` | `(v: T) => string` | The item's serializer |
182
- | `deserialize` | `(v: string) => T` | The item's deserializer |
183
- | `scope` | `StorageScope` | The item's scope |
184
- | `key` | `string` | The resolved key (includes namespace prefix) |
201
+ | Method / Property | Type | Description |
202
+ | ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
203
+ | `get()` | `() => T` | Read current value (synchronous) |
204
+ | `getWithVersion()` | `() => { value: T; version: StorageVersion }` | Read value plus current storage version token |
205
+ | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
206
+ | `setIfVersion(...)` | `(version: StorageVersion, value: T \| ((prev: T) => T)) => boolean` | Write only if version matches (optimistic concurrency) |
207
+ | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
208
+ | `has()` | `() => boolean` | Check if a value exists in storage |
209
+ | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
210
+ | `serialize` | `(v: T) => string` | The item's serializer |
211
+ | `deserialize` | `(v: string) => T` | The item's deserializer |
212
+ | `scope` | `StorageScope` | The item's scope |
213
+ | `key` | `string` | The resolved key (includes namespace prefix) |
214
+
215
+ **Non-React subscription example:**
216
+
217
+ ```ts
218
+ const unsubscribe = sessionItem.subscribe(() => {
219
+ console.log("session changed:", sessionItem.get());
220
+ });
221
+
222
+ sessionItem.set("next-session");
223
+ unsubscribe();
224
+ ```
185
225
 
186
226
  ---
187
227
 
@@ -220,21 +260,93 @@ setToken("new-token");
220
260
  import { storage, StorageScope } from "react-native-nitro-storage";
221
261
  ```
222
262
 
223
- | Method | Description |
224
- | --------------------------------------- | ---------------------------------------------------------------------------- |
225
- | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
226
- | `storage.clearAll()` | Clear Memory + Disk + Secure |
227
- | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
228
- | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
229
- | `storage.has(key, scope)` | Check if a key exists |
230
- | `storage.getAllKeys(scope)` | Get all key names |
231
- | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
232
- | `storage.size(scope)` | Number of stored keys |
233
- | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
234
- | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
263
+ | Method | Description |
264
+ | ---------------------------------------- | ---------------------------------------------------------------------------- |
265
+ | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
266
+ | `storage.clearAll()` | Clear Memory + Disk + Secure |
267
+ | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
268
+ | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
269
+ | `storage.has(key, scope)` | Check if a key exists |
270
+ | `storage.getAllKeys(scope)` | Get all key names |
271
+ | `storage.getKeysByPrefix(prefix, scope)` | Get keys that start with a prefix |
272
+ | `storage.getByPrefix(prefix, scope)` | Get raw key-value pairs for keys matching a prefix |
273
+ | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
274
+ | `storage.size(scope)` | Number of stored keys |
275
+ | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
276
+ | `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
277
+ | `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
278
+ | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
279
+ | `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
280
+ | `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
281
+ | `storage.resetMetrics()` | Reset in-memory metrics counters |
235
282
 
236
283
  > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
237
284
 
285
+ #### Global utility examples
286
+
287
+ ```ts
288
+ import {
289
+ AccessControl,
290
+ storage,
291
+ StorageScope,
292
+ } from "react-native-nitro-storage";
293
+
294
+ storage.has("session", StorageScope.Disk);
295
+ storage.getAllKeys(StorageScope.Disk);
296
+ storage.getKeysByPrefix("user-42:", StorageScope.Disk);
297
+ storage.getByPrefix("user-42:", StorageScope.Disk);
298
+ storage.getAll(StorageScope.Disk);
299
+ storage.size(StorageScope.Disk);
300
+
301
+ storage.clearNamespace("user-42", StorageScope.Disk);
302
+ storage.clearBiometric();
303
+
304
+ storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
305
+ storage.setKeychainAccessGroup("group.com.example.shared");
306
+
307
+ storage.clear(StorageScope.Memory);
308
+ storage.clearAll();
309
+ ```
310
+
311
+ #### Android secure write mode
312
+
313
+ `storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
314
+ Use this for non-critical secure writes when lower latency matters more than immediate durability.
315
+
316
+ Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
317
+
318
+ ```ts
319
+ import { storage } from "react-native-nitro-storage";
320
+
321
+ storage.setSecureWritesAsync(true);
322
+
323
+ // ...multiple secure writes happen (including coalesced item writes)
324
+
325
+ storage.flushSecureWrites(); // deterministic durability boundary
326
+ ```
327
+
328
+ #### Custom web secure backend
329
+
330
+ By default, web Secure scope uses `localStorage` with `__secure_` key prefixing. You can replace it with a custom backend (for example encrypted IndexedDB adapter).
331
+
332
+ ```ts
333
+ import {
334
+ getWebSecureStorageBackend,
335
+ setWebSecureStorageBackend,
336
+ } from "react-native-nitro-storage";
337
+
338
+ setWebSecureStorageBackend({
339
+ getItem: (key) => encryptedStore.get(key) ?? null,
340
+ setItem: (key, value) => encryptedStore.set(key, value),
341
+ removeItem: (key) => encryptedStore.delete(key),
342
+ clear: () => encryptedStore.clear(),
343
+ getAllKeys: () => encryptedStore.keys(),
344
+ });
345
+
346
+ const backend = getWebSecureStorageBackend();
347
+ console.log("custom backend active:", backend !== undefined);
348
+ ```
349
+
238
350
  ---
239
351
 
240
352
  ### `createSecureAuthStorage<K>(config, options?)`
@@ -250,7 +362,7 @@ function createSecureAuthStorage<K extends string>(
250
362
 
251
363
  - Default namespace: `"auth"`
252
364
  - Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
253
- - Supports per-key TTL, biometric, and access control
365
+ - Supports per-key TTL, biometric level policy, and access control
254
366
 
255
367
  ---
256
368
 
@@ -398,11 +510,11 @@ enum BiometricLevel {
398
510
  ### Persisted User Preferences
399
511
 
400
512
  ```ts
401
- interface UserPreferences {
513
+ type UserPreferences = {
402
514
  theme: "light" | "dark" | "system";
403
515
  language: string;
404
516
  notifications: boolean;
405
- }
517
+ };
406
518
 
407
519
  const prefsItem = createStorageItem<UserPreferences>({
408
520
  key: "prefs",
@@ -446,21 +558,29 @@ storage.clearNamespace("myapp-auth", StorageScope.Secure);
446
558
  ### Feature Flags with Validation
447
559
 
448
560
  ```ts
449
- interface FeatureFlags {
561
+ type FeatureFlags = {
450
562
  darkMode: boolean;
451
563
  betaFeature: boolean;
452
564
  maxUploadMb: number;
453
- }
565
+ };
566
+
567
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
568
+ typeof value === "object" && value !== null;
569
+
570
+ const isFeatureFlags = (value: unknown): value is FeatureFlags => {
571
+ if (!isRecord(value)) return false;
572
+ return (
573
+ typeof value.darkMode === "boolean" &&
574
+ typeof value.betaFeature === "boolean" &&
575
+ typeof value.maxUploadMb === "number"
576
+ );
577
+ };
454
578
 
455
579
  const flagsItem = createStorageItem<FeatureFlags>({
456
580
  key: "feature-flags",
457
581
  scope: StorageScope.Disk,
458
582
  defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
459
- validate: (v): v is FeatureFlags =>
460
- typeof v === "object" &&
461
- v !== null &&
462
- typeof (v as any).darkMode === "boolean" &&
463
- typeof (v as any).maxUploadMb === "number",
583
+ validate: isFeatureFlags,
464
584
  onValidationError: () => ({
465
585
  darkMode: false,
466
586
  betaFeature: false,
@@ -471,6 +591,29 @@ const flagsItem = createStorageItem<FeatureFlags>({
471
591
  });
472
592
  ```
473
593
 
594
+ ### Biometric-protected Secrets
595
+
596
+ ```ts
597
+ import {
598
+ AccessControl,
599
+ BiometricLevel,
600
+ createStorageItem,
601
+ StorageScope,
602
+ } from "react-native-nitro-storage";
603
+
604
+ const paymentPin = createStorageItem<string>({
605
+ key: "payment-pin",
606
+ scope: StorageScope.Secure,
607
+ defaultValue: "",
608
+ biometricLevel: BiometricLevel.BiometryOnly,
609
+ accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
610
+ });
611
+
612
+ paymentPin.set("4829");
613
+ const pin = paymentPin.get();
614
+ paymentPin.delete();
615
+ ```
616
+
474
617
  ### Multi-Tenant / Namespaced Storage
475
618
 
476
619
  ```ts
@@ -515,6 +658,40 @@ otpItem.set("482917");
515
658
  const code = otpItem.get();
516
659
  ```
517
660
 
661
+ ### Bulk Bootstrap with Batch APIs
662
+
663
+ ```ts
664
+ import {
665
+ createStorageItem,
666
+ getBatch,
667
+ removeBatch,
668
+ setBatch,
669
+ StorageScope,
670
+ } from "react-native-nitro-storage";
671
+
672
+ const firstName = createStorageItem({
673
+ key: "first-name",
674
+ scope: StorageScope.Disk,
675
+ defaultValue: "",
676
+ });
677
+ const lastName = createStorageItem({
678
+ key: "last-name",
679
+ scope: StorageScope.Disk,
680
+ defaultValue: "",
681
+ });
682
+
683
+ setBatch(
684
+ [
685
+ { item: firstName, value: "Ada" },
686
+ { item: lastName, value: "Lovelace" },
687
+ ],
688
+ StorageScope.Disk,
689
+ );
690
+
691
+ const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
692
+ removeBatch([firstName, lastName], StorageScope.Disk);
693
+ ```
694
+
518
695
  ### Atomic Balance Transfer
519
696
 
520
697
  ```ts
@@ -555,6 +732,116 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
555
732
  });
556
733
  ```
557
734
 
735
+ ### Coalesced Secure Writes with Deterministic Flush
736
+
737
+ ```ts
738
+ import {
739
+ createStorageItem,
740
+ storage,
741
+ StorageScope,
742
+ } from "react-native-nitro-storage";
743
+
744
+ const sessionToken = createStorageItem<string>({
745
+ key: "session-token",
746
+ scope: StorageScope.Secure,
747
+ defaultValue: "",
748
+ coalesceSecureWrites: true,
749
+ });
750
+
751
+ sessionToken.set("token-v1");
752
+ sessionToken.set("token-v2");
753
+
754
+ // force pending secure writes to native persistence
755
+ storage.flushSecureWrites();
756
+ ```
757
+
758
+ ### Storage Snapshots and Cleanup
759
+
760
+ ```ts
761
+ import { storage, StorageScope } from "react-native-nitro-storage";
762
+
763
+ const diskKeys = storage.getAllKeys(StorageScope.Disk);
764
+ const diskValues = storage.getAll(StorageScope.Disk);
765
+ const secureCount = storage.size(StorageScope.Secure);
766
+
767
+ if (storage.has("legacy-flag", StorageScope.Disk)) {
768
+ storage.clearNamespace("legacy", StorageScope.Disk);
769
+ }
770
+
771
+ storage.clearBiometric();
772
+ ```
773
+
774
+ ### Prefix Queries and Namespace Inspection
775
+
776
+ ```ts
777
+ import { storage, StorageScope } from "react-native-nitro-storage";
778
+
779
+ const userKeys = storage.getKeysByPrefix("user-42:", StorageScope.Disk);
780
+ const userRawEntries = storage.getByPrefix("user-42:", StorageScope.Disk);
781
+
782
+ console.log(userKeys);
783
+ console.log(userRawEntries);
784
+ ```
785
+
786
+ ### Optimistic Versioned Writes
787
+
788
+ ```ts
789
+ import { createStorageItem, StorageScope } from "react-native-nitro-storage";
790
+
791
+ const profileItem = createStorageItem({
792
+ key: "profile",
793
+ scope: StorageScope.Disk,
794
+ defaultValue: { name: "Guest" },
795
+ });
796
+
797
+ const snapshot = profileItem.getWithVersion();
798
+ const didWrite = profileItem.setIfVersion(snapshot.version, {
799
+ ...snapshot.value,
800
+ name: "Ada",
801
+ });
802
+
803
+ if (!didWrite) {
804
+ // value changed since snapshot; reload and retry
805
+ }
806
+ ```
807
+
808
+ ### Storage Metrics Instrumentation
809
+
810
+ ```ts
811
+ import { storage } from "react-native-nitro-storage";
812
+
813
+ storage.setMetricsObserver((event) => {
814
+ console.log(
815
+ `[nitro-storage] ${event.operation} scope=${event.scope} duration=${event.durationMs}ms keys=${event.keysCount}`,
816
+ );
817
+ });
818
+
819
+ const metrics = storage.getMetricsSnapshot();
820
+ console.log(metrics["item:get"]?.avgDurationMs);
821
+
822
+ storage.resetMetrics();
823
+ storage.setMetricsObserver(undefined);
824
+ ```
825
+
826
+ ### Low-level Subscription (outside React)
827
+
828
+ ```ts
829
+ import { createStorageItem, StorageScope } from "react-native-nitro-storage";
830
+
831
+ const notificationsItem = createStorageItem<boolean>({
832
+ key: "notifications-enabled",
833
+ scope: StorageScope.Disk,
834
+ defaultValue: true,
835
+ });
836
+
837
+ const unsubscribe = notificationsItem.subscribe(() => {
838
+ console.log("notifications changed:", notificationsItem.get());
839
+ });
840
+
841
+ notificationsItem.set(false);
842
+ unsubscribe();
843
+ ```
844
+
558
845
  ### Migrating From MMKV
559
846
 
560
847
  ```ts
@@ -587,6 +874,12 @@ import type {
587
874
  MigrationContext,
588
875
  Migration,
589
876
  TransactionContext,
877
+ StorageVersion,
878
+ VersionedValue,
879
+ StorageMetricsEvent,
880
+ StorageMetricsObserver,
881
+ StorageMetricSummary,
882
+ WebSecureStorageBackend,
590
883
  MMKVLike,
591
884
  SecureAuthStorageConfig,
592
885
  } from "react-native-nitro-storage";
@@ -600,7 +893,11 @@ From repository root:
600
893
 
601
894
  ```bash
602
895
  bun run test -- --filter=react-native-nitro-storage
896
+ bun run lint -- --filter=react-native-nitro-storage
897
+ bun run format:check -- --filter=react-native-nitro-storage
603
898
  bun run typecheck -- --filter=react-native-nitro-storage
899
+ bun run test:types -- --filter=react-native-nitro-storage
900
+ bun run test:cpp -- --filter=react-native-nitro-storage
604
901
  bun run build -- --filter=react-native-nitro-storage
605
902
  ```
606
903
 
@@ -612,7 +909,10 @@ bun run test:coverage # run tests with coverage
612
909
  bun run lint # eslint (expo-magic rules)
613
910
  bun run format:check # prettier check
614
911
  bun run typecheck # tsc --noEmit
615
- bun run build # tsup build
912
+ bun run test:types # public type-level API tests
913
+ bun run test:cpp # C++ binding/core tests
914
+ bun run check:pack # npm pack content guard
915
+ bun run build # bob build
616
916
  bun run benchmark # performance benchmarks
617
917
  ```
618
918
 
@@ -11,6 +11,8 @@ file(GLOB SOURCES
11
11
  "../cpp/core/*.cpp"
12
12
  "./src/main/cpp/*.cpp"
13
13
  )
14
+ # Unit/integration C++ tests define `main()` and must never be linked into the Android shared library.
15
+ list(FILTER SOURCES EXCLUDE REGEX ".*/[^/]*Test\\.cpp$")
14
16
 
15
17
  # 2. Create the library target
16
18
  add_library(
@@ -90,6 +90,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
90
90
  return fromJavaStringArray(keys);
91
91
  }
92
92
 
93
+ std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
94
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
95
+ local_ref<JavaStringArray>(std::string)
96
+ >("getKeysByPrefixDisk");
97
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
98
+ return fromJavaStringArray(keys);
99
+ }
100
+
93
101
  size_t AndroidStorageAdapterCpp::sizeDisk() {
94
102
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeDisk");
95
103
  return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -166,6 +174,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
166
174
  return fromJavaStringArray(keys);
167
175
  }
168
176
 
177
+ std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
178
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
179
+ local_ref<JavaStringArray>(std::string)
180
+ >("getKeysByPrefixSecure");
181
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
182
+ return fromJavaStringArray(keys);
183
+ }
184
+
169
185
  size_t AndroidStorageAdapterCpp::sizeSecure() {
170
186
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeSecure");
171
187
  return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -213,13 +229,21 @@ void AndroidStorageAdapterCpp::clearSecure() {
213
229
  // --- Config (no-ops on Android; access control / groups are iOS-specific) ---
214
230
 
215
231
  void AndroidStorageAdapterCpp::setSecureAccessControl(int /*level*/) {}
232
+ void AndroidStorageAdapterCpp::setSecureWritesAsync(bool enabled) {
233
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(jboolean)>("setSecureWritesAsync");
234
+ method(AndroidStorageAdapterJava::javaClassStatic(), enabled);
235
+ }
216
236
  void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group*/) {}
217
237
 
218
238
  // --- Biometric ---
219
239
 
220
240
  void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const std::string& value) {
221
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string)>("setSecureBiometric");
222
- method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
241
+ setSecureBiometricWithLevel(key, value, 2);
242
+ }
243
+
244
+ 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);
223
247
  }
224
248
 
225
249
  std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
@@ -30,6 +30,7 @@ public:
30
30
  void deleteDisk(const std::string& key) override;
31
31
  bool hasDisk(const std::string& key) override;
32
32
  std::vector<std::string> getAllKeysDisk() override;
33
+ std::vector<std::string> getKeysByPrefixDisk(const std::string& prefix) override;
33
34
  size_t sizeDisk() override;
34
35
  void setDiskBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
35
36
  std::vector<std::optional<std::string>> getDiskBatch(const std::vector<std::string>& keys) override;
@@ -40,6 +41,7 @@ public:
40
41
  void deleteSecure(const std::string& key) override;
41
42
  bool hasSecure(const std::string& key) override;
42
43
  std::vector<std::string> getAllKeysSecure() override;
44
+ std::vector<std::string> getKeysByPrefixSecure(const std::string& prefix) override;
43
45
  size_t sizeSecure() override;
44
46
  void setSecureBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
45
47
  std::vector<std::optional<std::string>> getSecureBatch(const std::vector<std::string>& keys) override;
@@ -49,9 +51,11 @@ public:
49
51
  void clearSecure() override;
50
52
 
51
53
  void setSecureAccessControl(int level) override;
54
+ void setSecureWritesAsync(bool enabled) override;
52
55
  void setKeychainAccessGroup(const std::string& group) override;
53
56
 
54
57
  void setSecureBiometric(const std::string& key, const std::string& value) override;
58
+ void setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) override;
55
59
  std::optional<std::string> getSecureBiometric(const std::string& key) override;
56
60
  void deleteSecureBiometric(const std::string& key) override;
57
61
  bool hasSecureBiometric(const std::string& key) override;