diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index 13aac37ce..32165dc55 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -45,7 +45,6 @@ import type { PropsWithChildren } from 'react'; import React, { createContext, useCallback, useContext, useMemo } from 'react'; import Keychain from 'react-native-keychain'; -import type { DocumentCategory, PassportData } from '@selfxyz/common/types'; import type { PublicKeyDetailsECDSA, PublicKeyDetailsRSA, @@ -54,6 +53,18 @@ import { brutforceSignatureAlgorithmDsc, parseCertificateSimple, } from '@selfxyz/common/utils'; +import type { + DocumentCatalog, + DocumentCategory, + DocumentMetadata, + PassportData, +} from '@selfxyz/common/utils/types'; +import { + DocumentsAdapter, + getAllDocuments, + SelfClient, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { unsafe_getPrivateKey, useAuth } from '@/providers/authProvider'; @@ -61,7 +72,7 @@ import { unsafe_getPrivateKey, useAuth } from '@/providers/authProvider'; // These need to be declared early to avoid dependency issues const safeLoadDocumentCatalog = async (): Promise => { try { - return await loadDocumentCatalog(); + return await loadDocumentCatalogDirectlyFromKeychain(); } catch (error) { console.warn( 'Error in safeLoadDocumentCatalog, returning empty catalog:', @@ -71,9 +82,9 @@ const safeLoadDocumentCatalog = async (): Promise => { } }; -const safeGetAllDocuments = async () => { +const safeGetAllDocuments = async (selfClient: SelfClient) => { try { - return await getAllDocuments(); + return await getAllDocuments(selfClient); } catch (error) { console.warn( 'Error in safeGetAllDocuments, returning empty object:', @@ -83,20 +94,6 @@ const safeGetAllDocuments = async () => { } }; -export interface DocumentMetadata { - id: string; // contentHash as ID for deduplication - documentType: string; // passport, mock_passport, id_card, etc. - documentCategory: DocumentCategory; // passport, id_card, aadhaar - data: string; // DG1/MRZ data for passports/IDs, relevant data for aadhaar - mock: boolean; // whether this is a mock document - isRegistered?: boolean; // whether the document is registered onChain -} - -export interface DocumentCatalog { - documents: DocumentMetadata[]; - selectedDocumentId?: string; // This is now a contentHash -} - type DocumentChangeCallback = (isMock: boolean) => void; const documentChangeCallbacks: DocumentChangeCallback[] = []; @@ -163,7 +160,7 @@ export const PassportContext = createContext({ clearPassportData: clearPassportData, clearSpecificData: clearSpecificPassportData, loadDocumentCatalog: safeLoadDocumentCatalog, - getAllDocuments: safeGetAllDocuments, + getAllDocuments: () => Promise.resolve({}), setSelectedDocument: setSelectedDocument, deleteDocument: deleteDocument, migrateFromLegacyStorage: migrateFromLegacyStorage, @@ -173,12 +170,12 @@ export const PassportContext = createContext({ markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered, updateDocumentRegistrationState: updateDocumentRegistrationState, checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration, - hasAnyValidRegisteredDocument: hasAnyValidRegisteredDocument, checkAndUpdateRegistrationStates: checkAndUpdateRegistrationStates, }); export const PassportProvider = ({ children }: PassportProviderProps) => { const { _getSecurely } = useAuth(); + const selfClient = useSelfClient(); const getData = useCallback( () => _getSecurely(loadPassportData, str => JSON.parse(str)), @@ -192,7 +189,10 @@ export const PassportProvider = ({ children }: PassportProviderProps) => { ); }, [_getSecurely]); - const getAllData = useCallback(() => loadAllPassportData(), []); + const getAllData = useCallback( + () => loadAllPassportData(selfClient), + [selfClient], + ); const getAvailableTypes = useCallback(() => getAvailableDocumentTypes(), []); @@ -224,7 +224,7 @@ export const PassportProvider = ({ children }: PassportProviderProps) => { clearPassportData: clearPassportData, clearSpecificData: clearSpecificPassportData, loadDocumentCatalog: safeLoadDocumentCatalog, - getAllDocuments: safeGetAllDocuments, + getAllDocuments: () => safeGetAllDocuments(selfClient), setSelectedDocument: setSelectedDocument, deleteDocument: deleteDocument, migrateFromLegacyStorage: migrateFromLegacyStorage, @@ -234,7 +234,6 @@ export const PassportProvider = ({ children }: PassportProviderProps) => { markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered, updateDocumentRegistrationState: updateDocumentRegistrationState, checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration, - hasAnyValidRegisteredDocument: hasAnyValidRegisteredDocument, checkAndUpdateRegistrationStates: checkAndUpdateRegistrationStates, }), [ @@ -263,7 +262,7 @@ export async function checkAndUpdateRegistrationStates(): Promise { export async function checkIfAnyDocumentsNeedMigration(): Promise { try { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); return catalog.documents.some(doc => doc.isRegistered === undefined); } catch (error) { console.warn('Error checking if documents need migration:', error); @@ -273,7 +272,7 @@ export async function checkIfAnyDocumentsNeedMigration(): Promise { export async function clearDocumentCatalogForMigrationTesting() { console.log('Clearing document catalog for migration testing...'); - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); // Delete all new-style documents for (const doc of catalog.documents) { @@ -301,7 +300,7 @@ export async function clearDocumentCatalogForMigrationTesting() { } export async function clearPassportData() { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); // Delete all documents for (const doc of catalog.documents) { @@ -317,7 +316,7 @@ export async function clearPassportData() { } export async function clearSpecificPassportData(documentType: string) { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); const docsToDelete = catalog.documents.filter( d => d.documentType === documentType, ); @@ -328,7 +327,7 @@ export async function clearSpecificPassportData(documentType: string) { } export async function deleteDocument(documentId: string): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); // Remove from catalog catalog.documents = catalog.documents.filter(d => d.id !== documentId); @@ -352,32 +351,14 @@ export async function deleteDocument(documentId: string): Promise { } } -export async function getAllDocuments(): Promise<{ - [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; -}> { - const catalog = await loadDocumentCatalog(); - const allDocs: { - [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; - } = {}; - - for (const metadata of catalog.documents) { - const data = await loadDocumentById(metadata.id); - if (data) { - allDocs[metadata.id] = { data, metadata }; - } - } - - return allDocs; -} - export async function getAvailableDocumentTypes(): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); return [...new Set(catalog.documents.map(d => d.documentType))]; } // Helper function to get current document type from catalog export async function getCurrentDocumentType(): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); if (!catalog.selectedDocumentId) return null; const metadata = catalog.documents.find( @@ -404,16 +385,6 @@ function getServiceNameForDocumentType(documentType: string): string { } } -export async function hasAnyValidRegisteredDocument(): Promise { - try { - const catalog = await loadDocumentCatalog(); - return catalog.documents.some(doc => doc.isRegistered === true); - } catch (error) { - console.error('Error loading document catalog:', error); - return false; - } -} - /** * Global initialization function to wait for native modules to be ready * Call this once at app startup before any native module operations @@ -460,10 +431,11 @@ export async function initializeNativeModules( return false; } -export async function loadAllPassportData(): Promise<{ +// TODO: is this used? +async function loadAllPassportData(selfClient: SelfClient): Promise<{ [service: string]: PassportData; }> { - const allDocs = await getAllDocuments(); + const allDocs = await getAllDocuments(selfClient); const result: { [service: string]: PassportData } = {}; // Convert to legacy format for backward compatibility @@ -475,7 +447,7 @@ export async function loadAllPassportData(): Promise<{ return result; } -export async function loadDocumentById( +export async function loadDocumentByIdDirectlyFromKeychain( documentId: string, ): Promise { try { @@ -499,7 +471,12 @@ export async function loadDocumentById( return null; } -export async function loadDocumentCatalog(): Promise { +export const selfClientDocumentsAdapter: DocumentsAdapter = { + loadDocumentCatalog: loadDocumentCatalogDirectlyFromKeychain, + loadDocumentById: loadDocumentByIdDirectlyFromKeychain, +}; + +export async function loadDocumentCatalogDirectlyFromKeychain(): Promise { try { // Extra safety check for module initialization if (typeof Keychain === 'undefined' || !Keychain) { @@ -524,6 +501,9 @@ export async function loadDocumentCatalog(): Promise { if (parsed === null) { throw new TypeError('Cannot parse null password'); } + + console.log('Successfully loaded document catalog from keychain'); + return parsed; } } catch (error) { @@ -592,7 +572,7 @@ export async function loadSelectedDocument(): Promise<{ data: PassportData; metadata: DocumentMetadata; } | null> { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); console.log('Catalog loaded'); if (!catalog.selectedDocumentId) { @@ -618,7 +598,9 @@ export async function loadSelectedDocument(): Promise<{ return null; } - const data = await loadDocumentById(catalog.selectedDocumentId); + const data = await loadDocumentByIdDirectlyFromKeychain( + catalog.selectedDocumentId, + ); if (!data) { console.log('Document data not found for id:', catalog.selectedDocumentId); return null; @@ -660,6 +642,7 @@ interface IPassportContext { signature: string; data: PassportData; } | null>; + // TODO: is this even used? getAllData: () => Promise<{ [service: string]: PassportData }>; getAvailableTypes: () => Promise; setData: (data: PassportData) => Promise; @@ -673,12 +656,15 @@ interface IPassportContext { } | null>; clearPassportData: () => Promise; clearSpecificData: (documentType: string) => Promise; + loadDocumentCatalog: () => Promise; getAllDocuments: () => Promise<{ [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; }>; + setSelectedDocument: (documentId: string) => Promise; deleteDocument: (documentId: string) => Promise; + migrateFromLegacyStorage: () => Promise; getCurrentDocumentType: () => Promise; clearDocumentCatalogForMigrationTesting: () => Promise; @@ -688,12 +674,11 @@ interface IPassportContext { isRegistered: boolean, ) => Promise; checkIfAnyDocumentsNeedMigration: () => Promise; - hasAnyValidRegisteredDocument: () => Promise; checkAndUpdateRegistrationStates: () => Promise; } export async function markCurrentDocumentAsRegistered(): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); if (catalog.selectedDocumentId) { await updateDocumentRegistrationState(catalog.selectedDocumentId, true); } else { @@ -703,7 +688,7 @@ export async function markCurrentDocumentAsRegistered(): Promise { export async function migrateFromLegacyStorage(): Promise { console.log('Migrating from legacy storage to new architecture...'); - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); // If catalog already has documents, skip migration if (catalog.documents.length > 0) { @@ -789,7 +774,7 @@ export async function saveDocumentCatalog( } export async function setDefaultDocumentTypeIfNeeded() { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); if (!catalog.selectedDocumentId && catalog.documents.length > 0) { await setSelectedDocument(catalog.documents[0].id); @@ -797,7 +782,7 @@ export async function setDefaultDocumentTypeIfNeeded() { } export async function setSelectedDocument(documentId: string): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); const metadata = catalog.documents.find(d => d.id === documentId); if (metadata) { @@ -812,7 +797,7 @@ export async function storeDocumentWithDeduplication( passportData: PassportData, ): Promise { const contentHash = calculateContentHash(passportData); - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); // Check for existing document with same content const existing = catalog.documents.find(d => d.id === contentHash); @@ -868,7 +853,7 @@ export async function updateDocumentRegistrationState( documentId: string, isRegistered: boolean, ): Promise { - const catalog = await loadDocumentCatalog(); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); const documentIndex = catalog.documents.findIndex(d => d.id === documentId); if (documentIndex !== -1) { @@ -885,3 +870,29 @@ export async function updateDocumentRegistrationState( export const usePassport = () => { return useContext(PassportContext); }; + +/** + * Get all documents directly from the keychain. + * + * It's here to avoid dependency on self client where it's not strictly necessary, + * for example when migrating legacy data. + * + * @returns A dictionary of document IDs to their data and metadata. + */ +export const getAllDocumentsDirectlyFromKeychain = async (): Promise<{ + [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; +}> => { + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); + const allDocs: { + [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; + } = {}; + + for (const metadata of catalog.documents) { + const data = await loadDocumentByIdDirectlyFromKeychain(metadata.id); + if (data) { + allDocs[metadata.id] = { data, metadata }; + } + } + + return allDocs; +}; diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index f1e5cd0dd..0686f45e0 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -12,6 +12,7 @@ import { } from '@selfxyz/mobile-sdk-alpha'; import { TrackEventParams } from '@selfxyz/mobile-sdk-alpha'; +import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider'; import analytics from '@/utils/analytics'; /** @@ -53,6 +54,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { }, }, }, + documents: selfClientDocumentsAdapter, crypto: { async hash( data: Uint8Array, diff --git a/app/src/screens/aesop/PassportOnboardingScreen.tsx b/app/src/screens/aesop/PassportOnboardingScreen.tsx index c33381090..ec693842b 100644 --- a/app/src/screens/aesop/PassportOnboardingScreen.tsx +++ b/app/src/screens/aesop/PassportOnboardingScreen.tsx @@ -7,6 +7,10 @@ import React, { useEffect, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; import { SystemBars } from 'react-native-edge-to-edge'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import passportOnboardingAnimation from '@/assets/animations/passport_onboarding.json'; @@ -21,9 +25,9 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import Scan from '@/images/icons/passport_camera_scan.svg'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { black, slate100, white } from '@/utils/colors'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; const PassportOnboardingScreen: React.FC = () => { + const client = useSelfClient(); const handleCameraPress = useHapticNavigation('PassportCamera'); const navigateToLaunch = useHapticNavigation('Launch', { action: 'cancel', @@ -32,7 +36,7 @@ const PassportOnboardingScreen: React.FC = () => { action: 'cancel', }); const onCancelPress = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(client); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/misc/SplashScreen.tsx b/app/src/screens/misc/SplashScreen.tsx index 7a9668039..0c2974ebd 100644 --- a/app/src/screens/misc/SplashScreen.tsx +++ b/app/src/screens/misc/SplashScreen.tsx @@ -8,13 +8,17 @@ import { StyleSheet } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; + import splashAnimation from '@/assets/animations/splash.json'; import type { RootStackParamList } from '@/navigation'; import { useAuth } from '@/providers/authProvider'; import { checkAndUpdateRegistrationStates, checkIfAnyDocumentsNeedMigration, - hasAnyValidRegisteredDocument, initializeNativeModules, migrateFromLegacyStorage, } from '@/providers/passportDataProvider'; @@ -23,6 +27,7 @@ import { black } from '@/utils/colors'; import { impactLight } from '@/utils/haptic'; const SplashScreen: React.FC = ({}) => { + const selfClient = useSelfClient(); const navigation = useNavigation>(); const { checkBiometricsAvailable } = useAuth(); @@ -60,7 +65,7 @@ const SplashScreen: React.FC = ({}) => { await checkAndUpdateRegistrationStates(); } - const hasValid = await hasAnyValidRegisteredDocument(); + const hasValid = await hasAnyValidRegisteredDocument(selfClient); setNextScreen(hasValid ? 'Home' : 'Launch'); } catch (error) { console.error(`Error in SplashScreen data loading: ${error}`); diff --git a/app/src/screens/passport/PassportCameraScreen.tsx b/app/src/screens/passport/PassportCameraScreen.tsx index dc6912c89..83584d4e9 100644 --- a/app/src/screens/passport/PassportCameraScreen.tsx +++ b/app/src/screens/passport/PassportCameraScreen.tsx @@ -8,7 +8,11 @@ import { Platform, StyleSheet } from 'react-native'; import { View, XStack, YStack } from 'tamagui'; import { useIsFocused, useNavigation } from '@react-navigation/native'; -import { formatDateToYYMMDD } from '@selfxyz/mobile-sdk-alpha'; +import { + formatDateToYYMMDD, + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import passportScanAnimation from '@/assets/animations/passport_scan.json'; @@ -25,12 +29,12 @@ import useUserStore from '@/stores/userStore'; import analytics from '@/utils/analytics'; import { black, slate400, slate800, white } from '@/utils/colors'; import { dinot } from '@/utils/fonts'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; import { checkScannedInfo } from '@/utils/utils'; const { trackEvent } = analytics(); const PassportCameraScreen: React.FC = () => { + const client = useSelfClient(); const navigation = useNavigation(); const isFocused = useIsFocused(); const store = useUserStore(); @@ -122,7 +126,7 @@ const PassportCameraScreen: React.FC = () => { }); const onCancelPress = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(client); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/passport/PassportNFCScanScreen.tsx b/app/src/screens/passport/PassportNFCScanScreen.tsx index 7b4242418..747acf303 100644 --- a/app/src/screens/passport/PassportNFCScanScreen.tsx +++ b/app/src/screens/passport/PassportNFCScanScreen.tsx @@ -25,7 +25,10 @@ import { CircleHelp } from '@tamagui/lucide-icons'; import type { PassportData } from '@selfxyz/common/types'; import { getSKIPEM } from '@selfxyz/common/utils/csca'; import { initPassportDataParsing } from '@selfxyz/common/utils/passports'; -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import passportVerifyAnimation from '@/assets/animations/passport_verify.json'; @@ -52,7 +55,6 @@ import { impactLight, } from '@/utils/haptic'; import { parseScanResponse, scan } from '@/utils/nfcScanner'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; import { sanitizeErrorMessage } from '@/utils/utils'; const emitter = @@ -75,7 +77,8 @@ type PassportNFCScanRoute = RouteProp< >; const PassportNFCScanScreen: React.FC = () => { - const { trackEvent } = useSelfClient(); + const selfClient = useSelfClient(); + const { trackEvent } = selfClient; const navigation = useNavigation(); const route = useRoute(); const { showModal } = useFeedback(); @@ -373,7 +376,7 @@ const PassportNFCScanScreen: React.FC = () => { }); const onCancelPress = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/passport/PassportNFCScanScreen.web.tsx b/app/src/screens/passport/PassportNFCScanScreen.web.tsx index 596bd27de..5b63b5ef2 100644 --- a/app/src/screens/passport/PassportNFCScanScreen.web.tsx +++ b/app/src/screens/passport/PassportNFCScanScreen.web.tsx @@ -5,6 +5,10 @@ import React from 'react'; import { Image } from 'tamagui'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; @@ -16,9 +20,9 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import NFC_IMAGE from '@/images/nfc.png'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { black, slate100, white } from '@/utils/colors'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; const PassportNFCScanScreen: React.FC = () => { + const selfClient = useSelfClient(); const navigateToLaunch = useHapticNavigation('Launch', { action: 'cancel', }); @@ -27,7 +31,7 @@ const PassportNFCScanScreen: React.FC = () => { }); const onCancelPress = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/passport/UnsupportedPassportScreen.tsx b/app/src/screens/passport/UnsupportedPassportScreen.tsx index 826a7dc68..0068704c2 100644 --- a/app/src/screens/passport/UnsupportedPassportScreen.tsx +++ b/app/src/screens/passport/UnsupportedPassportScreen.tsx @@ -11,6 +11,10 @@ import type { RouteProp } from '@react-navigation/native'; import { countryCodes } from '@selfxyz/common/constants'; import type { PassportData } from '@selfxyz/common/types'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { PrimaryButton } from '@/components/buttons/PrimaryButton'; @@ -23,7 +27,6 @@ import analytics from '@/utils/analytics'; import { black, slate500, white } from '@/utils/colors'; import { sendCountrySupportNotification } from '@/utils/email'; import { notificationError } from '@/utils/haptic'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; const { flush: flushAnalytics } = analytics(); @@ -43,6 +46,7 @@ interface UnsupportedPassportScreenProps { const UnsupportedPassportScreen: React.FC = ({ route, }) => { + const selfClient = useSelfClient(); const navigateToLaunch = useHapticNavigation('Launch'); const navigateToHome = useHapticNavigation('Home'); const passportData = route.params?.passportData; @@ -101,7 +105,7 @@ const UnsupportedPassportScreen: React.FC = ({ const CountryFlagComponent = getCountryFlag(country2AlphaCode); const onDismiss = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index 6eb8f2924..34ef884fa 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -32,7 +32,8 @@ import { useProvingStore } from '@/utils/proving/provingMachine'; type ConfirmBelongingScreenProps = StaticScreenProps>; const ConfirmBelongingScreen: React.FC = () => { - const { trackEvent } = useSelfClient(); + const selfClient = useSelfClient(); + const { trackEvent } = selfClient; const navigate = useHapticNavigation('LoadingScreen', { params: {}, }); @@ -42,11 +43,10 @@ const ConfirmBelongingScreen: React.FC = () => { const setFcmToken = useProvingStore(state => state.setFcmToken); const setUserConfirmed = useProvingStore(state => state.setUserConfirmed); const isReadyToProve = currentState === 'ready_to_prove'; - useEffect(() => { notificationSuccess(); - init('dsc'); - }, [init]); + init(selfClient, 'dsc'); + }, [init, selfClient]); const onOkPress = async () => { try { diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index 87b8ce623..479954eee 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -41,7 +41,8 @@ import { buttonTap } from '@/utils/haptic'; import { useProvingStore } from '@/utils/proving/provingMachine'; const ProveScreen: React.FC = () => { - const { trackEvent } = useSelfClient(); + const selfClient = useSelfClient(); + const { trackEvent } = selfClient; const { navigate } = useNavigation(); const isFocused = useIsFocused(); const selectedApp = useSelfAppStore(state => state.selfApp); @@ -57,7 +58,6 @@ const ProveScreen: React.FC = () => { () => scrollViewContentHeight <= scrollViewHeight, [scrollViewContentHeight, scrollViewHeight], ); - const provingStore = useProvingStore(); const currentState = useProvingStore(state => state.currentState); const isReadyToProve = currentState === 'ready_to_prove'; @@ -95,10 +95,10 @@ const ProveScreen: React.FC = () => { setDefaultDocumentTypeIfNeeded(); if (selectedAppRef.current?.sessionId !== selectedApp.sessionId) { - provingStore.init('disclose'); + provingStore.init(selfClient, 'disclose'); } selectedAppRef.current = selectedApp; - }, [selectedApp, isFocused, provingStore]); + }, [selectedApp, isFocused, provingStore, selfClient]); const disclosureOptions = useMemo(() => { return (selectedApp?.disclosures as SelfAppDisclosureConfig) || []; diff --git a/app/src/screens/recovery/PassportDataNotFoundScreen.tsx b/app/src/screens/recovery/PassportDataNotFoundScreen.tsx index f31a46d1c..8f2c48c32 100644 --- a/app/src/screens/recovery/PassportDataNotFoundScreen.tsx +++ b/app/src/screens/recovery/PassportDataNotFoundScreen.tsx @@ -4,6 +4,11 @@ import React, { useEffect } from 'react'; +import { + hasAnyValidRegisteredDocument, + useSelfClient, +} from '@selfxyz/mobile-sdk-alpha'; + import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import Description from '@/components/typography/Description'; import { Title } from '@/components/typography/Title'; @@ -11,16 +16,16 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import analytics from '@/utils/analytics'; import { black, slate200, white } from '@/utils/colors'; -import { hasAnyValidRegisteredDocument } from '@/utils/proving/validateDocument'; const { flush: flushAnalytics } = analytics(); const PassportDataNotFound: React.FC = () => { + const selfClient = useSelfClient(); const navigateToLaunch = useHapticNavigation('Launch'); const navigateToHome = useHapticNavigation('Home'); const onPress = async () => { - const hasValidDocument = await hasAnyValidRegisteredDocument(); + const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); } else { diff --git a/app/src/screens/settings/ManageDocumentsScreen.tsx b/app/src/screens/settings/ManageDocumentsScreen.tsx index e9f29295e..cf5b1b4e5 100644 --- a/app/src/screens/settings/ManageDocumentsScreen.tsx +++ b/app/src/screens/settings/ManageDocumentsScreen.tsx @@ -10,6 +10,10 @@ import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Check, Eraser } from '@tamagui/lucide-icons'; +import type { + DocumentCatalog, + DocumentMetadata, +} from '@selfxyz/common/utils/types'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; @@ -17,10 +21,6 @@ import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; import ButtonsContainer from '@/components/ButtonsContainer'; import type { RootStackParamList } from '@/navigation'; -import type { - DocumentCatalog, - DocumentMetadata, -} from '@/providers/passportDataProvider'; import { usePassport } from '@/providers/passportDataProvider'; import { borderColor, textBlack, white } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; diff --git a/app/src/utils/proving/index.ts b/app/src/utils/proving/index.ts index 210a48b34..f3705d109 100644 --- a/app/src/utils/proving/index.ts +++ b/app/src/utils/proving/index.ts @@ -17,9 +17,6 @@ export { export { getLoadingScreenText } from '@/utils/proving/loadingScreenStateText'; // From validateDocument - used in recovery and splash screens -export { - hasAnyValidRegisteredDocument, - isUserRegisteredWithAlternativeCSCA, -} from '@/utils/proving/validateDocument'; +export { isUserRegisteredWithAlternativeCSCA } from '@/utils/proving/validateDocument'; export { useProvingStore } from '@/utils/proving/provingMachine'; diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index d4e3b2985..49897ee79 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -25,13 +25,19 @@ import { getPayload, getWSDbRelayerUrl, } from '@selfxyz/common/utils/proving'; +import { + hasAnyValidRegisteredDocument, + SelfClient, +} from '@selfxyz/mobile-sdk-alpha'; import { PassportEvents, ProofEvents, } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { navigationRef } from '@/navigation'; +// this will be pass as property of from selfClient import { unsafe_getPrivateKey } from '@/providers/authProvider'; +// will need to be passed in from selfClient import { clearPassportData, loadSelectedDocument, @@ -49,7 +55,6 @@ import { import { checkIfPassportDscIsInTree, checkPassportSupported, - hasAnyValidRegisteredDocument, isDocumentNullified, isUserRegistered, isUserRegisteredWithAlternativeCSCA, @@ -182,19 +187,20 @@ interface ProvingState { env: 'prod' | 'stg' | null; setFcmToken: (token: string) => void; init: ( + selfClient: SelfClient, circuitType: 'dsc' | 'disclose' | 'register', userConfirmed?: boolean, ) => Promise; startFetchingData: () => Promise; - validatingDocument: () => Promise; + validatingDocument: (selfClient: SelfClient) => Promise; initTeeConnection: () => Promise; startProving: () => Promise; - postProving: () => void; + postProving: (selfClient: SelfClient) => void; setUserConfirmed: () => void; _closeConnections: () => void; _generatePayload: () => Promise; _handleWebSocketMessage: (event: MessageEvent) => Promise; - _handleRegisterErrorOrFailure: () => void; + _handleRegisterErrorOrFailure: (selfClient: SelfClient) => void; _startSocketIOStatusListener: ( receivedUuid: string, endpointType: EndpointType, @@ -207,7 +213,10 @@ interface ProvingState { export const useProvingStore = create((set, get) => { let actor: AnyActorRef | null = null; - function setupActorSubscriptions(newActor: AnyActorRef) { + function setupActorSubscriptions( + newActor: AnyActorRef, + selfClient: SelfClient, + ) { newActor.subscribe((state: StateFrom) => { console.log(`State transition: ${state.value}`); trackEvent(ProofEvents.PROVING_STATE_CHANGE, { state: state.value }); @@ -217,7 +226,7 @@ export const useProvingStore = create((set, get) => { get().startFetchingData(); } if (state.value === 'validating_document') { - get().validatingDocument(); + get().validatingDocument(selfClient); } if (state.value === 'init_tee_connexion') { @@ -229,7 +238,7 @@ export const useProvingStore = create((set, get) => { } if (state.value === 'post_proving') { - get().postProving(); + get().postProving(selfClient); } if ( get().circuitType !== 'disclose' && @@ -237,7 +246,7 @@ export const useProvingStore = create((set, get) => { ) { setTimeout(() => { if (navigationRef.isReady()) { - get()._handleRegisterErrorOrFailure(); + get()._handleRegisterErrorOrFailure(selfClient); } }, 3000); } @@ -420,9 +429,10 @@ export const useProvingStore = create((set, get) => { } }, - _handleRegisterErrorOrFailure: async () => { + _handleRegisterErrorOrFailure: async (selfClient: SelfClient) => { try { - const hasValid = await hasAnyValidRegisteredDocument(); + const hasValid = await hasAnyValidRegisteredDocument(selfClient); + if (navigationRef.isReady()) { if (hasValid) { navigationRef.navigate('Home'); @@ -596,6 +606,7 @@ export const useProvingStore = create((set, get) => { }, init: async ( + selfClient: SelfClient, circuitType: 'dsc' | 'disclose' | 'register', userConfirmed: boolean = false, ) => { @@ -626,7 +637,7 @@ export const useProvingStore = create((set, get) => { }); actor = createActor(provingMachine); - setupActorSubscriptions(actor); + setupActorSubscriptions(actor, selfClient); actor.start(); trackEvent(ProofEvents.DOCUMENT_LOAD_STARTED); @@ -640,6 +651,7 @@ export const useProvingStore = create((set, get) => { const { data: passportData } = selectedDocument; + // TODO call on self client const secret = await unsafe_getPrivateKey(); if (!secret) { console.error('Could not load secret'); @@ -682,7 +694,7 @@ export const useProvingStore = create((set, get) => { } }, - validatingDocument: async () => { + validatingDocument: async (selfClient: SelfClient) => { _checkActorInitialized(actor); // TODO: for the disclosure, we could check that the selfApp is a valid one. trackEvent(ProofEvents.VALIDATION_STARTED); @@ -925,7 +937,7 @@ export const useProvingStore = create((set, get) => { } }, - postProving: () => { + postProving: (selfClient: SelfClient) => { _checkActorInitialized(actor); const { circuitType } = get(); trackEvent(ProofEvents.POST_PROVING_STARTED); @@ -935,7 +947,7 @@ export const useProvingStore = create((set, get) => { from: 'dsc', to: 'register', }); - get().init('register', true); + get().init(selfClient, 'register', true); }, 1500); } else if (circuitType === 'register') { trackEvent(ProofEvents.POST_PROVING_COMPLETED); diff --git a/app/src/utils/proving/validateDocument.ts b/app/src/utils/proving/validateDocument.ts index c26f3952a..4ab69c3b2 100644 --- a/app/src/utils/proving/validateDocument.ts +++ b/app/src/utils/proving/validateDocument.ts @@ -27,8 +27,7 @@ import { isPassportDataValid } from '@selfxyz/mobile-sdk-alpha'; import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { - getAllDocuments, - loadDocumentCatalog, + getAllDocumentsDirectlyFromKeychain, loadPassportDataAndSecret, loadSelectedDocument, setSelectedDocument, @@ -51,7 +50,8 @@ export type PassportSupportStatus = * This function checks and updates registration states for all documents and updates the `isRegistered`. */ export async function checkAndUpdateRegistrationStates(): Promise { - const allDocuments = await getAllDocuments(); + const allDocuments = await getAllDocumentsDirectlyFromKeychain(); + for (const documentId of Object.keys(allDocuments)) { try { await setSelectedDocument(documentId); @@ -316,16 +316,6 @@ function formatCSCAPem(cscaPem: string): string { return cleanedPem; } -export async function hasAnyValidRegisteredDocument(): Promise { - try { - const catalog = await loadDocumentCatalog(); - return catalog.documents.some(doc => doc.isRegistered === true); - } catch (error) { - console.error('Error loading document catalog:', error); - return false; - } -} - export async function isDocumentNullified(passportData: PassportData) { const nullifier = generateNullifier(passportData); const nullifierHex = `0x${BigInt(nullifier).toString(16)}`; diff --git a/app/src/utils/testingUtils.ts b/app/src/utils/testingUtils.ts index 54a87fdc3..b247d9724 100644 --- a/app/src/utils/testingUtils.ts +++ b/app/src/utils/testingUtils.ts @@ -4,7 +4,7 @@ import Keychain from 'react-native-keychain'; -import { loadDocumentCatalog } from '@/providers/passportDataProvider'; +import { loadDocumentCatalogDirectlyFromKeychain as loadDocumentCatalog } from '@/providers/passportDataProvider'; /** * Testing utility function to clear the document catalog for migration testing. diff --git a/app/tests/src/providers/passportDataProvider.test.tsx b/app/tests/src/providers/passportDataProvider.test.tsx index e28f3bcce..af6745379 100644 --- a/app/tests/src/providers/passportDataProvider.test.tsx +++ b/app/tests/src/providers/passportDataProvider.test.tsx @@ -6,12 +6,16 @@ import React, { useEffect, useState } from 'react'; import { Text } from 'react-native'; import { render, waitFor } from '@testing-library/react-native'; +import { SelfClientProvider } from '@selfxyz/mobile-sdk-alpha'; + // Import after mocking import { PassportProvider, usePassport, } from '@/providers/passportDataProvider'; +import { mockAdapters } from '../../utils/selfClientProvider'; + // Mock react-native-keychain before importing the module const mockKeychain = { getGenericPassword: jest.fn(), @@ -128,9 +132,11 @@ describe('PassportDataProvider', () => { it('should provide context values to children', () => { const { getByTestId } = render( - - - , + + + + + , ); expect(getByTestId('getData-available')).toBeTruthy(); @@ -140,9 +146,11 @@ describe('PassportDataProvider', () => { it('should provide all required context functions', () => { const { getByTestId } = render( - - - , + + + + + , ); const functionsCount = getByTestId('context-functions-count'); @@ -159,9 +167,11 @@ describe('PassportDataProvider', () => { it('should support multiple consumers accessing the same context', () => { const { getByTestId } = render( - - - , + + + + + , ); const consumer1Functions = getByTestId('consumer1-functions'); @@ -176,9 +186,11 @@ describe('PassportDataProvider', () => { it('should handle context updates and trigger re-renders', async () => { const { getByTestId } = render( - - - , + + + + + , ); const updateCount = getByTestId('update-count'); @@ -199,9 +211,11 @@ describe('PassportDataProvider', () => { it('should handle errors gracefully in context consumers', () => { const { getByTestId } = render( - - - , + + + + + , ); const errorTestResult = getByTestId('error-test-result'); @@ -210,15 +224,21 @@ describe('PassportDataProvider', () => { it('should render without children gracefully', () => { expect(() => { - render(); + render( + + + , + ); }).not.toThrow(); }); it('should provide consistent context values across re-renders', () => { const { getByTestId, rerender } = render( - - - , + + + + + , ); const initialFunctionsCount = getByTestId('context-functions-count').props @@ -226,9 +246,11 @@ describe('PassportDataProvider', () => { // Re-render the component rerender( - - - , + + + + + , ); const newFunctionsCount = getByTestId('context-functions-count').props @@ -238,9 +260,11 @@ describe('PassportDataProvider', () => { it('should maintain context stability across provider re-renders', () => { const { getByTestId, rerender } = render( - - - , + + + + + , ); const initialFunctionsList = getByTestId('context-functions-list').props @@ -248,9 +272,11 @@ describe('PassportDataProvider', () => { // Re-render with different props rerender( - - - , + + + + + , ); const newFunctionsList = getByTestId('context-functions-list').props @@ -437,7 +463,8 @@ describe('PassportDataProvider', () => { jest.doMock('react-native-keychain', () => mockKeychain); const passportModule = require('@/providers/passportDataProvider'); - loadDocumentCatalogLocal = passportModule.loadDocumentCatalog; + loadDocumentCatalogLocal = + passportModule.loadDocumentCatalogDirectlyFromKeychain; }); it('should return empty catalog when Keychain is undefined', async () => { @@ -449,7 +476,7 @@ describe('PassportDataProvider', () => { // Re-import the module after mocking to ensure mock is applied const passportModule = require('@/providers/passportDataProvider'); const loadDocumentCatalogLocalUndefined = - passportModule.loadDocumentCatalog; + passportModule.loadDocumentCatalogDirectlyFromKeychain; const result = await loadDocumentCatalogLocalUndefined(); diff --git a/app/tests/src/utils/proving/validateDocument.test.ts b/app/tests/src/utils/proving/validateDocument.test.ts index 0578ca291..834e8edb1 100644 --- a/app/tests/src/utils/proving/validateDocument.test.ts +++ b/app/tests/src/utils/proving/validateDocument.test.ts @@ -79,6 +79,10 @@ function createTestClient() { hash: jest.fn(), sign: jest.fn(), }, + documents: { + loadDocumentCatalog: jest.fn(), + loadDocumentById: jest.fn(), + }, }, }); } diff --git a/app/tests/utils/selfClientProvider.ts b/app/tests/utils/selfClientProvider.ts new file mode 100644 index 000000000..38231e174 --- /dev/null +++ b/app/tests/utils/selfClientProvider.ts @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 + +/* eslint-disable sort-exports/sort-exports */ + +import type { + CryptoAdapter, + DocumentsAdapter, + NetworkAdapter, + ScannerAdapter, +} from '@selfxyz/mobile-sdk-alpha'; + +export const mockCrypto: CryptoAdapter = { + hash: async () => new Uint8Array(), + sign: async () => new Uint8Array(), +}; + +export const mockDocuments: DocumentsAdapter = { + loadDocumentCatalog: async () => ({ documents: [] }), + loadDocumentById: async () => null, +}; + +export const mockNetwork: NetworkAdapter = { + http: { + fetch: async () => + ({ + ok: true, + status: 200, + text: async () => '', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(0), + }) as any, + }, + ws: { + connect: () => ({ + send: () => {}, + close: () => {}, + onMessage: () => {}, + onError: () => {}, + onClose: () => {}, + }), + }, +}; + +export const mockScanner: ScannerAdapter = { + scan: async () => ({ + mode: 'mrz', + passportNumber: '', + dateOfBirth: '', + dateOfExpiry: '', + }), +}; + +export const mockAdapters = { + scanner: mockScanner, + network: mockNetwork, + crypto: mockCrypto, + documents: mockDocuments, +}; diff --git a/common/src/utils/types.ts b/common/src/utils/types.ts index 68fb8a838..9f73ff6bd 100644 --- a/common/src/utils/types.ts +++ b/common/src/utils/types.ts @@ -5,6 +5,20 @@ export type DocumentCategory = 'passport' | 'id_card'; export type DocumentType = 'passport' | 'id_card' | 'mock_passport' | 'mock_id_card'; +export interface DocumentCatalog { + documents: DocumentMetadata[]; + selectedDocumentId?: string; // This is now a contentHash +} + +export interface DocumentMetadata { + id: string; // contentHash as ID for deduplication + documentType: string; // passport, mock_passport, id_card, etc. + documentCategory: DocumentCategory; // passport, id_card, aadhaar + data: string; // DG1/MRZ data for passports/IDs, relevant data for aadhaar + mock: boolean; // whether this is a mock document + isRegistered?: boolean; // whether the document is registered onChain +} + export type OfacTree = { passportNoAndNationality: any; nameAndDob: any; diff --git a/packages/mobile-sdk-alpha/src/adapters/index.ts b/packages/mobile-sdk-alpha/src/adapters/index.ts index 29f543797..040c70fce 100644 --- a/packages/mobile-sdk-alpha/src/adapters/index.ts +++ b/packages/mobile-sdk-alpha/src/adapters/index.ts @@ -6,6 +6,7 @@ export type { Adapters, ClockAdapter, CryptoAdapter, + DocumentsAdapter, HttpAdapter, LoggerAdapter, NetworkAdapter, diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index ba23b597d..8dd3951c5 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -54,6 +54,8 @@ export { defaultConfig } from './config/defaults'; /** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { getAllDocuments, hasAnyValidRegisteredDocument } from './documents/utils'; + // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/client.ts b/packages/mobile-sdk-alpha/src/client.ts index 511ab2605..f8541b8bb 100644 --- a/packages/mobile-sdk-alpha/src/client.ts +++ b/packages/mobile-sdk-alpha/src/client.ts @@ -47,6 +47,8 @@ const optionalDefaults: Partial = { }, }; +const REQUIRED_ADAPTERS = ['scanner', 'network', 'crypto', 'documents'] as const; + /** * Creates a fully configured {@link SelfClient} instance. * @@ -56,9 +58,9 @@ const optionalDefaults: Partial = { */ export function createSelfClient({ config, adapters }: { config: Config; adapters: Partial }): SelfClient { const cfg = mergeConfig(defaultConfig, config); - const required: (keyof Adapters)[] = ['scanner', 'network', 'crypto']; - for (const name of required) { - if (!(name in adapters) || !adapters[name]) throw notImplemented(name); + + for (const name of REQUIRED_ADAPTERS) { + if (!(name in adapters) || !adapters[name as keyof Adapters]) throw notImplemented(name); } const _adapters = { ...optionalDefaults, ...adapters } as Adapters; @@ -136,5 +138,13 @@ export function createSelfClient({ config, adapters }: { config: Config; adapter extractMRZInfo: parseMRZInfo, on, emit, + + // TODO: inline for now + loadDocumentCatalog: async () => { + return _adapters.documents.loadDocumentCatalog(); + }, + loadDocumentById: async (id: string) => { + return _adapters.documents.loadDocumentById(id); + }, }; } diff --git a/packages/mobile-sdk-alpha/src/documents/utils.ts b/packages/mobile-sdk-alpha/src/documents/utils.ts new file mode 100644 index 000000000..704fbfd02 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/documents/utils.ts @@ -0,0 +1,52 @@ +// 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 { DocumentMetadata, PassportData } from '@selfxyz/common/utils/types'; + +import { SelfClient } from '../types/public'; + +/** + * Gets all documents from the document catalog. + * + * @param selfClient - The SelfClient instance to use for loading the document catalog. + * @returns A dictionary of document IDs to their data and metadata. + */ +export const getAllDocuments = async ( + selfClient: SelfClient, +): Promise<{ + [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; +}> => { + const catalog = await selfClient.loadDocumentCatalog(); + const allDocs: { + [documentId: string]: { data: PassportData; metadata: DocumentMetadata }; + } = {}; + + for (const metadata of catalog.documents) { + const data = await selfClient.loadDocumentById(metadata.id); + if (data) { + allDocs[metadata.id] = { data, metadata }; + } + } + + return allDocs; +}; + +/** + * Checks if there are any valid registered documents in the document catalog. + * + * @param client - The SelfClient instance to use for loading the document catalog. + * @returns True if there are any valid registered documents, false otherwise. + */ +export const hasAnyValidRegisteredDocument = async (client: SelfClient): Promise => { + console.log('Checking if there are any valid registered documents'); + + try { + const catalog = await client.loadDocumentCatalog(); + + return catalog.documents.some(doc => doc.isRegistered === true); + } catch (error) { + console.error('Error loading document catalog:', error); + return false; + } +}; diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index ab29c1e2c..76bd091a8 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -9,6 +9,7 @@ export type { ClockAdapter, Config, CryptoAdapter, + DocumentsAdapter, HttpAdapter, LogLevel, LoggerAdapter, @@ -87,6 +88,8 @@ export { extractMRZInfo } from './mrz'; export { formatDateToYYMMDD, scanMRZ } from './mrz'; +export { getAllDocuments, hasAnyValidRegisteredDocument } from './documents/utils'; + // Core functions export { isPassportDataValid } from './validation/document'; @@ -99,5 +102,6 @@ export { scanQRProof } from './qr'; // Hooks export { useDocumentManager } from './hooks/useDocumentManager'; + // Error handling export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index 4cbbf14c3..7c57fbf90 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -export type { PassportData } from '@selfxyz/common/utils/types'; +import { DocumentCatalog, PassportData } from '@selfxyz/common/utils/types'; + export type { PassportValidationCallbacks } from '../validation/document'; export interface Config { endpoints?: { api?: string; teeWs?: string; artifactsCdn?: string }; @@ -91,6 +92,7 @@ export interface Adapters { clock: ClockAdapter; logger: LoggerAdapter; analytics: AnalyticsAdapter; + documents: DocumentsAdapter; } export interface ProofHandle { @@ -148,6 +150,12 @@ export type ScanResult = export interface ScannerAdapter { scan(opts: ScanOpts & { signal?: AbortSignal }): Promise; } + +export interface DocumentsAdapter { + loadDocumentCatalog(): Promise; + loadDocumentById(id: string): Promise; +} + export interface SelfClient { scanDocument(opts: ScanOpts & { signal?: AbortSignal }): Promise; validateDocument(input: ValidationInput): Promise; @@ -165,6 +173,9 @@ export interface SelfClient { trackEvent(event: string, payload?: TrackEventParams): void; on(event: E, cb: (payload: SDKEventMap[E]) => void): Unsubscribe; emit(event: E, payload: SDKEventMap[E]): void; + + loadDocumentCatalog(): Promise; + loadDocumentById(id: string): Promise; } export type Unsubscribe = () => void; export interface StorageAdapter { diff --git a/packages/mobile-sdk-alpha/src/validation/document.ts b/packages/mobile-sdk-alpha/src/validation/document.ts index a2cca7a27..04015536c 100644 --- a/packages/mobile-sdk-alpha/src/validation/document.ts +++ b/packages/mobile-sdk-alpha/src/validation/document.ts @@ -4,8 +4,7 @@ import { hash } from '@selfxyz/common/utils/hash/sha'; import { formatMrz } from '@selfxyz/common/utils/passportFormat'; - -import type { PassportData } from '../types/public'; +import type { PassportData } from '@selfxyz/common/utils/types'; /** * Checks if two numeric arrays contain the same values in the same order. diff --git a/packages/mobile-sdk-alpha/tests/client.test.ts b/packages/mobile-sdk-alpha/tests/client.test.ts index 71c05e547..140edc589 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.ts +++ b/packages/mobile-sdk-alpha/tests/client.test.ts @@ -4,29 +4,44 @@ import { describe, expect, it, vi } from 'vitest'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; +import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { createSelfClient } from '../src/index'; describe('createSelfClient', () => { // Test eager validation during client creation it('throws when scanner adapter missing during creation', () => { - expect(() => createSelfClient({ config: {}, adapters: {} })).toThrow('scanner adapter not provided'); + expect(() => + createSelfClient({ + config: {}, + adapters: { + documents, + network, + crypto, + }, + }), + ).toThrow('scanner adapter not provided'); }); it('throws when network adapter missing during creation', () => { - expect(() => createSelfClient({ config: {}, adapters: { scanner, crypto } })).toThrow( + expect(() => createSelfClient({ config: {}, adapters: { scanner, crypto, documents } })).toThrow( 'network adapter not provided', ); }); it('throws when crypto adapter missing during creation', () => { - expect(() => createSelfClient({ config: {}, adapters: { scanner, network } })).toThrow( + expect(() => createSelfClient({ config: {}, adapters: { scanner, network, documents } })).toThrow( 'crypto adapter not provided', ); }); + it('throws when documents adapter missing during creation', () => { + expect(() => createSelfClient({ config: {}, adapters: { scanner, network, crypto } })).toThrow( + 'documents adapter not provided', + ); + }); + it('creates client successfully with all required adapters', () => { - const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto, documents } }); expect(client).toBeTruthy(); }); @@ -34,7 +49,7 @@ describe('createSelfClient', () => { const scanMock = vi.fn().mockResolvedValue({ mode: 'qr', data: 'self://ok' }); const client = createSelfClient({ config: {}, - adapters: { scanner: { scan: scanMock }, network, crypto }, + adapters: { scanner: { scan: scanMock }, network, crypto, documents }, }); const result = await client.scanDocument({ mode: 'qr' }); expect(result).toEqual({ mode: 'qr', data: 'self://ok' }); @@ -46,7 +61,7 @@ describe('createSelfClient', () => { const scanMock = vi.fn().mockRejectedValue(err); const client = createSelfClient({ config: {}, - adapters: { scanner: { scan: scanMock }, network, crypto }, + adapters: { scanner: { scan: scanMock }, network, crypto, documents }, }); await expect(client.scanDocument({ mode: 'qr' })).rejects.toBe(err); }); @@ -55,7 +70,7 @@ describe('createSelfClient', () => { const network = { http: { fetch: vi.fn() }, ws: { connect: vi.fn() } } as any; const crypto = { hash: vi.fn(), sign: vi.fn() } as any; const scanner = { scan: vi.fn() } as any; - const client = createSelfClient({ config: {}, adapters: { network, crypto, scanner } }); + const client = createSelfClient({ config: {}, adapters: { network, crypto, scanner, documents } }); const handle = await client.generateProof({ type: 'register', payload: {} }); expect(handle.id).toBe('stub'); expect(handle.status).toBe('pending'); @@ -64,7 +79,7 @@ describe('createSelfClient', () => { }); it('emits and unsubscribes events', () => { - const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto, documents } }); const cb = vi.fn(); const originalSet = Map.prototype.set; let eventSet: Set<(p: any) => void> | undefined; @@ -83,7 +98,7 @@ describe('createSelfClient', () => { }); it('parses MRZ via client', () => { - const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto, documents } }); const sample = `P { }); it('returns stub registration status', async () => { - const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto, documents } }); await expect(client.registerDocument({} as any)).resolves.toEqual({ registered: false, reason: 'SELF_REG_STATUS_STUB', @@ -102,7 +117,7 @@ describe('createSelfClient', () => { const trackEvent = vi.fn(); const client = createSelfClient({ config: {}, - adapters: { scanner, network, crypto, analytics: { trackEvent } }, + adapters: { scanner, network, crypto, analytics: { trackEvent }, documents }, }); client.trackEvent('test_event'); @@ -134,3 +149,8 @@ const crypto: CryptoAdapter = { hash: async () => new Uint8Array(), sign: async () => new Uint8Array(), }; + +const documents: DocumentsAdapter = { + loadDocumentCatalog: async () => ({ documents: [] }), + loadDocumentById: async () => null, +}; diff --git a/packages/mobile-sdk-alpha/tests/client.test.tsx b/packages/mobile-sdk-alpha/tests/client.test.tsx index 72ff9f97c..53595fe14 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.tsx +++ b/packages/mobile-sdk-alpha/tests/client.test.tsx @@ -4,8 +4,7 @@ import { describe, expect, it } from 'vitest'; -import { createSelfClient } from '../src/index'; -import { MrzParseError } from '../src/processing/mrz'; +import { createSelfClient, MrzParseError } from '../src/index'; import { badCheckDigitsMRZ, expectedMRZResult, invalidMRZ, mockAdapters, sampleMRZ } from './utils/testHelpers'; describe('createSelfClient API', () => { diff --git a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts index c288123c5..1ea309255 100644 --- a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts +++ b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts @@ -3,7 +3,7 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. /* eslint-disable sort-exports/sort-exports */ -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../../src'; +import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, ScannerAdapter } from '../../src'; // Shared test data export const sampleMRZ = `P new Uint8Array(), }; +export const mockDocuments: DocumentsAdapter = { + loadDocumentCatalog: async () => ({ documents: [] }), + loadDocumentById: async () => null, +}; + export const mockAdapters = { scanner: mockScanner, network: mockNetwork, crypto: mockCrypto, + documents: mockDocuments, }; // Shared test expectations