diff --git a/.gitignore b/.gitignore index 4d29575d..600c0930 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules +./inji-verify/node_modules /.pnp .pnp.js diff --git a/README.md b/README.md index 4d053260..1f3269ad 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # inji-verify Repository host the source code, documentation, and other related files for the Inji Verify project. + +# Project Setup - Development + +Prerequisites: +* Node 18 - Can be installed using [nvm](https://github.com/nvm-sh/nvm). Run following command to install node + + ```curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash``` + + ```nvm install 18``` + +Run following commands to start the application + +```npm install``` + +```npm start``` + + diff --git a/inji-verify/package-lock.json b/inji-verify/package-lock.json index dcc1dada..ffae729b 100644 --- a/inji-verify/package-lock.json +++ b/inji-verify/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.5", "@mui/material": "^5.15.14", "@mui/styled-engine": "^5.15.14", + "@openhealthnz-credentials/pdf-image-qr-scanner": "^1.0.2", "@sunbird-rc/verification-sdk": "^0.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -3831,6 +3832,15 @@ "node": ">= 8" } }, + "node_modules/@openhealthnz-credentials/pdf-image-qr-scanner": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@openhealthnz-credentials/pdf-image-qr-scanner/-/pdf-image-qr-scanner-1.0.2.tgz", + "integrity": "sha512-GA4TQqWVoldNRLOru/Z+V2xIBFUkBueRz/QUfBC65UfkKeVg5J5d8exrQATGbI2ZMu+iaFFNV/Q7AEPKNqJ/oQ==", + "dependencies": { + "jsqr": "^1.4.0", + "pdfjs-dist": "^2.12.313" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -7735,6 +7745,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dommatrix": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", + "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", + "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix." + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -12962,6 +12978,11 @@ "node": ">=0.10.0" } }, + "node_modules/jsqr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", + "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==" + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -14050,6 +14071,23 @@ "node": ">=8" } }, + "node_modules/pdfjs-dist": { + "version": "2.16.105", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", + "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", + "dependencies": { + "dommatrix": "^1.0.3", + "web-streams-polyfill": "^3.2.1" + }, + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", diff --git a/inji-verify/package.json b/inji-verify/package.json index 5c53cbc3..39e1895f 100644 --- a/inji-verify/package.json +++ b/inji-verify/package.json @@ -7,6 +7,7 @@ "@emotion/styled": "^11.11.5", "@mui/material": "^5.15.14", "@mui/styled-engine": "^5.15.14", + "@openhealthnz-credentials/pdf-image-qr-scanner": "^1.0.2", "@sunbird-rc/verification-sdk": "^0.1.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", diff --git a/inji-verify/public/index.html b/inji-verify/public/index.html index 6b2a9446..28d900f4 100644 --- a/inji-verify/public/index.html +++ b/inji-verify/public/index.html @@ -2,7 +2,7 @@ - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper.tsx deleted file mode 100644 index 448aea86..00000000 --- a/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import {Box, Step, StepContent, StepLabel, Stepper, Typography} from "@mui/material"; - -const steps = [ - { - label: 'Scan QR Code', - description: 'Tap the button and display the QR code shown on your digital certificate / card', - }, - { - label: 'Activate your device’s camera', - description: - 'A notification will prompt to activate your device’s camera', - }, - { - label: 'Verification', - description: 'Validating and verification of your digital document / card' - }, - { - label: 'Result', - description: 'Credibility result of your digital document / card' - } -]; - -const InjiStepper = ({activeStep}: any) => { - return ( - - - {steps.map((step, index) => ( - - - - {step.label} - - - - - ))} - - - ); -} - -export default InjiStepper; diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/DesktopStepper.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/DesktopStepper.tsx new file mode 100644 index 00000000..71284c01 --- /dev/null +++ b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/DesktopStepper.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {Box, Step, StepContent, StepLabel, Stepper, Typography} from "@mui/material"; +import {InjiStepperStep} from "../../../../types/data-types"; + +function DesktopStepper({steps, activeStep}: {steps: InjiStepperStep[], activeStep: number}) { + return ( + + {steps.map((step, index) => ( + + + + {step.label} + + + + + ))} + + ); +} + +export default DesktopStepper; \ No newline at end of file diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/MobileStepper.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/MobileStepper.tsx new file mode 100644 index 00000000..6192b713 --- /dev/null +++ b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/MobileStepper.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import {Step, StepLabel, Stepper, Typography} from "@mui/material"; +import {InjiStepperStep} from "../../../../types/data-types"; + +function MobileStepper({steps, activeStep}: {steps: InjiStepperStep[], activeStep: number}) { + return ( + + {steps.map((step, index) => ( + + + + {step.label} + + + + ))} + + ); +} + +export default MobileStepper; diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/index.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/index.tsx new file mode 100644 index 00000000..21823ee5 --- /dev/null +++ b/inji-verify/src/components/Home/VerificationProgressTracker/InjiStepper/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {Box, Step, StepContent, StepLabel, Stepper, Typography, useMediaQuery} from "@mui/material"; +import DesktopStepper from "./DesktopStepper"; +import MobileStepper from "./MobileStepper"; +import {InjiStepperStep} from "../../../../types/data-types"; + +const steps: InjiStepperStep[] = [ + { + label: 'Scan QR Code', + description: 'Tap the button and display the QR code shown on your digital certificate / card', + }, + { + label: 'Activate your device’s camera', + description: + 'A notification will prompt to activate your device’s camera', + }, + { + label: 'Verification', + description: 'Validating and verification of your digital document / card' + }, + { + label: 'Result', + description: 'Credibility result of your digital document / card' + } +]; + +const InjiStepper = ({activeStep}: {activeStep: number}) => { + const isDesktop = useMediaQuery('@media (min-width:768px)'); + + return ( + + { + isDesktop + ? () + : () + } + + ); +} + +export default InjiStepper; diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/QrScanner.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/QrScanner.tsx index 527e00e0..d331b6e6 100644 --- a/inji-verify/src/components/Home/VerificationProgressTracker/QrScanner.tsx +++ b/inji-verify/src/components/Home/VerificationProgressTracker/QrScanner.tsx @@ -1,8 +1,9 @@ import React, {useCallback, useState} from 'react'; import {Scanner} from '@yudiel/react-qr-scanner'; - -function QrScanner({setVerifying, setActiveStep, setQrData}: any) { +function QrScanner({setVerifying, setActiveStep, setQrData}: { + setVerifying: (verifying: boolean) => void, setQrData: (data: string) => void, setActiveStep: (activeStep: number) => void +}) { const [dataRead, setDataRead] = useState(false) const isDataRead = useCallback(() => dataRead, [dataRead]); @@ -46,8 +47,7 @@ function QrScanner({setVerifying, setActiveStep, setQrData}: any) { video: { zIndex: 1000 } - } - } + }} /> ); } diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/StepperContentHeader.tsx b/inji-verify/src/components/Home/VerificationProgressTracker/StepperContentHeader.tsx index bf7e54f3..6db0a8ec 100644 --- a/inji-verify/src/components/Home/VerificationProgressTracker/StepperContentHeader.tsx +++ b/inji-verify/src/components/Home/VerificationProgressTracker/StepperContentHeader.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import {Box, Typography} from "@mui/material"; +import {Box, Typography, useMediaQuery} from "@mui/material"; function StepperContentHeader(props: any) { + const isTabletOrAbove = useMediaQuery("@media(min-width:600px)"); return ( Verify your credentials in 4 easy steps - - Credentials are digitally signed documents with tamper-evident QR codes. These QR codes can be easily verified using the Inji Verify app. Simply scan the QR code with your smartphone camera or use the dedicated verification tool on this page. - + { + isTabletOrAbove && ( + + Credentials are digitally signed documents with tamper-evident QR codes. These QR codes can be easily verified using the Inji Verify app. Simply scan the QR code with your smartphone camera or use the dedicated verification tool on this page. + + ) + } ); } diff --git a/inji-verify/src/components/Home/VerificationProgressTracker/styles.ts b/inji-verify/src/components/Home/VerificationProgressTracker/styles.ts index ebb144dc..68f4c1f6 100644 --- a/inji-verify/src/components/Home/VerificationProgressTracker/styles.ts +++ b/inji-verify/src/components/Home/VerificationProgressTracker/styles.ts @@ -7,5 +7,4 @@ export const VerificationProgressTrackerContainer = styled(Box)` margin-top: 0; height: 100vh; max-height: 100vh; - overflow-y: scroll; `; diff --git a/inji-verify/src/components/Home/VerificationSection/ImportFromFile.tsx b/inji-verify/src/components/Home/VerificationSection/ImportFromFile.tsx new file mode 100644 index 00000000..5529b184 --- /dev/null +++ b/inji-verify/src/components/Home/VerificationSection/ImportFromFile.tsx @@ -0,0 +1,33 @@ +import {scanFilesForQr} from "../../../utils/qr-utils"; +import {QrScanResult} from "../../../types/data-types"; +import {SetScanResultFunction} from "../../../types/function-types"; +import StyledButton from "./commons/StyledButton"; + +export const ImportFromFile = ({setScanResult}: {setScanResult: SetScanResultFunction}) => { + return ( +
+ + + +
+ { + const file = e?.target?.files && e?.target?.files[0]; + if (!file) return; + scanFilesForQr(file) + .then(scanResult => { + setScanResult(scanResult); + }); + }} + /> +
); +} diff --git a/inji-verify/src/components/Home/VerificationSection/Result/ResultSummary.tsx b/inji-verify/src/components/Home/VerificationSection/Result/ResultSummary.tsx index 8e2a9ce4..6f192849 100644 --- a/inji-verify/src/components/Home/VerificationSection/Result/ResultSummary.tsx +++ b/inji-verify/src/components/Home/VerificationSection/Result/ResultSummary.tsx @@ -2,11 +2,12 @@ import React from 'react'; import {Grid, Typography} from "@mui/material"; import {ReactComponent as VerificationSuccessIcon} from "../../../../assets/verification-success-icon.svg"; import {ReactComponent as VerificationFailedIcon} from "../../../../assets/verification-failed-icon.svg"; +import {SetActiveStepFunction} from "../../../../types/function-types"; const ResultSummary = ({success, vc, setActiveStep}: { success: boolean, vc: any, - setActiveStep: (activeStep: number) => void + setActiveStep: SetActiveStepFunction }) => { return ( diff --git a/inji-verify/src/components/Home/VerificationSection/Result/VcDisplayCard.tsx b/inji-verify/src/components/Home/VerificationSection/Result/VcDisplayCard.tsx index d3189c88..b9751142 100644 --- a/inji-verify/src/components/Home/VerificationSection/Result/VcDisplayCard.tsx +++ b/inji-verify/src/components/Home/VerificationSection/Result/VcDisplayCard.tsx @@ -1,39 +1,54 @@ import React from 'react'; -import {Grid, Typography} from '@mui/material'; +import {Box, Grid, Typography} from '@mui/material'; import {convertToTitleCase, getDisplayValue} from "../../../../utils/common-utils"; +import StyledButton from "../commons/StyledButton"; +import {SAMPLE_VERIFIABLE_CREDENTIAL} from "../../../../utils/samples"; +import {SetActiveStepFunction} from "../../../../types/function-types"; -function VcDisplayCard({vc}: any) { +function VcDisplayCard({vc, setActiveStep}: {vc: any, setActiveStep: SetActiveStepFunction}) { return ( - - { - vc && Object.keys(vc.credentialSubject) - .filter(key => key?.toLowerCase() !== "id" && key?.toLowerCase() !== "type") - .map(key => ( - - - {convertToTitleCase(key)} - - - {getDisplayValue(vc.credentialSubject[key])} - - - )) - } - + + + { + vc && Object.keys(vc.credentialSubject) + .filter(key => key?.toLowerCase() !== "id" && key?.toLowerCase() !== "type") + .map(key => ( + + + {convertToTitleCase(key)} + + + {getDisplayValue(vc.credentialSubject[key])} + + + )) + } + + + { + setActiveStep(0) + }}> + Scan Another QR Code + + + ); } diff --git a/inji-verify/src/components/Home/VerificationSection/Result/index.tsx b/inji-verify/src/components/Home/VerificationSection/Result/index.tsx index 596dc228..b3345131 100644 --- a/inji-verify/src/components/Home/VerificationSection/Result/index.tsx +++ b/inji-verify/src/components/Home/VerificationSection/Result/index.tsx @@ -3,8 +3,11 @@ import ResultSummary from "./ResultSummary"; import VcDisplayCard from "./VcDisplayCard"; import {Box} from "@mui/material"; import StyledButton from "../commons/StyledButton"; +import {CardPositioning, VcStatus} from "../../../../types/data-types"; +import {SetActiveStepFunction} from "../../../../types/function-types"; +import {SAMPLE_VERIFIABLE_CREDENTIAL} from "../../../../utils/samples"; -const getPositioning = (resultSectionRef: React.RefObject) => { +const getPositioning = (resultSectionRef: React.RefObject): CardPositioning => { // top = 340 - it is precalculated based in the xd design const positioning = {top: 212, right: 0}; if (!!resultSectionRef?.current) { @@ -18,23 +21,15 @@ const getPositioning = (resultSectionRef: React.RefObject) => { } const Result = ({vc, setActiveStep, vcStatus}: { - vc: any, setActiveStep: (activeStep: number) => void, vcStatus: { - status: "OK" | "NOK", checks: { - "active": string | null, - "revoked": "OK" | "NOK", - "expired": "OK" | "NOK", - "proof": "OK" | "NOK" - }[] - } | null + vc: any, setActiveStep: SetActiveStepFunction, vcStatus: VcStatus | null }) => { - const initialPositioning: { top?: number, right?: number } = {}; + const initialPositioning: CardPositioning = {}; const resultSectionRef = React.createRef(); const [vcDisplayCardPositioning, setVcDisplayCardPositioning] = useState(initialPositioning); useEffect(() => { if (resultSectionRef?.current && !(!!vcDisplayCardPositioning.top)) { let positioning = getPositioning(resultSectionRef); - console.log("Positioning: ", positioning); setVcDisplayCardPositioning(positioning); } }, [resultSectionRef]); @@ -56,19 +51,23 @@ const Result = ({vc, setActiveStep, vcStatus}: { top: `${vcDisplayCardPositioning.top ?? 212}px`, right: `${vcDisplayCardPositioning.right ?? 0}px` }}> - {vc && } - - - { - setActiveStep(0) - }}> - Scan Another QR Code - + {vc && } + { + !vc && ( + + { + setActiveStep(0) + }}> + Scan Another QR Code + + + ) + } ); } diff --git a/inji-verify/src/components/Home/VerificationSection/ScanQrCode.tsx b/inji-verify/src/components/Home/VerificationSection/ScanQrCode.tsx index 5c92d565..4b31e3d5 100644 --- a/inji-verify/src/components/Home/VerificationSection/ScanQrCode.tsx +++ b/inji-verify/src/components/Home/VerificationSection/ScanQrCode.tsx @@ -2,9 +2,13 @@ import React from 'react'; import {Box, Button, Grid, Typography} from "@mui/material"; import scanQr from "../../../assets/scanner-ouline.svg"; import StyledButton from "./commons/StyledButton"; +import {ImportFromFile} from "./ImportFromFile"; +import {QrScanResult} from "../../../types/data-types"; +import {SetActiveStepFunction, SetScanResultFunction} from "../../../types/function-types"; -const ScanQrCode = ({setActiveStep}: { - setActiveStep: (activeStep: number) => void +const ScanQrCode = ({setActiveStep, setScanResult}: { + setActiveStep: SetActiveStepFunction, + setScanResult: SetScanResultFunction }) => { return ( @@ -13,13 +17,19 @@ const ScanQrCode = ({setActiveStep}: { marginBottom: "44px" }}>Scan QR Code - + - setActiveStep(1)}> + setActiveStep(1)}> Scan the QR Code + + OR + + + + ); } diff --git a/inji-verify/src/components/Home/VerificationSection/Verification.tsx b/inji-verify/src/components/Home/VerificationSection/Verification.tsx index 7a94f815..6b4e3e11 100644 --- a/inji-verify/src/components/Home/VerificationSection/Verification.tsx +++ b/inji-verify/src/components/Home/VerificationSection/Verification.tsx @@ -4,37 +4,12 @@ import scanQr from "../../../assets/scanner-ouline.svg"; import Loader from "../../commons/Loader"; import QrScanner from "../VerificationProgressTracker/QrScanner"; import {verify} from "../../../utils/verification-utils"; +import {SetActiveStepFunction, SetQrDataFunction} from "../../../types/function-types"; -const Verification = ({setVc, setVcStatus, setActiveStep}: { - setVc: (vc: any) => void, setVcStatus: (status: any) => void, setActiveStep: (activeStep: number) => void +const Verification = ({setQrData, setActiveStep}: { + setQrData: SetQrDataFunction, setActiveStep: SetActiveStepFunction }) => { const [verifying, setVerifying] = useState(false); - const [qrData, setQrData] = useState(""); - - useEffect(() => { - if (qrData === "") return; - try { - let vc = JSON.parse(qrData); - verify(vc) - .then(status => { - setVc(vc); - setVcStatus(status); - setActiveStep(3); - }) - .catch(error => { - console.error("Error occurred while verifying the VC: ", error); - setVc(null); - setVcStatus({status: "NOK"}); - }); - } catch (error) { - console.error("Error occurred while reading the qrData: ", error); - setVc(null); - setVcStatus({status: "NOK"}); - } finally { - setQrData(""); - setActiveStep(3); - } - }, [qrData]); return ( diff --git a/inji-verify/src/components/Home/VerificationSection/commons/StyledButton.tsx b/inji-verify/src/components/Home/VerificationSection/commons/StyledButton.tsx index 45cdb8b7..f2634d6b 100644 --- a/inji-verify/src/components/Home/VerificationSection/commons/StyledButton.tsx +++ b/inji-verify/src/components/Home/VerificationSection/commons/StyledButton.tsx @@ -1,16 +1,21 @@ import React from 'react'; -import {Button, ButtonProps, Typography} from "@mui/material"; +import {Button, ButtonProps} from "@mui/material"; -function StyledButton(props: ButtonProps) { +type StyledButtonProps = ButtonProps & { + fill?: boolean +} + +function StyledButton(props: StyledButtonProps) { return (