Merge pull request #156 from covidpass-org/dev

Dev
This commit is contained in:
Marvin Sextro 2022-01-10 04:36:03 +01:00 committed by GitHub
commit d173ed8a16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1581 additions and 1064 deletions

View File

@ -1,5 +1,5 @@
# Install dependencies only when needed
FROM node:14-alpine AS deps
FROM node:16-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
@ -7,14 +7,14 @@ COPY package.json ./
RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:14-alpine AS builder
FROM node:16-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN yarn build
# Production image, copy all the files and run next
FROM node:14-alpine AS runner
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

View File

@ -1,24 +1,22 @@
import {useTranslation} from 'next-i18next';
interface AlertProps {
onClose: () => void;
errorMessage: string;
message: string;
isWarning: boolean;
}
function Alert(props: AlertProps): JSX.Element {
const { t } = useTranslation(['index', 'errors']);
return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mt-5 rounded relative" role="alert">
<span className="block sm:inline pr-6" id="message">{props.errorMessage}</span>
<span className="absolute top-0 bottom-0 right-0 px-4 py-3" onClick={props.onClose}>
<svg className="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<title>{t('index:errorClose')}</title>
<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"/>
</svg>
</span>
<div className={`${props.isWarning ? "bg-yellow-100 border border-yellow-400 text-yellow-700" : "bg-red-100 border border-red-400 text-red-700"} px-4 py-3 mt-5 rounded-md relative flex justify-between items-center`} role="alert">
<span className={`${props.isWarning ? "" : "pr-7"} block sm:inline text-lg`} id="message">{props.message}</span>
{
!props.isWarning &&
<span className="absolute top-0 right-0 p-1 m-2" onClick={props.onClose}>
<svg className="text-red-500 fill-current h-6 w-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="M6.2253 4.81108C5.83477 4.42056 5.20161 4.42056 4.81108 4.81108C4.42056 5.20161 4.42056 5.83477 4.81108 6.2253L10.5858 12L4.81114 17.7747C4.42062 18.1652 4.42062 18.7984 4.81114 19.1889C5.20167 19.5794 5.83483 19.5794 6.22535 19.1889L12 13.4142L17.7747 19.1889C18.1652 19.5794 18.7984 19.5794 19.1889 19.1889C19.5794 18.7984 19.5794 18.1652 19.1889 17.7747L13.4142 12L19.189 6.2253C19.5795 5.83477 19.5795 5.20161 19.189 4.81108C18.7985 4.42056 18.1653 4.42056 17.7748 4.81108L12 10.5858L6.2253 4.81108Z" />
</svg>
</span>
}
</div>
)
}

31
components/Button.tsx Normal file
View File

@ -0,0 +1,31 @@
interface ButtonProps {
text?: string,
icon?: string,
loading?: boolean,
onClick: () => void,
}
function Button(props: ButtonProps): JSX.Element {
function handleTouchEnd(event: React.TouchEvent<HTMLButtonElement>) {
event.preventDefault();
event.stopPropagation();
props.onClick();
}
return (
<button
type="button"
onClick={props.onClick}
onTouchEnd={handleTouchEnd}
className="bg-gray-400 dark:bg-gray-600 hover:bg-gray-500 relative focus:outline-none h-20 text-white font-semibold rounded-md items-center flex justify-center">
{
props.icon && <img src={props.icon} className="w-12 h-12 mr-2 -ml-4" />
}
{props.text}
</button>
)
}
export default Button;

View File

@ -1,20 +1,16 @@
import {useTranslation} from 'next-i18next';
interface CheckProps {
text: string;
}
function Check(props: CheckProps): JSX.Element {
const { t } = useTranslation(["index"]);
return (
<li className="flex flex-row space-x-4 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>
{props.text}
</li>
)
return (
<li className="flex flex-row space-x-4 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>
{props.text}
</li>
)
}
export default Check;

View File

