Scan
// Real QR Scanner Implementation
class QRScanner {
constructor() {
this.stream = null;
this.isScanning = false;
this.video = document.getElementById('qrVideo');
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.cameraSelect = document.getElementById('cameraSelect');
this.statusEl = document.getElementById('qrScannerStatus');
this.resultEl = document.getElementById('qrScanResult');
this.overlay = document.getElementById('scannerOverlay');
this.init();
}
async init() {
await this.loadCameras();
this.bindEvents();
}
async loadCameras() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
this.cameraSelect.innerHTML = '';
videoDevices.forEach(device => {
const option = document.createElement('option');
option.value = device.deviceId;
option.text = device.label || `Camera ${this.cameraSelect.options.length}`;
this.cameraSelect.appendChild(option);
});
} catch (err) {
this.setStatus('Cannot access cameras: ' + err.message);
}
}
bindEvents() {
document.getElementById('startScanner').addEventListener('click', () => this.start());
document.getElementById('stopScanner').addEventListener('click', () => this.stop());
document.getElementById('scanFromImageBtn').addEventListener('click', () => {
document.getElementById('imageFileInput').click();
});
document.getElementById('copyResult').addEventListener('click', () => this.copyResult());
document.getElementById('imageFileInput').addEventListener('change', (e) => {
this.scanFromFile(e.target.files[0]);
});
// Close scanner when modal closes
document.querySelector('[data-close="qrScannerModal"]').addEventListener('click', () => {
this.stop();
});
}
setStatus(message) {
if (this.statusEl) {
this.statusEl.textContent = message;
}
}
setResult(result, isError = false) {
if (this.resultEl) {
this.resultEl.textContent = result;
this.resultEl.style.color = isError ? 'var(--danger)' : 'var(--success)';
}
}
async start() {
if (this.isScanning) return;
const deviceId = this.cameraSelect.value;
try {
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: deviceId ? undefined : 'environment'
}
};
if (deviceId) {
constraints.video.deviceId = { exact: deviceId };
}
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
this.video.srcObject = this.stream;
this.setStatus('Scanner active - point camera at QR code');
this.isScanning = true;
this.scanLoop();
} catch (err) {
this.setStatus('Camera error: ' + err.message);
this.setResult('Failed to access camera', true);
}
}
stop() {
this.isScanning = false;
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
this.video.srcObject = null;
this.setStatus('Scanner stopped');
this.overlay.style.display = 'none';
}
async scanLoop() {
if (!this.isScanning) return;
try {
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.ctx.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const result = await this.scanQRCode(imageData);
if (result) {
this.handleQRResult(result);
this.overlay.style.display = 'block';
} else {
this.overlay.style.display = 'none';
}
}
} catch (err) {
console.error('Scan error:', err);
}
if (this.isScanning) {
requestAnimationFrame(() => this.scanLoop());
}
}
async scanQRCode(imageData) {
// Try using native BarcodeDetector if available (modern browsers)
if ('BarcodeDetector' in window) {
try {
const barcodeDetector = new BarcodeDetector({ formats: ['qr_code'] });
const barcodes = await barcodeDetector.detect(imageData);
if (barcodes.length > 0) {
return {
data: barcodes[0].rawValue,
format: barcodes[0].format
};
}
} catch (err) {
console.log('BarcodeDetector not supported:', err);
}
}
// Fallback: Simple QR pattern detection (basic implementation)
return this.simpleQRDetection(imageData);
}
simpleQRDetection(imageData) {
// This is a basic QR pattern detector
// For production, you should use a proper QR library like jsQR
const data = imageData.data;
const width = imageData.width;
const height = imageData.height;
// Look for finder patterns (3 nested squares)
const finders = this.findFinderPatterns(data, width, height);
if (finders.length >= 3) {
return {
data: "QR Code Detected (Use proper QR library for full decoding)",
format: "qr_code"
};
}
return null;
}
findFinderPatterns(data, width, height) {
const finders = [];
const blockSize = 7; // Standard QR finder pattern size
for (let y = blockSize; y < height - blockSize; y += 2) {
for (let x = blockSize; x < width - blockSize; x += 2) {
if (this.isFinderPattern(data, width, x, y, blockSize)) {
finders.push({x, y});
}
}
}
return finders;
}
isFinderPattern(data, width, x, y, size) {
// Check for nested square pattern (black-white-black-white-black)
const half = Math.floor(size / 2);
// Outer black square
if (!this.isSquareColor(data, width, x, y, size, true)) return false;
// Inner white square
if (!this.isSquareColor(data, width, x + 1, y + 1, size - 2, false)) return false;
// Inner black square
if (!this.isSquareColor(data, width, x + 2, y + 2, size - 4, true)) return false;
return true;
}
isSquareColor(data, width, x, y, size, isBlack) {
const threshold = 128;
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const pixelX = x + i;
const pixelY = y + j;
const index = (pixelY * width + pixelX) * 4;
const brightness = (data[index] + data[index + 1] + data[index + 2]) / 3;
if (isBlack && brightness > threshold) return false;
if (!isBlack && brightness <= threshold) return false;
}
}
return true;
}
async scanFromFile(file) {
if (!file) return;
try {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = async () => {
this.canvas.width = img.width;
this.canvas.height = img.height;
this.ctx.drawImage(img, 0, 0);
const imageData = this.ctx.getImageData(0, 0, img.width, img.height);
const result = await this.scanQRCode(imageData);
if (result) {
this.handleQRResult(result);
} else {
this.setResult('No QR code found in image', true);
}
URL.revokeObjectURL(url);
};
img.onerror = () => {
this.setResult('Failed to load image', true);
URL.revokeObjectURL(url);
};
img.src = url;
this.setStatus('Scanning image...');
} catch (err) {
this.setResult('Error scanning image: ' + err.message, true);
}
}
handleQRResult(result) {
this.setResult(`Format: ${result.format}\nData: ${result.data}`);
this.setStatus('QR Code Detected!');
// Auto-process MyFin transaction QR codes
this.processMyFinQR(result.data);
}
processMyFinQR(data) {
try {
const qrData = JSON.parse(data);
if (qrData.type === 'tx' && qrData.id) {
// Auto-open transaction slip
setTimeout(() => {
if (window.openSlipModal) {
window.openSlipModal(qrData.id);
}
this.stop();
}, 1000);
}
} catch (e) {
// Not a JSON QR code, ignore
}
}
copyResult() {
const text = this.resultEl.textContent;
navigator.clipboard.writeText(text).then(() => {
this.setStatus('Result copied to clipboard!');
}).catch(err => {
this.setStatus('Failed to copy: ' + err.message);
});
}
}
// Initialize scanner when modal opens
let scanner = null;
window.openScannerModal = function() {
const modal = document.getElementById('qrScannerModal');
modal.style.display = 'flex';
if (!scanner) {
scanner = new QRScanner();
}
};
window.closeQRScannerModal = function() {
const modal = document.getElementById('qrScannerModal');
modal.style.display = 'none';
if (scanner) {
scanner.stop();
}
};