@chipmobilesdk/rn-camera 0.1.0 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  All notable changes to this package are documented here. The package follows semantic versioning.
4
4
 
5
+ ## [0.2.0] — 2026-11-01
6
+
7
+ Lifecycle and capture-path fixes. **No code change is required**; two behaviours
8
+ change (see Changed), and a store that should manage VisionCamera captures needs a
9
+ `FileSystemBridge` with `move()`.
10
+
11
+ ### Fixed
12
+
13
+ - **`useCameraController` survives a StrictMode remount.** The cleanup disposed the
14
+ only controller and the remount kept it, so the screen showed `idle` forever and
15
+ backgrounding no longer released the camera. A disposed controller is now replaced,
16
+ and constructing one has no side effects (it subscribes on first use), so a
17
+ controller React builds and discards no longer leaks listeners.
18
+ - **`CameraPreview` subscribes to the controller.** It read `controller.state` once
19
+ per render, so the feed could stay unmounted after the session became active unless
20
+ the parent re-rendered. `useCameraState` now uses `useSyncExternalStore`.
21
+ - **A failed capture no longer leaves `capturing: true` published**, and `capturing:
22
+ true` is now published at all — the single-flight slot was taken after the first
23
+ emit. A raw native or engine error is turned into `CameraError('captureFailed')`
24
+ (or `storageFailure` when saving fails), with no file path in the message.
25
+ - **Foregrounding restarts only a session that backgrounding interrupted.** It used
26
+ to start any idle or stopped session while a preview was mounted — including one
27
+ the user had stopped, one never started, and one whose permission had been revoked.
28
+ A foreground that arrives while the release is still running now waits for it
29
+ instead of being dropped.
30
+ - **Use after dispose is defined.** `start`, `resume`, `capture`, and the permission
31
+ and capability reads reject with `CameraError('notReady')`; `stop`, `pause` and
32
+ `cleanup` are safe no-ops.
33
+ - **VisionCamera captures can be cleaned up.** They were saved in VisionCamera's own
34
+ temporary directory, outside the store, so `cleanup()` refused them and the 24-hour
35
+ sweep never saw them. The controller now moves each package-temporary capture into
36
+ the store (`ArtifactStore.adopt`), and the sweep spares captures adopted by the
37
+ running process. With no store, or a bridge without `move()`, the logger reports
38
+ `unmanaged:no-store` / `unmanaged:store-cannot-adopt` once.
39
+ - **No more "Camera is not active" error on every session start.** The feed sent
40
+ `torchMode="off"` on mount; VisionCamera applies it before the camera is active
41
+ and leaves the native rejection unhandled. The prop is now left unset until the
42
+ torch is first turned on.
43
+ - **One throwing state listener no longer breaks a transition** or starves the other
44
+ listeners; it is logged as `listener-threw`.
45
+
46
+ ### Changed
47
+
48
+ - **`effectiveQuality` and `effectiveMaxDimension` report what was applied.** The
49
+ VisionCamera adapter echoed the request hints. Quality is fixed on the photo output
50
+ (0.9) and nothing resizes, so it now reports `0.9` and an unavailable max dimension.
51
+ - **Unmounting a preview over an idle controller leaves it `idle`**, rather than
52
+ driving it through `stopping` to `stopped` and calling the engine's `stop()`.
53
+
54
+ ### Added
55
+
56
+ - `ArtifactStore.adopt?(uri)` and `FileSystemBridge.move?(from, to)`, both optional.
57
+ `createMemoryFileSystem()` implements `move`.
58
+
59
+ ### Docs
60
+
61
+ - The README no longer claims quality and max-dimension hints are applied, or a
62
+ single active session per process.
63
+
5
64
  ## 0.1.0 — 2026-07-26
6
65
 
7
66
  Initial release. New package, no existing consumers, **no migration required**.
@@ -10,8 +69,8 @@ Initial release. New package, no existing consumers, **no migration required**.
10
69
 
11
70
  - **Permission handling** as a seven-state contract (`undetermined`, `requesting`, `granted`, `denied`, `deniedPermanently`, `unavailable`, `unsupported`). Reading status never prompts; requesting never re-prompts after a permanent denial; revocations made in system settings are pushed to subscribers without an app restart.
12
71
  - **`CameraPreview`** surface with `cover`/`contain` resize modes, an overlay slot via `children`, and pass-through of consumer accessibility properties. Ships no styled UI and takes no theme dependency.
