mirror of
https://github.com/selfxyz/self.git
synced 2026-01-08 22:28:11 -05:00
SELF-1572: Update document scan prompt (#1511)
* Update document scan prompt * formatting * fix scan instruction location * use helper for title text
This commit is contained in:
@@ -7,7 +7,11 @@ import { StyleSheet } from 'react-native';
|
|||||||
import { View, XStack, YStack } from 'tamagui';
|
import { View, XStack, YStack } from 'tamagui';
|
||||||
import { useIsFocused } from '@react-navigation/native';
|
import { useIsFocused } from '@react-navigation/native';
|
||||||
|
|
||||||
import { DelayedLottieView, dinot } from '@selfxyz/mobile-sdk-alpha';
|
import {
|
||||||
|
DelayedLottieView,
|
||||||
|
dinot,
|
||||||
|
useSelfClient,
|
||||||
|
} from '@selfxyz/mobile-sdk-alpha';
|
||||||
import {
|
import {
|
||||||
Additional,
|
Additional,
|
||||||
Description,
|
Description,
|
||||||
@@ -31,14 +35,21 @@ import Scan from '@/assets/icons/passport_camera_scan.svg';
|
|||||||
import { PassportCamera } from '@/components/native/PassportCamera';
|
import { PassportCamera } from '@/components/native/PassportCamera';
|
||||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||||
|
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||||
|
|
||||||
const DocumentCameraScreen: React.FC = () => {
|
const DocumentCameraScreen: React.FC = () => {
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
|
const selfClient = useSelfClient();
|
||||||
|
const selectedDocumentType = selfClient.useMRZStore(
|
||||||
|
state => state.documentType,
|
||||||
|
);
|
||||||
|
|
||||||
// Add a ref to track when the camera screen is mounted
|
// Add a ref to track when the camera screen is mounted
|
||||||
const scanStartTimeRef = useRef(Date.now());
|
const scanStartTimeRef = useRef(Date.now());
|
||||||
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
|
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
|
||||||
|
|
||||||
|
const scanPrompt = getDocumentScanPrompt(selectedDocumentType);
|
||||||
|
|
||||||
const navigateToHome = useHapticNavigation('Home', {
|
const navigateToHome = useHapticNavigation('Home', {
|
||||||
action: 'cancel',
|
action: 'cancel',
|
||||||
});
|
});
|
||||||
@@ -63,7 +74,7 @@ const DocumentCameraScreen: React.FC = () => {
|
|||||||
<ExpandableBottomLayout.BottomSection backgroundColor={white}>
|
<ExpandableBottomLayout.BottomSection backgroundColor={white}>
|
||||||
<YStack alignItems="center" gap="$2.5">
|
<YStack alignItems="center" gap="$2.5">
|
||||||
<YStack alignItems="center" gap="$6" paddingBottom="$2.5">
|
<YStack alignItems="center" gap="$6" paddingBottom="$2.5">
|
||||||
<Title>Scan your ID</Title>
|
<Title>{scanPrompt}</Title>
|
||||||
<XStack gap="$6" alignSelf="flex-start" alignItems="flex-start">
|
<XStack gap="$6" alignSelf="flex-start" alignItems="flex-start">
|
||||||
<View paddingTop="$2">
|
<View paddingTop="$2">
|
||||||
<Scan height={40} width={40} color={slate800} />
|
<Scan height={40} width={40} color={slate800} />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { StyleSheet } from 'react-native';
|
|||||||
import { SystemBars } from 'react-native-edge-to-edge';
|
import { SystemBars } from 'react-native-edge-to-edge';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
|
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||||
import {
|
import {
|
||||||
Additional,
|
Additional,
|
||||||
ButtonsContainer,
|
ButtonsContainer,
|
||||||
@@ -28,12 +29,19 @@ import passportOnboardingAnimation from '@/assets/animations/passport_onboarding
|
|||||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||||
import { impactLight } from '@/integrations/haptics';
|
import { impactLight } from '@/integrations/haptics';
|
||||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||||
|
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||||
|
|
||||||
const DocumentOnboardingScreen: React.FC = () => {
|
const DocumentOnboardingScreen: React.FC = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const selfClient = useSelfClient();
|
||||||
|
const selectedDocumentType = selfClient.useMRZStore(
|
||||||
|
state => state.documentType,
|
||||||
|
);
|
||||||
const handleCameraPress = useHapticNavigation('DocumentCamera');
|
const handleCameraPress = useHapticNavigation('DocumentCamera');
|
||||||
const animationRef = useRef<LottieView>(null);
|
const animationRef = useRef<LottieView>(null);
|
||||||
|
|
||||||
|
const scanPrompt = getDocumentScanPrompt(selectedDocumentType);
|
||||||
|
|
||||||
const onCancelPress = () => {
|
const onCancelPress = () => {
|
||||||
impactLight();
|
impactLight();
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
@@ -69,7 +77,7 @@ const DocumentOnboardingScreen: React.FC = () => {
|
|||||||
</ExpandableBottomLayout.TopSection>
|
</ExpandableBottomLayout.TopSection>
|
||||||
<ExpandableBottomLayout.BottomSection backgroundColor={white}>
|
<ExpandableBottomLayout.BottomSection backgroundColor={white}>
|
||||||
<TextsContainer>
|
<TextsContainer>
|
||||||
<Title>Scan your ID</Title>
|
<Title>{scanPrompt}</Title>
|
||||||
<Description textBreakStrategy="balanced">
|
<Description textBreakStrategy="balanced">
|
||||||
Open to the photo page
|
Open to the photo page
|
||||||
</Description>
|
</Description>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import {
|
|||||||
import type { PassportData } from '@selfxyz/common/types/passport';
|
import type { PassportData } from '@selfxyz/common/types/passport';
|
||||||
import { isAadhaarDocument, isMRZDocument } from '@selfxyz/common/utils/types';
|
import { isAadhaarDocument, isMRZDocument } from '@selfxyz/common/utils/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the scan prompt for a document type.
|
||||||
|
* @param documentType - Document type code ('p' = Passport, 'i' = ID card, 'a' = Aadhaar)
|
||||||
|
* @returns Scan prompt text
|
||||||
|
*/
|
||||||
export interface DocumentAttributes {
|
export interface DocumentAttributes {
|
||||||
nameSlice: string;
|
nameSlice: string;
|
||||||
dobSlice: string;
|
dobSlice: string;
|
||||||
@@ -22,6 +27,71 @@ export interface DocumentAttributes {
|
|||||||
isPassportType: boolean;
|
isPassportType: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a document expiration date (in YYMMDD format) has passed.
|
||||||
|
* we assume dateOfExpiry is this century because ICAO standard for biometric passport
|
||||||
|
* became standard around 2002
|
||||||
|
* @param dateOfExpiry - Expiration date in YYMMDD format from MRZ
|
||||||
|
* @returns true if the document is expired, false otherwise
|
||||||
|
*/
|
||||||
|
export function checkDocumentExpiration(dateOfExpiry: string): boolean {
|
||||||
|
if (!dateOfExpiry || dateOfExpiry.length !== 6) {
|
||||||
|
return false; // Invalid format, don't treat as expired
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = parseInt(dateOfExpiry.slice(0, 2), 10);
|
||||||
|
const fullyear = 2000 + year;
|
||||||
|
const month = parseInt(dateOfExpiry.slice(2, 4), 10) - 1; // JS months are 0-indexed
|
||||||
|
const day = parseInt(dateOfExpiry.slice(4, 6), 10);
|
||||||
|
|
||||||
|
const expiryDateUTC = new Date(Date.UTC(fullyear, month, day, 0, 0, 0, 0));
|
||||||
|
const nowUTC = new Date();
|
||||||
|
const todayUTC = new Date(
|
||||||
|
Date.UTC(
|
||||||
|
nowUTC.getFullYear(),
|
||||||
|
nowUTC.getMonth(),
|
||||||
|
nowUTC.getDate(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return todayUTC >= expiryDateUTC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats date from YYMMDD format to DD/MM/YYYY format
|
||||||
|
* For expiry (isDOB is false), we assume its this century because ICAO standard for biometric passport
|
||||||
|
* became standard around 2002
|
||||||
|
*/
|
||||||
|
export function formatDateFromYYMMDD(
|
||||||
|
dateString: string,
|
||||||
|
isDOB: boolean = false,
|
||||||
|
): string {
|
||||||
|
if (dateString.length !== 6) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yy = parseInt(dateString.substring(0, 2), 10);
|
||||||
|
const mm = dateString.substring(2, 4);
|
||||||
|
const dd = dateString.substring(4, 6);
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const century = Math.floor(currentYear / 100) * 100;
|
||||||
|
let year = century + yy;
|
||||||
|
|
||||||
|
if (isDOB) {
|
||||||
|
// For birth: if year is in the future, assume previous century
|
||||||
|
if (year > currentYear) {
|
||||||
|
year -= 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${dd}/${mm}/${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts attributes from Aadhaar document data
|
* Extracts attributes from Aadhaar document data
|
||||||
*/
|
*/
|
||||||
@@ -119,71 +189,6 @@ function getPassportAttributes(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a document expiration date (in YYMMDD format) has passed.
|
|
||||||
* we assume dateOfExpiry is this century because ICAO standard for biometric passport
|
|
||||||
* became standard around 2002
|
|
||||||
* @param dateOfExpiry - Expiration date in YYMMDD format from MRZ
|
|
||||||
* @returns true if the document is expired, false otherwise
|
|
||||||
*/
|
|
||||||
export function checkDocumentExpiration(dateOfExpiry: string): boolean {
|
|
||||||
if (!dateOfExpiry || dateOfExpiry.length !== 6) {
|
|
||||||
return false; // Invalid format, don't treat as expired
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = parseInt(dateOfExpiry.slice(0, 2), 10);
|
|
||||||
const fullyear = 2000 + year;
|
|
||||||
const month = parseInt(dateOfExpiry.slice(2, 4), 10) - 1; // JS months are 0-indexed
|
|
||||||
const day = parseInt(dateOfExpiry.slice(4, 6), 10);
|
|
||||||
|
|
||||||
const expiryDateUTC = new Date(Date.UTC(fullyear, month, day, 0, 0, 0, 0));
|
|
||||||
const nowUTC = new Date();
|
|
||||||
const todayUTC = new Date(
|
|
||||||
Date.UTC(
|
|
||||||
nowUTC.getFullYear(),
|
|
||||||
nowUTC.getMonth(),
|
|
||||||
nowUTC.getDate(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return todayUTC >= expiryDateUTC;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats date from YYMMDD format to DD/MM/YYYY format
|
|
||||||
* For expiry (isDOB is false), we assume its this century because ICAO standard for biometric passport
|
|
||||||
* became standard around 2002
|
|
||||||
*/
|
|
||||||
export function formatDateFromYYMMDD(
|
|
||||||
dateString: string,
|
|
||||||
isDOB: boolean = false,
|
|
||||||
): string {
|
|
||||||
if (dateString.length !== 6) {
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const yy = parseInt(dateString.substring(0, 2), 10);
|
|
||||||
const mm = dateString.substring(2, 4);
|
|
||||||
const dd = dateString.substring(4, 6);
|
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
|
||||||
const century = Math.floor(currentYear / 100) * 100;
|
|
||||||
let year = century + yy;
|
|
||||||
|
|
||||||
if (isDOB) {
|
|
||||||
// For birth: if year is in the future, assume previous century
|
|
||||||
if (year > currentYear) {
|
|
||||||
year -= 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${dd}/${mm}/${year}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions to safely extract document data
|
// Helper functions to safely extract document data
|
||||||
export function getDocumentAttributes(
|
export function getDocumentAttributes(
|
||||||
document: PassportData | AadhaarData,
|
document: PassportData | AadhaarData,
|
||||||
@@ -208,6 +213,31 @@ export function getDocumentAttributes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the display name for a document type code.
|
||||||
|
* @param documentType - Document type code ('p' = Passport, 'i' = ID card, 'a' = Aadhaar)
|
||||||
|
* @returns Human-readable document name
|
||||||
|
*/
|
||||||
|
export function getDocumentScanPrompt(
|
||||||
|
documentType: string | undefined,
|
||||||
|
): string {
|
||||||
|
const documentName = getDocumentTypeName(documentType);
|
||||||
|
return `Scan your ${documentName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocumentTypeName(documentType: string | undefined): string {
|
||||||
|
switch (documentType) {
|
||||||
|
case 'p':
|
||||||
|
return 'Passport';
|
||||||
|
case 'i':
|
||||||
|
return 'ID';
|
||||||
|
case 'a':
|
||||||
|
return 'Aadhaar';
|
||||||
|
default:
|
||||||
|
return 'ID';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses name from MRZ format (surname<<given names)
|
* Parses name from MRZ format (surname<<given names)
|
||||||
* Returns separated surname and names arrays
|
* Returns separated surname and names arrays
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
|
|||||||
const onSelectDocumentType = (docType: string) => {
|
const onSelectDocumentType = (docType: string) => {
|
||||||
buttonTap();
|
buttonTap();
|
||||||
|
|
||||||
|
selfClient.getMRZState().update({ documentType: docType });
|
||||||
|
|
||||||
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
|
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
|
||||||
documentType: docType,
|
documentType: docType,
|
||||||
documentName: getDocumentNameForEvent(docType),
|
documentName: getDocumentNameForEvent(docType),
|
||||||
|
|||||||
Reference in New Issue
Block a user