feat: replace barcode-detector polyfill with local @zxing/browser for cross-browser barcode scanning
Run Tests on Branches / Backend Tests (push) Successful in 3m35s
Run Tests on Branches / Frontend Tests (push) Successful in 3m33s

- Remove barcode-detector polyfill (made remote CDN calls to download zxing-wasm)
- Use @zxing/library@0.22.0 + @zxing/browser@0.2.0 locally
- Support PDF417, Code128, Code39, Code93, QR, EAN, UPC, ITF formats
- Clean special characters from scanned results before CIP validation
- Fix video ref timing issue with waitForVideo loop
This commit is contained in:
Antoni Nuñez Romeu
2026-07-03 11:58:04 +02:00
parent 0ebd2dc35c
commit 5b080c4134
3 changed files with 109 additions and 91 deletions
+64 -37
View File
@@ -1,10 +1,11 @@
import React, { useState, useCallback, useRef } from 'react';
import { BarcodeScanner, BarcodeFormat } from '@capacitor-mlkit/barcode-scanning';
import { Capacitor } from '@capacitor/core';
import 'barcode-detector/polyfill';
import { BrowserMultiFormatReader } from '@zxing/browser';
import { BarcodeFormat as ZxingFormat, DecodeHintType } from '@zxing/library';
import './ScannerView.css';
const CIP_REGEX = /^[A-Z0-9]{16}$/i;
const CIP_REGEX = /^[A-Z0-9]{8,30}$/i;
function playBeep() {
try {
@@ -29,6 +30,12 @@ function ScannerView({ onClose, onSelectMedicine }) {
const rafRef = useRef(null);
const detectorRef = useRef(null);
// Log de diagnóstico al montar
React.useEffect(() => {
console.log('[Scanner] BarcodeDetector global:', typeof BarcodeDetector);
console.log('[Scanner] Navegador:', navigator.userAgent);
}, []);
const [phase, setPhase] = useState('idle');
const [manualCip, setManualCip] = useState('');
const [prescriptions, setPrescriptions] = useState([]);
@@ -112,6 +119,7 @@ function ScannerView({ onClose, onSelectMedicine }) {
async function handleWebScan() {
setErrorMsg('');
console.log('[Scanner] Iniciando escaneo web...');
try {
if (!navigator.mediaDevices?.getUserMedia) {
setErrorMsg('Cámara no disponible en este navegador.');
@@ -119,58 +127,77 @@ function ScannerView({ onClose, onSelectMedicine }) {
return;
}
let detector;
try {
detector = new BarcodeDetector({ formats: ['pdf417', 'code_128', 'qr_code'] });
} catch {
setErrorMsg('La detección de códigos no está soportada en este navegador.');
setPhase('error');
return;
}
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
});
console.log('[Scanner] Stream de cámara obtenido');
streamRef.current = stream;
detectorRef.current = detector;
setPhase('scanning');
await new Promise((resolve) => {
requestAnimationFrame(() => {
const waitForVideo = () => {
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.play().then(resolve).catch(resolve);
videoRef.current.play().then(() => {
console.log('[Scanner] Video reproduciéndose');
console.log('[Scanner] Video dims:', videoRef.current.videoWidth, 'x', videoRef.current.videoHeight);
resolve();
}).catch((e) => {
console.error('[Scanner] Error al reproducir video:', e);
resolve();
});
} else {
resolve();
requestAnimationFrame(waitForVideo);
}
});
};
requestAnimationFrame(waitForVideo);
});
let found = false;
async function scanFrame() {
if (found || !videoRef.current || !detectorRef.current) return;
try {
const barcodes = await detectorRef.current.detect(videoRef.current);
if (barcodes.length > 0) {
const rawValue = barcodes[0].rawValue;
if (rawValue && CIP_REGEX.test(rawValue)) {
found = true;
stopCamera();
playBeep();
setScannedCip(rawValue);
setPhase('prescriptions');
fetchPrescriptions(rawValue);
return;
}
console.log('[Scanner] Usando zxing local (@zxing/browser)...');
const hints = new Map();
hints.set(DecodeHintType.POSSIBLE_FORMATS, [
ZxingFormat.CODE_128,
ZxingFormat.CODE_39,
ZxingFormat.CODE_93,
ZxingFormat.PDF_417,
ZxingFormat.QR_CODE,
ZxingFormat.EAN_13,
ZxingFormat.EAN_8,
ZxingFormat.UPC_A,
ZxingFormat.UPC_E,
ZxingFormat.ITF,
]);
hints.set(DecodeHintType.TRY_HARDER, true);
const zxingReader = new BrowserMultiFormatReader(hints);
detectorRef.current = zxingReader;
const formatNames = { 0:'AZTEC', 1:'CODABAR', 2:'CODE_39', 3:'CODE_93', 4:'CODE_128', 5:'DATA_MATRIX', 6:'EAN_8', 7:'EAN_13', 8:'ITF', 10:'PDF_417', 11:'QR_CODE', 12:'RSS_14', 14:'UPC_A', 15:'UPC_E' };
console.log('[Scanner] Iniciando decodeFromVideoElement...');
const controls = await zxingReader.decodeFromVideoElement(videoRef.current, (result, error) => {
if (result) {
const rawValue = result.getText();
const cleaned = rawValue.replace(/[^A-Z0-9]/gi, '');
const format = result.getBarcodeFormat();
console.log('[Scanner] zxing detectó! raw:', rawValue, '| limpio:', cleaned, '| formato:', formatNames[format] || format);
if (cleaned && CIP_REGEX.test(cleaned)) {
console.log('[Scanner] CIP válido ✓');
controls.stop();
stopCamera();
playBeep();
setScannedCip(cleaned);
setPhase('prescriptions');
fetchPrescriptions(cleaned);
} else if (rawValue) {
console.log('[Scanner] Código no cumple regex:', cleaned);
}
} catch { }
if (!found) {
rafRef.current = requestAnimationFrame(scanFrame);
}
}
scanFrame();
});
console.log('[Scanner] Escaneando...');
} catch (err) {
console.error('[Scanner] Error general:', err);
stopCamera();
if (err.name === 'NotAllowedError') {
setErrorMsg('Permiso de cámara denegado. Permite el acceso e intenta de nuevo.');