From 64584315e5d4db528a8176f85839a876bd0da470 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 19:51:08 +0000 Subject: [PATCH 1/2] Fix barcode scanner on iOS Safari iOS WebKit does not provide a reliable native Barcode Detection API, and ZXing often failed due to strict camera constraints and video startup timing. - Install @undecaf/barcode-detector-polyfill (ZBar WASM) on Apple devices - Fall back through progressively looser getUserMedia constraints - Wait for video metadata/playback before decoding frames - Throttle native scans on iOS and tune ZXing retry intervals - Defer scanner startup until the modal video element is mounted Co-authored-by: Ned Halksworth --- package-lock.json | 67 +++++------- package.json | 1 + src/components/BarcodeScannerModal.tsx | 44 +++++--- src/lib/barcodeDetectorSupport.ts | 35 +++++++ src/lib/barcodeScanner.ts | 138 ++++++++++++++++++++----- vite.config.ts | 1 + 6 files changed, 203 insertions(+), 83 deletions(-) create mode 100644 src/lib/barcodeDetectorSupport.ts diff --git a/package-lock.json b/package-lock.json index 7aee627..a41cf41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@undecaf/barcode-detector-polyfill": "^0.9.23", "@vitejs/plugin-react": "^4.3.4", "@zxing/browser": "^0.2.0", "appwrite": "^25.0.0", @@ -1160,9 +1161,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1176,9 +1174,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1192,9 +1187,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1208,9 +1200,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1224,9 +1213,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1240,9 +1226,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1256,9 +1239,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1272,9 +1252,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1288,9 +1265,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1304,9 +1278,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1320,9 +1291,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1336,9 +1304,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1352,9 +1317,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1880,6 +1842,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@undecaf/barcode-detector-polyfill": { + "version": "0.9.23", + "resolved": "https://registry.npmjs.org/@undecaf/barcode-detector-polyfill/-/barcode-detector-polyfill-0.9.23.tgz", + "integrity": "sha512-qVr7jSUbE5a30X9dByDym2NzsqyH+MFwyFiu4QSHDQMLCImTJj/et7pEcOtGqlL4UB5J6J3d0hK4/5d4MMowYA==", + "license": "MIT", + "dependencies": { + "@undecaf/zbar-wasm": "^0.9.16" + } + }, + "node_modules/@undecaf/zbar-wasm": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/@undecaf/zbar-wasm/-/zbar-wasm-0.9.16.tgz", + "integrity": "sha512-T5PcT6g+tLScGjR4WmnRErNvfKqEc3kRg2ux14wHmIDNbvNeXa0BkFK19PRK/jb6zGy5NyWtn4ko6KeNuZc/fQ==", + "license": "LGPL-2.1+", + "dependencies": { + "jschardet": "^3.0.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -3622,6 +3602,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", diff --git a/package.json b/package.json index 7e7fec6..517f57f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "setup:appwrite": "node scripts/setup-appwrite.mjs" }, "dependencies": { + "@undecaf/barcode-detector-polyfill": "^0.9.23", "@vitejs/plugin-react": "^4.3.4", "@zxing/browser": "^0.2.0", "appwrite": "^25.0.0", diff --git a/src/components/BarcodeScannerModal.tsx b/src/components/BarcodeScannerModal.tsx index 6bc5705..7caab25 100644 --- a/src/components/BarcodeScannerModal.tsx +++ b/src/components/BarcodeScannerModal.tsx @@ -191,24 +191,32 @@ export function BarcodeScannerModal({ window.setTimeout(() => closeButtonRef.current?.focus(), 80); let active = true; - const video = videoRef.current; - if (!video) return undefined; + let frameId = 0; - void startBarcodeScanner(video, handleScannerResult, handleScannerError) - .then((controller) => { - if (!active) { - controller.stop(); - return; - } - controllerRef.current = controller; - setScannerMode(controller.mode); - setPhase("scanning"); - }) - .catch((error: BarcodeScannerError) => { - if (!active) return; - setScannerError(error); - setPhase("error"); - }); + const startScanner = () => { + const video = videoRef.current; + if (!video || !active) return; + + void startBarcodeScanner(video, handleScannerResult, handleScannerError) + .then((controller) => { + if (!active) { + controller.stop(); + return; + } + controllerRef.current = controller; + setScannerMode(controller.mode); + setPhase("scanning"); + }) + .catch((error: BarcodeScannerError) => { + if (!active) return; + setScannerError(error); + setPhase("error"); + }); + }; + + frameId = window.requestAnimationFrame(() => { + window.requestAnimationFrame(startScanner); + }); void listBarcodeCatalog() .then((catalog) => { @@ -224,6 +232,7 @@ export function BarcodeScannerModal({ return () => { active = false; + window.cancelAnimationFrame(frameId); stopScanner(); }; }, [applyManualDefaults, handleScannerError, handleScannerResult, open, stopScanner, userId]); @@ -335,6 +344,7 @@ export function BarcodeScannerModal({