13
- - **Still capture** returning a file reference plus metadata. Undeterminable fields (`fileSize`, `orientation`) are explicitly marked unavailable rather than guessed or omitted. Quality and max-dimension hints are applied within device limits and reported back as effective values.
14
- - **Session lifecycle** — start, pause, resume, stop — that releases the camera on background, unmount, and stop, restarts on foreground when a surface is still mounted, and reports a single active session per process.
72
+ - **Still capture** returning a file reference plus metadata. Undeterminable fields (`fileSize`, `orientation`) are explicitly marked unavailable rather than guessed or omitted. Quality and max-dimension hints are normalized and reported back as effective values.
73
+ - **Session lifecycle** — start, pause, resume, stop — that releases the camera on background, unmount, and stop, restarts on foreground when a surface is still mounted, and reports a single active session per controller.
15
74
  - **Artifact ownership and cleanup**: an optional `destination` on the capture request switches the result to app-owned and writes there directly; everything else lands in the package cache directory. `cleanup()` is idempotent and refuses anything the package does not own. Orphans are swept at initialization under a 24-hour time-to-live.
16
75
  - **Capability reporting** for camera positions, flash, torch, orientation, and camera-less devices, with capture-time flash modes and a continuous torch that is forced off on pause, stop, and background.
17
76
  - **Coordinate mapping** — pure, bidirectional, accounting for scale, letterbox or crop, orientation, and front-camera mirroring, accurate within 2 device-independent pixels on a round trip.
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Reusable camera infrastructure for ChipMobileSdk-family React Native apps: permission-gated preview, still capture with consistent metadata, a session lifecycle that reliably releases the camera, app-controlled artifact retention, capability-gated flash and torch, coordinate mapping for app-drawn overlays, and a test adapter.
4
4
 
5
- The package is **infrastructure, not a feature**. It contains no business logic, does not know what your photos are for, sends nothing over the network, and writes nothing to the device gallery.
6
-
7
- > **License:** `UNLICENSED`. Published publicly for use by the owner's
8
- > applications; no open-source license is granted.
5
+ The package is **infrastructure, not a feature**. It contains no business logic, does not know what your photos are for, sends nothing over the network, and writes nothing to the device gallery.
9
6
 
10
- - **Status**: `0.1.0`, public npm package
7
+ > **License:** `UNLICENSED`. Published publicly for use by the owner's
8
+ > applications; no open-source license is granted.
9
+
10
+ - **Status**: `0.2.x`, public npm package (see [CHANGELOG](CHANGELOG.md))
11
11
  - **Entrypoint**: `src/index.ts` — the only public entrypoint
