Added direct camera-scan support for creating QR codes

* SHC QR codes can now be scanned directly from the camera, eliminating the need for
  error-prone screenshotting or photos - the scanner will auto-detect an SHC QR code
  and proceed to the next step
This commit is contained in:
Ryan Slobojan 2021-10-17 13:30:18 -04:00
parent b179c19e3d
commit 8e83c65f6c
3 changed files with 86 additions and 49 deletions

View File

@ -9,15 +9,11 @@ import Card from "./Card";
import Alert from "./Alert";
import Check from './Check';
import {PayloadBody} from "../src/payload";
import {getPayloadBodyFromFile} from "../src/process";
import {getPayloadBodyFromFile, processSHCCode} from "../src/process";
import {PassData} from "../src/pass";
import {Photo} from "../src/photo";
import {COLORS} from "../src/colors";
import Colors from './Colors';
import {isChrome, isIOS, isIPad13, isMacOs, isSafari, deviceDetect, osName, osVersion} from 'react-device-detect';
import {isIOS, isMacOs, isSafari, osVersion} from 'react-device-detect';
import * as Sentry from '@sentry/react';
import { counterReset } from 'html2canvas/dist/types/css/property-descriptors/counter-reset';
import { color } from 'html2canvas/dist/types/css/types/color';
import Bullet from './Bullet';
@ -27,9 +23,6 @@ function Form(): JSX.Element {
// Whether camera is open or not
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
// Currently selected color
const [selectedColor, setSelectedColor] = useState<COLORS>(COLORS.WHITE);
// Currently selected dose
const [selectedDose, setSelectedDose] = useState<number>(2);
@ -98,7 +91,7 @@ function Form(): JSX.Element {
if (inputFile && inputFile.current) {
inputFile.current.addEventListener('change', () => {
let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) {
if (selectedFile) {
setFileLoading(true);
setQrCode(undefined);
setPayloadBody(undefined);
@ -132,7 +125,7 @@ function Form(): JSX.Element {
} catch (e) {
setFile(file);
setFileLoading(false);
if (e != undefined) {
if (e) {
console.error(e);
// Don't report known errors to Sentry
@ -151,11 +144,15 @@ function Form(): JSX.Element {
setFileErrorMessage("Unexpected error. Sorry.");
}
}
}
// Show file Dialog
async function showFileDialog() {
hideCameraView();
// Clear out any currently-selected files
inputFile.current.value = '';
inputFile.current.click();
}
@ -172,10 +169,11 @@ function Form(): JSX.Element {
// Hide camera view
async function hideCameraView() {
if (globalControls !== undefined) {
if (globalControls) {
globalControls.stop();
}
setIsCameraOpen(false);
_setFileErrorMessages([]);
}
// Show camera view
@ -189,13 +187,13 @@ function Form(): JSX.Element {
try {
deviceList = await BrowserQRCodeReader.listVideoInputDevices();
} catch (e) {
setAddErrorMessage('noCameraAccess');
setFileErrorMessage('noCameraAccess');
return;
}
// Check if camera device is present
if (deviceList.length == 0) {
setAddErrorMessage("noCameraFound");
setFileErrorMessage("noCameraFound");
return;
}
@ -207,24 +205,46 @@ function Form(): JSX.Element {
// Start decoding from video device
await codeReader.decodeFromVideoDevice(undefined,
previewElem,
(result, error, controls) => {
if (result !== undefined) {
setQrCode(result);
setFile(undefined);
async (result, error, controls) => {
if (result) {
const qrCode = result.getText();
// Check if this was a valid SHC QR code - if it was not, display an error
if (!qrCode.startsWith('shc:/')) {
setFileErrorMessage('The scanned QR code was not a valid Smart Health Card QR code!');
} else {
_setFileErrorMessages([]);
setQrCode(result);
setFile(undefined);
setPayloadBody(undefined);
setShowDoseOption(false);
setGenerated(false);
checkBrowserType();
const payloadBody = await processSHCCode(qrCode);
setPayloadBody(payloadBody);
controls.stop();
// Reset
setGlobalControls(undefined);
setIsCameraOpen(false);
controls.stop();
// Reset
setGlobalControls(undefined);
setIsCameraOpen(false);
}
}
if (error !== undefined) {
setAddErrorMessage(error.message);
if (error) {
setFileErrorMessage(error.message);
}
}
)
);
setQrCode(undefined);
setPayloadBody(undefined);
setFile(undefined);
setShowDoseOption(false);
setGenerated(false);
_setFileErrorMessages([]);
setIsCameraOpen(true);
}
@ -442,6 +462,12 @@ function Form(): JSX.Element {
className="focus:outline-none h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
{t('index:openFile')}
</button>
<button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
</button>
<div id="spin" className={fileLoading ? undefined : "hidden"}>
<svg className="animate-spin h-5 w-5 ml-4" viewBox="0 0 24 24">
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
@ -452,6 +478,8 @@ function Form(): JSX.Element {
</div>
</div>
<video id="cameraPreview"
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
<input type='file'
id='file'
accept="application/pdf,.png,.jpg,.jpeg,.gif,.webp"

View File

@ -1,17 +1,17 @@
iosHint: On iOS, please use Safari
errorClose: Close
selectCertificate: Select vaccination receipt (PDF or image)
selectCertificate: Select or scan vaccination receipt (PDF or image)
selectCertificateDescription: |
Press "Select File", "Browse..." and select the PDF file or picture you have saved in Step 1
#stopCamera: Stop Camera
#startCamera: Start Camera
You can scan the QR code with your camera, or press "Select File", "Browse..." and select the PDF file or picture you have saved in Step 1
stopCamera: Stop Camera
startCamera: Start Camera
openFile: Select File
#foundQrCode: Found QR Code!
foundQrCode: Found valid Smart Health Card QR Code!
downloadReceipt: Download or take a picture of your QR code
visit: If you are in Ontario, please visit
ontarioHealth: Ontario Ministry of Health
gotoOntarioHealth: Go to Ontario Ministry of Health
downloadSignedPDF: and enter your information to display your official vaccination receipt. Press the Share Icon at the bottom, "Save As Files" to store it onto your iPhone. You can also take a picture or screenshot of your QR code with your phone (please make sure the picture is good-quality, is not blurry, and captures ALL of the QR code!)
downloadSignedPDF: and enter your information to display your official vaccination receipt. Press the Share Icon at the bottom, "Save As Files" to store it onto your iPhone. You can also take a picture or screenshot of your QR code with your phone (please make sure the picture is good-quality, is not blurry, and captures ALL of the QR code!), or scan it directly with your camera below in step 2
reminderNotToRepeat: If you have completed this step before, simply proceed to Step 2
addToWallet: Add to Apple Wallet
addToWalletHeader: Add to Apple Wallet / Save as Photo

View File

@ -129,6 +129,31 @@ async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData[]
return Promise.resolve(retArray);
}
export async function processSHCCode(shcQrCode : string) : Promise<PayloadBody> {
console.log('processSHCCode');
try {
// We found a QR code of some kind - start analyzing now
const jws = getScannedJWS(shcQrCode);
const decoded = await decodeJWS(jws);
//console.log(decoded);
const verified = verifyJWS(jws, decoded.iss);
if (verified) {
const shcReceipt = Decode.decodedStringToReceipt(decoded);
//console.log(shcReceipt);
return Promise.resolve({receipts: null, shcReceipt, rawData: shcQrCode});
} else {
// If we got here, we found an SHC which was not verifiable. Consider it fatal and stop processing.
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
}
} catch (e) {
return Promise.reject(e);
}
}
async function processSHC(allImageData : ImageData[]) : Promise<PayloadBody> {
console.log('processSHC');
@ -144,23 +169,7 @@ async function processSHC(allImageData : ImageData[]) : Promise<PayloadBody> {
if (code) {
try {
// We found a QR code of some kind - start analyzing now
const rawData = code.data;
const jws = getScannedJWS(rawData);
const decoded = await decodeJWS(jws);
//console.log(decoded);
const verified = verifyJWS(jws, decoded.iss);
if (verified) {
const shcReceipt = Decode.decodedStringToReceipt(decoded);
//console.log(shcReceipt);
return Promise.resolve({receipts: null, shcReceipt, rawData});
} else {
// If we got here, we found an SHC which was not verifiable. Consider it fatal and stop processing.
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
}
return await processSHCCode(code.data);
} catch (e) {
// We blew up during processing - log it and move on to the next page
console.log(e);