react-native-nitro-storage 0.5.9 → 0.7.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 (60) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +193 -16
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +225 -76
  4. package/app.plugin.js +2 -0
  5. package/cpp/bindings/HybridStorage.cpp +29 -21
  6. package/cpp/bindings/HybridStorage.hpp +5 -0
  7. package/docs/secure-storage.md +4 -0
  8. package/lib/commonjs/index.js +19 -2
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/index.web.js +19 -2
  11. package/lib/commonjs/index.web.js.map +1 -1
  12. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  13. package/lib/commonjs/internal.js.map +1 -1
  14. package/lib/commonjs/shared.js.map +1 -1
  15. package/lib/commonjs/storage-core.js +306 -10
  16. package/lib/commonjs/storage-core.js.map +1 -1
  17. package/lib/commonjs/storage-events.js.map +1 -1
  18. package/lib/commonjs/storage-hooks.js +16 -1
  19. package/lib/commonjs/storage-hooks.js.map +1 -1
  20. package/lib/commonjs/testing.js +267 -0
  21. package/lib/commonjs/testing.js.map +1 -0
  22. package/lib/module/index.js +5 -1
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/index.web.js +5 -1
  25. package/lib/module/index.web.js.map +1 -1
  26. package/lib/module/indexeddb-backend.js.map +1 -1
  27. package/lib/module/internal.js.map +1 -1
  28. package/lib/module/shared.js.map +1 -1
  29. package/lib/module/storage-core.js +307 -11
  30. package/lib/module/storage-core.js.map +1 -1
  31. package/lib/module/storage-events.js.map +1 -1
  32. package/lib/module/storage-hooks.js +15 -2
  33. package/lib/module/storage-hooks.js.map +1 -1
  34. package/lib/module/testing.js +188 -0
  35. package/lib/module/testing.js.map +1 -0
  36. package/lib/typescript/index.d.ts +28 -5
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/index.web.d.ts +28 -5
  39. package/lib/typescript/index.web.d.ts.map +1 -1
  40. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  41. package/lib/typescript/shared.d.ts +1 -0
  42. package/lib/typescript/shared.d.ts.map +1 -1
  43. package/lib/typescript/storage-core.d.ts +62 -3
  44. package/lib/typescript/storage-core.d.ts.map +1 -1
  45. package/lib/typescript/storage-events.d.ts +1 -1
  46. package/lib/typescript/storage-events.d.ts.map +1 -1
  47. package/lib/typescript/storage-hooks.d.ts +18 -1
  48. package/lib/typescript/storage-hooks.d.ts.map +1 -1
  49. package/lib/typescript/testing.d.ts +158 -0
  50. package/lib/typescript/testing.d.ts.map +1 -0
  51. package/package.json +11 -6
  52. package/src/index.ts +17 -4
  53. package/src/index.web.ts +17 -4
  54. package/src/indexeddb-backend.ts +1 -2
  55. package/src/internal.ts +1 -1
  56. package/src/shared.ts +1 -0
  57. package/src/storage-core.ts +463 -11
  58. package/src/storage-events.ts +3 -2
  59. package/src/storage-hooks.ts +42 -3
  60. package/src/testing.ts +270 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format follows Keep a Changelog and the project adheres to SemVer.
6
6
 
