Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ocrvs 7955/qr code scanner poc #7962

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@types/xregexp": "^3.0.29",
"@vitejs/plugin-react": "^4.2.1",
"apollo3-cache-persist": "^0.14.1",
"barcode-detector": "^2.3.1",
"bcryptjs": "^2.4.3",
"bowser": "^2.11.0",
"browser-image-compression": "^1.0.6",
Expand All @@ -77,6 +78,7 @@
"patch-package": "^6.1.2",
"pdfmake": "^0.2.5",
"postinstall-postinstall": "^2.0.0",
"qr-scanner": "^1.4.2",
"query-string": "^6.1.0",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
20 changes: 19 additions & 1 deletion packages/client/src/components/form/FormFieldGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ import {
DependencyInfo,
Ii18nButtonFormField,
REDIRECT,
IDocumentUploaderWithOptionsFormField
IDocumentUploaderWithOptionsFormField,
QR_SCANNER
} from '@client/forms'
import { getValidationErrorsForForm, Errors } from '@client/forms/validation'
import { InputField } from '@client/components/form/InputField'
Expand Down Expand Up @@ -135,6 +136,7 @@ import { Heading2, Heading3 } from '@opencrvs/components/lib/Headings/Headings'
import { SignatureUploader } from './SignatureField/SignatureUploader'
import { ButtonField } from '@client/components/form/Button'
import { RedirectField } from '@client/components/form/Redirect'
import QRCodeScanner from './QRCodeScanner/QRCodeScanner'

const SignatureField = styled(Stack)`
margin-top: 8px;
Expand Down Expand Up @@ -263,6 +265,22 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
</InputField>
)
}

if (fieldDefinition.type === QR_SCANNER) {
return (
<InputField {...inputFieldProps} hideInputHeader>
<QRCodeScanner
label={fieldDefinition.label}
fallbackErrorMessage="Video capture not allowed by the browser"
onScanSuccess={(data) => {
setFieldValue(fieldDefinition.name, JSON.parse(data))
}}
variant_Experimental={fieldDefinition.variant_Experimental}
/>
</InputField>
)
}

if (fieldDefinition.type === DOCUMENT_UPLOADER_WITH_OPTION) {
return (
<InputField {...inputFieldProps}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import {
PrimaryButton,
SecondaryButton
} from '@client/../../components/lib/buttons'
import React, { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import Scanner1 from './variants/Scanner1'
import Scanner2 from './variants/Scanner2'

interface QRCodeScannerProps {
label: string
fallbackErrorMessage: string
onScanSuccess: (data: any) => void
variant_Experimental: number
}

function useScanner(
variant: number,
onScanSuccuss: QRCodeScannerProps['onScanSuccess'],
fallbackErrorMessage: QRCodeScannerProps['fallbackErrorMessage']
) {
const [isScanInitiated, setIsScanInitiated] = useState(false)
const [isScanPermitted, setIsScanPermitted] = useState(true)
const [isScanComplete, setIsScanComplete] = useState(false)

const handleScanSuccess = (data: any) => {
setIsScanComplete(true)
onScanSuccuss(data)
}

return {
isScanInitiated,
isScanPermitted,
isScanComplete,
initiateScan: () => setIsScanInitiated(true),
stopScan: () => setIsScanInitiated(false),
renderScanner: () =>
variant === 1 ? (
<Scanner1
onPermissionDenined={() => setIsScanPermitted(false)}
onScanSuccess={handleScanSuccess}
fallbackErrorMessage={fallbackErrorMessage}
/>
) : (
<Scanner2
onPermissionDenined={() => setIsScanPermitted(false)}
onScanSuccess={handleScanSuccess}
fallbackErrorMessage={fallbackErrorMessage}
/>
)
}
}

const QRCodeScanner = (props: QRCodeScannerProps) => {
const {
isScanInitiated,
isScanPermitted,
isScanComplete,
initiateScan,
stopScan,
renderScanner
} = useScanner(
props.variant_Experimental,
props.onScanSuccess,
props.fallbackErrorMessage
)
const handleClickButton =
isScanInitiated && !isScanComplete ? stopScan : initiateScan
return (
<div>
{isScanInitiated && isScanPermitted && !isScanComplete && renderScanner()}
<PrimaryButton id="start-button" onClick={handleClickButton}>
{isScanInitiated ? 'Close scan' : props.label}
</PrimaryButton>
</div>
)
}

export default QRCodeScanner
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import { BarcodeDetector } from 'barcode-detector'
import React, { useEffect, useRef, useState } from 'react'

interface ScannerProps {
onPermissionDenined: () => void
fallbackErrorMessage: string
onScanSuccess: (data: any) => void
}

const Scanner1 = (props: ScannerProps) => {
const videoRef = useRef<HTMLVideoElement>(null)
const [barcodeDetector, setBarcodeDetector] =
useState<BarcodeDetector | null>(null)
const { onPermissionDenined } = props
useEffect(() => {
const videoCurrent = videoRef.current
if (!('BarcodeDetector' in window)) {
console.error('Barcode Detector is not supported by this browser.')
return
}
const detector = new BarcodeDetector({ formats: ['qr_code'] })
setBarcodeDetector(detector)

const constraints = {
video: {
width: 518,
facingMode: 'environment'
}
}

navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
if (videoRef.current) {
videoRef.current.srcObject = stream
videoRef.current.play()
}
})
.catch((err) => {
onPermissionDenined()
console.error('Error accessing the camera: ', err)
})

return () => {
if (videoCurrent && videoCurrent.srcObject) {
const stream = videoCurrent.srcObject as MediaStream
stream.getTracks().forEach((track) => track.stop())
}
}
}, [onPermissionDenined])

useEffect(() => {
const detectQRCode = () => {
if (barcodeDetector && videoRef.current) {
barcodeDetector
.detect(videoRef.current)
.then((barcodes) => {
if (barcodes.length > 0) {
props.onScanSuccess(barcodes[0].rawValue)
// Stop capturing on successful scan
if (videoRef.current && videoRef.current.srcObject) {
const stream = videoRef.current.srcObject as MediaStream
stream.getTracks().forEach((track) => track.stop())
}
}
})
.catch((err) => {
console.error('Barcode detection failed: ', err)
})
}
}

const interval = setInterval(detectQRCode, 1000) // Check every second
return () => clearInterval(interval)
}, [barcodeDetector, props])

return (
<div>
<video ref={videoRef} id="video" muted>
{props.fallbackErrorMessage}
</video>
</div>
)
}

export default Scanner1
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* OpenCRVS is also distributed under the terms of the Civil Registration
* & Healthcare Disclaimer located at http://opencrvs.org/license.
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import React, { useEffect, useRef, useState } from 'react'
import QRScanner from 'qr-scanner'
import QRFrameSVG from './qr-frame.svg'
import styled from 'styled-components'

interface ScannerProps {
onPermissionDenined: () => void
fallbackErrorMessage: string
onScanSuccess: (data: any) => void
}

const QRReader = styled.div`
width: 518px;
height: 518px;
margin: 0 auto;
position: relative;

