Give /scanner a md+ camera, live match inspector, and history strip (with device picker, batch scan, and tips) without regressing the mobile immersive checkout. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.8 KiB
Markdown
90 lines
3.8 KiB
Markdown
---
|
|
convoy: scanner-desktop-layout
|
|
brief_number: 1
|
|
depends_on: []
|
|
recommended_model: composer-2.5-fast
|
|
model_tier: fast
|
|
files:
|
|
- lib/use-camera-scanner.js
|
|
- test/lib/use-camera-scanner.test.js
|
|
cross_brief_commitments:
|
|
- brief: 6
|
|
description: |
|
|
Exports `videoDevices`, `selectedDeviceId`, `setSelectedDeviceId`,
|
|
`devicePickerStatus` (`loading` | `ready` | `empty` | `denied` | `error`),
|
|
and `devicePickerMessage` for the desk Camera `<select>` in Brief 6.
|
|
Mobile callers keep using `facingMode` / `switchFacingMode` only — do not
|
|
mount the device picker below `md`.
|
|
---
|
|
|
|
# Brief 1: Webcam device picker hook
|
|
|
|
## Goal (1 sentence)
|
|
|
|
Extend `useCameraScanner` with `enumerateDevices` + `deviceId` stream selection for desktop webcams while preserving mobile facing-mode swap.
|
|
|
|
## Files in scope (do not edit anything else)
|
|
|
|
- `lib/use-camera-scanner.js`
|
|
- `test/lib/use-camera-scanner.test.js`
|
|
|
|
## Conventions to follow
|
|
|
|
- Tagged-template / relative imports only; no path aliases.
|
|
- No hex in JS — use CSS variables if any inline styles are needed in tests.
|
|
- `getUserMedia` constraints: when `selectedDeviceId` is set, use
|
|
`{ video: { deviceId: { exact: selectedDeviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } }`;
|
|
when unset, keep existing `facingMode` constraint shape.
|
|
- Call `enumerateDevices` after the first successful stream (labels require an
|
|
active permission grant). Filter `kind === 'videoinput'`.
|
|
- Optional cheap persist: `sessionStorage` key `scanner:last-camera-device-id`
|
|
— read on init, write on `setSelectedDeviceId`.
|
|
- Restart stream on `selectedDeviceId` change (same pattern as `activeFacingMode`
|
|
effect). Guard with a ref to skip the initial mount double-start.
|
|
- `verificationPausedRef` contract unchanged — camera hook does not own pause logic.
|
|
- Mirror existing test style in `test/lib/scanner-card-detection.test.js` (vitest,
|
|
mocks for `navigator.mediaDevices`).
|
|
|
|
## Implementation shape (verified against current hook)
|
|
|
|
Current hook returns `facingMode`, `switchFacingMode` only — no `deviceId`.
|
|
`startCamera` uses `facingMode: activeFacingModeRef.current` exclusively.
|
|
|
|
Add:
|
|
|
|
```js
|
|
const [videoDevices, setVideoDevices] = useState([]);
|
|
const [selectedDeviceId, setSelectedDeviceId] = useState(() => {
|
|
try {
|
|
return sessionStorage.getItem('scanner:last-camera-device-id') || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
});
|
|
const [devicePickerStatus, setDevicePickerStatus] = useState('loading');
|
|
```
|
|
|
|
`refreshVideoDevices` async helper:
|
|
|
|
1. If `!navigator.mediaDevices?.enumerateDevices`, set status `error`.
|
|
2. Map videoinputs; if length === 0 → `empty`.
|
|
3. If `selectedDeviceId` not in list, fall back to first device or ''.
|
|
4. On `NotAllowedError` from prior getUserMedia → `denied`.
|
|
|
|
Export `setSelectedDeviceId` wrapper that persists to sessionStorage.
|
|
|
|
**Do not** remove `switchFacingMode` — mobile Brief 3 camera chrome still uses it.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [ ] `enumerateDevices` populates `videoDevices` after stream start in tests (mocked)
|
|
- [ ] `selectedDeviceId` switches `getUserMedia` constraints to `deviceId: { exact }`
|
|
- [ ] `switchFacingMode` still toggles environment/user when no deviceId forced
|
|
- [ ] `devicePickerStatus` + message cover loading, empty, denied, error per Design direction table
|
|
- [ ] sessionStorage persist for last device id (read/write with try/catch)
|
|
- [ ] tests added in `test/lib/use-camera-scanner.test.js`
|
|
- [ ] no scope expansion (do not edit files outside `files:` above)
|
|
|
|
## Rationale (≤3 sentences)
|
|
|
|
Desktop workstations need a real webcam picker (C4), not a relabeled facing-mode swap. Isolating device enumeration in the hook keeps `ScannerCamera` and `pages/scanner.js` thin. Mobile behavior stays untouched because deviceId is only consumed at the page/camera wiring layer in Brief 6.
|