7
+ ## 0.7.0 - 2026-07-30
8
+
9
+ ### Changes
10
+
11
+ - **Breaking change:** Android secure key discovery, existence checks, and cleanup now surface locked, unavailable, or invalidated biometric-store errors instead of treating inaccessible protected values as absent. Catch storage errors around these operations and use `isKeychainLockedError()` when retrying after device authentication is appropriate.
12
+ - Upgrade the validated package baseline to Expo SDK 57, React Native 0.86.2, and Nitro Modules/Nitrogen 0.36.4.
13
+ - Preserve each item/value relationship in heterogeneous `setBatch()` calls so TypeScript rejects values assigned to the wrong storage item.
14
+ - Serialize native key-index hydration with concurrent mutations so `has`, `size`, and key queries cannot remain stale after a racing write.
15
+ - Enforce Android biometric policy levels with distinct Keystore keys, propagate locked or invalidated biometric failures, and keep secure preference files excluded from backup.
16
+ - Preflight biometric store access before aggregate secure mutations and surface native commit or corruption-recovery failures.
17
+
18
+ ## 0.6.0 - 2026-06-15
19
+
20
+ ### Added
21
+
22
+ - Object-state ergonomics on `StorageItem<T>`: `item.merge(partial)` for shallow object updates, `item.reset()` to return to the default value, and `item.setOrDelete(value)` which deletes on `null`/`undefined` and sets otherwise.
23
+ - Scoped item factories `memoryItem`, `diskItem`, and `secureItem` so call sites no longer repeat `scope: StorageScope.X`.
24
+ - `createSetItem()` for set-membership state backed by storage, with `add`/`delete`/`has`/`toggle`/`values`/`size`/`clear`/`reset` and no-op-safe writes (no event/render churn when adding an existing member or deleting an absent one).
25
+ - Lifecycle helpers: `storage.clear(scope, { except })` to wipe a scope while preserving listed keys/items, per-item `group` config plus `storage.clearGroup(group)` and `storage.getGroupItems(group)` (which compose for "clear all except a group").
26
+ - Declarative legacy migration: `renameFrom` on items (and per-key on `createSecureAuthStorage`) copies a legacy key to the new key on first read and removes the legacy entry. `createSecureAuthStorage` also accepts `group` and `fallbackToCacheOnReadError`.
27
+ - Secure read resilience: `fallbackToCacheOnReadError` returns the last cached value when a secure read throws a locked-keychain error, plus an `onReadError` hook.
28
+ - Global expiration events: TTL expiry now emits a `"expire"` change event (memory and disk) routed through the event bus, with `storage.subscribeExpired(scope, listener)`.
29
+ - Hook ergonomics: `useStorage` now returns a third, render-stable `actions` element (`set`/`merge`/`reset`/`remove`/`setOrDelete`); new `useStorageValue` (read-only) and `useStorageActions` hooks.
30
+ - Dev introspection: `storage.findDuplicateKeys()` and `storage.getRegisteredKeys()` to audit accidental `(scope, key)` collisions at startup.
31
+ - New `react-native-nitro-storage/testing` entrypoint: a faithful in-memory implementation of the full public surface plus `createNitroStorageMock()` and `resetNitroStorageMock()` for Jest/Storybook without native modules.
32
+
33
+ ### Changed
34
+
35
+ - Faster writes when nothing is subscribed: the native write/notify path now takes a lock-free fast path (per-scope atomic listener counts) and skips locking and copying the listener vector when a scope has no listeners. Applies to both iOS and Android via the shared C++ `HybridStorage`, and is thread-safe (verified under the C++ AddressSanitizer, ThreadSanitizer, and UndefinedBehaviorSanitizer suites).
36
+
37
+ ### Breaking Changes
38
+
39
+ All new APIs are additive — existing code keeps working. These behavior and
40
+ type changes can affect advanced consumers:
41
+
42
+ - TTL expiry now emits a `"expire"` change event instead of `"remove"`. Previously, a disk/secure value expiring on read emitted `operation: "remove"` and an expiring memory value emitted no event at all. If you subscribe to storage events and branch on `operation === "remove"` to detect expiry, also handle `"expire"` (or use the new `storage.subscribeExpired()`).
43
+ - `StorageChangeOperation` gained the `"expire"` and `"clearGroup"` members. Exhaustive `switch` statements over a change event's `operation` need cases for the new members.
44
+ - `useStorage()` now returns a three-element tuple `[value, setter, actions]` (was two). Array destructuring such as `const [value, setStore] = useStorage(item)` is unaffected; only code that annotated the result with an explicit two-element tuple type needs to widen the annotation.
45
+
7
46
  ## 0.5.9 - 2026-06-11
8
47
 
9
48
  ### Fixed
package/README.md CHANGED
@@ -5,8 +5,8 @@
5
5
  [![CI](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/actions/workflows/ci.yml/badge.svg)](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/actions/workflows/ci.yml)
