diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index f063d4a83..f94dabb5b 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -754,6 +754,7 @@ async function storeDocumentDirectlyToKeychain( }); } +// Duplicate funciton. prefer one on mobile sdk export async function storeDocumentWithDeduplication( passportData: PassportData | AadhaarData, ): Promise { @@ -801,7 +802,7 @@ export async function storeDocumentWithDeduplication( return contentHash; } - +// Duplicate function. prefer one in mobile sdk export async function storePassportData( passportData: PassportData | AadhaarData, ) { diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index f56a80537..d23b30e10 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -201,6 +201,18 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { } }); + addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS, () => { + if (navigationRef.isReady()) { + navigationRef.navigate('AadhaarUploadSuccess'); + } + }); + addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_FAILURE, ({ errorType }) => { + if (navigationRef.isReady()) { + // @ts-expect-error + navigationRef.navigate('AadhaarUploadError', { errorType }); + } + }); + return map; }, []); diff --git a/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx b/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx index 73f103459..c5ef05f47 100644 --- a/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx +++ b/app/src/screens/document/aadhaar/AadhaarUploadErrorScreen.tsx @@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { getErrorMessages } from '@selfxyz/mobile-sdk-alpha/onboarding/import-aadhaar'; import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; @@ -34,24 +35,7 @@ const AadhaarUploadErrorScreen: React.FC = () => { const { trackEvent } = useSelfClient(); const errorType = route.params?.errorType || 'general'; - // Define error messages based on error type - const getErrorMessages = () => { - if (errorType === 'expired') { - return { - title: 'QR Code Has Expired', - description: - 'You uploaded a valid Aadhaar QR code, but unfortunately it has expired. Please generate a new QR code from the mAadhaar app and try again.', - }; - } - - return { - title: 'There was a problem reading the code', - description: - 'Please ensure the QR code is clear and well-lit, then try again. For best results, take a screenshot of the QR code instead of photographing it.', - }; - }; - - const { title, description } = getErrorMessages(); + const { title, description } = getErrorMessages(errorType); return ( diff --git a/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx b/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx index 70f1d51f5..f5f1b66ef 100644 --- a/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx +++ b/app/src/screens/document/aadhaar/AadhaarUploadScreen.tsx @@ -8,13 +8,9 @@ import { Image, XStack, YStack } from 'tamagui'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { - extractQRDataFields, - getAadharRegistrationWindow, -} from '@selfxyz/common/utils'; -import type { AadhaarData } from '@selfxyz/common/utils/types'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { AadhaarEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useAadhaar } from '@selfxyz/mobile-sdk-alpha/onboarding/import-aadhaar'; import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import { BodyText } from '@/components/typography/BodyText'; @@ -22,7 +18,6 @@ import { useModal } from '@/hooks/useModal'; import AadhaarImage from '@/images/512w.png'; import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context'; import type { RootStackParamList } from '@/navigation'; -import { storePassportData } from '@/providers/passportDataProvider'; import { slate100, slate200, slate400, slate500, white } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; import { @@ -32,6 +27,7 @@ import { const AadhaarUploadScreen: React.FC = () => { const { bottom } = useSafeAreaInsets(); + const navigation = useNavigation>(); const { trackEvent } = useSelfClient(); @@ -65,110 +61,7 @@ const AadhaarUploadScreen: React.FC = () => { } }, [trackEvent]); - const validateAAdhaarTimestamp = useCallback( - async (timestamp: string) => { - //timestamp is in YYYY-MM-DD HH:MM format - trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_STARTED); - - const currentTimestamp = new Date().getTime(); - const timestampDate = new Date(timestamp); - const timestampTimestamp = timestampDate.getTime(); - const diff = currentTimestamp - timestampTimestamp; - const diffMinutes = diff / (1000 * 60); - - const allowedWindow = await getAadharRegistrationWindow(); - const isValid = diffMinutes <= allowedWindow; - - if (isValid) { - trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_SUCCESS); - } else { - trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_FAILED); - } - - return isValid; - }, - [trackEvent], - ); - - const processAadhaarQRCode = useCallback( - async (qrCodeData: string) => { - try { - if ( - !qrCodeData || - typeof qrCodeData !== 'string' || - qrCodeData.length < 100 - ) { - trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); - throw new Error('Invalid QR code format - too short or not a string'); - } - - if (!/^\d+$/.test(qrCodeData)) { - trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); - throw new Error('Invalid QR code format - not a numeric string'); - } - - if (qrCodeData.length < 100) { - trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); - throw new Error( - 'QR code too short - likely not a valid Aadhaar QR code', - ); - } - - trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_STARTED); - let extractedFields; - try { - extractedFields = extractQRDataFields(qrCodeData); - trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_SUCCESS); - } catch { - trackEvent(AadhaarEvents.QR_CODE_PARSE_FAILED); - throw new Error('Failed to parse Aadhaar QR code - invalid format'); - } - - if ( - !extractedFields.name || - !extractedFields.dob || - !extractedFields.gender - ) { - trackEvent(AadhaarEvents.QR_CODE_MISSING_FIELDS); - throw new Error('Invalid Aadhaar QR code - missing required fields'); - } - - if (!(await validateAAdhaarTimestamp(extractedFields.timestamp))) { - trackEvent(AadhaarEvents.QR_CODE_EXPIRED); - throw new Error('QRCODE_EXPIRED'); - } - - const aadhaarData: AadhaarData = { - documentType: 'aadhaar', - documentCategory: 'aadhaar', - mock: false, - qrData: qrCodeData, - extractedFields: extractedFields, - signature: [], - publicKey: '', - photoHash: '', - }; - - trackEvent(AadhaarEvents.DATA_STORAGE_STARTED); - await storePassportData(aadhaarData); - trackEvent(AadhaarEvents.DATA_STORAGE_SUCCESS); - - trackEvent(AadhaarEvents.QR_UPLOAD_SUCCESS); - - navigation.navigate('AadhaarUploadSuccess'); - } catch (error) { - // Check if it's a QR code expiration error - const errorType: 'expired' | 'general' = - error instanceof Error && error.message === 'QRCODE_EXPIRED' - ? 'expired' - : 'general'; - - trackEvent(AadhaarEvents.ERROR_SCREEN_NAVIGATED, { errorType }); - (navigation.navigate as any)('AadhaarUploadError', { errorType }); - } - }, - [navigation, trackEvent, validateAAdhaarTimestamp], - ); + const { processAadhaarQRCode } = useAadhaar(); const onPhotoLibraryPress = useCallback(async () => { if (isProcessing) { diff --git a/packages/mobile-sdk-alpha/src/flows/onboarding/import-aadhaar.ts b/packages/mobile-sdk-alpha/src/flows/onboarding/import-aadhaar.ts new file mode 100644 index 000000000..9291576e4 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/flows/onboarding/import-aadhaar.ts @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback } from 'react'; + +import { extractQRDataFields, getAadharRegistrationWindow } from '@selfxyz/common/utils'; +import type { AadhaarData } from '@selfxyz/common/utils/types'; + +import { AadhaarEvents } from '../../constants/analytics'; +import { useSelfClient } from '../../context'; +import { storePassportData } from '../../documents/utils'; +import { SdkEvents } from '../../types/events'; + +export const getErrorMessages = (errorType: 'general' | 'expired') => { + if (errorType === 'expired') { + return { + title: 'QR Code Has Expired', + description: + 'You uploaded a valid Aadhaar QR code, but unfortunately it has expired. Please generate a new QR code from the mAadhaar app and try again.', + }; + } + + return { + title: 'There was a problem reading the code', + description: + 'Please ensure the QR code is clear and well-lit, then try again. For best results, take a screenshot of the QR code instead of photographing it.', + }; +}; + +export function useAadhaar() { + const selfClient = useSelfClient(); + + const validateAAdhaarTimestamp = useCallback( + async (timestamp: string) => { + //timestamp is in YYYY-MM-DD HH:MM format + selfClient.trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_STARTED); + + const currentTimestamp = new Date().getTime(); + const timestampDate = new Date(timestamp); + const timestampTimestamp = timestampDate.getTime(); + const diff = currentTimestamp - timestampTimestamp; + const diffMinutes = diff / (1000 * 60); + + const allowedWindow = await getAadharRegistrationWindow(); + const isValid = diffMinutes <= allowedWindow; + + if (isValid) { + selfClient.trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_SUCCESS); + } else { + selfClient.trackEvent(AadhaarEvents.TIMESTAMP_VALIDATION_FAILED); + } + + return isValid; + }, + [selfClient.trackEvent], + ); + + const processAadhaarQRCode = useCallback( + async (qrCodeData: string) => { + try { + if (!qrCodeData || typeof qrCodeData !== 'string' || qrCodeData.length < 100) { + selfClient.trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error('Invalid QR code format - too short or not a string'); + } + + if (!/^\d+$/.test(qrCodeData)) { + selfClient.trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error('Invalid QR code format - not a numeric string'); + } + + if (qrCodeData.length < 100) { + selfClient.trackEvent(AadhaarEvents.QR_CODE_INVALID_FORMAT); + throw new Error('QR code too short - likely not a valid Aadhaar QR code'); + } + + selfClient.trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_STARTED); + let extractedFields; + try { + extractedFields = extractQRDataFields(qrCodeData); + selfClient.trackEvent(AadhaarEvents.QR_DATA_EXTRACTION_SUCCESS); + } catch { + selfClient.trackEvent(AadhaarEvents.QR_CODE_PARSE_FAILED); + throw new Error('Failed to parse Aadhaar QR code - invalid format'); + } + + if (!extractedFields.name || !extractedFields.dob || !extractedFields.gender) { + selfClient.trackEvent(AadhaarEvents.QR_CODE_MISSING_FIELDS); + throw new Error('Invalid Aadhaar QR code - missing required fields'); + } + + if (!(await validateAAdhaarTimestamp(extractedFields.timestamp))) { + selfClient.trackEvent(AadhaarEvents.QR_CODE_EXPIRED); + throw new Error('QRCODE_EXPIRED'); + } + + const aadhaarData: AadhaarData = { + documentType: 'aadhaar', + documentCategory: 'aadhaar', + mock: false, + qrData: qrCodeData, + extractedFields: extractedFields, + signature: [], + publicKey: '', + photoHash: '', + }; + + selfClient.trackEvent(AadhaarEvents.DATA_STORAGE_STARTED); + await storePassportData(selfClient, aadhaarData); + selfClient.trackEvent(AadhaarEvents.DATA_STORAGE_SUCCESS); + + selfClient.trackEvent(AadhaarEvents.QR_UPLOAD_SUCCESS); + selfClient.emit(SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS); + } catch (error) { + // Check if it's a QR code expiration error + const errorType: 'expired' | 'general' = + error instanceof Error && error.message === 'QRCODE_EXPIRED' ? 'expired' : 'general'; + + selfClient.trackEvent(AadhaarEvents.ERROR_SCREEN_NAVIGATED, { + errorType, + }); + selfClient.emit(SdkEvents.PROVING_AADHAAR_UPLOAD_FAILURE, { + errorType, + }); + } + }, + [selfClient, validateAAdhaarTimestamp], + ); + + return { processAadhaarQRCode }; +} diff --git a/packages/mobile-sdk-alpha/src/types/events.ts b/packages/mobile-sdk-alpha/src/types/events.ts index 1024c6c26..50480c339 100644 --- a/packages/mobile-sdk-alpha/src/types/events.ts +++ b/packages/mobile-sdk-alpha/src/types/events.ts @@ -23,6 +23,20 @@ export enum SdkEvents { */ PROGRESS = 'PROGRESS', + /** + * Emitted when Aadhaar QR code upload is successful. + * + * **Required:** Navigate to the AadhaarUploadSuccess screen to inform users of the successful upload. + */ + PROVING_AADHAAR_UPLOAD_SUCCESS = 'PROVING_AADHAAR_UPLOAD_SUCCESS', + + /** + * Emitted when Aadhaar QR code upload fails. + * + * **Required:** Navigate to the AadhaarUploadError screen to inform users of the failure and provide troubleshooting steps. + */ + PROVING_AADHAAR_UPLOAD_FAILURE = 'PROVING_AADHAAR_UPLOAD_FAILURE', + /** * Emitted when no passport data is found on the device during initialization. * @@ -72,7 +86,7 @@ export enum SdkEvents { /** * Emitted when a user selects a country in the document flow. * - * **Recommended:** Use this event to track user selection patterns and analytics. + * **Required:** Navigate the user to the screen where they will select the document type. * The event includes the selected country code and available document types. */ DOCUMENT_COUNTRY_SELECTED = 'DOCUMENT_COUNTRY_SELECTED', @@ -80,7 +94,7 @@ export enum SdkEvents { /** * Emitted when a user selects a document type for verification. * - * **Recommended:** Use this event to track document type preferences and analytics. + * **Required:** Navigate the user to the document type screen that was selected. * The event includes the selected document type, country code, and document name. */ DOCUMENT_TYPE_SELECTED = 'DOCUMENT_TYPE_SELECTED', @@ -153,6 +167,8 @@ export interface SDKEventMap { isMock: boolean; context: ProofContext; }; + [SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS]: undefined; + [SdkEvents.PROVING_AADHAAR_UPLOAD_FAILURE]: { errorType: 'expired' | 'general' }; [SdkEvents.PROGRESS]: Progress; [SdkEvents.ERROR]: Error;