expose useReadMRZ hook for DocumentCameraScreen (#1188)

This commit is contained in:
Leszek Stachowski
2025-10-03 00:26:40 +02:00
committed by GitHub
parent c1d30d153a
commit f2cceb3150
7 changed files with 389 additions and 107 deletions

View File

@@ -189,6 +189,18 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
logNFCEvent(level, event, context, details);
});
addListener(SdkEvents.DOCUMENT_MRZ_READ_SUCCESS, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('DocumentNFCScan');
}
});
addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('DocumentCameraTrouble');
}
});
return map;
}, []);

View File

@@ -3,21 +3,23 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import LottieView from 'lottie-react-native';
import React, { useCallback, useRef } from 'react';
import { Platform, StyleSheet } from 'react-native';
import React, { useRef } from 'react';
import { StyleSheet } from 'react-native';
import { View, XStack, YStack } from 'tamagui';
import { useIsFocused, useNavigation } from '@react-navigation/native';
import { useIsFocused } from '@react-navigation/native';
import {
formatDateToYYMMDD,
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import {
mrzReadInstructions,
useReadMRZ,
} from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
import passportScanAnimation from '@/assets/animations/passport_scan.json';
import { SecondaryButton } from '@/components/buttons/SecondaryButton';
import type { PassportCameraProps } from '@/components/native/PassportCamera';
import { PassportCamera } from '@/components/native/PassportCamera';
import Additional from '@/components/typography/Additional';
import Description from '@/components/typography/Description';
@@ -25,99 +27,17 @@ import { Title } from '@/components/typography/Title';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import Scan from '@/images/icons/passport_camera_scan.svg';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import analytics from '@/utils/analytics';
import { black, slate400, slate800, white } from '@/utils/colors';
import { dinot } from '@/utils/fonts';
import { checkScannedInfo } from '@/utils/utils';
const { trackEvent } = analytics();
const DocumentCameraScreen: React.FC = () => {
const client = useSelfClient();
const { useMRZStore } = client;
const { setMRZForNFC } = useMRZStore();
const navigation = useNavigation();
const isFocused = useIsFocused();
// Add a ref to track when the camera screen is mounted
const scanStartTimeRef = useRef(Date.now());
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
const onPassportRead = useCallback<PassportCameraProps['onPassportRead']>(
(error, result) => {
// Calculate scan duration in seconds with exactly 2 decimal places
const scanDurationSeconds = (
(Date.now() - scanStartTimeRef.current) /
1000
).toFixed(2);
if (error) {
console.error(error);
trackEvent(PassportEvents.CAMERA_SCAN_FAILED, {
reason: 'unknown_error',
error: error.message || 'Unknown error',
duration_seconds: parseFloat(scanDurationSeconds),
});
//TODO: Add error handling here
return;
}
if (!result) {
console.error('No result from passport scan');
trackEvent(PassportEvents.CAMERA_SCAN_FAILED, {
reason: 'invalid_input',
error: 'No result from scan',
duration_seconds: parseFloat(scanDurationSeconds),
});
return;
}
const {
documentNumber,
dateOfBirth,
dateOfExpiry,
documentType,
issuingCountry,
} = result;
const formattedDateOfBirth =
Platform.OS === 'ios' ? formatDateToYYMMDD(dateOfBirth) : dateOfBirth;
const formattedDateOfExpiry =
Platform.OS === 'ios' ? formatDateToYYMMDD(dateOfExpiry) : dateOfExpiry;
if (
!checkScannedInfo(
documentNumber,
formattedDateOfBirth,
formattedDateOfExpiry,
)
) {
trackEvent(PassportEvents.CAMERA_SCAN_FAILED, {
reason: 'invalid_format',
passportNumberLength: documentNumber.length,
dateOfBirthLength: formattedDateOfBirth.length,
dateOfExpiryLength: formattedDateOfExpiry.length,
duration_seconds: parseFloat(scanDurationSeconds),
});
navigation.navigate('DocumentCameraTrouble');
return;
}
setMRZForNFC({
passportNumber: documentNumber,
dateOfBirth: formattedDateOfBirth,
dateOfExpiry: formattedDateOfExpiry,
documentType: documentType?.trim() || '',
countryCode: issuingCountry?.trim().toUpperCase() || '',
});
trackEvent(PassportEvents.CAMERA_SCAN_SUCCESS, {
duration_seconds: parseFloat(scanDurationSeconds),
});
navigation.navigate('DocumentNFCScan');
},
[setMRZForNFC, navigation],
);
const navigateToLaunch = useHapticNavigation('Launch', {
action: 'cancel',
});
@@ -160,8 +80,7 @@ const DocumentCameraScreen: React.FC = () => {
Open to the photograph page
</Description>
<Additional style={styles.description}>
Lay your document flat and position the machine readable text
in the viewfinder
{mrzReadInstructions()}
</Additional>
</View>
</XStack>

View File

@@ -2,23 +2,6 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export function checkScannedInfo(
passportNumber: string,
dateOfBirth: string,
dateOfExpiry: string,
): boolean {
if (passportNumber.length > 9) {
return false;
}
if (dateOfBirth.length !== 6) {
return false;
}
if (dateOfExpiry.length !== 6) {
return false;
}
return true;
}
// Redacts 9+ consecutive digits and MRZ-like blocks to reduce PII exposure
export const sanitizeErrorMessage = (msg: string): string => {
try {