move common aadhaar functions to msdk (#1200)

* move common aadhaar functions to msdk

* remove comment

* add license headers
This commit is contained in:
Aaron DeRuvo
2025-10-03 14:37:53 +02:00
committed by GitHub
parent 187fc185e2
commit a00cb7e58d
6 changed files with 168 additions and 131 deletions

View File

@@ -754,6 +754,7 @@ async function storeDocumentDirectlyToKeychain(
});
}
// Duplicate funciton. prefer one on mobile sdk
export async function storeDocumentWithDeduplication(
passportData: PassportData | AadhaarData,
): Promise<string> {
@@ -801,7 +802,7 @@ export async function storeDocumentWithDeduplication(
return contentHash;
}
// Duplicate function. prefer one in mobile sdk
export async function storePassportData(
passportData: PassportData | AadhaarData,
) {

View File

@@ -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;
}, []);

View File

@@ -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 (
<YStack flex={1} backgroundColor={slate100}>

View File

@@ -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<NativeStackNavigationProp<RootStackParamList>>();
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) {

View File

@@ -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 };
}

View File

@@ -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;