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>
3.8 KiB
3.8 KiB
| convoy | brief_number | depends_on | recommended_model | model_tier | files | cross_brief_commitments | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| scanner-desktop-layout | 1 | composer-2.5-fast | fast |
|
|
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.jstest/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.
getUserMediaconstraints: whenselectedDeviceIdis set, use{ video: { deviceId: { exact: selectedDeviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } }; when unset, keep existingfacingModeconstraint shape.- Call
enumerateDevicesafter the first successful stream (labels require an active permission grant). Filterkind === 'videoinput'. - Optional cheap persist:
sessionStoragekeyscanner:last-camera-device-id— read on init, write onsetSelectedDeviceId. - Restart stream on
selectedDeviceIdchange (same pattern asactiveFacingModeeffect). Guard with a ref to skip the initial mount double-start. verificationPausedRefcontract unchanged — camera hook does not own pause logic.- Mirror existing test style in
test/lib/scanner-card-detection.test.js(vitest, mocks fornavigator.mediaDevices).
Implementation shape (verified against current hook)
Current hook returns facingMode, switchFacingMode only — no deviceId.
startCamera uses facingMode: activeFacingModeRef.current exclusively.
Add:
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:
- If
!navigator.mediaDevices?.enumerateDevices, set statuserror. - Map videoinputs; if length === 0 →
empty. - If
selectedDeviceIdnot in list, fall back to first device or ''. - On
NotAllowedErrorfrom prior getUserMedia →denied.
Export setSelectedDeviceId wrapper that persists to sessionStorage.
Do not remove switchFacingMode — mobile Brief 3 camera chrome still uses it.
Acceptance criteria
enumerateDevicespopulatesvideoDevicesafter stream start in tests (mocked)selectedDeviceIdswitchesgetUserMediaconstraints todeviceId: { exact }switchFacingModestill toggles environment/user when no deviceId forceddevicePickerStatus+ 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.