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: id verification status handling, QR code validation, and error handling #8387

Merged
merged 28 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9131a70
feat: introduce optional validator function prop
tahmidrahman-dsi Jan 23, 2025
9db7ef0
fix: destroy scanner instance on unmount
tahmidrahman-dsi Jan 23, 2025
01cabc3
fix: wrap onScan, onError by useCallback
tahmidrahman-dsi Jan 23, 2025
b42b4e6
feat: throttle onScanError down to once in every 5 seconds
tahmidrahman-dsi Jan 23, 2025
cfb3857
docs: add story of the id reader component implementd with a validato…
tahmidrahman-dsi Jan 24, 2025
943505f
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Jan 27, 2025
a630d3a
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Jan 28, 2025
9b11318
feat: set validation and show error message configured from countryco…
tahmidrahman-dsi Jan 28, 2025
7515bf8
fix: imports and error construction
tahmidrahman-dsi Jan 28, 2025
3deff52
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Jan 28, 2025
301421e
fix: ignore ts error for now
tahmidrahman-dsi Jan 28, 2025
da332f7
chore: upgrade toolkit
tahmidrahman-dsi Jan 28, 2025
36013ae
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Jan 30, 2025
61c4a42
chore: bump up toolkit version
tahmidrahman-dsi Jan 30, 2025
7dae0e4
fix: log error to browser console
tahmidrahman-dsi Jan 30, 2025
5f11dd9
fix: date field reset issu
tahmidrahman-dsi Jan 30, 2025
26963d4
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Jan 31, 2025
cce1a94
fix: log full error
tahmidrahman-dsi Jan 31, 2025
4247c75
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Feb 3, 2025
8d9b1d2
chore: update id reader component UI according to the latest design u…
tahmidrahman-dsi Feb 3, 2025
2772f6f
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Feb 3, 2025
81442bc
feat: add authenticated status
tahmidrahman-dsi Feb 3, 2025
c651024
refactor: files and declutter id verification banner file
tahmidrahman-dsi Feb 4, 2025
6c9070c
fix: typo
tahmidrahman-dsi Feb 4, 2025
c315ee3
fix: add small margin to the pill icons
tahmidrahman-dsi Feb 4, 2025
b7c2e74
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Feb 4, 2025
aba9e6a
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Feb 4, 2025
6195729
Merge branch 'develop' into feat/qr-data-error-handling
tahmidrahman-dsi Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/client/src/components/form/ReaderGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
tutorialMessages
} from '@client/i18n/messages/views/qr-reader'
import { useIntl } from 'react-intl'
import { JSONSchema, validate } from '@opencrvs/commons/client'