6
6
  [![license](https://img.shields.io/npm/l/react-native-nitro-storage?color=007ec6)](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/blob/main/LICENSE)
7
7
  [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
8
- [![Expo](https://img.shields.io/badge/expo-SDK%2056-000020)](https://docs.expo.dev/)
9
- [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.7-black)](https://nitro.margelo.com/)
8
+ [![Expo](https://img.shields.io/badge/expo-SDK%2057-000020)](https://docs.expo.dev/versions/latest/)
9
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-0.36.x-black)](https://nitro.margelo.com/)
10
10
  [![TypeScript](https://img.shields.io/badge/typescript-6.0-3178c6)](https://www.typescriptlang.org/)
11
11
 
12
12
  Synchronous Memory, Disk, and Secure storage for React Native, Expo development
@@ -27,6 +27,10 @@ pagination, conflict resolution, or remote synchronization.
27
27
  - [Expo Config](#expo-config)
28
28
  - [Quick Start](#quick-start)
29
29
  - [Typed Storage Items](#typed-storage-items)
30
+ - [Item Ergonomics](#item-ergonomics)
31
+ - [Set Items](#set-items)
32
+ - [Groups And Lifecycle](#groups-and-lifecycle)
33
+ - [Legacy Key Migration And Secure Resilience](#legacy-key-migration-and-secure-resilience)
30
34
  - [React Hooks](#react-hooks)
31
35
  - [Storage Scopes](#storage-scopes)
32
36
  - [Secure Storage](#secure-storage)
@@ -34,10 +38,12 @@ pagination, conflict resolution, or remote synchronization.
34
38
  - [Events And Observability](#events-and-observability)
35
39
  - [Migrations And Transactions](#migrations-and-transactions)
36
40
  - [Web Backends](#web-backends)
41
+ - [Testing](#testing)
37
42
  - [Platform Support](#platform-support)
38
43
  - [Documentation](#documentation)
39
44
  - [Troubleshooting](#troubleshooting)
40
45
  - [Development](#development)
46
+ - [License](#license)
41
47
 
42
48
  ## Install
43
49
 
@@ -47,13 +53,16 @@ bun add react-native-nitro-storage react-native-nitro-modules
47
53
 
48
54
  Peer dependencies:
49
55
 
50
- | Package | Version |
51
- | ---------------------------- | ---------- |
52
- | `react` | `>=18.2.0` |
53
- | `react-native` | `>=0.75.0` |
54
- | `react-native-nitro-modules` | `>=0.35.7` |
56
+ | Package | Version |
57
+ | ---------------------------- | ------------------ |
58
+ | `react` | `>=18.2.0` |
59
+ | `react-native` | `>=0.75.0` |
60
+ | `react-native-nitro-modules` | `>=0.36.4 <0.37.0` |
55
61
 
56
- Nitro peer requirement: `react-native-nitro-modules >=0.35.7`.
62
+ Nitro peer requirement: `react-native-nitro-modules >=0.36.4 <0.37.0`.
63
+
64
+ Validated example baseline: Expo SDK 57, React Native 0.86.2, React 19.2.3,
65
+ and Nitro Modules 0.36.4.
57
66
 
58
67
  For Expo development builds:
59
68
 
@@ -153,9 +162,108 @@ const didWrite = preferencesItem.setIfVersion(snapshot.version, {
153
162
  });
154
163
  ```
155
164
 
156
- The package exports `StorageItem`, `StorageItemConfig`, `StorageSetter`,
157
- `VersionedValue`, `StorageBatchSetItem`, web backend types, event types, secure
158
- metadata types, and capability types for IDE-safe integrations.
165
+ The package ships its own TypeScript types, so editors and AI tools catch
166
+ mistakes before they reach the runtime. It exports `StorageItem`,
167
+ `StorageItemConfig`, `StorageSetter`, `StorageActions`, `VersionedValue`,
168
+ `StorageBatchSetItem`, `StorageClearOptions`, `StorageKeyRef`, `SetItemConfig`,
169
+ `SetStorageItem`, plus web backend, event, secure-metadata, and capability types.
170
+
171
+ ## Item Ergonomics
172
+
173
+ `merge`, `reset`, and `setOrDelete` cover the most common object-state edits
174
+ without re-reading or hand-writing compare-and-swap loops. Scoped factories
175
+ (`memoryItem`, `diskItem`, `secureItem`) drop the repeated `scope` field.
176
+
177
+ ```ts
178
+ import { diskItem, memoryItem } from "react-native-nitro-storage";
179
+
180
+ const config = diskItem<{ theme: "light" | "dark"; compact: boolean }>({
181
+ key: "config",
182
+ defaultValue: { theme: "light", compact: false },
183
+ });
184
+
185
+ config.merge({ compact: true }); // shallow object update
186
+ config.reset(); // back to the default value
187
+ const loginMethod = memoryItem<string | null>({
188
+ key: "loginMethod",
189
+ defaultValue: null,
190
+ });
191
+ loginMethod.setOrDelete(maybeMethod); // null/undefined deletes, value sets
192
+ ```
193
+
194
+ ## Set Items
195
+
196
+ `createSetItem()` models set-membership state (seen ids, dismissed prompts)
197
+ without hand-rolling `Record<string, true>` helpers. Adding an existing member
198
+ or deleting an absent one is a no-op, so subscribers do not re-render.
199
+
200
+ ```ts
201
+ import { createSetItem, StorageScope } from "react-native-nitro-storage";
202
+
203
+ const dismissedTips = createSetItem({
204
+ key: "dismissedTips",
205
+ scope: StorageScope.Disk,
206
+ });
207
+
208
+ dismissedTips.add("welcome");
209
+ dismissedTips.has("welcome"); // true
210
+ dismissedTips.toggle("welcome"); // false (removed)
211
+ dismissedTips.values(); // string[]
212
+ ```
213
+
214
+ ## Groups And Lifecycle
215
+
216
+ Tag items with a `group` to clear related state in one call, or keep specific
217
+ keys while wiping the rest of a scope. This replaces manual snapshot-and-restore
218
+ logout flows.
219
+
220
+ ```ts
221
+ import { secureItem, storage, StorageScope } from "react-native-nitro-storage";
222
+
223
+ const accessToken = secureItem<string>({
224
+ key: "accessToken",
225
+ defaultValue: "",
226
+ group: "session",
227
+ });
228
+
229
+ // Wipe everything tied to the session.
230
+ storage.clearGroup("session");
231
+
232
+ // Wipe Disk but keep a few opt-in preferences.
233
+ storage.clear(StorageScope.Disk, {
234
+ except: [apiEnvironmentItem, "onboardingComplete"],
235
+ });
236
+ ```
237
+
238
+ ## Legacy Key Migration And Secure Resilience
239
+
240
+ `renameFrom` migrates an old key to a new one on first read and deletes the
241
+ legacy entry. Secure items can fall back to the last cached value when the
242
+ keychain is locked instead of throwing.
243
+
244
+ ```ts
245
+ import {
246
+ secureItem,
247
+ createSecureAuthStorage,
248
+ } from "react-native-nitro-storage";
249
+
250
+ const accessToken = secureItem<string>({
251
+ key: "accessToken",
252
+ namespace: "auth",
253
+ defaultValue: "",
254
+ renameFrom: "authToken", // copied + cleaned up on first read
255
+ fallbackToCacheOnReadError: true,
256
+ onReadError: (error) => reportSecureReadError(error),
257
+ });
258
+
259
+ const auth = createSecureAuthStorage(
260
+ {
261
+ accessToken: { renameFrom: "authToken" },
262
+ refreshToken: { renameFrom: "refreshToken" },
263
+ },
264
+ { namespace: "auth", group: "session", fallbackToCacheOnReadError: true },
265
+ );
266
+ ```
159
267
 
160
268
  ## React Hooks
161
269
 
@@ -188,6 +296,24 @@ const [compactMode] = useStorageSelector(
188
296
  );
189
297
  ```
190
298
 
299
+ `useStorage` also returns a render-stable `actions` object as a third element,
300
+ and `useStorageValue` / `useStorageActions` split read and write concerns.
301
+
302
+ ```tsx
303
+ import {
304
+ useStorage,
305
+ useStorageActions,
306
+ useStorageValue,
307
+ } from "react-native-nitro-storage";
308
+
309
+ const [config, setConfig, actions] = useStorage(configItem);
310
+ actions.merge({ compact: true });
311
+ actions.reset();
312
+
313
+ const theme = useStorageValue(themeItem); // read-only, no setter
314
+ const tokenActions = useStorageActions(tokenItem); // { set, merge, reset, remove, setOrDelete }
315
+ ```
316
+
191
317
  ## Storage Scopes
192
318
 
193
319
  | Scope | Backing store | Use it for |
@@ -248,10 +374,20 @@ exporting secure values unless you are intentionally doing a short-lived
248
374
  in-memory migration. `storage.export(StorageScope.Secure)` throws unless you
249
375
  explicitly opt into `{ includeSecureValues: true }`.
250
376
 
377
+ On Android 11 and newer, `BiometricLevel.BiometryOnly` and
378
+ `BiometricLevel.BiometryOrPasscode` use separate Keystore policies. Android 10
379
+ and older support `BiometryOrPasscode`; `BiometryOnly` throws
380
+ `biometric_unavailable` because those releases cannot safely enforce the
381
+ biometric-only distinction. Secure existence, discovery, and cleanup operations
382
+ can also throw when a protected store is locked or its key is invalidated. Catch
383
+ those failures and use `isKeychainLockedError()` when authentication-aware retry
384
+ behavior is appropriate.
385
+
251
386
  ## Batch Operations
252
387
 
253
388
  `getBatch()` preserves tuple value types, so IDEs infer each result from the
254
- matching item.
389
+ matching item. `setBatch()` validates every item/value pair independently,
390
+ including heterogeneous batches.
255
391
 
256
392
  ```ts
257
393
  import { getBatch, removeBatch, setBatch } from "react-native-nitro-storage";
@@ -303,6 +439,21 @@ Secure event observer values are redacted by default. Pass
303
439
  `{ redactSecureValues: false }` only in trusted debug tooling where raw values
304
440
  are safe to inspect.
305
441
 
442
+ TTL expiry emits a dedicated `"expire"` change event. Use
443
+ `storage.subscribeExpired()` to react to keys that lapse on read.
444
+
445
+ ```ts
446
+ const unsubscribeExpired = storage.subscribeExpired(
447
+ StorageScope.Disk,
448
+ (event) => {
449
+ console.log("expired", event.key);
450
+ },
451
+ );
452
+ ```
453
+
454
+ `storage.findDuplicateKeys()` and `storage.getRegisteredKeys()` help audit
455
+ accidental `(scope, key)` collisions; call them once at startup in development.
456
+
306
457
  ## Migrations And Transactions
307
458
 
308
459
  ```ts
@@ -324,15 +475,16 @@ registerMigration(2, ({ getRaw, setRaw, removeRaw }) => {
324
475
 
325
476
  migrateToLatest(StorageScope.Disk);
326
477
 
327
- runTransaction(() => {
328
- themeItem.set("dark");
329
- localeItem.set("en-US");
478
+ runTransaction(StorageScope.Disk, (tx) => {
479
+ tx.setItem(themeItem, "dark");
480
+ tx.setItem(localeItem, "en-US");
330
481
  });
331
482
 
332
483
  migrateFromMMKV(mmkvInstance, themeItem);
333
484
  ```
334
485
 
335
- Transactions roll back local writes if the callback throws.
486
+ `runTransaction(scope, callback)` rolls back every write made through the `tx`
487
+ context if the callback throws.
336
488
 
337
489
  ## Web Backends
338
490
 
@@ -355,6 +507,31 @@ setWebSecureStorageBackend(backend);
355
507
  Browser storage cannot provide iOS Keychain or Android Keystore guarantees. Web
356
508
  Secure scope is only as strong as the backend you configure.
357
509
 
510
+ ## Testing
511
+
512
+ The `react-native-nitro-storage/testing` entrypoint is a faithful in-memory
513
+ implementation of the full public surface, so unit tests and Storybook run
514
+ without native modules. Mock the package with it, or use it directly.
515
+
516
+ ```ts
517
+ import {
518
+ createNitroStorageMock,
519
+ resetNitroStorageMock,
520
+ } from "react-native-nitro-storage/testing";
521
+
522
+ // Jest: swap the real module for the in-memory implementation.
523
+ jest.mock("react-native-nitro-storage", () =>
524
+ require("react-native-nitro-storage/testing"),
525
+ );
526
+
527
+ beforeEach(() => {
528
+ resetNitroStorageMock();
529
+ });
530
+
531
+ // Or build an isolated instance per test file.
532
+ const { storage, memoryItem } = createNitroStorageMock();
533
+ ```
534
+
358
535
  ## Platform Support
359
536
 
360
537
  | Platform | Status |