12
12
  - **Native code authored here**: none (see [Native capability](#native-capability))
13
13
 
@@ -25,14 +25,14 @@ The package is **infrastructure, not a feature**. It contains no business logic,
25
25
 
26
26
  ## Install (consuming app)
27
27
 
28
- ```bash
29
- npm install @chipmobilesdk/rn-camera react-native-vision-camera react-native-nitro-modules react-native-nitro-image
30
- cd ios && pod install && cd ..
31
- ```
32
-
33
- `react-native-vision-camera` is a **peer dependency** — your app installs and links it once, exactly as it does `react-native-keychain` for `@chipmobilesdk/rn-auth`.
34
- VisionCamera 5.1.x in turn requires the Nitro modules and image packages shown
35
- above; they remain app-owned native dependencies.
28
+ ```bash
29
+ npm install @chipmobilesdk/rn-camera react-native-vision-camera react-native-nitro-modules react-native-nitro-image
30
+ cd ios && pod install && cd ..
31
+ ```
32
+
33
+ `react-native-vision-camera` is a **peer dependency** — your app installs and links it once, exactly as it does `react-native-keychain` for `@chipmobilesdk/rn-auth`.
34
+ VisionCamera 5.1.x in turn requires the Nitro modules and image packages shown
35
+ above; they remain app-owned native dependencies.
36
36
 
37
37
  > **If your app also uses `react-native-unistyles`** (as the ChipMobileSdk demo app does), both it and VisionCamera depend on `react-native-nitro-modules`, and only one Nitro version can exist in a build. After installing, verify your theme system still works on both platforms before going further.
38
38
 
@@ -56,57 +56,57 @@ That is the only declaration the package requires. No microphone, no photo libra
56
56
 
57
57
  ## Quick start
58
58
 
59
- ```tsx
60
- import React, { useCallback, useEffect, useState } from 'react';
61
- import {
62
- CameraPreview,
63
- createVisionCameraEngine,
64
- createVisionCameraPermissions,
65
- useCameraController,
66
- useCameraState,
67
- type CapturedImage,
68
- } from '@chipmobilesdk/rn-camera';
69
-
70
- function CameraScreen() {
71
- const [photo, setPhoto] = useState<CapturedImage | null>(null);
72
- const controller = useCameraController({
73
- engine: createVisionCameraEngine(),
74
- permissions: createVisionCameraPermissions(),
75
- });
76
- const state = useCameraState(controller);
77
-
78
- useEffect(() => {
79
- void (async () => {
80
- await controller.getCapabilities();
81
- if (await controller.getPermission() === 'granted') {
82
- await controller.start();
83
- }
84
- })();
85
- }, [controller]);
86
-
87
- const requestPermission = useCallback(async () => {
88
- if (await controller.requestPermission() === 'granted') {
89
- await controller.start();
90
- }
91
- }, [controller]);
92
-
93
- if (state.permission !== 'granted') {
94
- // Your copy, your layout — the package supplies state, not UI.
95
- return <PermissionGate onRequest={requestPermission} state={state.permission} />;
96
- }
59
+ ```tsx
60
+ import React, { useCallback, useEffect, useState } from 'react';
61
+ import {
62
+ CameraPreview,
63
+ createVisionCameraEngine,
64
+ createVisionCameraPermissions,
65
+ useCameraController,
66
+ useCameraState,
67
+ type CapturedImage,
68
+ } from '@chipmobilesdk/rn-camera';
69
+
70
+ function CameraScreen() {
71
+ const [photo, setPhoto] = useState<CapturedImage | null>(null);
72
+ const controller = useCameraController({
73
+ engine: createVisionCameraEngine(),
74
+ permissions: createVisionCameraPermissions(),
75
+ });
76
+ const state = useCameraState(controller);
77
+
78
+ useEffect(() => {
79
+ void (async () => {
80
+ await controller.getCapabilities();
81
+ if (await controller.getPermission() === 'granted') {
82
+ await controller.start();
83
+ }
84
+ })();
85
+ }, [controller]);
86
+
87
+ const requestPermission = useCallback(async () => {
88
+ if (await controller.requestPermission() === 'granted') {
89
+ await controller.start();
90
+ }
91
+ }, [controller]);
92
+
93
+ if (state.permission !== 'granted') {
94
+ // Your copy, your layout — the package supplies state, not UI.
95
+ return <PermissionGate onRequest={requestPermission} state={state.permission} />;
96
+ }
97
97
 
98
98
  return (
99
99
  <CameraPreview controller={controller} accessibilityLabel="Camera preview">
100
100
  <ShutterButton onPress={async () => setPhoto(await controller.capture())} />
101
101
  </CameraPreview>
102
102
  );
103
- }
104
- ```
105
-
106
- `CameraPreview` mounts the native feed only after `controller.start()` reaches
107
- the `active` state. `getPermission()` only reads state; it does not prompt or
108
- start a session. The controller hook releases the session when the screen
109
- unmounts.
103
+ }
104
+ ```
105
+
106
+ `CameraPreview` mounts the native feed only after `controller.start()` reaches
107
+ the `active` state. `getPermission()` only reads state; it does not prompt or
108
+ start a session. The controller hook releases the session when the screen
109
+ unmounts.
110
110
 
111
111
  ## Core concepts
112
112
 
@@ -129,42 +129,65 @@ image.fileSize.available ? image.fileSize.value : 'not reported by this platform
129
129
 
130
130
  A bare optional could not tell "the platform does not report this" from "nobody asked", which is why the wrapper exists.
131
131
 
132
- ### You decide what is kept
133
-
134
- The returned ownership metadata is decided at capture time by one thing —
135
- whether you pass a `destination`:
136
-
137
- | | No `destination` | With `destination` |
138
- |---|---|---|
139
- | Ownership | `packageTemporary` | `appOwned` |
140
- | Default VisionCamera write | Native temporary file | Your path, directly |
141
- | Automatic persistence | No | Controlled by your destination |
132
+ ### You decide what is kept
133
+
134
+ The returned ownership metadata is decided at capture time by one thing —
135
+ whether you pass a `destination`:
136
+
137
+ | | No `destination` | With `destination` |
138
+ |---|---|---|
139
+ | Ownership | `packageTemporary` | `appOwned` |
140
+ | Default VisionCamera write | Native temporary file | Your path, directly |
141
+ | Automatic persistence | No | Controlled by your destination |
142
142
 
143
143
  ```ts
144
144
  const temp = await controller.capture(); // package-owned
145
145
  const kept = await controller.capture({ destination: '/my/app/x.jpg' }); // app-owned
146
146
 
147
- // cleanup() only acts when the controller was given an ArtifactStore and the
148
- // store reports that it owns this URI.
149
- await controller.cleanup(temp);
150
- ```
151
-
152
- The default VisionCamera adapter does not automatically move its native
153
- temporary file into the optional package store. With no `store`, `cleanup()` is
154
- a no-op and no sweep runs. To enable bounded cleanup, create an
155
- `ArtifactStore` with an app-supplied `FileSystemBridge`, pass it to the
156
- controller, and place managed files under that store's temporary directory.
157
- Initialization then sweeps only URIs the store owns, using a 24-hour default
158
- TTL. `cleanup()` is idempotent and refuses paths outside the store root.
147
+ // cleanup() only acts when the controller was given an ArtifactStore and the
148
+ // store reports that it owns this URI.
149
+ await controller.cleanup(temp);
150
+ ```
151
+
152
+ With a `store`, the controller moves every package-temporary capture out of
153
+ VisionCamera's own temporary directory into the store (`ArtifactStore.adopt`), so
154
+ `cleanup()` can delete it and the sweep can reach orphans. That move needs a
155
+ `FileSystemBridge` that implements `move()`; the app supplies the bridge, because
156
+ the package takes no filesystem dependency of its own:
157
+
158
+ ```ts
159
+ import * as RNFS from '@dr.pogodin/react-native-fs';
160
+
161
+ const store = createArtifactStore({
162
+ cacheDirectory: () => RNFS.CachesDirectoryPath,
163
+ exists: RNFS.exists,
164
+ unlink: RNFS.unlink,
165
+ list: async dir => (await RNFS.readDir(dir)).filter(e => e.isFile()).map(e => e.path),
166
+ createdAt: async path => (await RNFS.stat(path)).mtime.getTime(),
167
+ ensureDirectory: async dir => { if (!(await RNFS.exists(dir))) await RNFS.mkdir(dir); },
168
+ move: RNFS.moveFile,
169
+ });
170
+ ```
171
+
172
+ With no `store`, captures stay in VisionCamera's directory, `cleanup()` is a no-op,
173
+ no sweep runs, and an opted-in logger reports `unmanaged:no-store` once. The sweep
174
+ runs when the controller is first used, uses a 24-hour default TTL, touches only
175
+ URIs the store owns, and spares captures adopted by the running process.
176
+ `cleanup()` is idempotent and refuses paths outside the store root.
159
177
 
160
178
  ### The camera is released, not held
161
179
 
162
- Backgrounding **stops and releases** the camera rather than pausing it. Both operating systems can reclaim it anyway, and holding it risks leaving the OS camera indicator lit while your app is not in front of the user. Returning to the foreground restarts the preview if the surface is still mounted.
180
+ Backgrounding **stops and releases** the camera rather than pausing it. Both operating systems can reclaim it anyway, and holding it risks leaving the OS camera indicator lit while your app is not in front of the user. Returning to the foreground restarts the preview only if backgrounding interrupted a running session, the surface is still mounted, and permission is still granted — a camera the user stopped stays stopped.
181
+
182
+ Each controller permits one active session; starting the same controller again
183
+ reports `sessionConflict`.
163
184
 
164
- Each controller permits one active session; starting the same controller again
165
- reports `sessionConflict`. The package does not implement a process-wide lock
166
- across different controller instances, so an app should mount only one active
167
- camera preview at a time.
185
+ `useCameraController` disposes its controller on unmount and replaces it if React
186
+ remounts the component (StrictMode does this in development). A disposed controller
187
+ rejects `start`, `resume` and `capture` with `notReady`; `stop` and `cleanup` stay
188
+ safe to call. The package does not implement a process-wide lock
189
+ across different controller instances, so an app should mount only one active
190
+ camera preview at a time.
168
191
 
169
192
  ### Capability gating
170
193
 
@@ -189,20 +212,20 @@ The package draws no bounding boxes, guide frames, or reticles — those are whe
189
212
  const inImageSpace = mapRectToImage(myGuideFrame, geometry);
190
213
  ```
191
214
 
192
- The pure mapping functions account for scale, letterbox or crop, orientation,
193
- and front-camera mirroring using the geometry you supply. In the current
194
- `CameraPreview`, `onGeometryChange` reports nominal image dimensions
195
- (`1920 × 1080`), orientation `0`, and mirroring from camera position; it does
196
- not yet read the selected native capture format. Use capture metadata or
197
- app-owned measured geometry when pixel-exact alignment is required.
198
-
199
- ### Quality and dimensions
200
-
201
- `quality` and `maxDimension` are normalized into `effectiveQuality` and
202
- `effectiveMaxDimension` metadata. The current VisionCamera 5.1 adapter does
203
- not yet resize or re-encode the saved image, so these fields are hints rather
204
- than a guarantee about output bytes. Perform app-side image processing when an
205
- exact upload size or quality is required.
215
+ The pure mapping functions account for scale, letterbox or crop, orientation,
216
+ and front-camera mirroring using the geometry you supply. In the current
217
+ `CameraPreview`, `onGeometryChange` reports nominal image dimensions
218
+ (`1920 × 1080`), orientation `0`, and mirroring from camera position; it does
219
+ not yet read the selected native capture format. Use capture metadata or
220
+ app-owned measured geometry when pixel-exact alignment is required.
221
+
222
+ ### Quality and dimensions
223
+
224
+ The VisionCamera 5.1 adapter configures the photo output once, at quality `0.9`,
225
+ and does not resize. Each capture therefore reports `effectiveQuality: 0.9` and an
226
+ unavailable `effectiveMaxDimension`, whatever the request asked for; the request
227
+ hints are not applied. Perform app-side image processing when an exact upload size
228
+ or quality is required.
206
229
 
207
230
  ## Testing without hardware
208
231
 
@@ -268,7 +291,7 @@ Use this when completing your store declarations:
268
291
  | What data is involved? | Camera imagery generated on-device by explicit user action, plus technical metadata (dimensions, mime type, size, orientation, timestamp, camera position) |
269
292
  | Transmitted off device? | **No.** The package makes no network requests of any kind |
270
293
  | Shared with third parties? | **No** |
271
- | Stored persistently? | A capture is a file: the default adapter uses VisionCamera's native temporary location unless the app supplies a destination. The package has no history database and does not write to the gallery |
294
+ | Stored persistently? | A capture is a file: the default adapter uses VisionCamera's native temporary location unless the app supplies a destination. The package has no history database and does not write to the gallery |
272
295
  | Location attached? | **No.** No location data is read and no location permission is requested |
273
296
  | Written to the photo library? | **No** |
274
297
  | Tracking, ads, identifiers? | **None** |
@@ -305,37 +328,37 @@ Still capture and real-time frame processing stay strictly separate.
305
328
 
306
329
  Everything is exported from `@chipmobilesdk/rn-camera`. Modules under `core/`, `native/`, `permissions/`, `storage/`, `geometry/`, `diagnostics/`, and `testing/` are private — do not deep-import them.
307
330
 
308
- - Controller and state: `createCameraController`, `CameraController`,
309
- `CameraControllerOptions`, `useCameraController`, `useCameraState`,
310
- `CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
311
- - Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
312
- `CapturedImage`, `StartOptions` and the camera/session/capability types
313
- - Ports and adapters: `CameraEngine`, `EngineOptions`,
314
- `createVisionCameraEngine`, `PermissionPort`,
315
- `createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
316
- `StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
317
- - Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
318
- `mapRectToPreview`, plus `Point`, `Rect`, and `PreviewGeometry`
319
- - Diagnostics: `CameraError`, `CameraErrorCode`, `isCameraError`, `redact`, and
320
- the logger event/type contracts
321
- - Testing: `createTestCamera`, `createMemoryFileSystem`,
322
- `DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
323
- - Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
331
+ - Controller and state: `createCameraController`, `CameraController`,
332
+ `CameraControllerOptions`, `useCameraController`, `useCameraState`,
333
+ `CameraState`, `AppStatus`, `DEFAULT_TEMPORARY_TTL_MS`
334
+ - Preview and capture: `CameraPreview`, `CameraPreviewProps`, `CaptureRequest`,
335
+ `CapturedImage`, `StartOptions` and the camera/session/capability types
336
+ - Ports and adapters: `CameraEngine`, `EngineOptions`,
337
+ `createVisionCameraEngine`, `PermissionPort`,
338
+ `createVisionCameraPermissions`, `ArtifactStore`, `FileSystemBridge`,
339
+ `StoreOptions`, `createCacheArtifactStore`, `createArtifactStore`
340
+ - Mapping: `mapPointToImage`, `mapPointToPreview`, `mapRectToImage`,
341
+ `mapRectToPreview`, plus `Point`, `Rect`, and `PreviewGeometry`
342
+ - Diagnostics: `CameraError`, `CameraErrorCode`, `isCameraError`, `redact`, and
343
+ the logger event/type contracts
344
+ - Testing: `createTestCamera`, `createMemoryFileSystem`,
345
+ `DEFAULT_TEST_CAPABILITIES`, and their configuration/handle types
346
+ - Metadata helpers: `known`, `unavailable`, `Known`, `Unavailable`, `Maybe`
324
347
 
325
348
  Stable for semver: types, controller, preview, mapping, errors. Experimental and may change in a minor: the diagnostics logger shape and the test adapter configuration.
326
349
 
327
- ## License
328
-
329
- `UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
330
- license or permission to copy, modify, or redistribute is granted.
331
-
332
- ## Demo and validation
333
-
334
- The demo integration lives in `src/screens/CameraDemoScreen.tsx`. Run the
335
- package commands from the repository root (camera currently has no root-level
336
- shortcut):
337
-
338
- ```bash
339
- npm run typecheck --workspace @chipmobilesdk/rn-camera
340
- npm run pack:dry-run --workspace @chipmobilesdk/rn-camera
341
- ```
350
+ ## License
351
+
352
+ `UNLICENSED`. The npm tarball is publicly downloadable, but no open-source
353
+ license or permission to copy, modify, or redistribute is granted.
354
+
355
+ ## Demo and validation
356
+
357
+ The demo integration lives in `src/screens/CameraDemoScreen.tsx`. Run the
358
+ package commands from the repository root (camera currently has no root-level
359
+ shortcut):
360
+
361
+ ```bash
362
+ npm run typecheck --workspace @chipmobilesdk/rn-camera
363
+ npm run pack:dry-run --workspace @chipmobilesdk/rn-camera
364
+ ```
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@chipmobilesdk/rn-camera",
3
- "version": "0.1.0",
4
- "private": false,
5
- "license": "UNLICENSED",
6
- "main": "src/index.ts",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "license": "UNLICENSED",
6
+ "main": "src/index.ts",
7
7
  "types": "src/index.ts",
8
8
  "exports": {
9
9
  ".": {
@@ -23,6 +23,7 @@
23
23
  "src/react",
24
24
  "src/storage",
25
25
  "src/testing",
26
+ "skills",
26
27
  "README.md",
27
28
  "CHANGELOG.md"
28
29
  ],
@@ -39,8 +40,8 @@
39
40
  "engines": {
40
41
  "node": ">=22.11.0"
41
42
  },
42
- "publishConfig": {
43
- "access": "public",
44
- "registry": "https://registry.npmjs.org/"
45
- }
46
- }
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org/"
46
+ }
47
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: sdk-rn-camera
3
+ description: "Camera capture via @chipmobilesdk/rn-camera: controller, VisionCamera engine, permissions, artifact storage, hardware-free testing. Use when adding capture/preview, handling camera permissions, storing captures, or writing camera tests."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-camera"
6
+ packageVersion: "0.1.0"
7
+ user-invocable: true
8
+ disable-model-invocation: false
9
+ ---
10
+
11
+ # @chipmobilesdk/rn-camera
12
+
13
+ Camera capture built on VisionCamera, with an injectable engine, a permission port,
14
+ artifact storage for captures, and a test double that needs no hardware.
15
+
16
+ ## Use this when
17
+
18
+ - Adding a capture or preview screen
19
+ - Requesting/handling camera permissions
20
+ - Persisting captured images and managing their lifetime
21
+ - Writing tests for a camera flow
22
+ - Answering a privacy/data-safety question about captures
23
+
24
+ ## Release the camera on unmount
25
+
26
+ The camera is a hardware resource. It MUST be released when the screen loses focus or
27
+ unmounts — a retained camera drains battery and blocks other screens from acquiring it.
28
+ Verify teardown explicitly; this is the most common defect in camera code. Prefer
29
+ `useCameraController`: it disposes on unmount and survives StrictMode remounts. A
30
+ disposed controller rejects `start`/`capture` with `notReady`.
31
+
32
+ ## Captures expire
33
+
34
+ `DEFAULT_TEMPORARY_TTL_MS` exists because cache-stored captures are temporary. If a
35
+ capture must survive, pass a `destination` (app-owned) or move it out deliberately —
36
+ do not assume persistence. Give the controller a `store` whose `FileSystemBridge`
37
+ implements `move()`, or VisionCamera captures stay outside it and `cleanup()` cannot
38
+ delete them. See `## Data behaviour` for what this means for App Privacy disclosures.
39
+
40
+ ## Test without hardware
41
+
42
+ `createTestCamera`, `createMemoryFileSystem` and `DEFAULT_TEST_CAPABILITIES` let camera
43
+ flows be unit-tested with no device. Use these rather than mocking VisionCamera by hand —
44
+ see `## Testing without hardware`.
45
+
46
+ ## Public API (v0.2.0)
47
+
48
+ Runtime:
49
+
50
+ - `createCameraController(...)`, `useCameraController`, `useCameraState`
51
+ - `createVisionCameraEngine(...)` — the default `CameraEngine`; injectable
52
+ - `createVisionCameraPermissions(...)` — implements `PermissionPort`
53
+ - `createArtifactStore(...)`, `createCacheArtifactStore(...)`
54
+ - `CameraPreview` (+ `CameraPreviewProps`)
55
+ - `CameraError`, `isCameraError`, `redact` — `redact` strips sensitive paths before logging
56
+ - Testing: `createTestCamera`, `createMemoryFileSystem`, `DEFAULT_TEST_CAPABILITIES`
57
+
58
+ Types: `CameraEngine`, `EngineOptions`, `PermissionPort`, `ArtifactStore`,
59
+ `FileSystemBridge`, `StoreOptions`, `CameraErrorCode`, `CameraLogEvent`,
60
+ `CameraLogEventType`, `CameraLogger`
61
+
62
+ Always pass camera log payloads through `redact` before logging.
63
+
64
+ ## Where the detail lives
65
+
66
+ Read one section of `node_modules/@chipmobilesdk/rn-camera/README.md` (~340 lines):
67
+
68
+ | Need | Section |
69
+ |---|---|
70
+ | First integration | `## Install (consuming app)`, `## Quick start` |
71
+ | Engine / port / store model | `## Core concepts` |
72
+ | Tests | `## Testing without hardware` |
73
+ | Failure handling | `## Errors`, `## Diagnostics` |
74
+ | Privacy disclosures | `## Data behaviour (for App Privacy and Data Safety)` |
75
+ | Native config | `## Native capability`, `## Compatibility` |
@@ -93,26 +93,28 @@ export function finalizeResult(
93
93
  }
94
94
 
95
95
  /**
96
- * Serializes captures. Holding the in-flight promise rather than a boolean
97
- * means the guard cannot be defeated by two synchronous calls in the same tick.
96
+ * Serializes captures. The slot is taken synchronously, before the operation
97
+ * runs, so two calls in the same tick cannot both pass the check — and the
98
+ * operation's own first `emit()` already sees `busy`. Holding the operation's
99
+ * promise instead set the slot only after that first emit, so `capturing: true`
100
+ * was never published at all.
98
101
  */
99
102
  export class SingleFlight {
100
- private inFlight: Promise<CapturedImage> | null = null;
103
+ private active = false;
101
104
 
102
105
  get busy(): boolean {
103
- return this.inFlight !== null;
106
+ return this.active;
104
107
  }
105
108
 
106
109
  async run(operation: () => Promise<CapturedImage>): Promise<CapturedImage> {
107
- if (this.inFlight) {
110
+ if (this.active) {
108
111
  throw new CameraError('captureInProgress', 'A capture is already in progress');
109
112
  }
110
- const promise = operation();
111
- this.inFlight = promise;
113
+ this.active = true;
112
114
  try {
113
- return await promise;
115
+ return await operation();
114
116
  } finally {
115
- this.inFlight = null;
117
+ this.active = false;
116
118
  }
117
119
  }
118
120
  }