125 lines
5.5 KiB
Markdown
125 lines
5.5 KiB
Markdown
|
|
---
|
||
|
|
convoy: scanner-mobile-checkout
|
||
|
|
brief_number: 3
|
||
|
|
depends_on: [2]
|
||
|
|
recommended_model: composer-2.5-fast
|
||
|
|
model_tier: fast
|
||
|
|
files:
|
||
|
|
- components/scanner/ScannerCamera.js
|
||
|
|
- components/scanner/ScannerCountPill.js
|
||
|
|
- components/scanner/ScannerScanPeek.js
|
||
|
|
- components/scanner/ScannerToast.js
|
||
|
|
- lib/use-camera-scanner.js
|
||
|
|
- lib/use-scanner-identification.js
|
||
|
|
cross_brief_commitments:
|
||
|
|
- brief: 4
|
||
|
|
description: |
|
||
|
|
`ScannerCamera` accepts orchestration props wired by Brief 4:
|
||
|
|
`onBack`, `onOpenCheckout`, `onGalleryIdentify`, `latestPeekCard`,
|
||
|
|
`cartCount`, `isCheckoutOpen`, `verificationPausedRef`. Defaults/no-ops
|
||
|
|
keep the component renderable before Brief 4 lands. Brief 4 owns sheet
|
||
|
|
open state and back-guard modal.
|
||
|
|
- brief: 4
|
||
|
|
description: |
|
||
|
|
`useScannerIdentification` exports `identifyFromGalleryFile(file)` —
|
||
|
|
thin adapter only; does not modify `pages/api/scan/identify.js` or
|
||
|
|
`lib/scanner-card-identify.js` server contract.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Brief 3: Camera chrome, facing mode, scan peek, gallery adapter
|
||
|
|
|
||
|
|
## Goal (1 sentence)
|
||
|
|
|
||
|
|
Refactor `ScannerCamera` into full-bleed immersive glass chrome (top/bottom bars, flash, camera swap, Review N pill, scan peek, gallery) and add rear/front `facingMode` support in `useCameraScanner`.
|
||
|
|
|
||
|
|
## Files in scope (do not edit anything else)
|
||
|
|
|
||
|
|
- `components/scanner/ScannerCamera.js`
|
||
|
|
- `components/scanner/ScannerCountPill.js`
|
||
|
|
- `components/scanner/ScannerScanPeek.js` (new)
|
||
|
|
- `components/scanner/ScannerToast.js`
|
||
|
|
- `lib/use-camera-scanner.js`
|
||
|
|
- `lib/use-scanner-identification.js` (gallery adapter + pause contract only)
|
||
|
|
|
||
|
|
## Conventions to follow
|
||
|
|
|
||
|
|
- Glass surfaces: `<GlassSurface tint="mid">` per UX brief; migrate hardcoded `rgba(0,0,0,0.55)` in `ScannerCamera` to tokens.
|
||
|
|
- `ScannerToast`: replace `ICON_MAP` Unicode glyphs with inline SVG check/error (design-direction checklist).
|
||
|
|
- `ScannerCountPill`: evolve to gradient **Review {N}** pill; keep `aria-label={`Review ${count} scanned cards`}`.
|
||
|
|
- Flash: `flash.flashSupported && facingMode === 'environment'` (UX spec). Hide on front camera; turning to front must call `toggleFlash` off if torch was on.
|
||
|
|
- Peek: new `ScannerScanPeek.js` — 3s auto-dismiss, `pointer-events-none` wrapper, single child button `pointer-events-auto`, tap calls `onOpenCheckout`.
|
||
|
|
- Copy: toast message `"{card.name} added"` with `role="status"`.
|
||
|
|
- Do **not** mount checkout sheet here — call `onOpenCheckout` only.
|
||
|
|
- Identification pipeline files (`scanner-card-identify.js`, API routes) are out of scope.
|
||
|
|
|
||
|
|
## `useCameraScanner` shape (verified)
|
||
|
|
|
||
|
|
Current `startCamera` hardcodes `facingMode: 'environment'` (line ~119). Extend:
|
||
|
|
|
||
|
|
```js
|
||
|
|
export function useCameraScanner({ onError, onVerifyCard, verificationPausedRef, facingMode = 'environment' }) {
|
||
|
|
const [activeFacingMode, setActiveFacingMode] = useState(facingMode);
|
||
|
|
// startCamera uses activeFacingMode in getUserMedia constraints
|
||
|
|
const switchFacingMode = useCallback(() => {
|
||
|
|
setActiveFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment'));
|
||
|
|
}, []);
|
||
|
|
// useEffect: when activeFacingMode changes and stream exists, stopCamera + startCamera
|
||
|
|
return { /* existing */, facingMode: activeFacingMode, switchFacingMode };
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`verificationPausedRef` early-return already exists at line ~91 in the tracking interval — no change needed.
|
||
|
|
|
||
|
|
## Gallery adapter (verified — no existing file-picker path)
|
||
|
|
|
||
|
|
Add to `useScannerIdentification` return object:
|
||
|
|
|
||
|
|
```js
|
||
|
|
const identifyFromGalleryFile = async (file) => {
|
||
|
|
if (!file || verificationPausedRef?.current) return;
|
||
|
|
const imageData = await readFileToImageData(file); // canvas draw; local helper in same file
|
||
|
|
const authHeaders = getScanAuthHeaders();
|
||
|
|
const result = await tryLayer1TextIdentify(imageData, authHeaders);
|
||
|
|
const outcome = resolveIdentifyOutcome(result);
|
||
|
|
const syntheticTracker = { id: `gallery-${Date.now()}`, status: 'verifying' };
|
||
|
|
await applyIdentifyOutcome(syntheticTracker, imageData, outcome);
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
Import `tryLayer1TextIdentify`, `resolveIdentifyOutcome` from `./scanner-card-identify.js` (already exported — verified).
|
||
|
|
|
||
|
|
## `ScannerCamera` props contract (for Brief 4)
|
||
|
|
|
||
|
|
```js
|
||
|
|
export default function ScannerCamera({
|
||
|
|
queue,
|
||
|
|
camera,
|
||
|
|
identification,
|
||
|
|
onBack,
|
||
|
|
onOpenCheckout,
|
||
|
|
onGalleryIdentify, // (file) => identification.identifyFromGalleryFile(file)
|
||
|
|
latestPeekCard, // most recent unprocessed queue entry or null
|
||
|
|
cartCount,
|
||
|
|
isCheckoutOpen,
|
||
|
|
verificationPausedRef,
|
||
|
|
}) { /* ... */ }
|
||
|
|
```
|
||
|
|
|
||
|
|
Remove: `deckMode`, `sessionDestination`, `onStopSession`, phase-based stop button. Back button calls `onBack` (Brief 4 shows leave modal).
|
||
|
|
|
||
|
|
Layout: viewport `min-h-[100dvh] max-md:rounded-none max-md:min-h-[100dvh]`; top bar with back / "Scan Cards" / gallery file input; bottom bar flash | status | Review N.
|
||
|
|
|
||
|
|
## Acceptance criteria
|
||
|
|
|
||
|
|
- [ ] `switchFacingMode` restarts stream with toggled `facingMode`; flash hidden on `user`
|
||
|
|
- [ ] `ScannerScanPeek` auto-dismisses ~3s; tap invokes `onOpenCheckout`
|
||
|
|
- [ ] `ScannerToast` uses SVG icons, not Unicode glyphs
|
||
|
|
- [ ] `identifyFromGalleryFile` calls existing `tryLayer1TextIdentify` — no API route changes
|
||
|
|
- [ ] `ScannerCountPill` hidden when `count <= 0`; shows gradient Review pill when `count > 0`
|
||
|
|
- [ ] No hex literals added in edited `.js` files
|
||
|
|
- [ ] No scope expansion
|
||
|
|
|
||
|
|
## Rationale (≤3 sentences)
|
||
|
|
|
||
|
|
Camera chrome files are disjoint from `pages/scanner.js`, so this brief can land after the cart model without file conflicts. Gallery needs a thin hook adapter because no file-picker path exists today (boot-the-brief finding). Brief 4 wires props and pause ref when the sheet opens.
|