Merge branch 'main' into shc-support

This commit is contained in:
Billy Lo 2021-09-29 16:13:13 -04:00
commit 66e450a2ed
26 changed files with 1020 additions and 478 deletions

5
.env.local Normal file
View File

@ -0,0 +1,5 @@
API_BASE_URL=http://localhost
VERIFIER_HOST=http://localhost:5001/grassroot-verifier/us-central1
HITCOUNT_HOST=http://localhost:8080
REGISTRATION_HOST=http://localhost:5001/grassroot-verifier/us-central1
FUNCTION_SUFFIX=v2

View File

@ -1,3 +1,4 @@
API_BASE_URL=https://covidpassapinet-pnrnxf7lvq-pd.a.run.app API_BASE_URL=https://covidpassapinet-pnrnxf7lvq-pd.a.run.app
VERIFIER_HOST=https://verifier.vaccine-ontario.ca VERIFIER_HOST=https://verifier.vaccine-ontario.ca
HITCOUNT_HOST=https://hitcount.vaccine-ontario.ca HITCOUNT_HOST=https://hitcount.vaccine-ontario.ca
REGISTRATION_HOST=https://us-central1-grassroot-verifier.cloudfunctions.net

1
.gitignore vendored
View File

@ -26,7 +26,6 @@ yarn-error.log*
# local env files # local env files
.env .env
.env.local
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local

View File

@ -25,6 +25,10 @@ docker build . -t covidpass
docker run -t -i -p 3000:3000 covidpass docker run -t -i -p 3000:3000 covidpass
``` ```
### Integration with other repos required
Docs being developed/tested on Sep 26. Should be done tomorrow.
# FAQ # FAQ
#### I do not want to trust a third party with my vaccination data, does this tool respect my privacy? #### I do not want to trust a third party with my vaccination data, does this tool respect my privacy?

112
SETUP.md Normal file
View File

