diff --git a/inji-verify/.env b/inji-verify/.env index 99f2032e..d5ad3cbd 100644 --- a/inji-verify/.env +++ b/inji-verify/.env @@ -1,4 +1,4 @@ INTERNET_CONNECTIVITY_CHECK_ENDPOINT=https://dns.google/ INTERNET_CONNECTIVITY_CHECK_TIMEOUT=10000 OVP_CLIENT_ID=https://injiverify.dev1.mosip.net -OVP_QR_HEADER=INJI_OVP://payload= \ No newline at end of file +OVP_QR_HEADER=INJI_OVP:// \ No newline at end of file diff --git a/inji-verify/package.json b/inji-verify/package.json index 53ed7339..c188bb73 100644 --- a/inji-verify/package.json +++ b/inji-verify/package.json @@ -4,7 +4,6 @@ "private": true, "dependencies": { "@mosip/pixelpass": "0.1.6", - "@openhealthnz-credentials/pdf-image-qr-scanner": "1.0.2", "@reduxjs/toolkit": "^2.2.3", "@sunbird-rc/verification-sdk": "0.1.0", "@testing-library/jest-dom": "^5.17.0", @@ -15,8 +14,10 @@ "@types/react-dom": "^18.2.23", "@types/react-redux": "^7.1.33", "@types/redux-thunk": "^2.1.0", - "@yudiel/react-qr-scanner": "2.0.0-beta.3", + "html5-qrcode": "^2.3.8", + "jsqr": "^1.4.0", "patch-package": "^8.0.0", + "pdfjs-dist": "^4.5.136", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.2.1", diff --git a/inji-verify/src/components/Home/VerificationSection/QrScanner.tsx b/inji-verify/src/components/Home/VerificationSection/QrScanner.tsx index 012ff0e4..bebdcbef 100644 --- a/inji-verify/src/components/Home/VerificationSection/QrScanner.tsx +++ b/inji-verify/src/components/Home/VerificationSection/QrScanner.tsx @@ -1,105 +1,86 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {Scanner} from '@yudiel/react-qr-scanner'; +import React, { useEffect, useRef, useState } from "react"; import CameraAccessDenied from "./CameraAccessDenied"; -import {ScanSessionExpiryTime} from "../../../utils/config"; -import {useAppDispatch} from "../../../redux/hooks"; -import {goHomeScreen, verificationInit} from "../../../redux/features/verification/verification.slice"; -import {raiseAlert} from "../../../redux/features/alerts/alerts.slice"; +import { ScanSessionExpiryTime } from "../../../utils/config"; +import { useAppDispatch } from "../../../redux/hooks"; +import { + goHomeScreen, + verificationInit, +} from "../../../redux/features/verification/verification.slice"; +import { raiseAlert } from "../../../redux/features/alerts/alerts.slice"; import "./ScanningLine.css"; +import { initiateQrScanning, terminateScanning } from "../../../utils/qr-utils"; let timer: NodeJS.Timeout; function QrScanner() { - const dispatch = useAppDispatch(); - const [isCameraBlocked, setIsCameraBlocked] = useState(false); + const dispatch = useAppDispatch(); + const [isCameraBlocked, setIsCameraBlocked] = useState(false); - const scannerRef = useRef(null); + const scannerRef = useRef(null); - useEffect(() => { - timer = setTimeout(() => { - dispatch(goHomeScreen({})); - dispatch(raiseAlert({ - open: true, - message: "The scan session has expired due to inactivity. Please initiate a new scan.", - severity: "error" - })) - }, ScanSessionExpiryTime); - return () => { - console.log('Clearing timeout'); - clearTimeout(timer) - }; - }, [dispatch]); + const onSuccess = (decodedText: any) => { + dispatch( + verificationInit({ + qrReadResult: { qrData: decodedText, status: "SUCCESS" }, + flow: "SCAN", + }) + ); + clearTimeout(timer); + }; + + useEffect(() => { + timer = setTimeout(() => { + dispatch(goHomeScreen({})); + dispatch( + raiseAlert({ + open: true, + message: + "The scan session has expired due to inactivity. Please initiate a new scan.", + severity: "error", + }) + ); + terminateScanning(); + }, ScanSessionExpiryTime); + initiateQrScanning(timer, onSuccess); + return () => { + console.log("Clearing timeout"); + clearTimeout(timer); + }; + }, [dispatch]); - useEffect(() => { - // Disable inbuilt border around the video - if (scannerRef?.current) { - let svgElements = scannerRef?.current?.getElementsByTagName('svg'); - if (svgElements.length === 1) { - svgElements[0].style.display = 'none'; - } - } - }, [scannerRef]); + useEffect(() => { + // Disable inbuilt border around the video + if (scannerRef?.current) { + let svgElements = scannerRef?.current?.getElementsByTagName("svg"); + if (svgElements.length === 1) { + svgElements[0].style.display = "none"; + } + } + }, [scannerRef]); - return ( -
- { - !isCameraBlocked && ( -
-
-
- ) - } - { - console.log(text, result); - dispatch(verificationInit({qrReadResult: {qrData: text, status: "SUCCESS"}, flow: "SCAN"})); - }} - onError={(error) => { - console.log('Clearing timeout - camera blocked'); - clearTimeout(timer); - setIsCameraBlocked(true); - }} - components={{ - torch: false - }} - options={{ - constraints: { - "width": { - "min": 640, - "ideal": 720, - "max": 1920 - }, - "height": { - "min": 640, - "ideal": 720, - "max": 1080 - }, - facingMode: "environment" - }, - delayBetweenScanSuccess: 1000000 // Scan once - }} - styles={{ - container: { - width: window.innerWidth < 1024 ? "250px" : "316px", - placeContent: "center", - display: "grid", - placeItems: "center", - borderRadius: "12px" - }, - video: { - objectFit: "cover", - objectPosition: "center" - } - }} - /> - { - console.log("closing camera"); - dispatch(goHomeScreen({})); - setIsCameraBlocked(false) - }}/> + return ( +
+ {!isCameraBlocked && ( +
+
- ); + )} + +
+ + { + console.log("closing camera"); + dispatch(goHomeScreen({})); + setIsCameraBlocked(false); + }} + /> +
+ ); } export default QrScanner; diff --git a/inji-verify/src/components/Home/VerificationSection/UploadQrCode.tsx b/inji-verify/src/components/Home/VerificationSection/UploadQrCode.tsx index f3fa39fa..6997d40a 100644 --- a/inji-verify/src/components/Home/VerificationSection/UploadQrCode.tsx +++ b/inji-verify/src/components/Home/VerificationSection/UploadQrCode.tsx @@ -82,11 +82,11 @@ export const UploadQrCode = ({displayMessage, className}: { displayMessage: stri return; } - dispatch(qrReadInit({method: "UPLOAD"})); scanFilesForQr(file) .then(scanResult => { if (scanResult.error) console.error(scanResult.error); if (!!scanResult.data) { + dispatch(qrReadInit({method: "UPLOAD"})); dispatch(raiseAlert({...AlertMessages.qrUploadSuccess, open: true})); dispatch(verificationInit({qrReadResult: {qrData: scanResult.data, status: "SUCCESS"}})); } else { diff --git a/inji-verify/src/components/Home/VerificationSection/Verification.tsx b/inji-verify/src/components/Home/VerificationSection/Verification.tsx index 449466ed..9139b66c 100644 --- a/inji-verify/src/components/Home/VerificationSection/Verification.tsx +++ b/inji-verify/src/components/Home/VerificationSection/Verification.tsx @@ -1,43 +1,49 @@ -import React from 'react'; +import React from "react"; import scanQr from "../../../assets/scanner-ouline.svg"; import Loader from "../../commons/Loader"; import QrScanner from "./QrScanner"; import StyledButton from "./commons/StyledButton"; -import {useAppDispatch} from "../../../redux/hooks"; -import {goHomeScreen} from "../../../redux/features/verification/verification.slice"; -import {VerificationSteps} from "../../../utils/config"; -import {useVerificationFlowSelector} from "../../../redux/features/verification/verification.selector"; +import { useAppDispatch } from "../../../redux/hooks"; +import { goHomeScreen } from "../../../redux/features/verification/verification.slice"; +import { VerificationSteps } from "../../../utils/config"; +import { useVerificationFlowSelector } from "../../../redux/features/verification/verification.selector"; +import { terminateScanning } from "../../../utils/qr-utils"; const Verification = () => { - const dispatch = useAppDispatch(); - const {activeScreen, method} = useVerificationFlowSelector(state => ({activeScreen: state.activeScreen, method: state.method})); - console.log({activeScreen}); - return ( -
-
- { - activeScreen === VerificationSteps[method].Verifying - ? () - : () - } -
-
- { - dispatch(goHomeScreen({})) - }}> - Back - -
-
- ); -} + const dispatch = useAppDispatch(); + const { activeScreen, method } = useVerificationFlowSelector((state) => ({ + activeScreen: state.activeScreen, + method: state.method, + })); + console.log({ activeScreen }); + return ( +
+
+ {activeScreen === VerificationSteps[method].Verifying ? ( + + ) : ( + + )} +
+
+ { + terminateScanning(); + dispatch(goHomeScreen({})); + }} + > + Back + +
+
+ ); +}; export default Verification; diff --git a/inji-verify/src/utils/pdfToQrData.js b/inji-verify/src/utils/pdfToQrData.js new file mode 100644 index 00000000..477828bc --- /dev/null +++ b/inji-verify/src/utils/pdfToQrData.js @@ -0,0 +1,57 @@ +import * as pdfjsLib from "pdfjs-dist/webpack"; +import jsQR from "jsqr"; + +const decodeQrCode = (imageDataUrl) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const decoded = jsQR(imageData.data, canvas.width, canvas.height); + if (decoded) { + resolve(decoded.data); + } else { + resolve(null); + } + }; + img.onerror = () => reject("Error loading image"); + img.src = imageDataUrl; + }); +}; + +export const pdfToQrData = async (file) => { + try { + const pdfData = await file.arrayBuffer(); + const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; + let qrData; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: 2.0 }); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + canvas.height = viewport.height; + canvas.width = viewport.width; + const renderContext = { + canvasContext: context, + viewport: viewport, + }; + await page.render(renderContext).promise; + const dataURL = canvas.toDataURL("image/png"); + qrData = await decodeQrCode(dataURL); + + if (qrData) { + break; // Exit loop if QR code is found + } + } + + return qrData; + } catch (err) { + console.error("Error processing PDF:", err); + throw new Error("Failed to process PDF file."); + } +}; diff --git a/inji-verify/src/utils/qr-utils.js b/inji-verify/src/utils/qr-utils.js index b994a17c..c8fe89de 100644 --- a/inji-verify/src/utils/qr-utils.js +++ b/inji-verify/src/utils/qr-utils.js @@ -1,35 +1,76 @@ -import { scanFile } from "@openhealthnz-credentials/pdf-image-qr-scanner"; -import {decode, generateQRData} from '@mosip/pixelpass'; -import {HEADER_DELIMITER, SUPPORTED_QR_HEADERS} from "./config"; +import { decode, generateQRData } from "@mosip/pixelpass"; +import { HEADER_DELIMITER, SUPPORTED_QR_HEADERS } from "./config"; +import { Html5Qrcode } from "html5-qrcode"; +import { pdfToQrData } from "./pdfToQrData"; export const scanFilesForQr = async (selectedFile) => { - let scanResult = { data: null, error: null }; - try { - scanResult.data = await scanFile(selectedFile); - } catch (e) { - // Example Error Handling - if (e?.name === "InvalidPDFException") { - scanResult.error = "Invalid PDF"; - } else if (e instanceof Event) { - scanResult.error = "Invalid Image"; - } else { - scanResult.error = "Unknown error:" + e; - } + let scanResult = { data: null, error: null }; + const html5QrCode = new Html5Qrcode("upload-qr"); + + try { + if (selectedFile.type === "application/pdf") { + const qrResult = await pdfToQrData(selectedFile); + scanResult.data = qrResult; + } else { + const qrData = await html5QrCode.scanFile(selectedFile); + scanResult.data = qrData; + } + } catch (e) { + // Example Error Handling + if (e?.name === "InvalidPDFException") { + scanResult.error = "Invalid PDF"; + } else if (e instanceof Event) { + scanResult.error = "Invalid Image"; + } else { + scanResult.error = "Unknown error:" + e; } - return scanResult; -} + } + return scanResult; +}; export const decodeQrData = (qrData) => { - if (!(!!qrData)) return; - let encodedData = qrData - if (!!HEADER_DELIMITER) { - const splitQrData = qrData.split(HEADER_DELIMITER); - const header = splitQrData[0]; - if (SUPPORTED_QR_HEADERS.indexOf(header) === -1) return; // throw some error and handle it - if (splitQrData.length !== 2) return; // throw some error and handle it - encodedData = splitQrData[1]; - } - return decode(encodedData); -} + if (!!!qrData) return; + let encodedData = qrData; + if (!!HEADER_DELIMITER) { + const splitQrData = qrData.split(HEADER_DELIMITER); + const header = splitQrData[0]; + if (SUPPORTED_QR_HEADERS.indexOf(header) === -1) return; // throw some error and handle it + if (splitQrData.length !== 2) return; // throw some error and handle it + encodedData = splitQrData[1]; + } + return decode(encodedData); +}; export const encodeData = (data) => generateQRData(data); + +let html5QrCode; + +export const initiateQrScanning = (timer, onSuccess) => { + const config = { + fps: 10, + disableFlip: false, + aspectRatio: 1.0, + }; + if (!html5QrCode?.getState()) { + html5QrCode = new Html5Qrcode("reader"); + const qrCodeSuccessCallback = (decodedText) => { + onSuccess(decodedText); + html5QrCode.stop(); + html5QrCode = null; + }; + + html5QrCode + .start({ facingMode: "environment" }, config, qrCodeSuccessCallback) + .catch((e) => { + console.error("Error occurred:", e.message); + clearTimeout(timer); + html5QrCode.stop(); + html5QrCode = null; + }); + } +}; + +export const terminateScanning = () => { + html5QrCode.stop(); + html5QrCode = null; +};