interface ReaderGeneratorProps {
readers: ReaderType[]
Expand All @@ -53,6 +54,13 @@ export const ReaderGenerator = ({
{readers.map((reader) => {
const { type } = reader
if (isReaderQR(reader)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validator = (data: any) => {
const result = validate(reader.validation.rule as JSONSchema, data)
if (!result) {
return intl.formatMessage(reader.validation.errorMessage)
}
}
return (
<QRReader
key={type}
Expand All @@ -71,12 +79,10 @@ export const ReaderGenerator = ({
)
}
}}
validator={validator}
onScan={(data) => {
setFieldValue(field.name, data)
}}
// Error handling to be handled in OCRVS-8330
// eslint-disable-next-line no-console
onError={(error) => console.error(error)}
/>
)
} else if (isReaderLinkButton(reader)) {
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,10 @@ export interface ILinkButtonFormField extends IFormFieldBase {

export interface QRReaderType {
type: 'QR'
validation: {
rule: unknown
errorMessage: MessageDescriptor
}
}

export type ReaderType = QRReaderType | ILinkButtonFormField
Expand Down
2 changes: 1 addition & 1 deletion packages/components/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const config: StorybookConfig = {
'@storybook/addon-a11y',
'@storybook/addon-docs'
],
staticDirs: ['../public'],
staticDirs: ['../public', '../static'],
framework: {
name: '@storybook/react-vite',
options: {}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/src/DateField/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const DateField = ({
const { dd, mm, yyyy } = date

useEffect(() => {
if (initialValue && typeof initialValue === 'string') {
if (typeof initialValue === 'string') {
const dateSegmentVals = initialValue?.split('-') || []
setDate({
yyyy: dateSegmentVals[0] || '',
Expand Down
87 changes: 83 additions & 4 deletions packages/components/src/IDReader/IDReader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import type { Meta, StoryObj } from '@storybook/react'
import { IDReader } from '.'
import React from 'react'
import { QRReader } from './readers/QRReader/QRReader'
import { Stack } from '../Stack'
import { Text } from '../Text'
import { ScannableQRReader } from './types'

const meta: Meta<typeof IDReader> = {
title: 'Controls/IDReader',
Expand All @@ -22,12 +25,13 @@ export default meta

type Story = StoryObj<typeof IDReader>

const IDReaderWithAlertFeedback = (args: any) => (
const IDReaderComponent = (qrReaderProps: Partial<ScannableQRReader>) => (
<IDReader
dividerLabel="Or"
manualInputInstructionLabel="Complete fields below"
>
<QRReader
{...qrReaderProps}
labels={{
button: 'Scan ID QR code',
scannerDialogSupportingCopy:
Expand All @@ -41,11 +45,86 @@ const IDReaderWithAlertFeedback = (args: any) => (
}
}}
onScan={(data) => alert(JSON.stringify(data))}
onError={(error) => console.error(error)}
onError={(type, error) =>
// eslint-disable-next-line no-console
console.error(`Error: ${type} - ${error.message}`)
}
/>
</IDReader>
)

export const IDReaderWithAlertFeedbackStory: Story = {
render: (args) => <IDReaderWithAlertFeedback {...args} />
export const IDReaderWithAlertFeedback: Story = {
render: () => <IDReaderComponent />
}

export const IDReaderWithValidator: Story = {
render: () => (
<Stack direction="column" gap={4} alignItems="stretch">
<IDReaderComponent
validator={(data: unknown) => {
if (
typeof data === 'object' &&
data !== null &&
'firstName' in data &&
'lastName' in data &&
!!data['firstName'] &&
!!data['lastName']
) {
return undefined
} else return 'Invalid QR code'
}}
/>
<Text element="h5" variant="h4">
This reader is provided with a validator function having such rule that
it will show alert only if the QR code has the below structure:
</Text>
<code
style={{
whiteSpace: 'pre-wrap',
padding: '16px',
background: '#f5f5f5'
}}
>
{JSON.stringify({ firstName: 'John', lastName: 'Doe' }, null, 2)}
</code>
<Text element="h5" variant="h4">
Otherwise it will show an error message in the scanner dialog
</Text>
<Text element="h2" variant="h2">
Example
</Text>
<Text element="p" variant="reg16">
Scan the below QR code with the scanner above and you should see an
alert as the data of the QR code is valid
</Text>
<Stack>
<img src="./static/valid_qr.png" alt="valid_qr" />
<code
style={{
whiteSpace: 'pre-wrap',
padding: '16px',
background: '#f5f5f5'
}}
>
{JSON.stringify({ firstName: 'John', lastName: 'Doe' }, null, 2)}
</code>
</Stack>
<Text element="p" variant="reg16">
You will see an error message in the scanner dialog if you scan the
below QR code as the data of the QR code is invalid
</Text>
<Stack>
<img src="./static/invalid_qr.png" alt="invalid_qr" />
<code
style={{
whiteSpace: 'pre-wrap',
padding: '16px',
background: '#f5f5f5'
}}
>
{JSON.stringify({ firstName: 'John', lastName: '' }, null, 2)}
</code>
</Stack>
</Stack>
)
}
43 changes: 33 additions & 10 deletions packages/components/src/IDReader/readers/QRReader/QRReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import React, { useState } from 'react'
import React, { useCallback, useState } from 'react'
import { Button } from '../../../Button'
import { Icon } from '../../../Icon'
import styled from 'styled-components'
Expand All @@ -18,7 +18,7 @@ import Scanner from './Scanner'
import { Box } from '../../../Box'
import { Stack } from '../../../Stack'
import { Text } from '../../../Text'
import { ScannableQRReader } from '../../../IDReader/types'
import { ErrorHandler, ScannableQRReader } from '../../../IDReader/types'
import { useWindowSize } from '../../../hooks'
import { getTheme } from '../../../theme'

Expand All @@ -32,17 +32,31 @@ const Info = styled(Stack)`
`

export const QRReader = (props: ScannableQRReader) => {
const { labels } = props
const { labels, validator, onScan, onError } = props
const [isScannerDialogOpen, setScannerDialogOpen] = useState(false)
const windowSize = useWindowSize()
const theme = getTheme()
const isSmallDevice = windowSize.width <= theme.grid.breakpoints.md
const handleScanSuccess = (
data: Parameters<ScannableQRReader['onScan']>[0]
) => {
props.onScan(data)
setScannerDialogOpen(false)
}
const [error, setError] = useState('')
const handleScanSuccess = useCallback(
(data: Parameters<ScannableQRReader['onScan']>[0]) => {
onScan(data)
setError('')
setScannerDialogOpen(false)
},
[onScan]
)
const handleScanError: ErrorHandler = useCallback(
(type, error) => {
if (type === 'invalid' || type === 'parse') {
setError(error.message)
}
if (onError) {
onError(type, error)
}
},
[onError]
)
return (
<>
<Button
Expand All @@ -65,8 +79,17 @@ export const QRReader = (props: ScannableQRReader) => {
{labels.scannerDialogSupportingCopy}
</Text>
<ScannerBox>
<Scanner onScan={handleScanSuccess} onError={props.onError} />
<Scanner
onScan={handleScanSuccess}
validator={validator}
onError={handleScanError}
/>
</ScannerBox>
{error && (
<Text element="p" variant="bold16" color="redDark">
{error}
</Text>
)}
<Info
gap={16}
justifyContent="space-between"
Expand Down
75 changes: 50 additions & 25 deletions packages/components/src/IDReader/readers/QRReader/Scanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
*
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
*/
import React, { useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import QRScanner from 'qr-scanner'
import styled from 'styled-components'
import { ErrorHandler, Validator } from '../../../IDReader/types'
import { throttle } from 'lodash'

interface ScannerProps {
onError: (error: 'mount' | 'parse') => void
onError: ErrorHandler
onScan: (data: Record<string, unknown>) => void
validator?: Validator
}

const QRReader = styled.div`
Expand All @@ -37,34 +40,55 @@ const Scanner = (props: ScannerProps) => {
const scanner = useRef<QRScanner>()
const videoElement = useRef<HTMLVideoElement>(null)
const [qrOn, setQrOn] = useState(true)
const { onError, onScan } = props
const { onError, onScan, validator } = props
const onScanSuccess = useCallback(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tahmidrahman-dsi do you think we need similar validator functionality in the transformHttpFieldIntoRequest function for the ESignet http field?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can. We can provide a validator function to the http field which will be executed in transformHttpFieldIntoRequest that can validate the response data. However we might need a design for that error case

Copy link
Collaborator Author

@tahmidrahman-dsi tahmidrahman-dsi Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if they have validated themsleves with MOSIP and we are now retrieving their data.
Then we can assume the id is valid.

I guess, we might not need validation for e-signet, then

(result: QRScanner.ScanResult) => {
if (result.data) {
try {
const data = JSON.parse(result.data)
const validationError = validator && validator(data)
if (validationError) {
onError('invalid', new Error(validationError))
return
}
onScan(data)
} catch (error) {
// log detailed error message to console for debugging
// eslint-disable-next-line no-console
console.error(error)
onError('parse', new Error('Invalid JSON format'))
}
}
},
[onScan, onError, validator]
)
const onScanError = useCallback(
(error: Error) => {
onError('parse', error)
},
[onError]
)

useEffect(() => {
const currentVideoElement = videoElement?.current
if (currentVideoElement && !scanner.current) {
scanner.current = new QRScanner(
currentVideoElement,
(result) => {
if (result.data) {
// TODO: handle parse error
onScan(JSON.parse(result.data))
}
},
{
// TODO: improve error handling
onDecodeError: () => onError('parse'),
preferredCamera: 'environment',
highlightCodeOutline: true,
highlightScanRegion: false
}
)
// implementation does not support the deprecated constructor overloads
// but supports the current signature. TS is throwing error. Need to have a closer
// look why TS is not able to detect the correct signature
naftis marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore
scanner.current = new QRScanner(currentVideoElement, onScanSuccess, {
onDecodeError: throttle(onScanError, 5000),
preferredCamera: 'environment',
highlightCodeOutline: true,
highlightScanRegion: false
})

scanner?.current
?.start()
.then(() => setQrOn(true))
.catch((err) => {
if (err) {
onError('mount')
.catch((error) => {
if (error) {
setQrOn(false)
}
})
}
Expand All @@ -76,16 +100,17 @@ const Scanner = (props: ScannerProps) => {
} else {
scanner?.current?.stop()
}
scanner.current?.destroy()
}
}, [onScan, onError])
}, [onScan, onError, validator, onScanSuccess, onScanError])

useEffect(() => {
if (!qrOn) alert('Could not scan')
if (!qrOn) alert('Please allow camera access to scan QR code')
}, [qrOn])

return (
<QRReader>
<Video ref={videoElement}></Video>
<Video ref={videoElement} />
</QRReader>
)
}
Expand Down
Loading
Loading