@ -0,0 +1,112 @@
# Environment Setup
The whole solution is made up of 4 repos.
1. covidpass (Wallet Pass creation front-end, port 3000)
2. covidpassApiDotNet (Wallet Pass signing service, Apple specific, port 80)
3. verifier (web app for scanning, port 5001; GCP cloud functions /register /verify in support of #1)
4. hit-counter (simple python script running in VM, web enabled using flask, port 8080)
The steps were tested against a standard ubuntu LTS 18 vm running in GCP
## GCP console - Firewall - New Firewall Rule
* allow-covidpass-ports, port 3000,80,5001,5003,8080 tcp/inbound
## GCP console - VM setup
* ubuntu LTS 18, 2 core, 4Gb RAM, allow all GCP API, network tag (allow-covidpass-ports), fixed external ip will be helpful
ssh into the newly created vm
Install docker
* https://docs.docker.com/engine/install/ubuntu/
Enable non-root usage of docker
```sh
sudo groupadd docker
sudo usermod -aG docker ${USER}
```
exit SSH session and login again
Install yarn
```sh
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
```
Upgrade node to 14
```sh
sudo apt-get install -y nodejs
```
Prepare to start
```sh
mkdir web
cd web
```
Repo 1 (https://github.com/billylo1/covidpass)
git clone https://github.com/billylo1/covidpass.git
cd covidpass
yarn install
note external IP of your dev machines
modify .env.local and replace localhost with your {vm-external-ip}
yarn dev
access it from your workstation's browser (http://vm-external-ip:3000)
you should see on the yarn output compiling... sentry initialized and browser showing page
Repo 2 (https://github.com/billylo1/CovidPassApiNet)
cd ~/web
git clone https://github.com/billylo1/CovidPassApiNet
cd CovidPassApiNet/CovidPassApiNet
cp appsettings.example.json appsettings.json
setup Apple Developer Certificate (assume current directory is the above)
1. Sign into your Apple Developer Account
2. Go to Certificates, Identifiers and Profiles
3. Register a new Pass Type Identifier under the Identifiers tab
4. Create a new Pass Type ID Certificate under the Certificates tab
5. Select your previously created Pass Type Identifier in the process
6. Move your new certificate to the My Certificates tab in the keychain
7. Export your certificate as a .p12 file (make a note of passphrase)
8. Create a text file named AppleDeveloperPassword with your passphrase in it
9. Install node.js and download the passkit-keys script
10. Create a keys folder and put the .p12 file inside
11. Run ./passkit-keys <path to your keys folder>
12. copy the .pem file to ~/web/CovidPassApiNet/CovidPassApiNet/AppDeveloperCerticate.pem
13. Open keychain - System Keychain - Certificates
14. Export Apple Developer Relations Certification Authority to AppleCaCertificate.pem
15. chmod 600 Apple*.pem
(Reminder: pls protect these files as they contain private key and passphrases. Do not add them to your repo.)
docker build . -t covidpassapinet
docker run covidpassapinet -p 80:80
Repo 3 (https://github.com/billylo1/verifier)
cd ~/web
git clone https://github.com/billylo1/verifier
sudo npm install -g firebase-tools
firebase init
sudo apt install default-jre
firebase emulators:start
Repo 4 (https://github.com/billylo1/hit-counter)
cd ~/web
git clone https://github.com/billylo1/hit-counter
sudo apt-get install python3.8 python3-pip
python3 server.py

View File

@ -1,5 +1,6 @@
docker build . -t covidpass -t gcr.io/broadcast2patients/covidpass docker build . -t covidpass -t gcr.io/broadcast2patients/covidpass
docker push gcr.io/broadcast2patients/covidpass docker push gcr.io/broadcast2patients/covidpass
docker image prune
#gcloud config set project broadcast2patients #gcloud config set project broadcast2patients
#gcloud config set run/region us-east1 #gcloud config set run/region us-east1
#gcloud run deploy covidpass --image gcr.io/broadcast2patients/covidpass:latest --platform managed #gcloud run deploy covidpass --image gcr.io/broadcast2patients/covidpass:latest --platform managed

View File

@ -1,24 +1,40 @@
import {useTranslation} from 'next-i18next'; import { useTranslation } from 'next-i18next';
interface AlertProps { interface AlertProps {
onClose: () => void; onClose?: () => void;
errorMessage: string; type: 'error' | 'warning';
message: string;
} }
function Alert(props: AlertProps): JSX.Element { function Alert(props: AlertProps): JSX.Element {
const { t } = useTranslation(['index', 'errors']); const { t } = useTranslation(['index', 'errors']);
let spanStyle = 'bg-red-100 border border-red-400 text-red-700';
let svgStyle = 'text-red-500';
let icon;
switch (props.type) {
case 'error':
// icon = () =>
// <svg className="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" fill-rule="evenodd"></path></svg>
break;
case 'warning':
spanStyle = 'bg-yellow-100 border border-yellow-400 text-yellow-700';
svgStyle = 'text-yellow-500';
break;
}
return ( return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mt-5 rounded relative" role="alert"> <div className={`flex items-center ${spanStyle} px-4 py-3 mt-5 rounded relative`} role="alert">
<span className="block sm:inline pr-6" id="message">{props.errorMessage}</span> {icon && icon()}
<span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={props.onClose}> <span className="block sm:inline pr-6" id="message">{props.message}</span>
<svg className="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg" {props.onClose && <span className="absolute right-0 px-4 py-3" onClick={props.onClose}>
<svg className={`fill-current h-6 w-6 ${svgStyle}`} role="button" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"> viewBox="0 0 20 20">
<title>{t('index:errorClose')}</title> <title>{t('index:errorClose')}</title>
<path <path
d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/> d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z" />
</svg> </svg>
</span> </span>}
</div> </div>
) )
} }

20
components/Bullet.tsx Normal file
View File

@ -0,0 +1,20 @@
import {useTranslation} from 'next-i18next';
interface BulletProps {
text: string;
}
function Bullet(props: BulletProps): JSX.Element {
const { t } = useTranslation(["index"]);
return (
<li className="flex flex-row space-x-4 items-center">
<svg className="h-5 w-5 mx-2 fill-current text-green-500" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<circle fillRule="evenodd" clipRule="evenodd" cx="10" cy="10" r="5" />
</svg>
{props.text}
</li>
)
}
export default Bullet;

View File

@ -17,6 +17,8 @@ import Colors from './Colors';
import {isChrome, isIOS, isIPad13, isMacOs, isSafari, deviceDetect, osName, osVersion} from 'react-device-detect'; import {isChrome, isIOS, isIPad13, isMacOs, isSafari, deviceDetect, osName, osVersion} from 'react-device-detect';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { counterReset } from 'html2canvas/dist/types/css/property-descriptors/counter-reset'; import { counterReset } from 'html2canvas/dist/types/css/property-descriptors/counter-reset';
import { color } from 'html2canvas/dist/types/css/types/color';
import Bullet from './Bullet';
function Form(): JSX.Element { function Form(): JSX.Element {
@ -28,63 +30,66 @@ function Form(): JSX.Element {
// Currently selected color // Currently selected color
const [selectedColor, setSelectedColor] = useState<COLORS>(COLORS.WHITE); const [selectedColor, setSelectedColor] = useState<COLORS>(COLORS.WHITE);
// Currently selected dose
const [selectedDose, setSelectedDose] = useState<number>(2);
// Global camera controls // Global camera controls
const [globalControls, setGlobalControls] = useState<IScannerControls>(undefined); const [globalControls, setGlobalControls] = useState<IScannerControls>(undefined);
// Currently selected QR Code / File. Only one of them is set. // Currently selected QR Code / File. Only one of them is set.
const [qrCode, setQrCode] = useState<Result>(undefined); const [qrCode, setQrCode] = useState<Result>(undefined);
const [file, setFile] = useState<File>(undefined); const [file, setFile] = useState<File>(undefined);
const [payloadBody, setPayloadBody] = useState<PayloadBody>(undefined);
const [errorMessage, _setErrorMessage] = useState<string>(undefined); const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false); const [fileLoading, setFileLoading] = useState<boolean>(false);
const [passCount, setPassCount] = useState<string>('');
const [generated, setGenerated] = useState<boolean>(false); // this flag represents the file has been used to generate a pass const [generated, setGenerated] = useState<boolean>(false); // this flag represents the file has been used to generate a pass
const [isDisabledAppleWallet, setIsDisabledAppleWallet] = useState<boolean>(false);
const [addErrorMessages, _setAddErrorMessages] = useState<Array<string>>([]);
const [fileErrorMessages, _setFileErrorMessages] = useState<Array<string>>([]);
const [showDoseOption, setShowDoseOption] = useState<boolean>(false);
// const [warningMessages, _setWarningMessages] = useState<Array<string>>([]);
const hitcountHost = 'https://stats.vaccine-ontario.ca'; const hitcountHost = 'https://stats.vaccine-ontario.ca';
useEffect(() => {
if (passCount.length == 0) {
getPassCount();
}
}, []);
const getPassCount = async () => {
const hitCount = await getHitCount();
console.log(`hitcount = ${hitCount}`);
setPassCount(hitCount);
};
async function getHitCount() {
try {
const request = `${hitcountHost}/nocount?url=pass.vaccine-ontario.ca`;
let response = await fetch(request);
const counter = await response.text();
console.log('getHitCount returns ' + counter);
return Promise.resolve(counter);
} catch (e) {
console.error(e);
return Promise.reject(e);
}
}
// Check if there is a translation and replace message accordingly // Check if there is a translation and replace message accordingly
const setErrorMessage = (message: string) => { const setAddErrorMessage = (message: string) => {
if (message == undefined) { if (!message) {
_setErrorMessage(undefined);
return; return;
} }
const translation = t('errors:'.concat(message)); const translation = t('errors:'.concat(message));
_setErrorMessage(translation !== message ? translation : message); _setAddErrorMessages(Array.from(new Set([...addErrorMessages, translation !== message ? translation : message])));
}; };
const setFileErrorMessage = (message: string) => {
if (!message) {
return;
}
const translation = t('errors:'.concat(message));
_setFileErrorMessages(Array.from(new Set([...addErrorMessages, translation !== message ? translation : message])));
};
// const setWarningMessage = (message: string) => {
// if (!message) {
// return;
// }
// const translation = t('errors:'.concat(message));
// _setWarningMessages(Array.from(new Set([...warningMessages, translation !== message ? translation : message])));
// }
const deleteAddErrorMessage = (message: string) =>{
_setAddErrorMessages(addErrorMessages.filter(item => item !== message))
}
const deleteFileErrorMessage = (message: string) =>{
_setFileErrorMessages(addErrorMessages.filter(item => item !== message))
}
// File Input ref // File Input ref
const inputFile = useRef<HTMLInputElement>(undefined) const inputFile = useRef<HTMLInputElement>(undefined)
@ -94,22 +99,67 @@ function Form(): JSX.Element {
inputFile.current.addEventListener('input', () => { inputFile.current.addEventListener('input', () => {
let selectedFile = inputFile.current.files[0]; let selectedFile = inputFile.current.files[0];
if (selectedFile !== undefined) { if (selectedFile !== undefined) {
setFileLoading(true);
setQrCode(undefined); setQrCode(undefined);
setFile(selectedFile); setPayloadBody(undefined);
setFile(undefined);
setShowDoseOption(false);
setGenerated(false); setGenerated(false);
deleteAddErrorMessage(t('errors:'.concat('noFileOrQrCode')));
_setFileErrorMessages([]);
checkBrowserType();
getPayload(selectedFile);
} }
}); });
} }
checkBrowserType(); checkBrowserType();
}, [inputFile]) }, [inputFile])
async function getPayload(file){
try {
const payload = await getPayloadBodyFromFile(file);
setPayloadBody(payload);
setFileLoading(false);
setFile(file);
if (Object.keys(payload.receipts).length === 1) {
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
}else{
setShowDoseOption(true);
}
} catch (e) {
setFile(file);
setFileLoading(false);
if (e != undefined) {
console.error(e);
Sentry.captureException(e);
if (e.message != undefined) {
setFileErrorMessage(e.message);
} else {
setFileErrorMessage("Unable to continue.");
}
} else {
setFileErrorMessage("Unexpected error. Sorry.");
}
}
}
// Show file Dialog // Show file Dialog
async function showFileDialog() { async function showFileDialog() {
inputFile.current.click(); inputFile.current.click();
} }
async function gotoOntarioHealth() { async function gotoOntarioHealth(e) {
window.location.href = 'https://covid19.ontariohealth.ca'; e.preventDefault();
window.open('https://covid19.ontariohealth.ca','_blank');
}
async function goToFAQ(e) {
e.preventDefault();
window.location.href = '/faq';
} }
// Hide camera view // Hide camera view
@ -131,13 +181,13 @@ function Form(): JSX.Element {
try { try {
deviceList = await BrowserQRCodeReader.listVideoInputDevices(); deviceList = await BrowserQRCodeReader.listVideoInputDevices();
} catch (e) { } catch (e) {
setErrorMessage('noCameraAccess'); setAddErrorMessage('noCameraAccess');
return; return;
} }
// Check if camera device is present // Check if camera device is present
if (deviceList.length == 0) { if (deviceList.length == 0) {
setErrorMessage("noCameraFound"); setAddErrorMessage("noCameraFound");
return; return;
} }
@ -161,7 +211,7 @@ function Form(): JSX.Element {
setIsCameraOpen(false); setIsCameraOpen(false);
} }
if (error !== undefined) { if (error !== undefined) {
setErrorMessage(error.message); setAddErrorMessage(error.message);
} }
} }
) )
@ -171,68 +221,77 @@ function Form(): JSX.Element {
} }
async function incrementCount() { async function incrementCount() {
try { try {
if (typeof generated == undefined || !generated) { if (typeof generated == undefined || !generated) {
const request = `${hitcountHost}/count?url=pass.vaccine-ontario.ca`; const request = `${hitcountHost}/count?url=pass.vaccine-ontario.ca`;
console.log(request); //console.log(request);
let response = await fetch(request); let response = await fetch(request);
console.log(request); //console.log(response);
const counter = await response.text(); // response count is not used intentionally so it always goes up by 1 only even if the server has changed
let newPasscount = Number(passCount) + 1;
console.log(counter);
setPassCount(counter);
setGenerated(true); setGenerated(true);
console.log(`new PassCount = ${newPasscount}`);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return Promise.reject(e); return Promise.reject(e);
} }
} }
// Add Pass to wallet // Add Pass to wallet
async function addToWallet(event: FormEvent<HTMLFormElement>) { async function addToWallet(event: FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
setLoading(true); setSaveLoading(true);
if (!file && !qrCode) { if (!file && !qrCode) {
setErrorMessage('noFileOrQrCode') setAddErrorMessage('noFileOrQrCode')
setLoading(false); setSaveLoading(false);
return; return;
} }
const color = selectedColor;
let payloadBody: PayloadBody;
try { try {
if (file) { if (payloadBody) {
const passName = payloadBody.receipts[selectedDose].name.replace(' ', '-');
const vaxName = payloadBody.receipts[selectedDose].vaccineName.replace(' ', '-');
const passDose = payloadBody.receipts[selectedDose].numDoses;
const covidPassFilename = `grassroots-receipt-${passName}-${vaxName}-${passDose}.pkpass`;
console.log('> generatePass'); //console.log('> increment count');
payloadBody = await getPayloadBodyFromFile(file);
await incrementCount(); await incrementCount();
let pass = await PassData.generatePass(payloadBody); // console.log('> generatePass');
const pass = await PassData.generatePass(payloadBody, selectedDose);
//console.log('> create blob');
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"}); const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
saveAs(passBlob, 'covid.pkpass');
setLoading(false); //console.log(`> save blob as ${covidPassFilename}`);
saveAs(passBlob, covidPassFilename);
setSaveLoading(false);
} }
} catch (e) { } catch (e) {
if (e != undefined) {
console.error(e); console.error(e);
setErrorMessage(e.message);
Sentry.captureException(e); Sentry.captureException(e);
setLoading(false);
if (e.message != undefined) {
setAddErrorMessage(e.message);
} else {
setAddErrorMessage("Unable to continue.");
}
} else {
setAddErrorMessage("Unexpected error. Sorry.");
}
setSaveLoading(false);
} }
} }
@ -240,21 +299,17 @@ function Form(): JSX.Element {
async function saveAsPhoto() { async function saveAsPhoto() {
setLoading(true); setSaveLoading(true);
if (!file && !qrCode) { if (!file && !qrCode) {
setErrorMessage('noFileOrQrCode') setAddErrorMessage('noFileOrQrCode');
setLoading(false); setSaveLoading(false);
return; return;
} }
let payloadBody: PayloadBody;
try { try {
payloadBody = await getPayloadBodyFromFile(file);
await incrementCount(); await incrementCount();
let photoBlob = await Photo.generatePass(payloadBody, selectedDose);
let photoBlob = await Photo.generatePass(payloadBody);
saveAs(photoBlob, 'pass.png'); saveAs(photoBlob, 'pass.png');
// need to clean up // need to clean up
@ -264,32 +319,54 @@ function Form(): JSX.Element {
const body = document.getElementById('pass-image'); const body = document.getElementById('pass-image');
body.hidden = true; body.hidden = true;
setLoading(false); setSaveLoading(false);
} catch (e) { } catch (e) {
Sentry.captureException(e); Sentry.captureException(e);
setErrorMessage(e.message); setAddErrorMessage(e.message);
setLoading(false); setSaveLoading(false);
} }
} }
const verifierLink = () => <li className="flex flex-row items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2 fill-current text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
<p>
{t('verifierLink')}&nbsp;
<Link href="https://verifier.vaccine-ontario.ca/">
<a className="underline">verifier.vaccine-ontario.ca </a>
</Link>
</p>
</li>
const setDose = (e) => {
setSelectedDose(e.target.value);
}
async function checkBrowserType() { function checkBrowserType() {
if (isIPad13) { // if (isIPad13) {
setErrorMessage('Sorry. Apple does not support the use of Wallet on iPad. Please use iPhone/Safari.'); // setAddErrorMessage('Sorry. Apple does not support the use of Wallet on iPad. Please use iPhone/Safari.');
document.getElementById('download').setAttribute('disabled','true'); // setIsDisabledAppleWallet(true);
} // }
if (!isSafari && !isChrome) { // if (!isSafari && !isChrome) {
setErrorMessage('Sorry. Apple Wallet pass can be added using Safari or Chrome only.'); // setAddErrorMessage('Sorry. Apple Wallet pass can be added using Safari or Chrome only.');
document.getElementById('download').setAttribute('disabled','true'); // setIsDisabledAppleWallet(true);
} // }
if (isIOS && (!osVersion.includes('13') && !osVersion.includes('14') && !osVersion.includes('15'))) { // if (isIOS && (!osVersion.includes('13') && !osVersion.includes('14') && !osVersion.includes('15'))) {
setErrorMessage('Sorry, iOS 13+ is needed for the Apple Wallet functionality to work') // setAddErrorMessage('Sorry, iOS 13+ is needed for the Apple Wallet functionality to work')
document.getElementById('download').setAttribute('disabled','true') // setIsDisabledAppleWallet(true);
} // }
if (isIOS && !isSafari) { if (isIOS && !isSafari) {
setErrorMessage('Sorry, only Safari can be used to add a Wallet Pass on iOS') // setAddErrorMessage('Sorry, only Safari can be used to add a Wallet Pass on iOS');
document.getElementById('download').setAttribute('disabled','true') setAddErrorMessage('Sorry, only Safari can be used to add a Wallet Pass on iOS');
setIsDisabledAppleWallet(true);
console.log('not safari')
} }
// } else if (!isIOS) {
// setWarningMessage('Only Safari on iOS is officially supported for Wallet import at the moment - ' +
// 'for other platforms, please ensure you have an application which can open Apple Wallet .pkpass files');
// setIsDisabledAppleWallet(false);
// }
} }
return ( return (
@ -299,13 +376,14 @@ function Form(): JSX.Element {
<div className="space-y-5"> <div className="space-y-5">
<p> <p>
{t('index:visit')}&nbsp; {t('index:visit')}&nbsp;
<Link href="https://covid19.ontariohealth.ca"> <Link href="https://covid19.ontariohealth.ca">
<a className="underline"> <a className="underline" target="_blank">
{t('index:ontarioHealth')} {t('index:ontarioHealth')}
</a> </a>
</Link>&nbsp; </Link>&nbsp;
{t('index:downloadSignedPDF')} {t('index:downloadSignedPDF')}<br/><br/>
{t('index:reminderNotToRepeat')}
</p> </p>
<button id="ontariohealth" onClick={gotoOntarioHealth} <button id="ontariohealth" onClick={gotoOntarioHealth}
@ -318,14 +396,21 @@ function Form(): JSX.Element {
<Card step="2" heading={t('index:selectCertificate')} content={ <Card step="2" heading={t('index:selectCertificate')} content={
<div className="space-y-5"> <div className="space-y-5">
<p>{t('index:selectCertificateDescription')}</p> <p>{t('index:selectCertificateDescription')}</p>
<p>{t('index:selectCertificateReminder')}</p> <div className="grid grid-cols-1 md:grid-cols-2 gap-5 items-center justify-start">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<button <button
type="button" type="button"
onClick={showFileDialog} onClick={showFileDialog}
className="focus:outline-none h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md"> className="focus:outline-none h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
{t('index:openFile')} {t('index:openFile')}
</button> </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"
strokeWidth="4"/>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
</div> </div>
<input type='file' <input type='file'
@ -351,43 +436,67 @@ function Form(): JSX.Element {
</span> </span>
</div> </div>
} }
{fileErrorMessages.map((message, i) =>
<Alert message={message} key={'error-' + i} type="error" />
)}
</div> </div>
}/> }/>
<Card step="3" heading={t('index:addToWalletHeader')} content={ {showDoseOption && <Card step="3" heading={'Choose dose number'} content={
<div className="space-y-5"> <div className="space-y-5">
<p> <p>
{t('index:formatChange')}
<br /><br />
{t('index:saveMultiple')}
</p>
<link href="https://cdn.jsdelivr.net/npm/@tailwindcss/custom-forms@0.2.1/dist/custom-forms.css" rel="stylesheet"/>
<div className="block">
<div className="mt-2">
{payloadBody && Object.keys(payloadBody.receipts).map(key =>
<div key={key}>
<label className="inline-flex items-center">
<input onChange={setDose} type="radio" className="form-radio" name="radio" value={key} checked={parseInt(key) == selectedDose} />
<span className="ml-2">Dose {key}</span>
</label>
</div>
)}
</div>
</div>
</div>
} />}
<Card step={showDoseOption ? '4' : '3'} heading={t('index:addToWalletHeader')} content={
<div className="space-y-5">
{/* <p>
{t('index:dataPrivacyDescription')} {t('index:dataPrivacyDescription')}
{/* <Link href="/privacy"> <Link href="/privacy">
<a> <a>
{t('index:privacyPolicy')} {t('index:privacyPolicy')}
</a> </a>
</Link>. */} </Link>.
</p> </p> */}
<div> <div>
<ul className="list-none"> <ul className="list-none">
<Check text={t('createdOnDevice')}/> <Check text={t('createdOnDevice')}/>
<Check text={t('qrCode')}/> <Check text={t('piiNotSent')}/>
<Check text={t('openSourceTransparent')}/> <Check text={t('openSourceTransparent')}/>
<Check text={t('verifierLink')}/> {verifierLink()}
{passCount && <Check text={passCount + ' ' + t('numPasses')}/>}
{/* <Check text={t('hostedInEU')}/> */}
</ul> </ul>
</div> </div>
<div className="flex flex-row items-center justify-start"> <div className="flex flex-row items-center justify-start">
<button id="download" type="submit" value='applewallet' name='action' <button disabled={isDisabledAppleWallet || saveLoading ||!payloadBody} id="download" type="submit" value='applewallet' name='action'
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400"> className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
{t('index:addToWallet')} {t('index:addToWallet')}
</button> </button>
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<button id="saveAsPhoto" type="button" value='photo' name='action' onClick={saveAsPhoto} <button id="saveAsPhoto" type="button" disabled={saveLoading || !payloadBody} value='photo' name='action' onClick={saveAsPhoto}
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400"> className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
{t('index:saveAsPhoto')} {t('index:saveAsPhoto')}
</button> </button>
<div id="spin" className={loading ? undefined : "hidden"}> <div id="spin" className={saveLoading ? undefined : "hidden"}>
<svg className="animate-spin h-5 w-5 ml-4" viewBox="0 0 24 24"> <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" <circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"/> strokeWidth="4"/>
@ -396,13 +505,39 @@ function Form(): JSX.Element {
</svg> </svg>
</div> </div>
</div> </div>
{addErrorMessages.map((message, i) =>
<Alert message={message} key={'error-' + i} type="error" />
)}
{/* {warningMessages.map((message, i) =>
<Alert message={message} key={'warning-' + i} type="warning" />
)} */}
</div>
}/>
<Card step="?" heading={t('index:questions')} content={
<div className="space-y-5">
<p>Do you want to use this tool but...</p>
<div>
<ul>
<Bullet text="You would like to understand how your data is handled?"/>
<Bullet text="You don't have a health card?"/>
<Bullet text="You have a Red/White OHIP card?"/>
<Bullet text='You have an iPhone 6 or older?'/>
<Bullet text='You have an Android?'/>
</ul>
</div>
<div className="flex flex-row items-center justify-start">
<button id="faq-redirect" onClick={goToFAQ}
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
Visit our FAQ section for the answers!
</button>
&nbsp;&nbsp;&nbsp;&nbsp;
</div>
</div> </div>
}/> }/>
</form> </form>
<canvas id="canvas" style={{display: "none"}}/> <canvas id="canvas" style={{ display: "none" }} />
{
errorMessage && <Alert errorMessage={errorMessage} onClose={() => setErrorMessage(undefined)}/>
}
</div> </div>
) )
} }

View File

@ -1,9 +1,13 @@
import {useTranslation} from 'next-i18next'; import {useTranslation} from 'next-i18next';
import usePassCount from '../src/hooks/use_pass_count';
import Link from 'next/link' import Link from 'next/link'
function Logo(): JSX.Element { function Logo(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const passCount = usePassCount();
const displayPassCount = (passCount? ` - ${passCount} receipts processed to date!` : '');
return ( return (
<Link href="/"> <Link href="/">
@ -21,6 +25,7 @@ function Logo(): JSX.Element {
</svg> </svg>
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
{t('common:title')} {t('common:title')}
{/* {displayPassCount} */}
</h1> </h1>
</a> </a>
</Link> </Link>

View File

@ -1,5 +1,8 @@
import React from "react";
import {useTranslation} from 'next-i18next'; import {useTranslation} from 'next-i18next';
import usePassCount from "../src/hooks/use_pass_count";
import Head from 'next/head' import Head from 'next/head'
import Logo from './Logo' import Logo from './Logo'
import Link from 'next/link' import Link from 'next/link'
@ -10,7 +13,6 @@ interface PageProps {
function Page(props: PageProps): JSX.Element { function Page(props: PageProps): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
return ( return (
<div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 py-12"> <div className="md:w-2/3 xl:w-2/5 md:mx-auto flex flex-col min-h-screen justify-center px-5 py-12">
<Head> <Head>
@ -34,7 +36,7 @@ function Page(props: PageProps): JSX.Element {
<a href="https://github.com/billylo1/covidpass" className="underline">{t('common:gitHub')}</a> <a href="https://github.com/billylo1/covidpass" className="underline">{t('common:gitHub')}</a>
<a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a> <a href="https://vaccine-ontario.ca" className="underline">{t('common:returnToMainSite')}</a>
</nav> </nav>
<div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-09-18 (v1.8)</div> <div className="flex pt-4 flex-row space-x-4 justify-center text-md flex-wrap">Last updated: 2021-09-27 (v1.9.9)</div>
</footer> </footer>
</main> </main>
</div> </div>
@ -71,9 +73,11 @@ function Page(props: PageProps): JSX.Element {
<tr style={{height: 20}}></tr> <tr style={{height: 20}}></tr>
<tr> <tr>
<td><b>NAME</b></td> <td><b>NAME</b></td>
<td><b>DATE OF BIRTH</b></td>
</tr> </tr>
<tr> <tr>
<td id='name' style={{fontSize: 16}}></td> <td id='name' style={{fontSize: 12}}></td>
<td id='dob' style={{fontSize: 12}}></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -2,4 +2,6 @@ docker build . -t covidpass -t gcr.io/broadcast2patients/covidpass
docker push gcr.io/broadcast2patients/covidpass docker push gcr.io/broadcast2patients/covidpass
gcloud config set project broadcast2patients gcloud config set project broadcast2patients
gcloud config set run/region us-east1 gcloud config set run/region us-east1
gcloud run services update-traffic covidpass --to-latest
gcloud run deploy covidpass --image gcr.io/broadcast2patients/covidpass:latest --platform managed gcloud run deploy covidpass --image gcr.io/broadcast2patients/covidpass:latest --platform managed
docker image prune

206
package-lock.json generated
View File

@ -11,11 +11,11 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^1.3.0", "@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9", "@ninja-labs/verify-pdf": "^0.3.9",
"@sentry/browser": "^6.12.0", "@sentry/browser": "^6.13.2",
"@sentry/integrations": "^6.12.0", "@sentry/integrations": "^6.13.2",
"@sentry/react": "^6.12.0", "@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.12.0", "@sentry/tracing": "^6.13.2",
"@sentry/types": "^6.12.0", "@sentry/types": "^6.13.2",
"@zxing/browser": "^0.0.9", "@zxing/browser": "^0.0.9",
"@zxing/library": "^0.18.6", "@zxing/library": "^0.18.6",
"base45": "^3.0.0", "base45": "^3.0.0",
@ -307,13 +307,13 @@
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",
"integrity": "sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==", "integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==",
"dependencies": { "dependencies": {
"@sentry/core": "6.12.0", "@sentry/core": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -321,14 +321,14 @@
} }
}, },
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
"integrity": "sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==", "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
"dependencies": { "dependencies": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -336,12 +336,12 @@
} }
}, },
"node_modules/@sentry/hub": { "node_modules/@sentry/hub": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
"integrity": "sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==", "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
"dependencies": { "dependencies": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -349,12 +349,12 @@
} }
}, },
"node_modules/@sentry/integrations": { "node_modules/@sentry/integrations": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.13.2.tgz",
"integrity": "sha512-M9gsVdWZp5fAFFpTjK2IBuWzW4SBxGAI3tVbYZvVx16S/BY0GsPC1dYpjJx9OTBS/8CmCWdGxnUmjACo/8w1LA==", "integrity": "sha512-CzxMtNr4nkZbifD0Rb6tXwqfqm+fWKl4IQTaFrJ92VNdgihBMVWYmflRqkMkGh1iFN8bVPpXrGyplY5tFN+2kA==",
"dependencies": { "dependencies": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"localforage": "^1.8.1", "localforage": "^1.8.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
@ -363,12 +363,12 @@
} }
}, },
"node_modules/@sentry/minimal": { "node_modules/@sentry/minimal": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
"integrity": "sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==", "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
"dependencies": { "dependencies": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -376,14 +376,14 @@
} }
}, },
"node_modules/@sentry/react": { "node_modules/@sentry/react": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz",
"integrity": "sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ==", "integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==",
"dependencies": { "dependencies": {
"@sentry/browser": "6.12.0", "@sentry/browser": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"hoist-non-react-statics": "^3.3.2", "hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
@ -395,14 +395,14 @@
} }
}, },
"node_modules/@sentry/tracing": { "node_modules/@sentry/tracing": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
"integrity": "sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==", "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
"dependencies": { "dependencies": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -410,19 +410,19 @@
} }
}, },
"node_modules/@sentry/types": { "node_modules/@sentry/types": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
"integrity": "sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA==", "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@sentry/utils": { "node_modules/@sentry/utils": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==", "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
"dependencies": { "dependencies": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
}, },
"engines": { "engines": {
@ -5536,95 +5536,95 @@
} }
}, },
"@sentry/browser": { "@sentry/browser": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz",
"integrity": "sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==", "integrity": "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag==",
"requires": { "requires": {
"@sentry/core": "6.12.0", "@sentry/core": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/core": { "@sentry/core": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz",
"integrity": "sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==", "integrity": "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ==",
"requires": { "requires": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/hub": { "@sentry/hub": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz",
"integrity": "sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==", "integrity": "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A==",
"requires": { "requires": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/integrations": { "@sentry/integrations": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.13.2.tgz",
"integrity": "sha512-M9gsVdWZp5fAFFpTjK2IBuWzW4SBxGAI3tVbYZvVx16S/BY0GsPC1dYpjJx9OTBS/8CmCWdGxnUmjACo/8w1LA==", "integrity": "sha512-CzxMtNr4nkZbifD0Rb6tXwqfqm+fWKl4IQTaFrJ92VNdgihBMVWYmflRqkMkGh1iFN8bVPpXrGyplY5tFN+2kA==",
"requires": { "requires": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"localforage": "^1.8.1", "localforage": "^1.8.1",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/minimal": { "@sentry/minimal": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz",
"integrity": "sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==", "integrity": "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw==",
"requires": { "requires": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/react": { "@sentry/react": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz",
"integrity": "sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ==", "integrity": "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ==",
"requires": { "requires": {
"@sentry/browser": "6.12.0", "@sentry/browser": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"hoist-non-react-statics": "^3.3.2", "hoist-non-react-statics": "^3.3.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/tracing": { "@sentry/tracing": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz",
"integrity": "sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==", "integrity": "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw==",
"requires": { "requires": {
"@sentry/hub": "6.12.0", "@sentry/hub": "6.13.2",
"@sentry/minimal": "6.12.0", "@sentry/minimal": "6.13.2",
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"@sentry/utils": "6.12.0", "@sentry/utils": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },
"@sentry/types": { "@sentry/types": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz",
"integrity": "sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA==" "integrity": "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
}, },
"@sentry/utils": { "@sentry/utils": {
"version": "6.12.0", "version": "6.13.2",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz",
"integrity": "sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==", "integrity": "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w==",
"requires": { "requires": {
"@sentry/types": "6.12.0", "@sentry/types": "6.13.2",
"tslib": "^1.9.3" "tslib": "^1.9.3"
} }
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "grassroots_covidpass", "name": "grassroots_covidpass",
"version": "1.8.0", "version": "1.9.7",
"author": "Billy Lo <billy@vaccine-ontario.ca>", "author": "Billy Lo <billy@vaccine-ontario.ca>",
"license": "MIT", "license": "MIT",
"private": false, "private": false,
@ -17,11 +17,11 @@
"dependencies": { "dependencies": {
"@headlessui/react": "^1.3.0", "@headlessui/react": "^1.3.0",
"@ninja-labs/verify-pdf": "^0.3.9", "@ninja-labs/verify-pdf": "^0.3.9",
"@sentry/browser": "^6.12.0", "@sentry/browser": "^6.13.2",
"@sentry/integrations": "^6.12.0", "@sentry/integrations": "^6.13.2",
"@sentry/react": "^6.12.0", "@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.12.0", "@sentry/tracing": "^6.13.2",
"@sentry/types": "^6.12.0", "@sentry/types": "^6.13.2",
"@zxing/browser": "^0.0.9", "@zxing/browser": "^0.0.9",
"@zxing/library": "^0.18.6", "@zxing/library": "^0.18.6",
"base45": "^3.0.0", "base45": "^3.0.0",

View File

@ -4,11 +4,13 @@ type ConfigData = {
apiBaseUrl: string apiBaseUrl: string
verifierHost: string verifierHost: string
hitcountHost: string hitcountHost: string
registrationHost: string
functionSuffix: string
} }
export default function handler(req: NextApiRequest, res: NextApiResponse<ConfigData>) { export default function handler(req: NextApiRequest, res: NextApiResponse<ConfigData>) {
// Return the API_BASE_URL. This Endpoint allows us to access the env Variable in client javascript // Return the API_BASE_URL. This Endpoint allows us to access the env Variable in client javascript
res.status(200).json({apiBaseUrl: process.env.API_BASE_URL, verifierHost: process.env.VERIFIER_HOST, hitcountHost: process.env.HITCOUNT_HOST}) res.status(200).json({apiBaseUrl: process.env.API_BASE_URL, verifierHost: process.env.VERIFIER_HOST, hitcountHost: process.env.HITCOUNT_HOST, registrationHost: process.env.REGISTRATION_HOST, functionSuffix: process.env.FUNCTION_SUFFIX})
} }

View File

@ -7,34 +7,40 @@ import Card from '../components/Card'
function Faq(): JSX.Element { function Faq(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'faq']); const { t } = useTranslation(['common', 'index', 'faq']);
const questionList = [ const questionList = [
{description: 'Which version of iOS does this support?', answer: 'iOS 13.7 is the minimum at the moment. We are looking for adjustments for older iOSes, but it will take a bit of time.', key: 1}, {description: 'Which version of iOS does this support?', answer: 'iOS 13.7 is the minimum at the moment. We are looking for adjustments for older iOS versions, but it will take a bit of time.'},
{description: 'Why have we taken time to build this?', answer: 'Gives Ontarians/organizations something easy to use (volunteered-developed, unofficial) until the official provincial app comes out in October.', key: 2}, {description: 'I\'m having issues with adding it to my iPhone 6.', answer: 'Unfortunately, the iPhone 6 supports up to iOS 12 while the minimum requirement for our app is iOS 13.7 however, we are looking for ways around this to make it more accessible. In the meantime you can try it on a computer or another device and save it as either a wallet card or a photo - if you save it as a card, you can then email it to your iPhone and you will be able to import it into Apple Wallet that way.'},
{description: 'Who made this?', answer: 'The same group of volunteers (Billy Lo, Ryan Slobojan, Evert Timberg, Jason Liu, Anujan Mathisekaran, Lisa Discepola, Samantha Finn, Madison Pearce) who created the all-in-one vaccine appointment finding tool at vaccine-ontario.ca.', key: 3}, {description: "What are the supported browsers?", answer: 'For iPhones, only Safari is supported for importing to your Apple Wallet. For any other devices, we recommend that you save it as photo using your browser of choice. Browsers built internally into mobile apps (e.g. Facebook, Twitter, Instagram) are known to have issues.'},
{description: 'Should I use the official provincial apps when they come out on 22nd October?', answer: 'YES. Once the official QR code from the province is available, you will also be able to refresh what\'s in your Apple Wallet as well. More details will follow.', key: 4}, {description: "How is my private information handled?", answer: 'Your proof-of-vaccination PDF (and most of the information in it) never leaves your device, and does NOT get sent to our server - the only information we send and store is non-personally-identifiable information such as vaccine type, date, and which organization gave you the vaccine. We share your concern about personal data being stored and lost, which is why we chose not to store or send any of it to our servers so there is no chance of it being lost or leaked.'},
{description: 'How is the data on my vaccination receipt processed?', answer: 'It checks the receipt for an official signature from the province. If present, the receipt data is converted into Apple\'s format and then added into your iOS Wallet app.', key: 5}, {description: 'Do you have plans for Android support?', answer: 'Yes. We are working with Google to gain access to the APIs required. Meanwhile, you can also use this tool to download an Apple Wallet pass and import that into Google Pay Wallet using apps such as Pass2Pay or simply save it as a photo.'},
{description: 'How can organizations validate this QR code?', answer: 'Just aim your standard camera app (iPhone/Android) at the code, and it will bring up a web page that shows the verification result.', key: 6}, {description: 'I have a Red/White OHIP card. Can I still use this tool?', answer: 'Yes you can! Just call the Provincial Vaccine Contact Centre at 1-833-943-3900. The call centre agent can email you a copy of the receipt.'},
{description: 'Is this free and private?', answer: 'Similar to VaxHuntersCanada, there are no commerical interests. Just volunteers trying to do our part to help the community.', key: 7}, {description: 'I do not have a health card. Can I still use this tool?', answer: 'First contact your local public health unit to verify your identity and receive a COVIDcovid ID/Personal Access Code. You can then call the Provincial Vaccine Contact Centre at 1-833-943-3900 to get an email copy of your receipt.'},
{description: 'Do you have plans for Android support?', answer: 'Yes. We are working with Google to gain access to the APIs required. Meanwhile, you can also use this tool to download an Apple Wallet pass and import that into Google Pay Wallet using apps such as Pass2Pay.', key: 8}, {description: 'I\'m seeing an error message saying “Failed byte range verification." What do I do?', answer: 'If you see this error then please try re-downloading your receipt from the provincial proof-of-vaccination portal and trying again. We have received reports from some people that this has resolved the problem for them.'},
{description: 'How about BC, Quebec and Alberta?', answer: 'We will be investigating BC shortly. If you are interested in contributing, email us at grassroots@vaccine-ontario.ca', key: 9}, {description: 'What does the colour of the Apple Wallet pass mean?', answer: 'Dose 1 is shown as Orange; dose 2+ in green for easy differentiation without reading the text. For the Janssen (Johnson & Johnson) vaccine, dose 1 is shown as green.'},
{description: 'How can I stay up-to-date on your progress?', answer: 'We will post regular updates on Twitter @grassroots_team.', key: 10}, {description: 'Should I use the official provincial apps when they come out on 22nd October?', answer: 'YES. Once the official QR code from the province is available, please come back to this site and you will be able to generate a new Apple Wallet pass which contains that new QR code'},
{description: 'I only have an emailed receipt (e.g. Red/White OHIP card users). Can I still use this tool?', answer: 'Not right now unfortunately. But we expect to be able to support that after the official app is released on 22 Oct.', key: 11}, {description: 'How is the data on my vaccination receipt processed?', answer: 'Inside your local web browser, it checks the receipt for a digital signature from the provincial proof-of-vaccination system. If present, the receipt data is converted into Apple\'s format and then added into your iOS Wallet app.'},
{description: 'What does the colour of the Apple Wallet pass mean?', answer: 'Dose 1 is shown as Orange; dose 2+ in green for easy differentiation without reading the text.', key: 12}, {description: 'How can organizations validate this QR code?', answer: 'You can use our verifier app at verifier.vaccine-ontario.ca to verify these passes quickly if you are a business - you should also be able to use any normal QR code scanner to scan this code and it will take you to a verification site which tells you whether the receipt is valid or not'},
{description: 'How about Apple Watch?', answer: 'If you have iCloud sync enabled, you will see the pass on the watch too.', key: 13}, {description: 'Can I use the same iPhone to store passes for my entire family?', answer: 'Yes.'},
{description: 'I have more questions. Can you please help me?', answer: 'Sure. Just email us at grassroots@vaccine-ontario.ca.', key: 14} {description: 'Is this free and non-commercial?', answer: 'Similar to VaxHuntersCanada, there are no commercial interests. Just volunteers trying to do our part to help the community.'},
{description: 'How about support for other provinces?', answer: 'We will be investigating BC and Québec support shortly. If you are interested in contributing, please email us at grassroots@vaccine-ontario.ca'},
{description: 'How about Apple Watch?', answer: 'If you have iCloud sync enabled, you will see the pass on the watch too.'},
{description: 'Why have we taken time to build this?', answer: 'Gives Ontarians/organizations something easy to use (volunteered-developed, unofficial) until the official provincial app comes out in October.'},
{description: 'Who made this?', answer: 'The same group of volunteers who created the all-in-one vaccine appointment finding tool at vaccine-ontario.ca'},
{description: 'How can I stay up-to-date on your progress?', answer: 'We will post regular updates on Twitter @grassroots_team'},
{description: 'I have more questions. Can you please help me?', answer: 'Sure. Just email us at grassroots@vaccine-ontario.ca'}
]; ];
return ( return (
<Page content={ <Page content={
<Card step="§" heading={t('common:faq')} content={ <Card step="?" heading={t('common:faq')} content={
<div className="space-y-3"> <div className="space-y-3">
<p className="font-bold">{t('faq:heading')}</p> <p className="font-bold">{t('faq:heading')}</p>
<ol> <ol>
{questionList.map(question => { {questionList.map((question, i) => {
return ( return (
<div> <div>
<li key={question.key}><b>{question.key}. {question.description}</b></li> <li key={i}><b>{i+1}. {question.description}</b></li>
<li key={question.key}>{question.answer}</li> <li key={i}>{question.answer}</li>
<br></br> <br></br>
</div> </div>
); );

View File

@ -5,10 +5,38 @@ import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import Form from '../components/Form'; import Form from '../components/Form';
import Card from '../components/Card'; import Card from '../components/Card';
import Page from '../components/Page'; import Page from '../components/Page';
import { useEffect, useState } from 'react'; import Alert from '../components/Alert';
import React, { useEffect, useState } from 'react';
import { isIOS, isSafari, isAndroid} from 'react-device-detect';
import usePassCount from "../src/hooks/use_pass_count";
import Link from 'next/link'
function Index(): JSX.Element { function Index(): JSX.Element {
const { t } = useTranslation(['common', 'index', 'errors']); const { t } = useTranslation(['common', 'index', 'errors']);
const passCount = usePassCount();
const displayPassCount = (passCount? `${passCount} receipts have been processed successfully to date!` : '');
const [warningMessages, _setWarningMessages] = useState<Array<string>>([]);
const setWarningMessage = (message: string) => {
if (!message) return;
const translation = t('errors:'.concat(message));
_setWarningMessages(Array.from(new Set([...warningMessages, translation !== message ? translation : message])));
};
const deleteWarningMessage = (message: string) => _setWarningMessages(warningMessages.filter(item => item !== message));
useEffect(() => {
if (isIOS && !isSafari) setWarningMessage("iPhone users, only Safari is supported at the moment. Please switch to Safari to prevent any unexpected errors.")
else if (!isIOS) {
setWarningMessage('Only Safari on iOS is officially supported for Apple Wallet import at the moment - ' +
'for other platforms, please ensure you have an application which can open Apple Wallet .pkpass files');
}
}, []);
// If you previously created a vaccination receipt before Sept. 23rd and need to add your date of birth on your vaccination receipt, please reimport your Ministry of Health official vaccination receipt again below and the date of birth will now be visible on the created receipt
const title = 'Grassroots - Ontario vaccination receipt to your Apple wallet'; const title = 'Grassroots - Ontario vaccination receipt to your Apple wallet';
const description = 'Stores it on iPhone with a QR code for others to validate in a privacy respecting way.'; const description = 'Stores it on iPhone with a QR code for others to validate in a privacy respecting way.';
@ -40,10 +68,28 @@ function Index(): JSX.Element {
/> />
<Page content={ <Page content={
<div className="space-y-5"> <div className="space-y-5">
{warningMessages.map((message, i) =>
<Alert message={message} key={'error-' + i} type="warning" onClose={() => deleteWarningMessage(message)} />
)}
<Card content={ <Card content={
<div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p></div> <div><p>{t('common:subtitle')}</p><br /><p>{t('common:subtitle2')}</p><br />
<b>{displayPassCount}</b><br/><br/>
Sept 29 afternoon update:
<br />
<br />
<ul className="list-decimal list-outside" style={{ marginLeft: '20px' }}>
<li>You can now select which page to import for multi-page receipts</li>
<li>System reminders (e.g. unsupported browsers) are now on the top to improve ease of use</li>
</ul><br />
<p>{t('common:continueSpirit')}</p>
<br />
<Link href="https://www.youtube.com/watch?v=AIrG5Qbjptg">
<a className="underline" target="_blank">
Click here for a video demo
</a>
</Link>&nbsp;
</div>
}/> }/>
<Form/> <Form/>
</div> </div>
}/> }/>

View File

@ -1,6 +1,9 @@
title: Vaccination Receipt to Wallet title: Vaccination Receipt to Wallet
subtitle: This tool converts your vaccination receipt from Ontario Ministry of Health to an Apple Wallet pass for easy access in the interim. subtitle: This utility (created by volunteers) converts your vaccination receipt from Ontario Ministry of Health to an Apple Wallet pass for easy access in the interim.
subtitle2: Once Ontario's official QR code is released on Oct 22, you will be able to update your Apple Wallet pass by visiting this site again. subtitle2: Once Ontario's official QR code is available on Oct 22, you will be able to update your Apple Wallet pass by visiting this site again.
update1Date: Sep 23 Updates
update1: Thanks so much for all the encouragements and suggestions to make this better. We plan to keep enhancing this to help more Canadians. Stay tuned!
continueSpirit: Continuing the spirit of ❤️ @VaxHuntersCanada ❤️.
privacyPolicy: Privacy Policy privacyPolicy: Privacy Policy
donate: Sponsor donate: Sponsor
gitHub: GitHub gitHub: GitHub

View File

@ -4,7 +4,7 @@ selectCertificate: Select vaccination receipt (PDF)
selectCertificateDescription: | selectCertificateDescription: |
Press "Select File", "Browse..." and select the PDF file you have saved in Step 1. Press "Select File", "Browse..." and select the PDF file you have saved in Step 1.
selectCertificateReminder: | selectCertificateReminder: |
Reminder : Receipts directly downloaded from the provincial web site is required. Emailed copies are not digitally signed and cannot be added to Apple Wallet. Reminder : Receipts directly downloaded from the provincial web site is required.
#stopCamera: Stop Camera #stopCamera: Stop Camera
#startCamera: Start Camera #startCamera: Start Camera
openFile: Select File openFile: Select File
@ -14,6 +14,9 @@ visit: Visit
ontarioHealth: Ontario Ministry of Health ontarioHealth: Ontario Ministry of Health
gotoOntarioHealth: Go to 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. 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.
reminderNotToRepeat: If you have completed this step before, simply proceed to Step 2.
formatChange: After the recent vaccination receipt formatting change, both doses are included in the same file. Please select which dose you which dose you want to save.
saveMultiple: To save multiple receipts, please select the first one you want to save and click the Wallet or Photo button below, then change which dose is selected here and push the button again to generate another Wallet or Photo for another dose.
pickColor: Pick a Color pickColor: Pick a Color
pickColorDescription: Pick a background color for your pass. pickColorDescription: Pick a background color for your pass.
colorWhite: white colorWhite: white
@ -31,11 +34,12 @@ dataPrivacyDescription: |
Press the "Add to Wallet" below to import data into Wallet. Press the "Add to Wallet" below to import data into Wallet.
iAcceptThe: I accept the iAcceptThe: I accept the
privacyPolicy: Privacy Policy privacyPolicy: Privacy Policy
createdOnDevice: No personal data is sent to the Internet. createdOnDevice: Your receipt is processed on your device only.
qrCode: QR code is for verification only, with no personal info. piiNotSent: No personally-identifiable information is sent to servers
openSourceTransparent: Source code is free and open for re-use/contributions on GitHub. openSourceTransparent: Source code is open for re-use/contributions on GitHub.
verifierLink: QR code verifier available at https://verifier.vaccine-ontario.ca verifierLink: QR code verifier available at
numPasses: receipts processed since Sept 2, 2021 numPasses: receipts processed since Sept 2, 2021
demo: Video Demo demo: Video Demo
whatsnew: What's New whatsnew: What's New
questions: Have Questions?
#hostedInEU: Hosted in the EU #hostedInEU: Hosted in the EU

View File

@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react';
export default function usePassCount() {
const [passCount, setPassCount] = useState<string>('');
const hitcountHost = 'https://stats.vaccine-ontario.ca';
useEffect(() => {
if (passCount.length == 0) {
getPassCount();
}
}, []);
const getPassCount = async () => {
const hitCount = await getHitCount();
setPassCount(hitCount);
};
async function getHitCount() {
try {
const request = `${hitcountHost}/nocount?url=pass.vaccine-ontario.ca`;
let response = await fetch(request);
const counter = await response.text();
return Promise.resolve(parseInt(counter, 10).toLocaleString());
} catch (e) {
console.error(e);
return Promise.reject(e);
}
}
return passCount;
};

View File

@ -4,6 +4,7 @@ import {v4 as uuid4} from 'uuid';
import {Constants} from "./constants"; import {Constants} from "./constants";
import {Payload, PayloadBody, PassDictionary} from "./payload"; import {Payload, PayloadBody, PassDictionary} from "./payload";
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { QRCodeMatrixUtil } from '@zxing/library';
const crypto = require('crypto') const crypto = require('crypto')
@ -80,11 +81,11 @@ export class PassData {
return await response.arrayBuffer() return await response.arrayBuffer()
} }
static async generatePass(payloadBody: PayloadBody): Promise<Buffer> { static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Buffer> {
// Create Payload // Create Payload
try { try {
const payload: Payload = new Payload(payloadBody); const payload: Payload = new Payload(payloadBody, numDose);
payload.serialNumber = uuid4(); payload.serialNumber = uuid4();
@ -104,19 +105,35 @@ export class PassData {
body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header body: JSON.stringify(clonedReceipt) // body data type must match "Content-Type" header
} }
console.log('registering ' + JSON.stringify(clonedReceipt, null, 2)); // console.log('registering ' + JSON.stringify(clonedReceipt, null, 2));
const configResponse = await fetch('/api/config') const configResponse = await fetch('/api/config');
const verifierHost = (await configResponse.json()).verifierHost
// const verifierHost = 'https://verifier.vaccine-ontario.ca'; const configResponseJson = await configResponse.json();
const response = await fetch('https://us-central1-grassroot-verifier.cloudfunctions.net/register', requestOptions); const verifierHost = configResponseJson.verifierHost;
const registrationHost = configResponseJson.registrationHost;
let functionSuffix = configResponseJson.functionSuffix;
if (functionSuffix == undefined)
functionSuffix = '';
const registerUrl = `${registrationHost}/register${functionSuffix}`;
// console.log(registerUrl);
const response = await fetch(registerUrl, requestOptions);
const responseJson = await response.json(); const responseJson = await response.json();
// console.log(JSON.stringify(responseJson,null,2)); console.log(JSON.stringify(responseJson,null,2));
if (responseJson["result"] != 'OK') if (responseJson["result"] != 'OK') {
console.error(responseJson);
return Promise.reject(); return Promise.reject();
}
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payload.receipt.vaccineName)}&vaccinationDate=${encodeURIComponent(payload.receipt.vaccinationDate)}&organization=${encodeURIComponent(payload.receipt.organization)}&dose=${encodeURIComponent(payload.receipt.numDoses)}`;
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
// console.log(qrCodeUrl);
let qrCodeMessage = payloadBody.rawData.startsWith('shc:/') let qrCodeMessage = payloadBody.rawData.startsWith('shc:/')
? payloadBody.rawData ? payloadBody.rawData
@ -124,7 +141,7 @@ export class PassData {
// Create QR Code Object // Create QR Code Object
const qrCode: QrCode = { const qrCode: QrCode = {
message: qrCodeMessage, message: qrCodeUrl,
format: QrFormat.PKBarcodeFormatQR, format: QrFormat.PKBarcodeFormatQR,
messageEncoding: Encoding.iso88591, messageEncoding: Encoding.iso88591,
// altText : payload.rawData // altText : payload.rawData

View File

@ -4,6 +4,9 @@ import {COLORS} from "./colors";
export class Receipt { export class Receipt {
constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {}; constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {};
} }
export interface HashTable<T> {
[key: string]: T;
}
enum TextAlignment { enum TextAlignment {
right = 'PKTextAlignmentRight', right = 'PKTextAlignmentRight',
@ -28,7 +31,7 @@ export interface PassDictionary {
export interface PayloadBody { export interface PayloadBody {
// color: COLORS; // color: COLORS;
rawData: string; rawData: string;
receipts: Receipt[]; receipts: HashTable<Receipt>;
} }
export class Payload { export class Payload {
@ -43,16 +46,27 @@ export class Payload {
serialNumber: string; serialNumber: string;
generic: PassDictionary; generic: PassDictionary;
constructor(body: PayloadBody) { constructor(body: PayloadBody, numDose: number) {
let receipt = body.receipts[0]; let receipt = body.receipts[0];
// Get name and date of birth information // Get name and date of birth information
const name = body.receipts[numDose].name;
const dateOfBirth = body.receipts[numDose].dateOfBirth;
const vaccineName = body.receipts[numDose].vaccineName;
let vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
if (vaccineName.includes('PFIZER'))
vaccineNameProper = 'Pfizer (Comirnaty)'
if (vaccineName.includes('MODERNA'))
vaccineNameProper = 'Moderna (SpikeVax)'
// vaccineNameProper = 'Pfizer (Comirnaty)'
if (vaccineName.includes('ASTRAZENECA'))
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
let doseVaccine = "#" + String(body.receipts[numDose].numDoses) + ": " + vaccineNameProper;
const name = receipt.name;
const dateOfBirth = receipt.dateOfBirth;
const vaccineName = receipt.vaccineName;
const vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
const doseVaccine = "Dose " + String(receipt.numDoses) + ": " + vaccineNameProper;
if (name == undefined) { if (name == undefined) {
throw new Error('nameMissing'); throw new Error('nameMissing');
} }
@ -75,13 +89,13 @@ export class Payload {
{ {
key: "issuer", key: "issuer",
label: "Authorized Organization", label: "Authorized Organization",
value: receipt.organization value: body.receipts[numDose].organization
}, },
{ {
key: "dov", key: "dov",
label: "Date", label: "Date",
value: receipt.vaccinationDate, value: body.receipts[numDose].vaccinationDate,
// textAlignment: TextAlignment.right // textAlignment: TextAlignment.right
} }
], ],
@ -90,25 +104,25 @@ export class Payload {
key: "name", key: "name",
label: "Name", label: "Name",
value: name value: name
},
{
key: "dob",
label: "Date of Birth",
value: dateOfBirth
} }
], ],
backFields: [ backFields: [
// { //TODO: add url link back to grassroots site
// key: "dob",
// label: "Date of Birth",
// value: body.receipt.dateOfBirth,
// textAlignment: TextAlignment.right
// }
] ]
} }
// Set Values // Set Values
this.receipts = body.receipts; // this.receipt = body.receipts[numDose];
this.rawData = body.rawData; this.rawData = body.rawData;
if (body.receipts.length > 1) { if (body.receipts[numDose].numDoses > 1 || body.receipts[numDose].vaccineName.toLowerCase().includes('janssen') || body.receipts[numDose].vaccineName.toLowerCase().includes('johnson') || body.receipts[numDose].vaccineName.toLowerCase().includes('j&j')) {
this.backgroundColor = COLORS.GREEN; this.backgroundColor = COLORS.GREEN;
} else { } else {
this.backgroundColor = COLORS.YELLOW; this.backgroundColor = COLORS.YELLOW;

View File

@ -35,11 +35,11 @@ export class Photo {
static async generatePass(payloadBody: PayloadBody): Promise<Blob> { static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Blob> {
// Create Payload // Create Payload
try { try {
const payload: Payload = new Payload(payloadBody); const payload: Payload = new Payload(payloadBody, numDose);
payload.serialNumber = uuid4(); payload.serialNumber = uuid4();
@ -73,9 +73,12 @@ export class Photo {
if (responseJson["result"] != 'OK') if (responseJson["result"] != 'OK')
return Promise.reject(); return Promise.reject();
const encodedUri = `serialNumber=${encodeURIComponent(payload.serialNumber)}&vaccineName=${encodeURIComponent(payload.receipt.vaccineName)}&vaccinationDate=${encodeURIComponent(payload.receipt.vaccinationDate)}&organization=${encodeURIComponent(payload.receipt.organization)}&dose=${encodeURIComponent(payload.receipt.numDoses)}`;
const qrCodeUrl = `${verifierHost}/verify?${encodedUri}`;
// Create QR Code Object // Create QR Code Object
const qrCode: QrCode = { const qrCode: QrCode = {
message: `${verifierHost}/verify?serialNumber=${payload.serialNumber}&vaccineName=${payload.receipt.vaccineName}&vaccinationDate=${payload.receipt.vaccinationDate}&organization=${payload.receipt.organization}&dose=${payload.receipt.numDoses}`, message: qrCodeUrl,
format: QrFormat.PKBarcodeFormatQR, format: QrFormat.PKBarcodeFormatQR,
messageEncoding: Encoding.iso88591, messageEncoding: Encoding.iso88591,
// altText : payload.rawData // altText : payload.rawData
@ -88,20 +91,30 @@ export class Photo {
// const body = domTree.getElementById('main'); // const body = domTree.getElementById('main');
const body = document.getElementById('pass-image'); const body = document.getElementById('pass-image');
body.hidden = false; body.hidden = false;
body.style.backgroundColor = payload.backgroundColor
if (payload.receipt.numDoses > 1) const name = payload.receipt.name;
body.style.backgroundColor = 'green'; const dateOfBirth = payload.receipt.dateOfBirth;
else
body.style.backgroundColor = 'orangered';
const vaccineName = payload.receipt.vaccineName; const vaccineName = payload.receipt.vaccineName;
const vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase(); let vaccineNameProper = vaccineName.charAt(0) + vaccineName.substr(1).toLowerCase();
const doseVaccine = "Dose " + String(payload.receipt.numDoses) + ": " + vaccineNameProper;
if (vaccineName.includes('PFIZER'))
vaccineNameProper = 'Pfizer (Comirnaty)'
if (vaccineName.includes('MODERNA'))
vaccineNameProper = 'Moderna (SpikeVax)'
if (vaccineName.includes('ASTRAZENECA'))
vaccineNameProper = 'AstraZeneca (Vaxzevria)'
let doseVaccine = "#" + String(payload.receipt.numDoses) + ": " + vaccineNameProper;
document.getElementById('vaccineName').innerText = doseVaccine; document.getElementById('vaccineName').innerText = doseVaccine;
document.getElementById('vaccinationDate').innerText = payload.receipt.vaccinationDate; document.getElementById('vaccinationDate').innerText = payload.receipt.vaccinationDate;
document.getElementById('organization').innerText = payload.receipt.organization; document.getElementById('organization').innerText = payload.receipt.organization;
document.getElementById('name').innerText = payload.receipt.name; document.getElementById('name').innerText = payload.receipt.name;
document.getElementById('dob').innerText = payload.receipt.dateOfBirth;
const codeWriter = new BrowserQRCodeSvgWriter(); const codeWriter = new BrowserQRCodeSvgWriter();
const svg = codeWriter.write(qrCode.message,200,200); const svg = codeWriter.write(qrCode.message,200,200);

View File

@ -1,7 +1,8 @@
import {PayloadBody, Receipt} from "./payload"; import {PayloadBody, Receipt, HashTable} from "./payload";
import * as PdfJS from 'pdfjs-dist' import * as PdfJS from 'pdfjs-dist'
import jsQR, {QRCode} from "jsqr"; import jsQR, {QRCode} from "jsqr";
import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6 import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6
import {COLORS} from "./colors";
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import * as Decode from './decode'; import * as Decode from './decode';
import {getScannedJWS, verifyJWS, decodeJWS} from "./shc"; import {getScannedJWS, verifyJWS, decodeJWS} from "./shc";
@ -18,19 +19,27 @@ PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pd
export async function getPayloadBodyFromFile(file: File): Promise<PayloadBody> { export async function getPayloadBodyFromFile(file: File): Promise<PayloadBody> {
// Read file // Read file
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
let receipts: HashTable<Receipt>;
switch (file.type) { switch (file.type) {
case 'application/pdf': case 'application/pdf':
return detectPDFTypeAndProcess(fileBuffer) const receiptType = await detectReceiptType(fileBuffer);
case 'image/png': receipts = await loadPDF(fileBuffer, receiptType) // receipt type is needed to decide if digital signature checking is needed
return processBCPNG(fileBuffer); break
default: default:
throw Error('invalidFileType') throw Error('invalidFileType')
} }
const rawData = ''; // unused at the moment, the original use was to store the QR code from issuer
return {
receipts: receipts,
rawData: rawData
}
} }
async function detectPDFTypeAndProcess(fileBuffer : ArrayBuffer): Promise<any> { async function detectReceiptType(fileBuffer : ArrayBuffer): Promise<string> {
// Ontario has 'COVID-19 vaccination receipt' // Ontario has 'COVID-19 vaccination receipt'
// BC has BC Vaccine Card // BC has BC Vaccine Card
@ -40,7 +49,6 @@ async function detectPDFTypeAndProcess(fileBuffer : ArrayBuffer): Promise<any> {
const typedArray = new Uint8Array(fileBuffer); const typedArray = new Uint8Array(fileBuffer);
let loadingTask = PdfJS.getDocument(typedArray); let loadingTask = PdfJS.getDocument(typedArray);
const pdfDocument = await loadingTask.promise; const pdfDocument = await loadingTask.promise;
let pageNumber = pdfDocument.numPages;
const pdfPage = await pdfDocument.getPage(1); //first page const pdfPage = await pdfDocument.getPage(1); //first page
const content = await pdfPage.getTextContent(); const content = await pdfPage.getTextContent();
const numItems = content.items.length; const numItems = content.items.length;
@ -50,16 +58,164 @@ async function detectPDFTypeAndProcess(fileBuffer : ArrayBuffer): Promise<any> {
console.log(value); console.log(value);
if (value.includes('BC Vaccine Card')) { if (value.includes('BC Vaccine Card')) {
console.log('detected bc'); console.log('detected bc');
return processBC(pdfPage); return Promise.resolve('BC');
} }
if (value.includes('COVID-19 vaccination receipt')) { if (value.includes('COVID-19 vaccination receipt')) {
console.log('detected on'); console.log('detected on');
return processON(fileBuffer, content); return Promise.resolve('ON');
} }
} }
} }
async function loadPDF(fileBuffer : ArrayBuffer, receiptType : string): Promise<HashTable<Receipt>> {
try {
if (receiptType == 'ON') {
const certs = getCertificatesInfoFromPDF(fileBuffer);
const result = certs[0];
const refcert = '-----BEGIN CERTIFICATE-----\r\n'+
'MIIHNTCCBh2gAwIBAgIQanhJa+fBXT8GQ8QG/t9p4TANBgkqhkiG9w0BAQsFADCB\r\n'+
'ujELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsT\r\n'+
'H1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAy\r\n'+
'MDE0IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEuMCwG\r\n'+
'A1UEAxMlRW50cnVzdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEwxTTAeFw0y\r\n'+
'MTA1MjAxMzQxNTBaFw0yMjA2MTkxMzQxNDlaMIHTMQswCQYDVQQGEwJDQTEQMA4G\r\n'+
'A1UECBMHT250YXJpbzEQMA4GA1UEBxMHVG9yb250bzETMBEGCysGAQQBgjc8AgED\r\n'+
'EwJDQTEYMBYGCysGAQQBgjc8AgECEwdPbnRhcmlvMRcwFQYDVQQKEw5PbnRhcmlv\r\n'+
'IEhlYWx0aDEaMBgGA1UEDxMRR292ZXJubWVudCBFbnRpdHkxEzARBgNVBAUTCjE4\r\n'+
'LTA0LTIwMTkxJzAlBgNVBAMTHmNvdmlkMTlzaWduZXIub250YXJpb2hlYWx0aC5j\r\n'+
'YTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL2bD+Ng1RNYCNVVtEQ3\r\n'+
'zg8JKFvRWFFPIF/UTXGg3iArK1tKr1xtjx6OdFtwosHyo+3ksPRicc4KeuV6/QMF\r\n'+
'qiVJ5IOy9TSVImJsmONgFyEiak0dGYG5SeHiWwyaUvkniWd7U3wWEl4nOZuLAYu4\r\n'+
'8ZLot8p8Q/UaNvAoNsRDv6YDGjL2yGHaXxi3Bb6XTQTLcevuEQeM6g1LtKyisZfB\r\n'+
'Q8TKThBq99EojwHfXIhddxbPKLeXvWJgK1TcL17UFIwx6ig74s0LyYqEPm8Oa8qR\r\n'+
'+IesFUT9Liv7xhV+tU52wmNfDi4znmLvs5Cmh/vmcHKyhEbxhYqciWJocACth5ij\r\n'+
'E3kCAwEAAaOCAxowggMWMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFoW3zt+jaHS\r\n'+
'pm1EV5hU4XD+mwO5MB8GA1UdIwQYMBaAFMP30LUqMK2vDZEhcDlU3byJcMc6MGgG\r\n'+
'CCsGAQUFBwEBBFwwWjAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5u\r\n'+
'ZXQwMwYIKwYBBQUHMAKGJ2h0dHA6Ly9haWEuZW50cnVzdC5uZXQvbDFtLWNoYWlu\r\n'+
'MjU2LmNlcjAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3JsLmVudHJ1c3QubmV0\r\n'+
'L2xldmVsMW0uY3JsMCkGA1UdEQQiMCCCHmNvdmlkMTlzaWduZXIub250YXJpb2hl\r\n'+
'YWx0aC5jYTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\r\n'+
'AQUFBwMCMEsGA1UdIAREMEIwNwYKYIZIAYb6bAoBAjApMCcGCCsGAQUFBwIBFhto\r\n'+
'dHRwczovL3d3dy5lbnRydXN0Lm5ldC9ycGEwBwYFZ4EMAQEwggF+BgorBgEEAdZ5\r\n'+
'AgQCBIIBbgSCAWoBaAB3AFYUBpov18Ls0/XhvUSyPsdGdrm8mRFcwO+UmFXWidDd\r\n'+
'AAABeYoCz+MAAAQDAEgwRgIhAKGKAoZMzwkh/3sZXq6vtEYhoYHfZzsjh9jqZvfS\r\n'+
'xQVZAiEAmJu/ftbkNFBr8751Z9wA2dpI0Qt+LoeL1TJQ833Kdg4AdQDfpV6raIJP\r\n'+
'H2yt7rhfTj5a6s2iEqRqXo47EsAgRFwqcwAAAXmKAs/cAAAEAwBGMEQCICsD/Vj+\r\n'+
'ypZeHhesMyv/TkS5ftQjqyIaAFTL/02Gtem4AiBcWdPQspH3vfzZr4LO9z4u5jTg\r\n'+
'Psfm5PZr66tI7yASrAB2AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr8vxw/m1H\r\n'+
'AAABeYoC0WkAAAQDAEcwRQIgTL5F11+7KhQ60jnODm9AkyvXRLY32Mj6tgudRAXO\r\n'+
'y7UCIQDd/dU+Ax1y15yiAA5xM+bWJ7T+Ztd99SD1lw/o8fEmOjANBgkqhkiG9w0B\r\n'+
'AQsFAAOCAQEAlpV3RoNvnhDgd2iFSF39wytf1R6/0u5FdL7eIkYNfnkqXu9Ux9cO\r\n'+
'/OeaGAFMSzaDPA8Xt9A0HqkEbh1pr7UmZVqBwDr4a7gczvt7+HFJRn//Q2fwhmaw\r\n'+
'vXTLLxcAPQF00G6ySsc9MUbsArh6AVhMf9tSXgNaTDj3X3UyYDfR+G8H9eVG/LPp\r\n'+
'34QV/8uvPUFXGj6MjdQysx6YG+K3mae0GEVpODEl4MiceEFZ7v4CPA6pFNadijRF\r\n'+
'6tdXky2psuo7VXfnE2WIlahKr56x+8R6To5pcWglKTywTqvCbnKRRVZhXXYo3Awd\r\n'+
'8h9+TbL3ACHDqA4fi5sAbZ7nMXp8RK4o5A==\r\n'+
'-----END CERTIFICATE-----';
const pdfCert = result.pemCertificate.trim();
const pdfOrg = result.issuedBy.organizationName;
const issuedpemCertificate = (pdfCert == refcert.trim());
//console.log(`pdf is signed by this cert ${result.pemCertificate.trim()}`);
//console.log(issuedpemCertificate);
//console.log(`PDF is signed by ${result.issuedBy.organizationName}, issued to ${result.issuedTo.commonName}`);
// const bypass = window.location.href.includes('grassroots2');
if (( issuedpemCertificate )) {
//console.log('getting receipt details inside PDF');
const receipt = await getPdfDetails(fileBuffer);
// console.log(JSON.stringify(receipt, null, 2));
return Promise.resolve(receipt);
} else {
// According to the Sentry docs, this can be up to 8KB in size
// https://develop.sentry.dev/sdk/data-handling/#variable-size
Sentry.setContext("certificate", {
pdfCert: pdfCert,
pdfOrg: pdfOrg,
});
Sentry.captureMessage('Certificate validation failed');
console.error('invalid certificate');
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
}
} else if (receiptType == 'BC') {
processBC(fileBuffer);
}
} catch (e) {
console.error(e);
if (e.message.includes('Failed to locate ByteRange')) {
e.message = 'Sorry. Selected PDF file is not digitally signed. Please download official copy from Step 1 and retry. Thanks.'
} else {
Sentry.captureException(e);
}
return Promise.reject(e);
}
}
async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<HashTable<Receipt>> {
try {
const typedArray = new Uint8Array(fileBuffer);
let loadingTask = PdfJS.getDocument(typedArray);
const pdfDocument = await loadingTask.promise;
// Load all dose numbers
const { numPages } = pdfDocument;
const receiptObj = {};
for (let pages = 1; pages <= numPages; pages++){
const pdfPage = await pdfDocument.getPage(pages);
const content = await pdfPage.getTextContent();
const numItems = content.items.length;
let name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization;
for (let i = 0; i < numItems; i++) {
let item = content.items[i] as TextItem;
const value = item.str;
if (value.includes('Name / Nom'))
name = (content.items[i+1] as TextItem).str;
if (value.includes('Date:')) {
vaccinationDate = (content.items[i+1] as TextItem).str;
vaccinationDate = vaccinationDate.split(',')[0];
}
if (value.includes('Product name')) {
vaccineName = (content.items[i+1] as TextItem).str;
vaccineName = vaccineName.split(' ')[0];
}
if (value.includes('Date of birth'))
dateOfBirth = (content.items[i+1] as TextItem).str;
if (value.includes('Authorized organization'))
organization = (content.items[i+1] as TextItem).str;
if (value.includes('You have received'))
numDoses = Number(value.split(' ')[3]);
}
receiptObj[numDoses] = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
}
return Promise.resolve(receiptObj);
} catch (e) {
Sentry.captureException(e);
return Promise.reject(e);
}
}
async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> { async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> {
const pdfScale = 2; const pdfScale = 2;
@ -85,8 +241,15 @@ async function getImageDataFromPdf(pdfPage: PDFPageProxy): Promise<ImageData> {
} }
async function processBC(pdfPage: PDFPageProxy) { async function processBC(fileBuffer : ArrayBuffer) {
try {
const typedArray = new Uint8Array(fileBuffer);
let loadingTask = PdfJS.getDocument(typedArray);
const pdfDocument = await loadingTask.promise;
// Load all dose numbers
const pdfPage = await pdfDocument.getPage(1);
const imageData = await getImageDataFromPdf(pdfPage); const imageData = await getImageDataFromPdf(pdfPage);
const code : QRCode = await Decode.getQRFromImage(imageData); const code : QRCode = await Decode.getQRFromImage(imageData);
let rawData = code.data; let rawData = code.data;
@ -111,74 +274,9 @@ async function processBC(pdfPage: PDFPageProxy) {
return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`); return Promise.reject(`Issuer ${decoded.iss} cannot be verified.`);
} }
}
async function processON(signedPdfBuffer : ArrayBuffer, content: TextContent): Promise<any> {
// check for certs first
try {
const certs = getCertificatesInfoFromPDF(signedPdfBuffer);
const result = certs[0];
const isClientCertificate = result.clientCertificate;
const issuedByEntrust = (result.issuedBy.organizationName == 'Entrust, Inc.');
const issuedToOntarioHealth = (result.issuedTo.commonName == 'covid19signer.ontariohealth.ca');
console.log(`PDF is signed by ${result.issuedBy.organizationName}, issued to ${result.issuedTo.commonName}`);
if ((isClientCertificate && issuedByEntrust && issuedToOntarioHealth)) {
console.log('getting receipt details inside PDF');
// to add logic to handle QR code here when it's available
const numItems = content.items.length;
let name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization;
for (let i = 0; i < numItems; i++) {
let item = content.items[i] as TextItem;
const value = item.str;
if (value.includes('Name / Nom'))
name = (content.items[i+1] as TextItem).str;
if (value.includes('Date:')) {
vaccinationDate = (content.items[i+1] as TextItem).str;
vaccinationDate = vaccinationDate.split(',')[0];
}
if (value.includes('Product name')) {
vaccineName = (content.items[i+1] as TextItem).str;
vaccineName = vaccineName.split(' ')[0];
}
if (value.includes('Date of birth'))
dateOfBirth = (content.items[i+1] as TextItem).str;
if (value.includes('Authorized organization'))
organization = (content.items[i+1] as TextItem).str;
if (value.includes('You have received'))
numDoses = Number(value.split(' ')[3]);
}
const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization);
return Promise.resolve({ receipt: receipt, rawData: ''});
} else {
console.error('invalid certificate');
return Promise.reject(`invalid certificate + ${JSON.stringify(result)}`);
}
} catch (e) { } catch (e) {
Promise.reject(e);
console.error(e);
if (e.message.includes('Failed to locate ByteRange')) {
e.message = 'Sorry. Selected PDF file is not digitally signed. Please download official copy from Step 1 and retry. Thanks.'
} else {
Sentry.captureException(e);
}
return Promise.reject(e);
} }
} }

View File

@ -3,8 +3,8 @@ import { Integrations } from '@sentry/tracing';
export const initSentry = () => { export const initSentry = () => {
SentryModule.init({ SentryModule.init({
release: 'grassroots_covidpass@1.8.0', // App version. Needs to be manually updated as we go unless we make the build smarter release: 'grassroots_covidpass@1.9.11', // App version. Needs to be manually updated as we go unless we make the build smarter
dsn: 'https://51370d7af0994761b465bc148129c1de@o997324.ingest.sentry.io/5955697', dsn: 'https://7120dcf8548c4c5cb148cdde2ed6a778@o1015766.ingest.sentry.io/5981424',
integrations: [ integrations: [
new Integrations.BrowserTracing(), new Integrations.BrowserTracing(),
], ],

136
yarn.lock
View File

@ -138,90 +138,90 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@sentry/browser@6.12.0", "@sentry/browser@^6.12.0": "@sentry/browser@^6.13.2", "@sentry/browser@6.13.2":
version "6.12.0" "integrity" "sha512-bkFXK4vAp2UX/4rQY0pj2Iky55Gnwr79CtveoeeMshoLy5iDgZ8gvnLNAz7om4B9OQk1u7NzLEa4IXAmHTUyag=="
resolved "https://registry.npmjs.org/@sentry/browser/-/browser-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/browser/-/browser-6.13.2.tgz"
integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ== "version" "6.13.2"
dependencies: dependencies:
"@sentry/core" "6.12.0" "@sentry/core" "6.13.2"
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/core@6.12.0": "@sentry/core@6.13.2":
version "6.12.0" "integrity" "sha512-snXNNFLwlS7yYxKTX4DBXebvJK+6ikBWN6noQ1CHowvM3ReFBlrdrs0Z0SsSFEzXm2S4q7f6HHbm66GSQZ/8FQ=="
resolved "https://registry.npmjs.org/@sentry/core/-/core-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/core/-/core-6.13.2.tgz"
integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ== "version" "6.13.2"
dependencies: dependencies:
"@sentry/hub" "6.12.0" "@sentry/hub" "6.13.2"
"@sentry/minimal" "6.12.0" "@sentry/minimal" "6.13.2"
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/hub@6.12.0": "@sentry/hub@6.13.2":
version "6.12.0" "integrity" "sha512-sppSuJdNMiMC/vFm/dQowCBh11uTrmvks00fc190YWgxHshodJwXMdpc+pN61VSOmy2QA4MbQ5aMAgHzPzel3A=="
resolved "https://registry.npmjs.org/@sentry/hub/-/hub-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/hub/-/hub-6.13.2.tgz"
integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg== "version" "6.13.2"
dependencies: dependencies:
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/integrations@^6.12.0": "@sentry/integrations@^6.13.2":
version "6.12.0" "integrity" "sha512-CzxMtNr4nkZbifD0Rb6tXwqfqm+fWKl4IQTaFrJ92VNdgihBMVWYmflRqkMkGh1iFN8bVPpXrGyplY5tFN+2kA=="
resolved "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.13.2.tgz"
integrity sha512-M9gsVdWZp5fAFFpTjK2IBuWzW4SBxGAI3tVbYZvVx16S/BY0GsPC1dYpjJx9OTBS/8CmCWdGxnUmjACo/8w1LA== "version" "6.13.2"
dependencies: dependencies:
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
localforage "^1.8.1" "localforage" "^1.8.1"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/minimal@6.12.0": "@sentry/minimal@6.13.2":
version "6.12.0" "integrity" "sha512-6iJfEvHzzpGBHDfLxSHcGObh73XU1OSQKWjuhDOe7UQDyI4BQmTfcXAC+Fr8sm8C/tIsmpVi/XJhs8cubFdSMw=="
resolved "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.13.2.tgz"
integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw== "version" "6.13.2"
dependencies: dependencies:
"@sentry/hub" "6.12.0" "@sentry/hub" "6.13.2"
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/react@^6.12.0": "@sentry/react@^6.13.2":
version "6.12.0" "integrity" "sha512-aLkWyn697LTcmK1PPnUg5UJcyBUPoI68motqgBY53SIYDAwOeYNUQt2aanDuOTY5aE2PdnJwU48klA8vuYkoRQ=="
resolved "https://registry.npmjs.org/@sentry/react/-/react-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/react/-/react-6.13.2.tgz"
integrity sha512-E8Nw9PPzP/EyMy64ksr9xcyYYlBmUA5ROnkPQp7o5wF0xf5/J+nMS1tQdyPnLQe2KUgHlN4kVs2HHft1m7mSYQ== "version" "6.13.2"
dependencies: dependencies:
"@sentry/browser" "6.12.0" "@sentry/browser" "6.13.2"
"@sentry/minimal" "6.12.0" "@sentry/minimal" "6.13.2"
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
hoist-non-react-statics "^3.3.2" "hoist-non-react-statics" "^3.3.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/tracing@^6.12.0": "@sentry/tracing@^6.13.2":
version "6.12.0" "integrity" "sha512-bHJz+C/nd6biWTNcYAu91JeRilsvVgaye4POkdzWSmD0XoLWHVMrpCQobGpXe7onkp2noU3YQjhqgtBqPHtnpw=="
resolved "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.13.2.tgz"
integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA== "version" "6.13.2"
dependencies: dependencies:
"@sentry/hub" "6.12.0" "@sentry/hub" "6.13.2"
"@sentry/minimal" "6.12.0" "@sentry/minimal" "6.13.2"
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
"@sentry/utils" "6.12.0" "@sentry/utils" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@sentry/types@6.12.0", "@sentry/types@^6.12.0": "@sentry/types@^6.13.2", "@sentry/types@6.13.2":
version "6.12.0" "integrity" "sha512-6WjGj/VjjN8LZDtqJH5ikeB1o39rO1gYS6anBxiS3d0sXNBb3Ux0pNNDFoBxQpOhmdDHXYS57MEptX9EV82gmg=="
resolved "https://registry.npmjs.org/@sentry/types/-/types-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/types/-/types-6.13.2.tgz"
integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA== "version" "6.13.2"
"@sentry/utils@6.12.0": "@sentry/utils@6.13.2":
version "6.12.0" "integrity" "sha512-foF4PbxqPMWNbuqdXkdoOmKm3quu3PP7Q7j/0pXkri4DtCuvF/lKY92mbY0V9rHS/phCoj+3/Se5JvM2ymh2/w=="
resolved "https://registry.npmjs.org/@sentry/utils/-/utils-6.12.0.tgz" "resolved" "https://registry.npmjs.org/@sentry/utils/-/utils-6.13.2.tgz"
integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA== "version" "6.13.2"
dependencies: dependencies:
"@sentry/types" "6.12.0" "@sentry/types" "6.13.2"
tslib "^1.9.3" "tslib" "^1.9.3"
"@types/eslint-scope@^3.7.0": "@types/eslint-scope@^3.7.0":
version "3.7.0" version "3.7.0"