diff --git a/package-lock.json b/package-lock.json index 0a6d710..6d1b76e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@types/node": "18.16.1", "@types/react": "18.2.0", "@types/react-dom": "18.2.1", - "@zxing/browser": "^0.1.5", "autoprefixer": "10.4.14", "bs58": "^5.0.0", "classnames": "^2.3.2", @@ -19,6 +18,7 @@ "eslint-config-next": "13.3.1", "next": "^15.1.6", "postcss": "^8.5.1", + "qr-scanner": "^1.4.2", "qrcode.react": "^4.2.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -832,6 +832,12 @@ "integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==", "license": "MIT" }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -965,41 +971,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@zxing/browser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz", - "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==", - "license": "MIT", - "optionalDependencies": { - "@zxing/text-encoding": "^0.9.0" - }, - "peerDependencies": { - "@zxing/library": "^0.21.0" - } - }, - "node_modules/@zxing/library": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz", - "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==", - "license": "MIT", - "peer": true, - "dependencies": { - "ts-custom-error": "^3.2.1" - }, - "engines": { - "node": ">= 10.4.0" - }, - "optionalDependencies": { - "@zxing/text-encoding": "~0.9.0" - } - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -4364,6 +4335,15 @@ "node": ">=6" } }, + "node_modules/qr-scanner": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/qr-scanner/-/qr-scanner-1.4.2.tgz", + "integrity": "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw==", + "license": "MIT", + "dependencies": { + "@types/offscreencanvas": "^2019.6.4" + } + }, "node_modules/qrcode.react": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", @@ -5335,16 +5315,6 @@ "node": ">=8.0" } }, - "node_modules/ts-custom-error": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", - "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/package.json b/package.json index 3d142f5..cf199c2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@types/node": "18.16.1", "@types/react": "18.2.0", "@types/react-dom": "18.2.1", - "@zxing/browser": "^0.1.5", "autoprefixer": "10.4.14", "bs58": "^5.0.0", "classnames": "^2.3.2", @@ -20,6 +19,7 @@ "eslint-config-next": "13.3.1", "next": "^15.1.6", "postcss": "^8.5.1", + "qr-scanner": "^1.4.2", "qrcode.react": "^4.2.0", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/src/app/QRDragAndDrop.tsx b/src/app/QRDragDrop.tsx similarity index 69% rename from src/app/QRDragAndDrop.tsx rename to src/app/QRDragDrop.tsx index 26600b1..2d035ff 100644 --- a/src/app/QRDragAndDrop.tsx +++ b/src/app/QRDragDrop.tsx @@ -1,38 +1,19 @@ import React, { useState, useRef, DragEvent, ReactNode } from 'react' -import { BrowserQRCodeReader } from '@zxing/browser' +import QrScanner from 'qr-scanner' -interface QRDragAndDropProps { +interface QRDragDropProps { children: ReactNode onQRCodesDetected: (qrCodes: (string | null)[]) => void className?: string } -const loadImage = (src: string): Promise => { - return new Promise((resolve, reject) => { - const img = new Image() - img.onload = () => resolve(img) - img.onerror = reject - img.src = src - }) -} - -const createCanvas = (img: HTMLImageElement): HTMLCanvasElement => { - const canvas = document.createElement('canvas') - const context = canvas.getContext('2d') - canvas.width = img.width - canvas.height = img.height - context?.drawImage(img, 0, 0) - return canvas -} - -export const QRDragAndDrop: React.FC = ({ +const QRDragDrop: React.FC = ({ children, onQRCodesDetected, className = '', }) => { const [isDragging, setIsDragging] = useState(false) const fileInputRef = useRef(null) - const reader = new BrowserQRCodeReader() const processFiles = async (files: FileList) => { const qrCodes: (string | null)[] = [] @@ -42,13 +23,10 @@ export const QRDragAndDrop: React.FC = ({ if (!file.type.includes('png')) continue try { - const imageUrl = URL.createObjectURL(file) - const img = await loadImage(imageUrl) - const canvas = createCanvas(img) - const result = await reader.decodeFromCanvas(canvas) - - qrCodes.push(result.getText()) - URL.revokeObjectURL(imageUrl) + const result = await QrScanner.scanImage(file, { + returnDetailedScanResult: true, + }) + qrCodes.push(result.data) } catch (error) { console.error('Error processing file:', error) qrCodes.push(null) @@ -118,3 +96,5 @@ export const QRDragAndDrop: React.FC = ({ ) } + +export default QRDragDrop diff --git a/src/app/QRScanner.tsx b/src/app/QRScanner.tsx index 9019c4b..e3aa1dd 100644 --- a/src/app/QRScanner.tsx +++ b/src/app/QRScanner.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { BrowserQRCodeReader } from '@zxing/browser' +import QrScanner from 'qr-scanner' interface QRScannerProps { onQRCodeScanned: (text: string) => void @@ -11,50 +11,64 @@ export const QRScanner: React.FC = ({ onError, }) => { const videoRef = useRef(null) - const codeReader = useRef(new BrowserQRCodeReader()) + const scannerRef = useRef(null) useEffect(() => { const startScanning = async () => { try { - const videoInputDevices = - await BrowserQRCodeReader.listVideoInputDevices() + const cameras = await QrScanner.listCameras() - if (videoInputDevices.length === 0) { + if (cameras.length === 0) { onError('No camera devices found. Tap to retry.') return } - // Select the back camera if available, otherwise default to the first device - const selectedDevice = - videoInputDevices.find( - (device) => - device.label.toLowerCase().includes('back') || - device.label.toLowerCase().includes('rear'), - ) || videoInputDevices[0] + // Select the back camera if available, otherwise default to the first camera + const selectedCamera = + cameras.find( + (camera) => + camera.label.toLowerCase().includes('back') || + camera.label.toLowerCase().includes('rear'), + ) || cameras[0] - const selectedDeviceId = selectedDevice.deviceId + if (!videoRef.current) return - if (videoRef.current) { - await codeReader.current.decodeFromVideoDevice( - selectedDeviceId, - videoRef.current, - (result, error) => { - if (result) { - onQRCodeScanned(result.getText()) - } - if (error) { - // Ignore errors as they occur frequently when no QR code is detected - return - } - }, - ) + // Stop any existing scanner + if (scannerRef.current) { + scannerRef.current.destroy() } + + // Create new scanner + scannerRef.current = new QrScanner( + videoRef.current, + (result) => { + onQRCodeScanned(result.data) + }, + { + preferredCamera: selectedCamera.id, + highlightScanRegion: false, + highlightCodeOutline: false, + returnDetailedScanResult: true, + }, + ) + + await scannerRef.current.start() } catch (error) { console.error('Error accessing camera:', error) + onError( + 'Failed to access camera. Please ensure camera permissions are granted.', + ) } } startScanning() + + // Cleanup function + return () => { + if (scannerRef.current) { + scannerRef.current.destroy() + } + } }, [onQRCodeScanned, onError]) return ( diff --git a/src/app/decrypt/page.tsx b/src/app/decrypt/page.tsx index c9af30a..02305c8 100644 --- a/src/app/decrypt/page.tsx +++ b/src/app/decrypt/page.tsx @@ -6,7 +6,7 @@ import { combine } from 'shamirs-secret-sharing-ts' import { decrypt } from '../crypto' import CloseIcon from '../CloseIcon' import { QRScanner } from '../QRScanner' -import { QRDragAndDrop } from '../QRDragAndDrop' +import QRDragDrop from '../QRDragDrop' export default function DecryptPage() { const [scans, setScans] = useState([]) @@ -51,7 +51,9 @@ export default function DecryptPage() { try { // QR Codes - shares = scans.map((s) => Buffer.from(bs58.decode(s))) + shares = scans.map((s) => + Buffer.from(bs58.decode(s)), + ) as unknown as Uint8Array[] } catch (e: any) { setHasError('QR Code: ' + e.message) return @@ -59,7 +61,7 @@ export default function DecryptPage() { try { // Shamir's Secret Sharing - recovered = combine(shares!) + recovered = combine(shares!) as unknown as Uint8Array } catch (e: any) { setHasError('SSS: ' + e.message) return @@ -78,10 +80,11 @@ export default function DecryptPage() { // AES-GCM data = await decrypt(salt, password, ciphertext) } catch (e: any) { + console.error(e) if (e.message.includes('base58')) { setHasError('Incomplete QR Codes') } else { - setHasError('AES-GCM: ' + e.message) + setHasError('AES-GCM: ' + (e.message || 'Possible incorrect password')) } return } @@ -104,7 +107,7 @@ export default function DecryptPage() { {scans.length} codes parsed
- { uploads.forEach((file) => { if (file) { @@ -128,7 +131,7 @@ export default function DecryptPage() { setErrorMsg('') }} /> - + {scans.length === 0 && (