import Head from 'next/head' import Link from 'next/link' import dynamic from 'next/dynamic' import React, { useEffect, useRef, useState } from 'react' import { QRCodeSVG } from 'qrcode.react' import styles from '../../styles/Post.module.scss' import localStyles from './ee-qrcode.module.scss' import HeaderNav from '../../lib/HeaderNav' import HeroImage from '../../lib/HeroImage' let scannerWorker = null async function startCapture(video, canvas, captureType) { if (typeof window !== 'undefined') { let captureStream = null try { if (captureType === 'screen') captureStream = await navigator.mediaDevices.getDisplayMedia({ audio: false, video: true }) else captureStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: 'environment' } }) } catch (e) { alert(`Failed to start screen capture: ${e}`) return } video.srcObject = captureStream video.play() await new Promise(resolve => { video.addEventListener('loadedmetadata', resolve) }) canvas.width = video.videoWidth canvas.height = video.videoHeight return (() => { captureStream.getTracks().forEach(track => track.stop()) video.srcObject = null }) } } function scanImage(imageData) { if (scannerWorker === null) { scannerWorker = new Worker(new URL('../../lib/ee-qrcode.worker.js', import.meta.url)) } return new Promise(resolve => { scannerWorker.onmessage = e => resolve(e.data.response) scannerWorker.postMessage({imageData}) }) } async function processImage(video, hiddenCanvas, previewCanvas, filterSymbols, onResult) { const hiddenCtx = hiddenCanvas.getContext('2d') hiddenCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight) const allSymbols = await scanImage(hiddenCtx.getImageData(0, 0, video.videoWidth, video.videoHeight)) previewCanvas.width = video.videoWidth previewCanvas.height = video.videoHeight const previewCtx = previewCanvas.getContext('2d') previewCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight) const symbols = allSymbols.filter(filterSymbols || (() => true)) previewCtx.strokeStyle = 'rgba(255, 0, 0, 0.7)' previewCtx.lineWidth = 6 symbols.map((sym) => { if (sym.points < 2) return const [firstPoint, ...points] = sym.points previewCtx.beginPath() previewCtx.moveTo(firstPoint.x, firstPoint.y) points.forEach(point => previewCtx.lineTo(point.x, point.y)) previewCtx.closePath() previewCtx.stroke() }) if (symbols.length > 0) { onResult(symbols[0].value) } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } async function runScanner({ video, canvas, previewCanvas, filterSymbols, onResult, running, captureType }) { const stopCapture = await startCapture(video, canvas, captureType) while (running()) { await processImage(video, canvas, previewCanvas, filterSymbols, onResult) await sleep(500) } stopCapture() } function CodeScanner({ filterSymbols, onResult, captureType }) { const videoRef = useRef(null) const canvasRef = useRef(null) const previewCanvasRef = useRef(null) useEffect(() => { let running = true; if (videoRef.current && canvasRef.current && previewCanvasRef.current) { runScanner({ video: videoRef.current, canvas: canvasRef.current, previewCanvas: previewCanvasRef.current, running: (() => running), filterSymbols, onResult, captureType }) } return (() => { running = false; }); }, [videoRef, canvasRef, previewCanvasRef, captureType]) return ( <div> <video ref={videoRef} className={localStyles.video} /> <canvas ref={canvasRef} className={localStyles.video} /> <canvas ref={previewCanvasRef} className={localStyles.canvas} /> </div> ) } function esimQrcodeFilter(symbol) { if (symbol.typeName !== 'ZBAR_QRCODE') return false if (!symbol.value.match(/^LPA:/)) return false return true } function fixData(srcData) { return srcData.replace(/[$][$]$/, '') } function StartCaptureButtons({ setRunning }) { const isBrowser = typeof window !== 'undefined' const hasScreenCapture = isBrowser && typeof navigator.mediaDevices.getDisplayMedia === 'function'; const hasWebcamCapture = isBrowser && typeof navigator.mediaDevices.getUserMedia === 'function'; return ( <span suppressHydrationWarning={true}> {hasScreenCapture ? <button onClick={() => setRunning('screen')}>Start Screen Capture</button> : null} {hasWebcamCapture ? <button onClick={() => setRunning('camera')}>Start Webcam Capture</button> : null} </span> ) } export default function EsimMangler() { const [running, setRunning] = useState(false) const [scannedData, setScannedData] = useState(null) return ( <div className={styles.container}> <HeaderNav /> <HeroImage image="https://farm9.staticflickr.com/8218/8437956869_05a4e887b0_k_d.jpg" credit={{ url: "https://www.flickr.com/photos/npobre/8437956869/", text: "Norlando Pobre, Flickr" }} withGradient="top-blue"> EE QR Code Mangler </HeroImage> <Head> <title>EE QR Code Mangler | Luke Granger-Brown</title> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <div className={styles.post}> <h1>EE eSIM QR Code Fixer</h1> <p>EE, as of 2022-05-01, still generates not-quite-spec-compliant eSIM QR Codes, which get rejected by some Android devices - including the Pixel series of devices.</p> <p>This page is intended to generate a fixed version of the QR code (which happens entirely on your computer!).</p> <p>{running ? <button onClick={() => setRunning(false)}>Stop Capture</button> : ( <StartCaptureButtons setRunning={setRunning} /> )}</p> {(scannedData !== null) ? <p><QRCodeSVG value={fixData(scannedData)} /></p> : null} {(scannedData !== null) ? <p><tt>{fixData(scannedData)}</tt></p> : null} {(running != false) ? <CodeScanner filterSymbols={esimQrcodeFilter} onResult={(result) => { setScannedData(result) setRunning(false) }} captureType={running} /> : null} </div> </main> </div> ) }