diff --git a/components/Form.tsx b/components/Form.tsx index e527608..544c1e6 100644 --- a/components/Form.tsx +++ b/components/Form.tsx @@ -30,31 +30,48 @@ function Form(): JSX.Element { // Currently selected color const [selectedColor, setSelectedColor] = useState(COLORS.WHITE); + // Currently selected dose + const [selectedDose, setSelectedDose] = useState(2); + // Global camera controls const [globalControls, setGlobalControls] = useState(undefined); // Currently selected QR Code / File. Only one of them is set. const [qrCode, setQrCode] = useState(undefined); const [file, setFile] = useState(undefined); + const [payloadBody, setPayloadBody] = useState(undefined); - const [loading, setLoading] = useState(false); + const [saveLoading, setSaveLoading] = useState(false); + const [fileLoading, setFileLoading] = useState(false); const [generated, setGenerated] = useState(false); // this flag represents the file has been used to generate a pass const [isDisabledAppleWallet, setIsDisabledAppleWallet] = useState(false); - const [errorMessages, _setErrorMessages] = useState>([]); + const [addErrorMessages, _setAddErrorMessages] = useState>([]); + const [fileErrorMessages, _setFileErrorMessages] = useState>([]); + + const [showDoseOption, setShowDoseOption] = useState(false); // const [warningMessages, _setWarningMessages] = useState>([]); const hitcountHost = 'https://stats.vaccine-ontario.ca'; // Check if there is a translation and replace message accordingly - const setErrorMessage = (message: string) => { + const setAddErrorMessage = (message: string) => { if (!message) { return; } const translation = t('errors:'.concat(message)); - _setErrorMessages(Array.from(new Set([...errorMessages, 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) => { @@ -66,9 +83,11 @@ function Form(): JSX.Element { // _setWarningMessages(Array.from(new Set([...warningMessages, translation !== message ? translation : message]))); // } - const deleteErrorMessage = (message: string) =>{ - console.log(errorMessages) - _setErrorMessages(errorMessages.filter(item => item !== message)) + const deleteAddErrorMessage = (message: string) =>{ + _setAddErrorMessages(addErrorMessages.filter(item => item !== message)) + } + const deleteFileErrorMessage = (message: string) =>{ + _setFileErrorMessages(addErrorMessages.filter(item => item !== message)) } // File Input ref @@ -80,17 +99,55 @@ function Form(): JSX.Element { inputFile.current.addEventListener('input', () => { let selectedFile = inputFile.current.files[0]; if (selectedFile !== undefined) { + setFileLoading(true); setQrCode(undefined); - setFile(selectedFile); + setPayloadBody(undefined); + setFile(undefined); + setShowDoseOption(false); setGenerated(false); - deleteErrorMessage(t('errors:'.concat('noFileOrQrCode'))); + deleteAddErrorMessage(t('errors:'.concat('noFileOrQrCode'))); + _setFileErrorMessages([]); checkBrowserType(); + getPayload(selectedFile); } }); } checkBrowserType(); }, [inputFile]) + async function getPayload(file){ + try { + const payload = await getPayloadBodyFromFile(file, COLORS.GREEN); + 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 async function showFileDialog() { inputFile.current.click(); @@ -124,13 +181,13 @@ function Form(): JSX.Element { try { deviceList = await BrowserQRCodeReader.listVideoInputDevices(); } catch (e) { - setErrorMessage('noCameraAccess'); + setAddErrorMessage('noCameraAccess'); return; } // Check if camera device is present if (deviceList.length == 0) { - setErrorMessage("noCameraFound"); + setAddErrorMessage("noCameraFound"); return; } @@ -154,7 +211,7 @@ function Form(): JSX.Element { setIsCameraOpen(false); } if (error !== undefined) { - setErrorMessage(error.message); + setAddErrorMessage(error.message); } } ) @@ -186,40 +243,33 @@ function Form(): JSX.Element { async function addToWallet(event: FormEvent) { event.preventDefault(); - setLoading(true); + setSaveLoading(true); if (!file && !qrCode) { - setErrorMessage('noFileOrQrCode') - setLoading(false); + setAddErrorMessage('noFileOrQrCode') + setSaveLoading(false); return; } - const color = selectedColor; - let payloadBody: PayloadBody; - try { - if (file) { - - //console.log('> get payload'); - payloadBody = await getPayloadBodyFromFile(file, color); - - const passName = payloadBody.receipt.name.replace(' ', '-'); - const vaxName = payloadBody.receipt.vaccineName.replace(' ', '-'); - const passDose = payloadBody.receipt.numDoses; + 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('> increment count'); await incrementCount(); - //console.log('> generatePass'); - 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"}); //console.log(`> save blob as ${covidPassFilename}`); saveAs(passBlob, covidPassFilename); - setLoading(false); + setSaveLoading(false); } @@ -231,16 +281,16 @@ function Form(): JSX.Element { Sentry.captureException(e); if (e.message != undefined) { - setErrorMessage(e.message); + setAddErrorMessage(e.message); } else { - setErrorMessage("Unable to continue."); + setAddErrorMessage("Unable to continue."); } } else { - setErrorMessage("Unexpected error. Sorry."); + setAddErrorMessage("Unexpected error. Sorry."); } - setLoading(false); + setSaveLoading(false); } } @@ -249,21 +299,17 @@ function Form(): JSX.Element { async function saveAsPhoto() { - setLoading(true); + setSaveLoading(true); if (!file && !qrCode) { - setErrorMessage('noFileOrQrCode'); - setLoading(false); + setAddErrorMessage('noFileOrQrCode'); + setSaveLoading(false); return; } - let payloadBody: PayloadBody; - try { - payloadBody = await getPayloadBodyFromFile(file, COLORS.GREEN); await incrementCount(); - - let photoBlob = await Photo.generatePass(payloadBody); + let photoBlob = await Photo.generatePass(payloadBody, selectedDose); saveAs(photoBlob, 'pass.png'); // need to clean up @@ -273,11 +319,11 @@ function Form(): JSX.Element { const body = document.getElementById('pass-image'); body.hidden = true; - setLoading(false); + setSaveLoading(false); } catch (e) { Sentry.captureException(e); - setErrorMessage(e.message); - setLoading(false); + setAddErrorMessage(e.message); + setSaveLoading(false); } } const verifierLink = () =>
  • @@ -292,23 +338,27 @@ function Form(): JSX.Element {

  • + const setDose = (e) => { + setSelectedDose(e.target.value); + } + function checkBrowserType() { // 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.'); // setIsDisabledAppleWallet(true); // } // 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.'); // setIsDisabledAppleWallet(true); // } // 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') // setIsDisabledAppleWallet(true); // } if (isIOS && !isSafari) { - // setErrorMessage('Sorry, only Safari can be used to add a Wallet Pass on iOS'); - 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'); + setAddErrorMessage('Sorry, only Safari can be used to add a Wallet Pass on iOS'); setIsDisabledAppleWallet(true); console.log('not safari') } @@ -346,13 +396,21 @@ function Form(): JSX.Element {

    {t('index:selectCertificateDescription')}

    -
    +
    +
    + + + + +
    } + + {fileErrorMessages.map((message, i) => + + )} }/> - +

    + {t('index:formatChange')} +

    + {t('index:saveMultiple')} +

    + +
    +
    + {payloadBody && Object.keys(payloadBody.receipts).map(key => +
    + +
    + )} +
    +
    + + } />} + + {/*

    {t('index:dataPrivacyDescription')} @@ -401,17 +486,17 @@ function Form(): JSX.Element {

    -      - -
    +
    @@ -420,7 +505,7 @@ function Form(): JSX.Element {
    - {errorMessages.map((message, i) => + {addErrorMessages.map((message, i) => )} {/* {warningMessages.map((message, i) => diff --git a/public/locales/en/index.yml b/public/locales/en/index.yml index 6c0db77..398a6ea 100644 --- a/public/locales/en/index.yml +++ b/public/locales/en/index.yml @@ -15,6 +15,8 @@ ontarioHealth: Ontario Ministry of Health gotoOntarioHealth: Go to Ontario Ministry of Health downloadSignedPDF: and enter your information to display your official vaccination receipt. Press the Share Icon at the bottom, "Save As Files" to store it onto your iPhone. 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 pickColorDescription: Pick a background color for your pass. colorWhite: white diff --git a/src/pass.ts b/src/pass.ts index 806630c..bbe6048 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -81,11 +81,11 @@ export class PassData { return await response.arrayBuffer() } - static async generatePass(payloadBody: PayloadBody): Promise { + static async generatePass(payloadBody: PayloadBody, numDose: number): Promise { // Create Payload try { - const payload: Payload = new Payload(payloadBody); + const payload: Payload = new Payload(payloadBody, numDose); payload.serialNumber = uuid4(); diff --git a/src/payload.ts b/src/payload.ts index 765b79b..201fef2 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -4,6 +4,9 @@ import {COLORS} from "./colors"; export class Receipt { constructor(public name: string, public vaccinationDate: string, public vaccineName: string, public dateOfBirth: string, public numDoses: number, public organization: string) {}; } +export interface HashTable { + [key: string]: T; +} enum TextAlignment { right = 'PKTextAlignmentRight', @@ -28,7 +31,7 @@ export interface PassDictionary { export interface PayloadBody { // color: COLORS; rawData: string; - receipt: Receipt; + receipts: HashTable; } export class Payload { @@ -43,12 +46,12 @@ export class Payload { serialNumber: string; generic: PassDictionary; - constructor(body: PayloadBody) { + constructor(body: PayloadBody, numDose: number) { // Get name and date of birth information - const name = body.receipt.name; - const dateOfBirth = body.receipt.dateOfBirth; - const vaccineName = body.receipt.vaccineName; + 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')) @@ -61,7 +64,7 @@ export class Payload { if (vaccineName.includes('ASTRAZENECA')) vaccineNameProper = 'AstraZeneca (Vaxzevria)' - let doseVaccine = "#" + String(body.receipt.numDoses) + ": " + vaccineNameProper; + let doseVaccine = "#" + String(body.receipts[numDose].numDoses) + ": " + vaccineNameProper; if (name == undefined) { throw new Error('nameMissing'); @@ -85,13 +88,13 @@ export class Payload { { key: "issuer", label: "Authorized Organization", - value: body.receipt.organization + value: body.receipts[numDose].organization }, { key: "dov", label: "Date", - value: body.receipt.vaccinationDate, + value: body.receipts[numDose].vaccinationDate, // textAlignment: TextAlignment.right } ], @@ -115,10 +118,10 @@ export class Payload { } // Set Values - this.receipt = body.receipt; + this.receipt = body.receipts[numDose]; this.rawData = body.rawData; - if (body.receipt.numDoses > 1 || body.receipt.vaccineName.toLowerCase().includes('janssen') || body.receipt.vaccineName.toLowerCase().includes('johnson') || body.receipt.vaccineName.toLowerCase().includes('j&j')) { + 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; } else { this.backgroundColor = COLORS.YELLOW; diff --git a/src/photo.ts b/src/photo.ts index 496f859..ed3ac4f 100644 --- a/src/photo.ts +++ b/src/photo.ts @@ -35,11 +35,11 @@ export class Photo { - static async generatePass(payloadBody: PayloadBody): Promise { + static async generatePass(payloadBody: PayloadBody, numDose: number): Promise { // Create Payload try { - const payload: Payload = new Payload(payloadBody); + const payload: Payload = new Payload(payloadBody, numDose); payload.serialNumber = uuid4(); diff --git a/src/process.ts b/src/process.ts index badc621..3b98cc6 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,4 +1,4 @@ -import {PayloadBody, Receipt} from "./payload"; +import {PayloadBody, Receipt, HashTable} from "./payload"; import * as PdfJS from 'pdfjs-dist' import {COLORS} from "./colors"; import { getCertificatesInfoFromPDF } from "@ninja-labs/verify-pdf"; // ES6 @@ -16,11 +16,11 @@ export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise // Read file const fileBuffer = await file.arrayBuffer(); - let receipt: Receipt; + let receipts: HashTable; switch (file.type) { case 'application/pdf': - receipt = await loadPDF(fileBuffer) + receipts = await loadPDF(fileBuffer) break default: throw Error('invalidFileType') @@ -29,12 +29,12 @@ export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise const rawData = ''; // unused at the moment, the original use was to store the QR code from issuer return { - receipt: receipt, + receipts: receipts, rawData: rawData } } -async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise { +async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise> { try { @@ -124,44 +124,47 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise { } -async function getPdfDetails(fileBuffer: ArrayBuffer): Promise { +async function getPdfDetails(fileBuffer: ArrayBuffer): Promise> { try { const typedArray = new Uint8Array(fileBuffer); let loadingTask = PdfJS.getDocument(typedArray); const pdfDocument = await loadingTask.promise; - // Load FIRST DUE TO NEW COVAXON FORMAT - const pageNumber = 1; + // Load all dose numbers + const { numPages } = pdfDocument; + const receiptObj = {}; - const pdfPage = await pdfDocument.getPage(pageNumber); - 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]; + 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]); } - 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); } - const receipt = new Receipt(name, vaccinationDate, vaccineName, dateOfBirth, numDoses, organization); - return Promise.resolve(receipt); + return Promise.resolve(receiptObj); } catch (e) { Sentry.captureException(e); return Promise.reject(e);