@ -13,6 +13,7 @@ import {getPayloadBodyFromFile, getPayloadBodyFromQR} from "../src/process";
import {PassData} from "../src/pass";
import {COLORS} from "../src/colors";
import Colors from './Colors';
import Button from './Button';
function Form(): JSX.Element {
const {t} = useTranslation(['index', 'errors', 'common']);
@ -59,6 +60,44 @@ function Form(): JSX.Element {
});
}
}, [inputFile])
// Whether Safari is used or not
let [isSafari, setIsSafari] = useState<boolean>(true);
// Check if Safari is used
useEffect(() => {
const navigator = window.navigator;
setIsSafari(
navigator.vendor &&
navigator.vendor.indexOf('Apple') > -1 &&
navigator.userAgent &&
navigator.userAgent.indexOf('CriOS') == -1 &&
navigator.userAgent.indexOf('FxiOS') == -1
)
}, [isSafari]);
// Whether Safari is used or not
let [isShareDialogAvailable, setIsShareDialogAvailable] = useState<boolean>(false);
// Check if share dialog is available
useEffect(() => {
setIsShareDialogAvailable(window.navigator && window.navigator.share !== undefined);
}, [isShareDialogAvailable]);
// Open share dialog
async function showShareDialog() {
const shareData = {
title: document.title,
text: t('common:title') + ' ' + t('common:subtitle'),
url: window.location.protocol + "//" + window.location.host,
};
try {
await window.navigator.share(shareData);
} catch(error) {
console.log(error);
}
}
// Show file Dialog
async function showFileDialog() {
@ -102,7 +141,7 @@ function Form(): JSX.Element {
// Start decoding from video device
await codeReader.decodeFromVideoDevice(undefined,
previewElem,
(result, error, controls) => {
(result, _error, controls) => {
if (result !== undefined) {
setQrCode(result);
setFile(undefined);
@ -113,9 +152,6 @@ function Form(): JSX.Element {
setGlobalControls(undefined);
setIsCameraOpen(false);
}
if (error !== undefined) {
setErrorMessage(error.message);
}
}
)
);
@ -155,6 +191,9 @@ function Form(): JSX.Element {
const passBlob = new Blob([pass], {type: "application/vnd.apple.pkpass"});
saveAs(passBlob, 'covid.pkpass');
setLoading(false);
var scrollingElement = (document.scrollingElement || document.body);
scrollingElement.scrollTop = scrollingElement.scrollHeight;
} catch (e) {
setErrorMessage(e.message);
setLoading(false);
@ -164,29 +203,22 @@ function Form(): JSX.Element {
return (
<div>
<form className="space-y-5" id="form" onSubmit={addToWallet}>
{
!isSafari && <Alert isWarning={true} message={t('iosHint')} onClose={() => {}}/>
}
<Card step="1" 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">
<button
type="button"
onClick={isCameraOpen ? hideCameraView : showCameraView}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{isCameraOpen ? t('index:stopCamera') : t('index:startCamera')}
</button>
<button
type="button"
onClick={showFileDialog}
className="focus:outline-none h-20 bg-gray-500 hover:bg-gray-700 text-white font-semibold rounded-md">
{t('index:openFile')}
</button>
<Button text={isCameraOpen ? t('index:stopCamera') : t('index:startCamera')} onClick={isCameraOpen ? hideCameraView : showCameraView} />
<Button text={t('index:openFile')} onClick={showFileDialog} />
</div>
<video id="cameraPreview"
className={`${isCameraOpen ? undefined : "hidden"} rounded-md w-full`}/>
<input type='file'
id='file'
accept="application/pdf,image/png"
accept="application/pdf,image/png,image/jpeg,image/webp,image/gif"
ref={inputFile}
style={{display: 'none'}}
/>
@ -195,7 +227,7 @@ function Form(): JSX.Element {
<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"/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M9 5l7 7-7 7"/>
</svg>
<span className="w-full truncate">
{
@ -245,27 +277,38 @@ function Form(): JSX.Element {
</Link>.
</p>
</label>
<div className="flex flex-row items-center justify-start">
<button id="download" type="submit"
className="focus:outline-none bg-green-600 py-2 px-3 text-white font-semibold rounded-md disabled:bg-gray-400">
<div className="grid grid-cols-1">
<button
type="submit"
className="bg-green-600 hover:bg-green-700 relative focus:outline-none h-20 text-white font-semibold rounded-md items-center flex justify-center">
<div id="spin" className={`${loading ? undefined : "hidden"} absolute left-2`}>
<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-80" 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>
{t('index:addToWallet')}
</button>
<div id="spin" className={loading ? 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>
}/>
{
errorMessage && <Alert isWarning={false} message={errorMessage} onClose={() => setErrorMessage(undefined)}/>
}
<Card content={
<div className={`${isShareDialogAvailable ? "md:grid-cols-2": ""} grid-cols-1 grid gap-5`}>
{
isShareDialogAvailable && <Button text={t('index:share')} onClick={showShareDialog} />
}
<Button icon="kofi.png" text={t('common:donate')} onClick={() => {
window.open('https://ko-fi.com/marvinsxtr', '_blank');
}} />
</div>
}/>
</form>
<canvas id="canvas" style={{display: "none"}}/>
{
errorMessage && <Alert errorMessage={errorMessage} onClose={() => setErrorMessage(undefined)}/>
}
</div>
)
}

View File

@ -12,7 +12,7 @@ function Page(props: PageProps): JSX.Element {
const { t } = useTranslation('common');
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 pt-12 pb-16">
<Head>
<title>{t('common:title')}</title>
<link rel="icon" href="/favicon.ico"/>
@ -24,8 +24,7 @@ function Page(props: PageProps): JSX.Element {
{props.content}
<footer>
<nav className="nav flex pt-4 flex-row space-x-4 justify-center text-md font-bold flex-wrap">
<a href="https://ko-fi.com/marvinsxtr" className="hover:underline">{t('common:donate')}</a>
<nav className="nav flex flex-row space-x-4 justify-center text-md font-bold flex-wrap">
<a href="https://github.com/marvinsxtr/covidpass" className="hover:underline">{t('common:gitHub')}</a>
<Link href="/privacy"><a className="hover:underline">{t('common:privacyPolicy')}</a></Link>
<Link href="/imprint"><a className="hover:underline">{t('common:imprint')}</a></Link>

3
next-env.d.ts vendored
View File

@ -1,3 +1,6 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -12,7 +12,8 @@ module.exports = {
'es', 'es-ES',
'no', 'no-NO',
'nb', 'nb-NO',
'sv', 'sv-SE', 'sv-FI'
'sv', 'sv-SE', 'sv-FI',
'ro', 'ro-RO', 'ro-MD'
],
localeExtension: 'yml',
},

View File

@ -2,4 +2,12 @@ const {i18n} = require('./next-i18next.config');
module.exports = {
i18n,
async rewrites() {
return [
{
source: '/pass/note',
destination: '/pass'
}
];
}
};

View File

@ -16,16 +16,15 @@
"cbor-js": "^0.1.0",
"do-not-zip": "^1.0.0",
"file-saver": "^2.0.5",
"jpeg-js": "^0.4.3",
"jsqr": "^1.4.0",
"next": "latest",
"next": "^11.1.0",
"next-i18next": "^8.5.1",
"next-seo": "^4.26.0",
"node-fetch": "^2.6.1",
"pdfjs-dist": "^2.5.207",
"pngjs": "^6.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-qr-code": "^2.0.3",
"uuid": "^8.3.2",
"webpack": "^5.0.0",
"worker-loader": "^3.0.7"

View File

@ -14,12 +14,12 @@ function Imprint(): JSX.Element {
<p className="font-bold">{t('imprint:heading')}</p>
<p>
Marvin Sextro<br />
Wilhelm-Busch-Str. 8A<br />
30167 Hannover<br />
Kopenhagener Straße 45<br />
10437 Berlin
</p>
<p className="font-bold">{t('imprint:contact')}</p>
<p>
<a href="mailto:marvin.sextro@gmail.com" className="underline">marvin.sextro@gmail.com</a>
<a href="mailto:covidpass@marvinsextro.de" className="underline">covidpass@marvinsextro.de</a>
</p>
<p className="font-bold">{t('imprint:euDisputeResolution')}</p>
<p>{t('imprint:euDisputeResolutionParagraph')}</p>

View File

@ -40,9 +40,8 @@ function Index(): JSX.Element {
<Page content={
<div className="space-y-5">
<Card content={
<p>{t('common:subtitle')}&nbsp;{t('index:iosHint')}</p>
<p>{t('common:subtitle')}</p>
}/>
<Form/>
</div>
}/>

66
pages/pass.tsx Normal file
View File

@ -0,0 +1,66 @@
import {useTranslation} from 'next-i18next';
import {serverSideTranslations} from 'next-i18next/serverSideTranslations';
import React, {useEffect, useState} from "react";
import QRCode from "react-qr-code";
import Alert from '../components/Alert';
import Card from '../components/Card';
import Logo from "../components/Logo";
function Pass(): JSX.Element {
const { t } = useTranslation(['common', 'index']);
const [fragment, setFragment] = useState<string>(undefined);
const [view, setView] = useState<boolean>(true);
useEffect(() => {
const rawFragment = window.location.hash.substring(1);
if (!rawFragment) {
setView(false);
}
const resizeTimeout = window.setTimeout(() => {
if (rawFragment) {
window.location.replace('/pass/note');
}
}, 200);
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
const decodedFragment = Buffer.from(rawFragment, 'base64').toString();
setFragment(decodedFragment);
});
}, []);
return (
<div className="py-5 flex flex-col space-y-5 md:w-2/3 xl:w-2/5 md:mx-auto items-center justify-center px-5">
<Logo/>
<div className="flex flex-row items-center">
{
fragment &&
<Card content={
<div className="p-2 bg-white rounded-md">
<QRCode value={fragment} size={280} level="L" />
</div>
} />
}
{
!view &&
<Alert isWarning={true} message={t('index:viewerNote')} onClose={undefined} />
}
</div>
</div>
)
}
export async function getStaticProps({ locale }) {
return {
props: {
...(await serverSideTranslations(locale, ['index', 'common'])),
},
};
}
export default Pass;

View File

@ -49,12 +49,11 @@ function Privacy(): JSX.Element {
</div>
<p className="font-bold">{t('privacy:contact')}</p>
<p>
Marvin Sextro<br/>
Wilhelm-Busch-Str. 8A<br/>
30167 Hannover<br/>
Marvin Sextro
<br/>
{t('privacy:email')}:
&nbsp;
<a href="mailto:marvin.sextro@gmail.com">marvin.sextro@gmail.com</a>
<a href="mailto:covidpass@marvinsextro.de" className="underline">covidpass@marvinsextro.de</a>
<br/>
{t('privacy:website')}:
&nbsp;
@ -137,9 +136,9 @@ function Privacy(): JSX.Element {
</a>
</li>
<li>
PayPal:
Ko-fi:
&nbsp;
<a href="https://www.paypal.com/de/webapps/mpp/ua/privacy-full?locale.x=en_EN" className="underline">
<a href="https://more.ko-fi.com/privacy" className="underline">
{t('common:privacyPolicy')}
</a>
</li>
@ -153,7 +152,7 @@ function Privacy(): JSX.Element {
<li>
{t('privacy:appleSync')}:
&nbsp;
<a href="https://www.apple.com/legal/privacy/en-ww/privacy.tsx" className="underline">
<a href="https://www.apple.com/legal/privacy/en-ww/" className="underline">
{t('common:privacyPolicy')}
</a>
</li>

BIN
public/kofi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,9 +1,7 @@
iosHint: Bitte verwende unter iOS den Safari Browser.
errorClose: Schließen
selectCertificate: Zertifikat auswählen
selectCertificateDescription: |
Scanne den QR-Code auf Deinem Zertifikat oder wähle einen Screenshot oder eine PDF-Datei mit dem QR-Code.
Bitte beachte, dass die Auswahl einer Datei direkt von der Kamera nicht unterstützt wird.
selectCertificateDescription: Scanne den QR-Code auf Deinem Zertifikat oder wähle einen Screenshot oder eine PDF-Datei mit dem QR-Code.
stopCamera: Kamera stoppen
startCamera: Kamera starten
openFile: Datei auswählen
@ -26,4 +24,6 @@ iAcceptThe: Ich akzeptiere die
privacyPolicy: Datenschutzerklärung
createdOnDevice: Auf Deinem Gerät erstellt
openSourceTransparent: Open Source und transparent
hostedInEU: In der EU gehostet
hostedInEU: In der EU gehostet
share: Weiterempfehlen
viewerNote: Bitte drücke und halte den Link auf der Rückseite des Passes, um den QR Code unter iOS vergrößert anzuzeigen.

View File

@ -1,9 +1,7 @@
iosHint: Στο iOS, παρακαλώ χρησιμοποιήστε τον περιηγητή Safari.
errorClose: Κλείσιμο
selectCertificate: Επιλογή Πιστοποιητικού
selectCertificateDescription: |
Παρακαλώ σαρώστε τον κωδικό QR του πιστοποιητικού σας ή επιλέξτε ένα στιγμιότυπο οθόνης ή την σελίδα PDF με τον κωδικό QR.
Λάβετε υπόψη πως η απευθείας επιλογή κάποιου αρχείου μέσω της κάμερας, δεν υποστηρίζεται.
selectCertificateDescription: Παρακαλώ σαρώστε τον κωδικό QR του πιστοποιητικού σας ή επιλέξτε ένα στιγμιότυπο οθόνης ή την σελίδα PDF με τον κωδικό QR.
stopCamera: Τερματισμός Κάμερας
startCamera: Εκκίνηση Κάμερας
openFile: Επιλογή Αρχείου
@ -27,3 +25,4 @@ privacyPolicy: Πολιτική Απορρήτου
createdOnDevice: Δημιουργείται στη συσκευή σας
openSourceTransparent: Ανοιχτού κώδικα και διαφανής
hostedInEU: Φιλοξενείται στην ΕΕ
share: Κοινοποιήστε το

View File

@ -1,9 +1,7 @@
iosHint: On iOS, please use the Safari Browser.
errorClose: Close
selectCertificate: Select Certificate
selectCertificateDescription: |
Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
Note that selecting a file directly from camera is not supported.
selectCertificateDescription: Please scan the QR code on your certificate or select a screenshot or PDF page with the QR code.
stopCamera: Stop Camera
startCamera: Start Camera
openFile: Select File
@ -26,4 +24,6 @@ iAcceptThe: I accept the
privacyPolicy: Privacy Policy
createdOnDevice: Created on your device
openSourceTransparent: Open source and transparent
hostedInEU: Hosted in the EU
hostedInEU: Hosted in the EU
share: Share
viewerNote: Please press and hold the link on the back of the pass in order to enlarge the QR code on iOS.

View File

@ -1,9 +1,7 @@
iosHint: En iOS, Por favor use el navegador Safari.
errorClose: Cerrar
selectCertificate: Seleccione Certificado
selectCertificateDescription: |
Escanee el código QR de su certificado, o seleccione una captura de pantalla o un PDF que contenga el código QR.
Tenga en cuenta que no se admite la selección de un archivo directamente desde la cámara.
selectCertificateDescription: Escanee el código QR de su certificado, o seleccione una captura de pantalla o un PDF que contenga el código QR.
stopCamera: Detener Cámara
startCamera: Iniciar Cámara
openFile: Seleccione archivo
@ -26,4 +24,5 @@ iAcceptThe: Acepto la
privacyPolicy: Política de Privacidad
createdOnDevice: Creado en su dispositivo
openSourceTransparent: Open source y transparente
hostedInEU: Alojado en la UE
hostedInEU: Alojado en la UE
share: Recomendar

View File

@ -1,9 +1,7 @@
iosHint: Käytä iOS:ssä Safari-selainta.
errorClose: Sulje
selectCertificate: Valitse todistus
selectCertificateDescription: |
Skannaa todistuksessa oleva QR-koodi tai valitse kuvakaappaus tai PDF-sivu, jossa on QR-koodi.
Huomaa, että tiedoston valitsemista suoraan kamerasta ei tueta.
selectCertificateDescription: Skannaa todistuksessa oleva QR-koodi tai valitse kuvakaappaus tai PDF-sivu, jossa on QR-koodi.
stopCamera: Lopeta Kamera
startCamera: Käynnistä Kamera
openFile: Valitse Tiedosto
@ -26,4 +24,5 @@ iAcceptThe: Hyväksyn
privacyPolicy: Tietosuojaselosteen
createdOnDevice: Luotu laitteellasi
openSourceTransparent: Avoin lähdekoodi ja läpinäkyvä
hostedInEU: Isännöidään EU:ssa.
hostedInEU: Isännöidään EU:ssa.
share: Suosittele

View File

@ -1,9 +1,7 @@
iosHint: Sur iOS, veuillez utiliser le navigateur Safari.
errorClose: Fermez
selectCertificate: Sélectionner le certificat
selectCertificateDescription: |
Veuillez scanner le QR Code de votre certificat ou sélectionner une capture d'écran ou une page PDF avec le QR Code.
Notez que la sélection d'un fichier directement à partir de l'appareil photo n'est pas prise en charge.
selectCertificateDescription: Veuillez scanner le QR Code de votre certificat ou sélectionner une capture d'écran ou une page PDF avec le QR Code.
stopCamera: Arrêter l'appareil photo
startCamera: Démarrer l'appareil photo
openFile: Sélectionner un fichier
@ -26,4 +24,5 @@ iAcceptThe: J'accepte la
privacyPolicy: Politique de confidentialité
createdOnDevice : Créé sur votre appareil
openSourceTransparent : Open source et transparent
hostedInEU : Hébergé dans l'UE
hostedInEU : Hébergé dans l'UE
share: Recommander

View File

@ -1,9 +1,7 @@
iosHint: Su iOS, si prega di utilizzare il browser Safari.
errorClose: Chiudi
selectCertificate: Seleziona il certificato
selectCertificateDescription: |
Scannerizza il codice QR sul tuo certificato o seleziona uno screenshot o una pagina PDF con il codice QR.
Nota che la selezione di un file direttamente dalla fotocamera non è supportata.
selectCertificateDescription: Scannerizza il codice QR sul tuo certificato o seleziona uno screenshot o una pagina PDF con il codice QR.
stopCamera: Blocca Fotocamera
startCamera: Avvia Fotocamera
openFile: Seleziona un File
@ -27,3 +25,4 @@ privacyPolicy: Privacy Policy
createdOnDevice: Creato sul tuo dispositivo
openSourceTransparent: Open source e trasparente
hostedInEU: Server nell'UE
share: Raccomandare

View File

@ -1,9 +1,7 @@
iosHint: På iOS, vennligst bruk Safari nettleseren.
errorClose: Lukk
selectCertificate: Velg Sertifikat
selectCertificateDescription: |
Skann QR-koden på sertifikatet ditt, eller velg et skjermbilde eller en PDF med QR-koden.
Vær oppmerksom på at det ikke støttes å velge en fil direkte fra kameraet.
selectCertificateDescription: Skann QR-koden på sertifikatet ditt, eller velg et skjermbilde eller en PDF med QR-koden.
stopCamera: Stopp Kamera
startCamera: Start Kamera
openFile: Velg Fil
@ -27,3 +25,4 @@ privacyPolicy: Personvernerklæring
createdOnDevice: Laget på enheten din
openSourceTransparent: Åpen kildekode og gjennomsiktig
hostedInEU: Driftet i EU
share: Anbefale

View File

@ -1,9 +1,7 @@
iosHint: Gebruik op iOS de Safari-browser.
errorClose: Sluiten
selectCertificate: Selecteer Certificaat
selectCertificateDescription: |
Scan de QR-code op uw certificaat of selecteer een screenshot of pdf-pagina met de QR-code.
Merk op dat het rechtstreeks vanaf de camera selecteren van een bestand niet wordt ondersteund.
selectCertificateDescription: Scan de QR-code op uw certificaat of selecteer een screenshot of pdf-pagina met de QR-code.
stopCamera: Stop Camera
startCamera: Start Camera
openFile: Selecteer Bestand
@ -26,4 +24,5 @@ iAcceptThe: Ik accepteer het
privacyPolicy: Privacybeleid
createdOnDevice: Aangemaakt op uw apparaat
openSourceTransparent: Open source en transparant
hostedInEU: Gehost in de EU
hostedInEU: Gehost in de EU
share: Aanbevelen

1
public/locales/ro-MD Symbolic link
View File

@ -0,0 +1 @@
ro

1
public/locales/ro-RO Symbolic link
View File

@ -0,0 +1 @@
ro

View File

@ -0,0 +1,6 @@
title: CovidPass
subtitle: Adaugă-ți Certificatul UE Covid in aplicația ta preferată de wallet.
privacyPolicy: Politica de Confidențialitate
donate: Donează
gitHub: GitHub
imprint: Mențiuni Legale

View File

@ -0,0 +1,20 @@
noFileOrQrCode: Te rog scaneaza un cod QR, sau alege un fisier
signatureFailed: Eroare in timpul semnarii certificatului pe server
decodingFailed: Imposibil de decodat continutul codului QR
invalidColor: Culoare Invalida
certificateData: Eroare in citirea datelor certificatului
nameMissing: Eroare in citirea numelui
dobMissing: Eroare in citirea datei de nastere
invalidMedicalProduct: Produs medical (Vaccin) invalid
invalidCountryCode: Codul tarii este invalid
invalidManufacturer: Fabricant invalid
invalidFileType: Tipul fisierului invalid
couldNotDecode: Eroare in decodarea codului QR din fisier
couldNotFindQrCode: Nu s-a gasit niciun cod QR valid in fisierul selectat
invalidQrCode: Cod QR invalid
certificateType: Tipul certificatului este invalid
invalidTestResult: Rezultatul testului este invalid
invalidTestType: Tipul testului este invalid
noCameraAccess: Nu s-a putut accesa camera. Schimbati permisiunile in Setari > Safari > Camera.
noCameraFound: Nu s-a putut accesa camera.
safariSupportOnly: in iOS, va rog sa folositi doar browser-ul safari.

View File

@ -0,0 +1,27 @@
heading: Informații conform articolului 5 TMG
contact: Contact
euDisputeResolution: Soluționarea Disputelor UE
euDisputeResolutionParagraph: |
Comisia Europeana dispune de o platforma pentru soluționarea disputelor online (OS) https://ec.europa.eu/consumers/odr.
Ne puteți găsi adresa de email in datele de mai sus.
consumerDisputeResolution: Rezolvarea litigiilor consumatorilor / comisia universală de arbitraj
consumerDisputeResolutionParagraph: Nu suntem dispuși sau obligați să participăm la procedurile de soluționare a litigiilor în fața unei comisii de arbitraj pentru consumatori.
liabilityForContents: Raspunderea pentru continut
liabilityForContentsParagraph: |
În calitate de furnizori de servicii, suntem responsabili pentru conținutul propriu în conformitate cu articolul 7 paragraful 1 TMG în conformitate cu legile generale.
Conform secțiunilor 8 până la 10, nu suntem obligați să monitorizăm informațiile transmise sau stocate sau să investigăm circumstanțe care indică o activitate ilegală.
Obligațiile de a elimina sau de a bloca utilizarea informațiilor conform legilor generale rămân neafectate.
Cu toate acestea, răspunderea în această privința este posibilă numai din momentul în care devine cunoscută o încălcare concretă a legii.
Dacă aflăm de existența unor asemenea încălcări, vom elimina imediat conținutul relevant.
liabilityForLinks: Raspunderea pentru linkuri
liabilityForLinksParagraph: |
Oferta noastră conține link-uri către site-uri externe ale terților, asupra cărora nu avem nicio influență.
Prin urmare, nu ne putem asuma nicio răspundere pentru aceste conținuturi externe.
Furnizorul sau operatorul respectiv al site-urilor este întotdeauna responsabil pentru conținutul site-urilor mentionate.
Paginile conectate au fost verificate pentru posibile încălcări legale la momentul punerii link-ului.
Conținuturile ilegale nu erau recunoscute în momentul punerii link-urilor.
Cu toate acestea, un control permanent al conținutului paginilor linkate nu este rezonabil fără dovezi concrete ale unei încălcări a legii.
Dacă aflăm că există încălcări, vom elimina imediat astfel de legături.
credits: Credite
creditsSource: Cu extrase din https://www.e-recht24.de/impressum-generator.html
creditsTranslation: Tradus cu https://www.DeepL.com/Translator (free version)

View File

@ -0,0 +1,28 @@
iosHint: In iOS, te rog foloseste browser-ul Safari.
errorClose: Inchide
selectCertificate: Alege certificatul
selectCertificateDescription: Te rog scaneaza codul QR de pe certificatul tau sau alege un screenshot/PDF cu codul QR
stopCamera: Opreste Camera
startCamera: Porneste Camera
openFile: Selecteaza fisierul
foundQrCode: Cod QR detectat!
pickColor: Alege o culoare
pickColorDescription: Alege o culoare de fundal pentru certificatul tau.
colorWhite: alb
colorBlack: negru
colorGrey: gri
colorGreen: verde
colorIndigo: indigo
colorBlue: albastru
colorPurple: mov
colorTeal: teal
addToWallet: Adauga in Wallet
dataPrivacyDescription: |
Confidentialitatea datelor este de o importanta speciala cand vine vorba de date medicale.
Pentru a face o alegere informata, va rugam cititi
iAcceptThe: Sunt de acord cu
privacyPolicy: Politica de Confidentialitate
createdOnDevice: Creat pe dispozitivul tau
openSourceTransparent: Open source si transparent
hostedInEU: Gazduit in UE
share: Distribuie

View File

@ -0,0 +1,57 @@
gdprNotice: |
Politica noastră de confidențialitate se bazează pe termenii folosiți de legiuitorul european
pentru adoptarea Regulamentului general privind protecția datelor (GDPR).
generalInfo: Informații Generale
generalInfoProcess: |
Întregul proces de generare a fișierului de Wallet are loc local în browserul dvs.
Pentru pasul de semnare (certificare), doar o reprezentare hashed (codata ireversibil) a datelor dvs. este trimisă către server.
generalInfoStoring: Datele dumneavoastră nu sunt stocate dincolo de sesiunea activă a browserului, iar site-ul nu utilizează cookie-uri.
generalInfoThirdParties: Nu sunt trimise date catre părți terțe.
generalInfoHttps: Datele sunt transmise în mod securizat prin https.
generalInfoLocation: Serverele noastre sunt găzduite in Nuremberg, Germania.
generalInfoGitHub: Codul sursă al acestui site este accesibil pe
generalInfoLockScreen: În mod normal, cardurile din Apple Wallet sunt accesibile de pe lock screen. Puteți schimba acest lucru in
settings: setări
generalInfoProvider: |
Furnizorul de server prelucrează date pentru a furniza acest site.
Pentru a înțelege mai bine ce măsuri iau aceștia pentru a vă proteja datele, vă rugăm să citiți și pe partea lor
privacyPolicy: Politica de Confidențialitate
andThe: și
dataPrivacyFaq: Întrebari puse des despre confidențialitate
contact: Contact
email: Email
website: Website
process: Explicație simplificata a procesului
processFirst: În primul rând, următorii pași au loc local în browserul dvs
processSecond: În al doilea rând, următorii pași au loc pe serverul nostru
processThird: În cele din urmă, următorii pași au loc local în browserul dvs
processRecognizing: Recunoașterea și extragerea datelor codului QR din certificatul selectat
processDecoding: Decodificarea datelor dvs. personale și medicale din încărcarea codului QR
processAssembling: Asamblarea unui fișier de Wallet incomplet din datele dvs
processGenerating: Generarea unui fișier care conține hash-uri ale datelor stocate în fișierul wallet
processSending: Trimiterea doar fișierul care conține hash-urile către serverul nostru
processReceiving: Primirea și verificarea hashurilor care au fost generate local
processSigning: Semnarea fișierului care conține hashurile
processSendingBack: Trimiterea semnăturii înapoi
processCompleting: Asamblarea fișierului wallet semnat din fișierul incomplet generat local și semnătura
processSaving: Salvarea fisierului pe dispozitiv
locallyProcessedData: Date procesate local
the:
schema: Schema Certificatului Digital Covid
specification: contine o schema complete cu datele care sunt continute de codul QR al certificatului covid si care vor fi procesate in browser.
serverProvider: Furnizorul de Servere
serverProviderIs: Furnizorul nostru de servere este
logFiles: Următoarele date pot fi colectate și stocate în fișierele jurnal ale serverului
logFilesBrowser: Tipurile și versiunile de browser utilizate
logFilesOs: Sistemul de operare utilizat de sistemul de accesare
logFilesReferrer: Site-ul web de pe care un sistem de accesare ajunge pe site-ul nostru web (așa-numiții recomandanți)
logFilesTime: Data și ora accesului
logFilesIpAddress: Adresele IP pseudonimizate
rights: Drepturile tale
rightsGranted: În conformitate cu GDPR, aveți următoarele drepturi
rightsAccess: Dreptul de acces la datele dumneavoastră; Ai dreptul să știi ce date au fost colectate despre tine și cum au fost prelucrate.
rightsErasure: Dreptul de a fi uitat; Ștergerea datelor dumneavoastră personale.
rightsRectification: Dreptul de rectificare; Aveți dreptul de a corecta datele eronate.
rightsPortability: Dreptul la portabilitatea datelor; Aveți dreptul să vă transferați datele dintr-un sistem de procesare în altul.
thirdParties: Părți terțe
appleSync: Este posibil ca Apple să vă sincronizeze cardurile wallet prin iCloud

View File

@ -1,9 +1,7 @@
iosHint: På iOS, vänligen använd Safari som webbläsare.
errorClose: Stäng
selectCertificate: Välj certifikat
selectCertificateDescription: |
Vänligen skanna QR-koden på ditt certifikat eller välj en skärmdump eller PDF-fil som innehåller QR-koden.
Observera att det saknas stöd för att välja en fil direkt från kameran.
selectCertificateDescription: Vänligen skanna QR-koden på ditt certifikat eller välj en skärmdump eller PDF-fil som innehåller QR-koden.
stopCamera: Stäng kamera
startCamera: Öppna kamera
openFile: Välj fil
@ -26,4 +24,5 @@ iAcceptThe: Jag godtar
privacyPolicy: integritetspolicyn
createdOnDevice: Skapad på din enhet
openSourceTransparent: Öppen källkod och transparent
hostedInEU: Drivs från EU
hostedInEU: Drivs från EU
share: Rekommendera

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Disallow: /pass
Disallow: /pass/note

View File

@ -20,6 +20,10 @@ export function typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
}
export function decodeData(data: string): Object {
if (data.startsWith('https://')) {
var url = new URL(data);
data = decodeURIComponent(url.hash.substring(1));
}
if (data.startsWith('HC1')) {
data = data.substring(3);
@ -45,4 +49,4 @@ export function decodeData(data: string): Object {
var decoded: Object = cbor.decode(typedArrayToBufferSliced(plaintext));
return decoded;
}
}

View File

@ -19,6 +19,7 @@ interface QrCode {
message: string;
format: QrFormat;
messageEncoding: Encoding;
altText: string;
}
interface SignData {
@ -83,6 +84,7 @@ export class PassData {
message: payload.rawData,
format: QrFormat.PKBarcodeFormatQR,
messageEncoding: Encoding.utf8,
altText: 'SCAN TO VERIFY',
}
// Create pass data

View File

@ -3,9 +3,9 @@ import {Constants} from "./constants";
import {COLORS} from "./colors";
enum CertificateType {
Vaccination = 'Vaccination Card',
Test = 'Test Certificate',
Recovery = 'Recovery Certificate',
Vaccination = 'Vaccination Pass',
Test = 'Test Pass',
Recovery = 'Recovery Pass',
}
enum TextAlignment {
@ -116,6 +116,11 @@ export class Payload {
const country = valueSets.countryCodes[countryCode].display;
// Encode raw data and get url
const encodedData = Buffer.from(body.rawData).toString('base64');
const url = window.location.protocol + "//" + window.location.host;
const generic: PassDictionary = {
headerFields: [
{
@ -134,6 +139,11 @@ export class Payload {
secondaryFields: [],
auxiliaryFields: [],
backFields: [
{
key: "enlarge",
label: "Enlarging the QR Code",
value: `Inside the Wallet app on iOS, press and hold the link below. This does not work when accessing the Wallet by double-clicking the side button.\n<a href='${url}/pass#${encodedData}'>Enlarge QR Code</a>`
},
{
key: "uvci",
label: "Unique Certificate Identifier (UVCI)",
@ -157,10 +167,27 @@ export class Payload {
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
this.dark = dark;
this.generic = Payload.fillPassData(this.certificateType, generic, properties, valueSets, country, dateOfBirth);
this.generic = Payload.fillPassData(
this.certificateType,
generic,
properties,
valueSets,
country,
dateOfBirth,
url
);
}
static fillPassData(type: CertificateType, data: PassDictionary, properties: Object, valueSets: ValueSets, country: string, dateOfBirth: string): PassDictionary {
static fillPassData(
type: CertificateType,
data: PassDictionary,
properties: Object,
valueSets: ValueSets,
country: string,
dateOfBirth: string,
url: string
): PassDictionary {
switch (type) {
case CertificateType.Vaccination:
const dose = `${properties['dn']}/${properties['sd']}`;
@ -235,7 +262,17 @@ export class Payload {
throw new Error('invalidTestType')
}
const testResult = valueSets.testResults[testResultKey].display;
let testResult = valueSets.testResults[testResultKey].display;
switch (testResult) {
case 'Not detected':
testResult = 'Negative';
break;
case 'Detected':
testResult = 'Positive';
break;
}
const testType = valueSets.testTypes[testTypeKey].display;
const testTime = testDateTimeString.replace(/.*T/, '').replace('Z', ' ') + 'UTC';
@ -339,6 +376,14 @@ export class Payload {
throw new Error('certificateType');
}
data.backFields.push(...[
{
key: "credits",
label: "",
value: `Created with <a href='${url}'>CovidPass</a>`
}
]);
return data;
}
}

View File

@ -1,5 +1,4 @@
import {PayloadBody} from "./payload";
import {PNG} from 'pngjs'
import * as PdfJS from 'pdfjs-dist'
import jsQR, {QRCode} from "jsqr";
import {decodeData} from "./decode";
@ -9,19 +8,19 @@ import {COLORS} from "./colors";
PdfJS.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${PdfJS.version}/pdf.worker.js`
export async function getPayloadBodyFromFile(file: File, color: COLORS): Promise<PayloadBody> {
// Read file
const fileBuffer = await file.arrayBuffer();
let imageData: ImageData;
switch (file.type) {
case 'application/pdf':
console.log('pdf')
// Read file
const fileBuffer = await file.arrayBuffer();
imageData = await getImageDataFromPdf(fileBuffer)
break
case 'image/png':
console.log('png')
imageData = await getImageDataFromPng(fileBuffer)
case 'image/jpeg':
case 'image/webp':
case 'image/gif':
imageData = await getImageDataFromImage(file)
break
default:
throw Error('invalidFileType')
@ -81,18 +80,48 @@ export async function getPayloadBodyFromQR(qrCodeResult: Result, color: COLORS):
}
}
async function getImageDataFromPng(fileBuffer: ArrayBuffer): Promise<ImageData> {
return new Promise(async (resolve, reject) => {
let png = new PNG({filterType: 4})
function getImageDataFromImage(file: File): Promise<ImageData> {
return new Promise((resolve, reject) => {
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
const canvasContext = canvas.getContext('2d');
png.parse(fileBuffer, (error, data) => {
if (error) {
reject();
// create Image object
const img = new Image();
img.onload = () => {
// constrain image to 2 Mpx
const maxPx = 2000000;
let width: number;
let height: number;
if (img.naturalWidth * img.naturalHeight > maxPx) {
const ratio = img.naturalHeight / img.naturalWidth;
width = Math.sqrt(maxPx / ratio);
height = Math.floor(width * ratio);
width = Math.floor(width);
} else {
width = img.naturalWidth;
height = img.naturalHeight;
}
resolve(data);
})
})
// Set correct canvas width / height
canvas.width = width;
canvas.height = height;
// draw image into canvas
canvasContext.clearRect(0, 0, width, height);
canvasContext.drawImage(img, 0, 0, width, height);
// Obtain image data
resolve(canvasContext.getImageData(0, 0, width, height));
};
img.onerror = (e) => {
reject(e);
};
// start loading image from file
img.src = URL.createObjectURL(file);
});
}
async function getImageDataFromPdf(fileBuffer: ArrayBuffer): Promise<ImageData> {

View File

@ -16,7 +16,8 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",

2003
yarn.lock

File diff suppressed because it is too large Load Diff