Merge pull request #30 from billylo1/dynamic-receipt-handling

new receipt handling
This commit is contained in:
Ryan Slobojan 2021-09-29 11:56:47 -04:00 committed by GitHub
commit 6fa67cd1e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 195 additions and 102 deletions

View File

@ -30,31 +30,48 @@ function Form(): JSX.Element {
// Currently selected color
const [selectedColor, setSelectedColor] = useState<COLORS>(COLORS.WHITE);
// Currently selected dose
const [selectedDose, setSelectedDose] = useState<number>(2);
// Global camera controls
const [globalControls, setGlobalControls] = useState<IScannerControls>(undefined);
// Currently selected QR Code / File. Only one of them is set.
const [qrCode, setQrCode] = useState<Result>(undefined);
const [file, setFile] = useState<File>(undefined);
const [payloadBody, setPayloadBody] = useState<PayloadBody>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const [saveLoading, setSaveLoading] = useState<boolean>(false);
const [fileLoading, setFileLoading] = useState<boolean>(false);
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 [errorMessages, _setErrorMessages] = useState<Array<string>>([]);
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';
// 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<HTMLFormElement>) {
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 = () => <li className="flex flex-row items-center">
@ -292,23 +338,27 @@ function Form(): JSX.Element {
</p>
</li>
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 {
<Card step="2" heading={t('index:selectCertificate')} content={
<div className="space-y-5">
<p>{t('index:selectCertificateDescription')}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 items-center justify-start">
<button
type="button"
onClick={showFileDialog}
className="focus:outline-none h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
{t('index:openFile')}
</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>
<input type='file'
@ -378,10 +436,37 @@ function Form(): JSX.Element {
</span>
</div>
}
{fileErrorMessages.map((message, i) =>
<Alert message={message} key={'error-' + i} type="error" />
)}
</div>
}/>
<Card step="3" heading={t('index:addToWalletHeader')} content={
{showDoseOption && <Card step="3" heading={'Choose dose number'} content={
<div className="space-y-5">
<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')}
@ -401,17 +486,17 @@ function Form(): JSX.Element {
</div>
<div className="flex flex-row items-center justify-start">
<button disabled={isDisabledAppleWallet || loading} 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">
{t('index:addToWallet')}
</button>
&nbsp;&nbsp;&nbsp;&nbsp;
<button id="saveAsPhoto" type="button" disabled={loading} 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">
{t('index:saveAsPhoto')}
</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">
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"/>
@ -420,7 +505,7 @@ function Form(): JSX.Element {
</svg>
</div>
</div>
{errorMessages.map((message, i) =>
{addErrorMessages.map((message, i) =>
<Alert message={message} key={'error-' + i} type="error" />
)}
{/* {warningMessages.map((message, i) =>

View File

@ -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

View File

@ -81,11 +81,11 @@ export class PassData {
return await response.arrayBuffer()
}
static async generatePass(payloadBody: PayloadBody): Promise<Buffer> {
static async generatePass(payloadBody: PayloadBody, numDose: number): Promise<Buffer> {
// Create Payload
try {
const payload: Payload = new Payload(payloadBody);
const payload: Payload = new Payload(payloadBody, numDose);
payload.serialNumber = uuid4();

View File

@ -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<T> {
[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<Receipt>;
}
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;

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
try {
const payload: Payload = new Payload(payloadBody);
const payload: Payload = new Payload(payloadBody, numDose);
payload.serialNumber = uuid4();

View File

@ -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<Receipt>;
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<any> {
async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<HashTable<Receipt>> {
try {
@ -124,44 +124,47 @@ async function loadPDF(signedPdfBuffer : ArrayBuffer): Promise<any> {
}
async function getPdfDetails(fileBuffer: ArrayBuffer): Promise<Receipt> {
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 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);