From a6194665ec589a25227537172d647b892b99c65e Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 16 Dec 2025 14:43:45 -0800 Subject: [PATCH] SELF-1572: Update document scan prompt (#1511) * Update document scan prompt * formatting * fix scan instruction location * use helper for title text --- .../scanning/DocumentCameraScreen.tsx | 15 +- .../selection/DocumentOnboardingScreen.tsx | 10 +- app/src/utils/documentAttributes.ts | 160 +++++++++++------- .../flows/onboarding/id-selection-screen.tsx | 2 + 4 files changed, 119 insertions(+), 68 deletions(-) diff --git a/app/src/screens/documents/scanning/DocumentCameraScreen.tsx b/app/src/screens/documents/scanning/DocumentCameraScreen.tsx index 3a4c61ea9..11115061a 100644 --- a/app/src/screens/documents/scanning/DocumentCameraScreen.tsx +++ b/app/src/screens/documents/scanning/DocumentCameraScreen.tsx @@ -7,7 +7,11 @@ import { StyleSheet } from 'react-native'; import { View, XStack, YStack } from 'tamagui'; import { useIsFocused } from '@react-navigation/native'; -import { DelayedLottieView, dinot } from '@selfxyz/mobile-sdk-alpha'; +import { + DelayedLottieView, + dinot, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { Additional, Description, @@ -31,14 +35,21 @@ import Scan from '@/assets/icons/passport_camera_scan.svg'; import { PassportCamera } from '@/components/native/PassportCamera'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; +import { getDocumentScanPrompt } from '@/utils/documentAttributes'; const DocumentCameraScreen: React.FC = () => { const isFocused = useIsFocused(); + const selfClient = useSelfClient(); + const selectedDocumentType = selfClient.useMRZStore( + state => state.documentType, + ); // Add a ref to track when the camera screen is mounted const scanStartTimeRef = useRef(Date.now()); const { onPassportRead } = useReadMRZ(scanStartTimeRef); + const scanPrompt = getDocumentScanPrompt(selectedDocumentType); + const navigateToHome = useHapticNavigation('Home', { action: 'cancel', }); @@ -63,7 +74,7 @@ const DocumentCameraScreen: React.FC = () => { - Scan your ID + {scanPrompt} diff --git a/app/src/screens/documents/selection/DocumentOnboardingScreen.tsx b/app/src/screens/documents/selection/DocumentOnboardingScreen.tsx index 82b8a8588..e1cfbf208 100644 --- a/app/src/screens/documents/selection/DocumentOnboardingScreen.tsx +++ b/app/src/screens/documents/selection/DocumentOnboardingScreen.tsx @@ -8,6 +8,7 @@ import { StyleSheet } from 'react-native'; import { SystemBars } from 'react-native-edge-to-edge'; import { useNavigation } from '@react-navigation/native'; +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { Additional, ButtonsContainer, @@ -28,12 +29,19 @@ import passportOnboardingAnimation from '@/assets/animations/passport_onboarding import useHapticNavigation from '@/hooks/useHapticNavigation'; import { impactLight } from '@/integrations/haptics'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; +import { getDocumentScanPrompt } from '@/utils/documentAttributes'; const DocumentOnboardingScreen: React.FC = () => { const navigation = useNavigation(); + const selfClient = useSelfClient(); + const selectedDocumentType = selfClient.useMRZStore( + state => state.documentType, + ); const handleCameraPress = useHapticNavigation('DocumentCamera'); const animationRef = useRef(null); + const scanPrompt = getDocumentScanPrompt(selectedDocumentType); + const onCancelPress = () => { impactLight(); navigation.goBack(); @@ -69,7 +77,7 @@ const DocumentOnboardingScreen: React.FC = () => { - Scan your ID + {scanPrompt} Open to the photo page diff --git a/app/src/utils/documentAttributes.ts b/app/src/utils/documentAttributes.ts index ad73e9c4d..4b7f47a96 100644 --- a/app/src/utils/documentAttributes.ts +++ b/app/src/utils/documentAttributes.ts @@ -10,6 +10,11 @@ import { import type { PassportData } from '@selfxyz/common/types/passport'; 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 { nameSlice: string; dobSlice: string; @@ -22,6 +27,71 @@ export interface DocumentAttributes { 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 */ @@ -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 export function getDocumentAttributes( 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< = props => { const onSelectDocumentType = (docType: string) => { buttonTap(); + selfClient.getMRZState().update({ documentType: docType }); + selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, { documentType: docType, documentName: getDocumentNameForEvent(docType),