@media (max-width: 426px) {
width: 100%;
}
`
const Video = styled.video`
width: 518px;
height: 518px;
object-fit: cover;
`

const QRBox = styled.div`
width: 100% !important;
left: 0 !important;
`

const QRFrame = styled.img`
position: absolute;
fill: none;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
`
const withValidation = (callback: (data: any) => void) => {
return (data: any) => {
if (Boolean(data)) {
callback(data)
}
}
}

const Scanner2 = (props: ScannerProps) => {
const scanner = useRef<QRScanner>()
const videoElement = useRef<HTMLVideoElement>(null)
const qrBoxElement = useRef<HTMLDivElement>(null)
const [qrOn, setQrOn] = useState(true)
const { onPermissionDenined, onScanSuccess, fallbackErrorMessage } = props

const onScanFail = (error: string | Error) => {
console.log(error)
}

useEffect(() => {
const currentVideoElement = videoElement?.current
if (currentVideoElement && !scanner.current) {
scanner.current = new QRScanner(
currentVideoElement,
withValidation(onScanSuccess),
{
onDecodeError: onScanFail,
preferredCamera: 'environment',
highlightCodeOutline: true,
highlightScanRegion: true,
overlay: qrBoxElement?.current || undefined
}
)

scanner?.current
?.start()
.then(() => setQrOn(true))
.catch((err) => {
if (err) {
onPermissionDenined()
}
})
}

return () => {
if (currentVideoElement && currentVideoElement.srcObject) {
const stream = currentVideoElement.srcObject as MediaStream
stream.getTracks().forEach((track) => track.stop())
} else {
scanner?.current?.stop()
}
}
}, [onScanSuccess, onPermissionDenined])

useEffect(() => {
if (!qrOn) alert(fallbackErrorMessage)
}, [qrOn, fallbackErrorMessage])

return (
<QRReader>
<Video ref={videoElement}></Video>
<QRBox ref={qrBoxElement}>
<QRFrame src={QRFrameSVG} alt="Qr Frame" width={256} height={256} />
</QRBox>
</QRReader>
)
}

export default Scanner2
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading