> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Barcode & QR scanning

> A camera-based barcode/QR scanner with react-webcam and the barcode-detector polyfill, tuned for reliable real-world scans.

The scanner runs a detection loop against the live `<video>` element and fires `onScan` only after the same value is confirmed across multiple frames. Both `react-webcam` and the `barcode-detector` polyfill are pre-installed.

## The scanner component

```tsx theme={null}
// apps/<app>/src/components/BarcodeScanner.tsx
import { useCallback, useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam';
import 'barcode-detector/pure';

// The polyfill registers BarcodeDetector on window; focusMode isn't in lib.dom yet.
declare global {
  class BarcodeDetector {
    constructor(options?: { formats: string[] });
    static getSupportedFormats(): Promise<string[]>;
    detect(image: ImageBitmapSource): Promise<{ rawValue: string; format: string }[]>;
  }
  interface MediaTrackConstraintSet {
    focusMode?: 'none' | 'manual' | 'single-shot' | 'continuous';
  }
}

// Mod-10 check digit catches most single-frame misreads on 1D retail barcodes.
const isValidEAN = (v: string, len: 8 | 13): boolean => {
  if (v.length !== len) return false;
  const nums = Array.from(v, c => c.charCodeAt(0) - 48);
  if (nums.some(n => n < 0 || n > 9)) return false;
  const check = nums.pop() as number;
  const weights = len === 13 ? [1, 3] : [3, 1];
  const sum = nums.reduce((acc, d, i) => acc + d * weights[i % 2], 0);
  return (10 - (sum % 10)) % 10 === check;
};
const isValidBarcode = (v: string, fmt: string): boolean =>
  fmt === 'ean_13' ? isValidEAN(v, 13)
  : fmt === 'ean_8' ? isValidEAN(v, 8)
  : fmt === 'upc_a' ? isValidEAN('0' + v, 13)
  : true; // qr_code / code_128 / code_39 / upc_e validate internally

type Props = {
  onScan: (value: string, format: string) => void;
  formats?: string[];
  confirmations?: number; // consecutive frames that must agree; default 2
};

export default function BarcodeScanner({
  onScan,
  formats = ['qr_code', 'ean_13', 'code_128'],
  confirmations = 2,
}: Props) {
  const webcamRef = useRef<Webcam>(null);
  const detectorRef = useRef<BarcodeDetector>();
  const [error, setError] = useState<string>();

  // Keep the latest callback/threshold in refs so the loop (mounted once) isn't
  // rebuilt on every parent render.
  const onScanRef = useRef(onScan);
  const confirmationsRef = useRef(confirmations);
  useEffect(() => { onScanRef.current = onScan; confirmationsRef.current = confirmations; });

  const formatsKey = formats.join(',');
  useEffect(() => {
    BarcodeDetector.getSupportedFormats().then(supported => {
      const toUse = formats.filter(f => supported.includes(f));
      if (!toUse.length) { setError(`No requested formats supported: ${formatsKey}`); return; }
      setError(undefined);
      detectorRef.current = new BarcodeDetector({ formats: toUse });
    }).catch(() => setError('Barcode detection not supported in this browser'));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [formatsKey]);

  // Continuous autofocus keeps a close barcode sharp on mobile.
  const handleUserMedia = useCallback((stream: MediaStream) => {
    stream.getVideoTracks()[0]
      ?.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }).catch(() => {});
  }, []);

  // One RAF detection loop per mount; all state is closure-local.
  useEffect(() => {
    let stopped = false, busy = false, lastValue: string | undefined, matchCount = 0, rafId = 0;
    const tick = async () => {
      const video = webcamRef.current?.video;
      // Gate on videoWidth — Chrome throws "Invalid element or state" before the first frame.
      if (video && detectorRef.current && !busy && video.readyState >= 2 && video.videoWidth > 0) {
        busy = true;
        try {
          const [hit] = await detectorRef.current.detect(video); // pass <video> directly
          if (stopped) return;
          if (hit && isValidBarcode(hit.rawValue, hit.format)) {
            if (hit.rawValue === lastValue) matchCount += 1;
            else { lastValue = hit.rawValue; matchCount = 1; }
            if (matchCount >= confirmationsRef.current) {
              stopped = true;
              navigator.vibrate?.(50);
              onScanRef.current(hit.rawValue, hit.format);
              return;
            }
          } else { lastValue = undefined; matchCount = 0; }
        } catch { /* transient frame error — skip, never kill the loop */ }
        finally { busy = false; }
      }
      if (!stopped) rafId = requestAnimationFrame(tick);
    };
    rafId = requestAnimationFrame(tick);
    return () => { stopped = true; cancelAnimationFrame(rafId); };
  }, []);

  if (error) return <div className="text-destructive">{error}</div>;
  return (
    <Webcam
      ref={webcamRef}
      audio={false}
      onUserMedia={handleUserMedia}
      videoConstraints={{ facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } }}
      className="w-full rounded-lg"
    />
  );
}
```

## Using it

```tsx theme={null}
const [scanned, setScanned] = useState<string>();

return scanned
  ? <SuccessMessage value={scanned} onNext={() => setScanned(undefined)} />
  : <BarcodeScanner onScan={value => setScanned(value)} />;
```

`onScan` receives `(value, format)`. Conditionally render the scanner so it unmounts and releases the camera once a scan lands. For back-to-back scanning, remount after each hit with a changing `key` to reset confirmation state.

## Why the defensive bits matter

* **Multi-frame confirmation** (`confirmations`, default 2) is the biggest defense against misreads — bump to 3 for high-stakes scans.
* **Check-digit validation** rejects single-character misreads on EAN-13 / EAN-8 / UPC-A for free.
* **Detect on the `<video>` element** — \~10× faster than screenshotting to base64, no JPEG artifacts.
* **`requestAnimationFrame` + busy lock**, not `setInterval` — scans at frame rate without overlapping calls.
* **HD constraints** (`1920×1080`) — the 640×480 default can't decode a retail barcode at arm's length.
* **Restrict `formats`** to what the app reads — fewer formats, fewer false positives.
