import { View, Platform } from 'react-native'; import React, { memo, useCallback, useEffect, useRef } from 'react'; import { Camera, useCameraFormat } from 'react-native-vision-camera'; import RNPhotoManipulator, { MimeType } from 'react-native-photo-manipulator'; import Logger from '../../utils/logger'; import { LivenessUserAction, type LivenessScanHandle, LivenessErrorCode, type InitialiseLivenessScanProps, LivenessError, } from '../../types/sdkTypes'; import { type GetIdSdk } from '../../index'; import * as GetidSdkTypes from '../../types/sdkTypes'; import { useCameraSetup } from '../../hooks'; import CameraStyles from '../../styles/CameraStyles'; import { FaceOverlay } from '../Overlay/FaceOverlay'; import { rectangleHeight, rectangleWidth, windowHeight, windowWidth, } from '../Overlay/faceConstants'; import { useLiveness, type LivenessCommand } from './useLiveness'; import { GetIDProvider } from '../../contexts/getid-context'; type LivenessScanProps = { sdk: GetIdSdk; } & InitialiseLivenessScanProps; const LivenessScan = React.forwardRef((props: LivenessScanProps, ref) => { useEffect(() => { Logger.log('LivenessScan component mounted'); }, []); const [captureStarted, setCaptureStarted] = React.useState(false); const [overlayStatus, setOverlayStatus] = React.useState(''); const [cameraStarted, setCameraStarted] = React.useState(false); const [appHeight, setAppHeight] = React.useState(windowHeight); const [isReady, setIsReady] = React.useState(false); const [isInitCalled, setIsInitCalled] = React.useState(false); const [postponedInit, setPostponedInit] = React.useState(false); const { cameraRef, hasPermission, device } = useCameraSetup({ type: 'front', }); const capturePromise = useRef({ resolve: (_data: any) => {}, reject: (_data: any) => {}, }); const takePhoto = useCallback(async () => { if (!cameraRef.current) { return null; } const photo = await cameraRef.current.takeSnapshot({ quality: 50, }); const imagePath = `file://${photo.path}`; try { const MAX_WIDTH = 140; const aspectRatio = photo.width / photo.height; const MAX_HEIGHT = MAX_WIDTH / aspectRatio; const targetSize = { width: Math.min(photo.width, MAX_WIDTH), height: Math.min(photo.height, MAX_HEIGHT), }; const cropRegion = { x: 0, y: 0, width: photo.width, height: photo.height, }; const resized = await RNPhotoManipulator.crop( imagePath, cropRegion, targetSize, MimeType.JPEG ); try { const result = await fetch(resized); const data = await result.blob(); return data; } catch (e) { console.error('Error:', e); } } catch (e) { console.error('Error:', e); } return null; }, [cameraRef]); const mapToCode = (value?: string): T => { if (!value) return value as T; return value.replace(/([a-z0–9])([A-Z])/g, '$1-$2').toLowerCase() as T; }; const onUserHintChangeLocal = useCallback( (data: LivenessCommand) => { switch (data.messageType) { case 'taskComplete': setOverlayStatus('success'); props.onUserHintChange?.({ type: 'action', value: LivenessUserAction.ACTION_COMPLETED, }); break; case 'task': setOverlayStatus('neutral'); const actionCode = mapToCode(data.task); props.onUserHintChange?.({ type: 'action', value: actionCode, }); break; case 'warning': setOverlayStatus('neutral'); const warningCode = mapToCode(data.warningType); capturePromise.current.reject( new LivenessError(data.messageType, warningCode) ); break; case 'failure': setOverlayStatus('neutral'); const failureCode = mapToCode(data.errorType); capturePromise.current.reject( new LivenessError(data.messageType, failureCode) ); break; case 'success': props.sdk.tracking.trySendEvent('liveness', 'completed'); setOverlayStatus('success'); props.sdk.addLivenessResult(data); capturePromise.current.resolve(undefined); break; } }, [props, capturePromise] ); const onError = useCallback( (_: any) => { capturePromise.current.reject( new LivenessError( 'Internal error occurred', GetidSdkTypes.LivenessErrorCode.SERVER_UNAVAILABLE ) ); }, [capturePromise] ); const { init, stop } = useLiveness({ sdk: props.sdk, onUserHintChange: onUserHintChangeLocal, onServerValidationStarted: props.onServerValidationStarted, takePhoto: takePhoto, onError, }); useEffect(() => { setTimeout(() => { setCameraStarted(true); }, 50); }, []); useEffect(() => { if (isReady && captureStarted && !isInitCalled && postponedInit) { setPostponedInit(false); try { setIsInitCalled(true); init(); } catch (e) { capturePromise.current.reject(e); } } }, [isReady, isInitCalled, captureStarted, init, postponedInit]); const wDiff = (windowWidth - rectangleWidth) / 2; const hDiff = (appHeight - rectangleHeight) / 2; const rect = { centerW: windowWidth / 2, centerH: appHeight / 2, width: rectangleWidth, height: rectangleHeight, top: hDiff, left: wDiff, right: windowWidth - wDiff, bottom: appHeight - hDiff, }; const stopCapture = () => { Logger.log('Stopping Capture process'); try { setCaptureStarted(false); setIsInitCalled(false); stop(); } catch (e) { Logger.error(`Error: ${e}`, props.sdk.tracking.trySendEvent); } }; const capture = () => { return new Promise((resolve, reject) => { Logger.log('Starting Capture process'); capturePromise.current = { resolve: (data) => { stopCapture(); return resolve(data); }, reject: (error) => { stopCapture(); return reject(error); }, }; if (!device) { Logger.log('Camera device not found'); return capturePromise.current.reject( new LivenessError( 'Camera device not found', GetidSdkTypes.GeneralErrorCode.NO_CAMERA_DEVICE ) ); } if (!hasPermission) { Logger.log('Camera permission not granted'); return capturePromise.current.reject( new LivenessError( 'Camera permission not granted', GetidSdkTypes.GeneralErrorCode.NO_CAMERA_PERMISSION ) ); } setCaptureStarted(true); try { if (isReady && !isInitCalled) { setIsInitCalled(true); init(); } else { setPostponedInit(true); } } catch (e) { reject(e); } }); }; React.useImperativeHandle( ref, () => ({ capture, stopCapture, }) as LivenessScanHandle ); const format = useCameraFormat(device, [ { photoResolution: { height: 1024, width: 768, }, photoHdr: false, videoResolution: { height: 1024, width: 768, }, videoHdr: false, videoStabilizationMode: 'off', fps: 30, }, ]); const setReadyState = () => { setTimeout(() => { setIsReady(true); setTimeout(() => { if (props.onCameraReady) { props.onCameraReady(); } }, 400); }, 50); }; return ( setAppHeight(e.nativeEvent.layout.height)} > {device && hasPermission && ( setReadyState()} ref={cameraRef} device={device} isActive={true} format={format} style={ cameraStarted ? CameraStyles.camera : CameraStyles.cameraDisabled } photo={true} photoHdr={false} photoQualityBalance="speed" video={Platform.OS === 'ios'} videoHdr={false} enableBufferCompression={true} videoStabilizationMode={'off'} exposure={0} /> )} {isReady && } ); }); export default memo(LivenessScan);