731 lines
30 KiB
TypeScript
731 lines
30 KiB
TypeScript
import {saveAs} from 'file-saver';
|
|
import React, {FormEvent, useEffect, useRef, useState} from "react";
|
|
import {BrowserQRCodeReader, IScannerControls} from "@zxing/browser";
|
|
import {Result} from "@zxing/library";
|
|
import {useTranslation} from 'next-i18next';
|
|
|
|
import Card from "./Card";
|
|
import Alert from "./Alert";
|
|
import Check from './Check';
|
|
import {PayloadBody} from "../src/payload";
|
|
import {getPayloadBodyFromFile, processSHCCode} from "../src/process";
|
|
import {PassData} from "../src/pass";
|
|
import {Photo} from "../src/photo";
|
|
import {isIOS, isMacOs, isAndroid, isSafari, osVersion, getUA, browserName, browserVersion} from 'react-device-detect';
|
|
import * as Sentry from '@sentry/react';
|
|
import Bullet from './Bullet';
|
|
import { GPayData } from '../src/gpay';
|
|
import Dropdown from './Dropdown';
|
|
|
|
const getTheme = () => {
|
|
if (typeof window !== 'undefined' &&
|
|
window.localStorage) {
|
|
|
|
const storedPrefs =
|
|
window.localStorage.getItem('color-theme')
|
|
if (typeof storedPrefs === 'string') {
|
|
return storedPrefs
|
|
}
|
|
|
|
const userMedia =
|
|
window.matchMedia('(prefers-color-scheme: dark)')
|
|
if (userMedia.matches) {
|
|
return 'dark'
|
|
}
|
|
}
|
|
|
|
// If you want to use light theme as the default,
|
|
// return "light" instead
|
|
return 'light'
|
|
}
|
|
|
|
const options = [
|
|
{ label: 'Alberta', value: 'https://covidrecords.alberta.ca/form'},
|
|
{ label: 'British Columbia', value: 'https://www.healthgateway.gov.bc.ca/vaccinecard'},
|
|
{ label: 'Ontario', value: 'https://covid19.ontariohealth.ca'},
|
|
{ label: 'Newfoundland and Labrador', value: 'https://vaccineportal.nlchi.nl.ca/'},
|
|
{ label: 'Northwest Territories', value: 'https://www.gov.nt.ca/covid-19/en/request/proof-vaccination'},
|
|
{ label: 'Nunavut', value: 'https://gov.nu.ca/sites/default/files/covid_proof_of_vaccination_qa_for_nunavut_october_14_2021.pdf'},
|
|
{ label: 'Nova Scotia', value: 'https://novascotia.flow.canimmunize.ca/en/portal'},
|
|
{ label: 'Prince Edward Island', value: 'https://pei.flow.canimmunize.ca/en/portal'},
|
|
{ label: 'Québec', value: 'https://covid19.quebec.ca/PreuveVaccinale'},
|
|
{ label: 'Saskatchewan', value: 'https://services.saskatchewan.ca/#/login'},
|
|
{ label: 'Yukon', value: 'https://service.yukon.ca/forms/en/get-covid19-proof-of-vaccination'},
|
|
]
|
|
|
|
function Form(): JSX.Element {
|
|
const {t} = useTranslation(['index', 'errors', 'common']);
|
|
|
|
// Whether camera is open or not
|
|
const [isCameraOpen, setIsCameraOpen] = useState<boolean>(false);
|
|
|
|
// 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 [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 [isDisabledGooglePay, setIsDisabledGooglePay] = 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';
|
|
|
|
// Check if there is a translation and replace message accordingly
|
|
const setAddErrorMessage = (message: string) => {
|
|
if (!message) {
|
|
return;
|
|
}
|
|
|
|
const translation = t('errors:'.concat(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 deleteAddErrorMessage = (message: string) =>{
|
|
_setAddErrorMessages(addErrorMessages.filter(item => item !== message))
|
|
}
|
|
|
|
// File Input ref
|
|
const inputFile = useRef<HTMLInputElement>(undefined)
|
|
|
|
// Add event listener to listen for file change events
|
|
useEffect(() => {
|
|
if (inputFile && inputFile.current) {
|
|
inputFile.current.addEventListener('change', async () => {
|
|
let selectedFile = inputFile.current.files[0];
|
|
if (selectedFile) {
|
|
setFileLoading(true);
|
|
setQrCode(undefined);
|
|
setPayloadBody(undefined);
|
|
setFile(undefined);
|
|
setShowDoseOption(false);
|
|
setGenerated(false);
|
|
deleteAddErrorMessage(t('errors:'.concat('noFileOrQrCode')));
|
|
_setFileErrorMessages([]);
|
|
checkBrowserType();
|
|
const payloadBody = await getPayload(selectedFile);
|
|
|
|
//TODO: feature flagging
|
|
|
|
// await createDataUrlForDisplay(selectedFile);
|
|
await renderPhoto(payloadBody);
|
|
}
|
|
});
|
|
}
|
|
checkBrowserType();
|
|
}, [inputFile])
|
|
|
|
async function createDataUrlForDisplay(file: File) {
|
|
|
|
if (window.localStorage) {
|
|
|
|
// https://stackoverflow.com/a/56738510/2789065
|
|
|
|
const buffer = Buffer.from(await new Response(file).arrayBuffer());
|
|
let dataUrl = `data:${file.type};base64,${buffer.toString("base64")}`;
|
|
localStorage.setItem('pdfDataUrl', dataUrl);
|
|
|
|
}
|
|
}
|
|
|
|
async function showPDF() {
|
|
let dataUrl = localStorage.getItem('pdfDataUrl');
|
|
// console.log(dataUrl)
|
|
let script = `function debugBase64(base64URL){
|
|
var win = window.open();
|
|
win.document.write('<iframe src="' + base64URL + '" frameborder="0" style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;" allowfullscreen></iframe>');
|
|
}
|
|
debugBase64('${dataUrl}');
|
|
`;
|
|
|
|
let hrefValue = `javascript:${script};`
|
|
document.getElementById('mylink').setAttribute('href', hrefValue);
|
|
|
|
}
|
|
|
|
async function getPayload(file) : Promise<PayloadBody> {
|
|
try {
|
|
const payload = await getPayloadBodyFromFile(file);
|
|
|
|
const buffer = Buffer.from(await new Response(file).arrayBuffer());
|
|
let dataUrl = `data:${file.type};base64,${buffer.toString("base64")}`;
|
|
|
|
payload.dataUrl = dataUrl;
|
|
|
|
setPayloadBody(payload);
|
|
setFileLoading(false);
|
|
setFile(file);
|
|
|
|
if (payload.rawData.length == 0) {
|
|
if (Object.keys(payload.receipts).length === 1) {
|
|
setSelectedDose(parseInt(Object.keys(payload.receipts)[0]));
|
|
} else {
|
|
setShowDoseOption(true);
|
|
}
|
|
}
|
|
|
|
return Promise.resolve(payload);
|
|
} catch (e) {
|
|
setFile(file);
|
|
setFileLoading(false);
|
|
if (e) {
|
|
console.error(e);
|
|
|
|
// Don't report known errors to Sentry
|
|
if (!e.message.includes('invalidFileType') &&
|
|
!e.message.includes('No SHC QR code found')) {
|
|
Sentry.captureException(e);
|
|
}
|
|
|
|
if (e.message != undefined) {
|
|
setFileErrorMessage(e.message);
|
|
} else {
|
|
setFileErrorMessage('unableToContinue');
|
|
}
|
|
|
|
} else {
|
|
setFileErrorMessage('unexpected');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show file Dialog
|
|
async function showFileDialog() {
|
|
hideCameraView();
|
|
|
|
// Clear out any currently-selected files
|
|
inputFile.current.value = '';
|
|
|
|
// Hide our existing pass image
|
|
document.getElementById('shc-pass-image').hidden = true;
|
|
document.getElementById('shc-image-header').hidden = true;
|
|
|
|
inputFile.current.click();
|
|
}
|
|
|
|
async function goToFAQ(e : any) {
|
|
e.preventDefault();
|
|
window.location.href = '/faq';
|
|
}
|
|
|
|
// Hide camera view
|
|
async function hideCameraView() {
|
|
if (globalControls) {
|
|
globalControls.stop();
|
|
}
|
|
setIsCameraOpen(false);
|
|
_setFileErrorMessages([]);
|
|
}
|
|
|
|
// Show camera view
|
|
async function showCameraView() {
|
|
// Create new QR Code Reader
|
|
const codeReader = new BrowserQRCodeReader();
|
|
|
|
// Needs to be called before any camera can be accessed
|
|
let deviceList: MediaDeviceInfo[];
|
|
|
|
try {
|
|
deviceList = await BrowserQRCodeReader.listVideoInputDevices();
|
|
} catch (e) {
|
|
setFileErrorMessage('noCameraAccess');
|
|
return;
|
|
}
|
|
|
|
// Check if camera device is present
|
|
if (deviceList.length == 0) {
|
|
setFileErrorMessage("noCameraFound");
|
|
return;
|
|
}
|
|
|
|
// Get preview Element to show camera stream
|
|
const previewElem: HTMLVideoElement = document.querySelector('#cameraPreview');
|
|
|
|
// Set Global controls
|
|
setGlobalControls(
|
|
// Start decoding from video device
|
|
await codeReader.decodeFromVideoDevice(undefined,
|
|
previewElem,
|
|
async (result, error, controls) => {
|
|
if (result) {
|
|
const qrCode = result.getText();
|
|
// Check if this was a valid SHC QR code - if it was not, display an error
|
|
if (!qrCode.startsWith('shc:/')) {
|
|
setFileErrorMessage(t('invalidSHC'));
|
|
} else {
|
|
_setFileErrorMessages([]);
|
|
setQrCode(result);
|
|
setFile(undefined);
|
|
setPayloadBody(undefined);
|
|
setShowDoseOption(false);
|
|
setGenerated(false);
|
|
checkBrowserType();
|
|
|
|
const payloadBody = await processSHCCode(qrCode);
|
|
|
|
setPayloadBody(payloadBody);
|
|
|
|
controls.stop();
|
|
|
|
// Reset
|
|
setGlobalControls(undefined);
|
|
setIsCameraOpen(false);
|
|
|
|
await renderPhoto(payloadBody);
|
|
}
|
|
}
|
|
if (error) {
|
|
setFileErrorMessage(error.message);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
|
|
// Hide our existing pass image
|
|
document.getElementById('shc-pass-image').hidden = true;
|
|
document.getElementById('shc-image-header').hidden = true;
|
|
|
|
setQrCode(undefined);
|
|
setPayloadBody(undefined);
|
|
setFile(undefined);
|
|
setShowDoseOption(false);
|
|
setGenerated(false);
|
|
_setFileErrorMessages([]);
|
|
|
|
setIsCameraOpen(true);
|
|
}
|
|
|
|
async function incrementCount() {
|
|
try {
|
|
if (typeof generated == undefined || !generated) {
|
|
|
|
const request = `${hitcountHost}/count?url=pass.vaccine-ontario.ca`;
|
|
//console.log(request);
|
|
|
|
let response = await fetch(request);
|
|
//console.log(response);
|
|
|
|
setGenerated(true);
|
|
}
|
|
|
|
} catch (e) {
|
|
// Fail silently - we shouldn't blow up receipt processing because we couldn't increment our counter
|
|
console.error(e);
|
|
//return Promise.reject(e);
|
|
}
|
|
}
|
|
|
|
// Add Pass to wallet
|
|
async function addToWallet(event: FormEvent<HTMLFormElement>) {
|
|
|
|
event.preventDefault();
|
|
setSaveLoading(true);
|
|
|
|
if (!file && !qrCode) {
|
|
setAddErrorMessage('noFileOrQrCode')
|
|
setSaveLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (payloadBody) {
|
|
|
|
const selectedReceipt = payloadBody.shcReceipt;
|
|
const filenameDetails = selectedReceipt.cardOrigin.replace(' ', '-');
|
|
const passName = selectedReceipt.name.replace(' ', '-');
|
|
const covidPassFilename = `grassroots-receipt-${passName}-${filenameDetails}.pkpass`;
|
|
|
|
const pass = await PassData.generatePass(payloadBody, selectedDose);
|
|
|
|
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
|
|
|
|
await incrementCount();
|
|
|
|
saveAs(passBlob, covidPassFilename);
|
|
setSaveLoading(false);
|
|
}
|
|
} catch (e) {
|
|
|
|
if (e) {
|
|
console.error(e);
|
|
Sentry.captureException(e);
|
|
|
|
if (e.message) {
|
|
setAddErrorMessage(e.message);
|
|
} else {
|
|
setAddErrorMessage('unableToContinue');
|
|
}
|
|
|
|
} else {
|
|
setAddErrorMessage('unexpected');
|
|
}
|
|
|
|
setSaveLoading(false);
|
|
}
|
|
}
|
|
|
|
// Add Pass to Google Pay
|
|
async function addToGooglePay(e : any) {
|
|
|
|
e.preventDefault();
|
|
setSaveLoading(true);
|
|
|
|
if (!file && !qrCode) {
|
|
setAddErrorMessage('noFileOrQrCode')
|
|
setSaveLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (payloadBody) {
|
|
|
|
console.log('> increment count');
|
|
await incrementCount();
|
|
|
|
console.log('> generatePass');
|
|
const jwt = await GPayData.generatePass(payloadBody, selectedDose);
|
|
|
|
const newUrl = `https://pay.google.com/gp/v/save/${jwt}`;
|
|
console.log('> redirect to save Google Pass');
|
|
|
|
setSaveLoading(false);
|
|
window.location.href = newUrl;
|
|
}
|
|
} catch (e) {
|
|
|
|
if (e) {
|
|
console.error(e);
|
|
Sentry.captureException(e);
|
|
|
|
if (e.message) {
|
|
setAddErrorMessage(e.message);
|
|
} else {
|
|
setAddErrorMessage("Unable to continue.");
|
|
}
|
|
} else {
|
|
setAddErrorMessage("Unexpected error. Sorry.");
|
|
}
|
|
|
|
setSaveLoading(false);
|
|
}
|
|
}
|
|
|
|
async function renderPhoto(payloadBody : PayloadBody, shouldRegister = true) {
|
|
console.log('renderPhoto');
|
|
if (!payloadBody) {
|
|
console.log('no payload body');
|
|
setAddErrorMessage('noFileOrQrCode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('beginning render');
|
|
if (payloadBody.rawData.length > 0) {
|
|
// This is an SHC receipt, so do our SHC thing
|
|
|
|
// need to clean up first
|
|
if (document.getElementById('shc-qrcode').hasChildNodes()) {
|
|
document.getElementById('shc-qrcode').firstChild.remove();
|
|
}
|
|
|
|
document.getElementById('shc-pass-image').hidden = false;
|
|
document.getElementById('shc-image-header').hidden = false;
|
|
console.log('made canvas visible');
|
|
|
|
await Photo.generateSHCPass(payloadBody, shouldRegister);
|
|
console.log('generated blob');
|
|
}
|
|
console.log('done photo render');
|
|
} catch (e) {
|
|
Sentry.captureException(e);
|
|
|
|
setAddErrorMessage(e.message);
|
|
}
|
|
}
|
|
|
|
async function refreshPhoto() {
|
|
await renderPhoto(payloadBody, false);
|
|
}
|
|
|
|
const setDose = (e : any) => {
|
|
setSelectedDose(e.target.value);
|
|
}
|
|
|
|
function checkBrowserType() {
|
|
|
|
// if (isIPad13) {
|
|
// setAddErrorMessage('Sorry. Apple does not support the use of Wallet on iPad. Please use iPhone/Safari.');
|
|
// setIsDisabledAppleWallet(true);
|
|
// }
|
|
// if (!isSafari && !isChrome) {
|
|
// setAddErrorMessage('Sorry. Apple Wallet pass can be added using Safari or Chrome only.');
|
|
// setIsDisabledAppleWallet(true);
|
|
// }
|
|
|
|
const uaIsiOS15 = getUA.includes('15_');
|
|
if (isIOS && ((!osVersion.startsWith('15')) && !uaIsiOS15)) {
|
|
const message = `Not iOS15 error: osVersion=${osVersion} UA=${getUA}`;
|
|
console.warn(message);
|
|
Sentry.captureMessage(message);
|
|
setAddErrorMessage(`Sorry, iOS 15+ is needed for the Apple Wallet functionality to work with Smart Health Card (detected iOS ${osVersion}, browser ${browserName} ${browserVersion})`);
|
|
setIsDisabledAppleWallet(true);
|
|
return;
|
|
}
|
|
|
|
if (isMacOs) {
|
|
setAddErrorMessage('iOSReminder')
|
|
return;
|
|
}
|
|
|
|
if (isIOS && !isSafari) {
|
|
setAddErrorMessage('safariSupportOnly');
|
|
setIsDisabledAppleWallet(true);
|
|
return;
|
|
}
|
|
|
|
if (isAndroid) {
|
|
if (Number(osVersion) > 8) {
|
|
setIsDisabledGooglePay(false);
|
|
} else {
|
|
setAddErrorMessage('androidVersionError')
|
|
setIsDisabledGooglePay(true);
|
|
}
|
|
} else {
|
|
if (window.location.hostname !== 'localhost') {
|
|
setIsDisabledGooglePay(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<form className="space-y-5" id="form" onSubmit={addToWallet}>
|
|
<Card step="1" heading={t('index:downloadReceipt')} content={
|
|
<div className="space-y-5">
|
|
<p>{t('index:visit')}</p>
|
|
<p><b>{t('index:reminderNotToRepeat')}</b></p>
|
|
<Dropdown label="Select Your Province" options={options} />
|
|
</div>
|
|
}/>
|
|
|
|
<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 items-center justify-start">
|
|
<button
|
|
type="button"
|
|
onClick={showFileDialog}
|
|
className="h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
|
|
{t('index:openFile')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={isCameraOpen ? hideCameraView : showCameraView}
|
|
className="h-20 bg-green-600 hover:bg-gray-700 text-white font-semibold rounded-md">
|
|
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
|
|
</button>
|
|
<div id="spin" className={fileLoading ? undefined : "hidden"}>
|
|
<svg className="animate-spin h-5 w-5 ml-4" viewBox="0 0 24 24">
|
|
<circle className="opacity-0" cx="12" cy="12" r="10" stroke="currentColor"
|
|
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>
|
|
|
|
<video id="cameraPreview"
|
|
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
|
|
<input type='file'
|
|
id='file'
|
|
accept="application/pdf,.png,.jpg,.jpeg,.gif,.webp"
|
|
ref={inputFile}
|
|
style={{display: 'none'}}
|
|
/>
|
|
|
|
{(qrCode || file) &&
|
|
<div className="flex items-center space-x-1">
|
|
<svg className="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"/>
|
|
</svg>
|
|
<span className="w-full truncate">
|
|
{
|
|
qrCode && t('index:foundQrCode')
|
|
}
|
|
{
|
|
file && file.name
|
|
}
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
{fileErrorMessages.map((message, i) =>
|
|
<Alert message={message} key={'error-' + i} type="error" />
|
|
)}
|
|
</div>
|
|
}/>
|
|
|
|
{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">
|
|
<div>
|
|
<ul className="list-none">
|
|
<Check text={t('createdOnDevice')}/>
|
|
<Check text={t('piiNotSent')}/>
|
|
<Check text={t('openSourceTransparent')}/>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 items-center justify-items-stretch">
|
|
<button disabled={saveLoading || isDisabledAppleWallet} id="download" type="submit" value='applewallet' name='action'
|
|
className={'outline-apple rounded-md ' + ((!payloadBody || saveLoading || isDisabledAppleWallet)? 'bg-gray-300 cursor-not-allowed':'bg-black cursor-pointer')}>
|
|
<div className="flex justify-center">
|
|
<img style={{height: 40}} src="apple_wallet.svg" alt={t('index:addToWallet')}/>
|
|
</div>
|
|
</button>
|
|
|
|
<button id="addToGooglePay" type="button" disabled={saveLoading || isDisabledGooglePay} value='gpay' name='action' onClick={addToGooglePay}
|
|
className={'rounded-md ' + ((!payloadBody || saveLoading || isDisabledGooglePay)? 'bg-gray-300 cursor-not-allowed':'bg-black cursor-pointer')}>
|
|
<div className="flex justify-center">
|
|
<img style={{height: 40}} src="gpay_light.svg" alt={t('index:addToGooglePay')}/>
|
|
</div>
|
|
</button>
|
|
|
|
<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"/>
|
|
<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>
|
|
{addErrorMessages.map((message, i) =>
|
|
<Alert message={message} key={'error-' + i} type="error" />
|
|
)}
|
|
<div id="shc-image-header" hidden>
|
|
<p><b>{t('index:saveVaccineCard')}</b></p>
|
|
<p>{t('index:refreshNote')}</p>
|
|
<br />
|
|
<button id="renderPhoto" type="button" disabled={saveLoading || !payloadBody} value='renderPhoto' name='action' onClick={refreshPhoto}
|
|
className=" bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
|
|
{t('index:refreshButton')}
|
|
</button>
|
|
</div>
|
|
<div id="shc-pass-image" style={{backgroundColor: "white", color: "black", fontFamily: 'Arial', fontSize: 10, width: '350px', padding: '10px', border: '1px solid', margin: '0px'}} hidden>
|
|
<table style={{verticalAlign: "middle"}}>
|
|
<tbody>
|
|
<tr>
|
|
<td><img src='shield-black.svg' width='50' height='50' /></td>
|
|
<td style={{fontSize: 20, width: 280}}>
|
|
<span style={{marginLeft: '11px', whiteSpace: 'nowrap'}}><b>COVID-19 Vaccination Card</b></span><br/>
|
|
<span style={{marginLeft: '11px'}}><b id='shc-card-origin'></b></span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<br/>
|
|
<br/>
|
|
<div style={{fontSize:14, textAlign: 'center'}}>
|
|
<span id='shc-card-name' ></span> (<span id='shc-card-dob'></span>)
|
|
</div>
|
|
<br/>
|
|
<br/>
|
|
<table style={{textAlign: "center", width: "100%"}}>
|
|
<tbody>
|
|
<tr>
|
|
<td id='shc-card-vaccine-name-1'></td> <td id='shc-card-vaccine-name-2'></td>
|
|
</tr>
|
|
<tr>
|
|
<td id='shc-card-vaccine-date-1'></td> <td id='shc-card-vaccine-date-2'></td>
|
|
</tr>
|
|
<tr id='extraRow1' hidden>
|
|
<td id='shc-card-vaccine-name-3'></td> <td id='shc-card-vaccine-name-4'></td>
|
|
</tr>
|
|
<tr id='extraRow2' hidden>
|
|
<td id='shc-card-vaccine-date-3'></td> <td id='shc-card-vaccine-date-4'></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div id='shc-card-vaccine' style={{width:'63%', display:'block', marginLeft: 'auto', marginRight: 'auto'}}></div>
|
|
|
|
<div id='shc-qrcode' style={{width:'63%', display:'block', marginLeft: 'auto', marginRight: 'auto'}}></div>
|
|
|
|
<br/>
|
|
</div>{<a id="shc-pass-img-link" download="vaccination-card.png"><img id="shc-pass-img"/></a>}
|
|
</div>
|
|
}/>
|
|
|
|
<Card step="?" heading={t('index:questions')} content={
|
|
<div className="space-y-5">
|
|
<p>{t('index:questionsNote')}</p>
|
|
<div>
|
|
<ul>
|
|
<Bullet text={t('index:question1')}/>
|
|
<Bullet text={t('index:question2')}/>
|
|
<Bullet text={t('index:question3')}/>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="flex flex-row items-center justify-start">
|
|
<button id="faq-redirect" onClick={goToFAQ}
|
|
className=" bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
|
|
{t('index:visitFAQ')}
|
|
</button>
|
|
|
|
</div>
|
|
</div>
|
|
}/>
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Form;
|