Skip to content

Commit fa8d90f

Browse files
committed
fix: change qr code scanner to one that is supported by safari
1 parent c772132 commit fa8d90f

5 files changed

Lines changed: 39 additions & 51 deletions

File tree

client/web/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"date-fns": "^4.1.0",
4747
"embla-carousel-react": "^8.6.0",
4848
"input-otp": "^1.4.2",
49+
"jsqr": "^1.4.0",
4950
"lucide-react": "^0.562.0",
5051
"next-themes": "^0.4.6",
5152
"react": "^19.2.0",

client/web/src/pages/admin/scans/components/ScannerDialog.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function ScannerDialog() {
3333
[performScan],
3434
);
3535

36-
const { videoRef, error, supported } = useQrScanner({
36+
const { videoRef, error } = useQrScanner({
3737
enabled: !!activeScanType,
3838
paused: !!lastScanResult || scanning,
3939
onDetect: handleScan,
@@ -61,17 +61,7 @@ export function ScannerDialog() {
6161
</DialogHeader>
6262

6363
<div className="relative">
64-
{!supported ? (
65-
<div className="flex aspect-square items-center justify-center rounded-lg bg-muted p-6 text-center text-sm text-muted-foreground">
66-
<div className="space-y-2">
67-
<AlertCircle className="mx-auto size-8" />
68-
<p>
69-
QR scanning is not supported in this browser. Please use
70-
Chrome or Safari.
71-
</p>
72-
</div>
73-
</div>
74-
) : error ? (
64+
{error ? (
7565
<div className="flex aspect-square items-center justify-center rounded-lg bg-muted p-6 text-center text-sm text-muted-foreground">
7666
<div className="space-y-2">
7767
<AlertCircle className="mx-auto size-8" />

client/web/src/pages/admin/scans/components/barcode-detector.d.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

client/web/src/pages/admin/scans/components/useQrScanner.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import jsQR from "jsqr";
12
import { useEffect, useRef, useState } from "react";
23

34
interface UseQrScannerOptions {
@@ -9,7 +10,6 @@ interface UseQrScannerOptions {
910
interface UseQrScannerReturn {
1011
videoRef: React.RefObject<HTMLVideoElement | null>;
1112
error: string | null;
12-
supported: boolean;
1313
}
1414

1515
const DETECTION_INTERVAL_MS = 100; // ~10 FPS
@@ -21,19 +21,18 @@ export function useQrScanner({
2121
}: UseQrScannerOptions): UseQrScannerReturn {
2222
const videoRef = useRef<HTMLVideoElement | null>(null);
2323
const streamRef = useRef<MediaStream | null>(null);
24+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
2425
const onDetectRef = useRef(onDetect);
2526
const [error, setError] = useState<string | null>(null);
2627

27-
const supported = "BarcodeDetector" in window;
28-
2928
// Keep callback ref current without restarting effects
3029
useEffect(() => {
3130
onDetectRef.current = onDetect;
3231
});
3332

3433
// Camera lifecycle
3534
useEffect(() => {
36-
if (!enabled || !supported) return;
35+
if (!enabled) return;
3736

3837
let cancelled = false;
3938

@@ -71,13 +70,17 @@ export function useQrScanner({
7170
video.srcObject = null;
7271
}
7372
};
74-
}, [enabled, supported]);
73+
}, [enabled]);
7574

7675
// Detection loop
7776
useEffect(() => {
78-
if (!enabled || paused || !supported) return;
77+
if (!enabled || paused) return;
78+
79+
if (!canvasRef.current) {
80+
canvasRef.current = document.createElement("canvas");
81+
}
82+
const canvas = canvasRef.current;
7983

80-
const detector = new BarcodeDetector({ formats: ["qr_code"] });
8184
let rafId: number;
8285
let lastTime = 0;
8386
let detected = false;
@@ -93,24 +96,31 @@ export function useQrScanner({
9396
if (!video || video.readyState < HTMLMediaElement.HAVE_ENOUGH_DATA)
9497
return;
9598

96-
detector
97-
.detect(video)
98-
.then((barcodes) => {
99-
if (detected || barcodes.length === 0) return;
100-
detected = true;
101-
onDetectRef.current(barcodes[0].rawValue);
102-
})
103-
.catch(() => {
104-
// Detection errors are non-fatal, continue loop
105-
});
99+
const { videoWidth, videoHeight } = video;
100+
if (videoWidth === 0 || videoHeight === 0) return;
101+
102+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
103+
if (!ctx) return;
104+
105+
canvas.width = videoWidth;
106+
canvas.height = videoHeight;
107+
ctx.drawImage(video, 0, 0, videoWidth, videoHeight);
108+
109+
const imageData = ctx.getImageData(0, 0, videoWidth, videoHeight);
110+
const result = jsQR(imageData.data, videoWidth, videoHeight);
111+
112+
if (result) {
113+
detected = true;
114+
onDetectRef.current(result.data);
115+
}
106116
}
107117

108118
rafId = requestAnimationFrame(tick);
109119

110120
return () => {
111121
cancelAnimationFrame(rafId);
112122
};
113-
}, [enabled, paused, supported]);
123+
}, [enabled, paused]);
114124

115-
return { videoRef, error, supported };
125+
return { videoRef, error };
116126
}

0 commit comments

Comments
 (0)