diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle
index f1c4e75c6..5ccd0a476 100644
--- a/app/android/app/build.gradle
+++ b/app/android/app/build.gradle
@@ -135,7 +135,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 121
- versionName "2.9.10"
+ versionName "2.9.11"
manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp']
externalNativeBuild {
cmake {
diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist
index 0aee45559..cb975feea 100644
--- a/app/ios/OpenPassport/Info.plist
+++ b/app/ios/OpenPassport/Info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.9.10
+ 2.9.11
CFBundleSignature
????
CFBundleURLTypes
diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj
index 6ee7b5d90..1917c7aac 100644
--- a/app/ios/Self.xcodeproj/project.pbxproj
+++ b/app/ios/Self.xcodeproj/project.pbxproj
@@ -546,7 +546,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
- MARKETING_VERSION = 2.9.10;
+ MARKETING_VERSION = 2.9.11;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -686,7 +686,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
- MARKETING_VERSION = 2.9.10;
+ MARKETING_VERSION = 2.9.11;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
diff --git a/app/package.json b/app/package.json
index 524d5c329..e1f3bdc3e 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@selfxyz/mobile-app",
- "version": "2.9.10",
+ "version": "2.9.11",
"private": true,
"type": "module",
"scripts": {
diff --git a/app/src/navigation/deeplinks.ts b/app/src/navigation/deeplinks.ts
index af4cd79ce..b2d803644 100644
--- a/app/src/navigation/deeplinks.ts
+++ b/app/src/navigation/deeplinks.ts
@@ -9,6 +9,7 @@ import { countries } from '@selfxyz/common/constants/countries';
import type { IdDocInput } from '@selfxyz/common/utils';
import type { SelfClient } from '@selfxyz/mobile-sdk-alpha';
+import type { RootStackParamList } from '@/navigation';
import { navigationRef } from '@/navigation';
import useUserStore from '@/stores/userStore';
import { IS_DEV_MODE } from '@/utils/devUtils';
@@ -108,6 +109,28 @@ export const getAndClearQueuedUrl = (): string | null => {
return url;
};
+const safeNavigate = (
+ navigationState: ReturnType,
+): void => {
+ const targetScreen = navigationState.routes[1]?.name as
+ | keyof RootStackParamList
+ | undefined;
+
+ const currentRoute = navigationRef.getCurrentRoute();
+ const isColdLaunch = currentRoute?.name === 'Splash';
+
+ if (!isColdLaunch && targetScreen) {
+ // Use object syntax to satisfy TypeScript's strict typing for navigate
+ // The params will be undefined for screens that don't require them
+ navigationRef.navigate({
+ name: targetScreen,
+ params: undefined,
+ } as Parameters[0]);
+ } else {
+ navigationRef.reset(navigationState);
+ }
+};
+
export const handleUrl = (selfClient: SelfClient, uri: string) => {
const validatedParams = parseAndValidateUrlParams(uri);
const {
@@ -125,7 +148,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => {
selfClient.getSelfAppState().setSelfApp(selfAppJson);
selfClient.getSelfAppState().startAppListener(selfAppJson.sessionId);
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState(
'ProvingScreenRouter',
correctParentScreen,
@@ -137,7 +160,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => {
if (IS_DEV_MODE) {
console.error('Error parsing selfApp:', error);
}
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
}
@@ -145,7 +168,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => {
selfClient.getSelfAppState().cleanSelfApp();
selfClient.getSelfAppState().startAppListener(sessionId);
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState('ProvingScreenRouter', correctParentScreen),
);
} else if (mock_passport) {
@@ -175,25 +198,26 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => {
});
// Reset navigation stack with correct parent -> MockDataDeepLink
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState('MockDataDeepLink', correctParentScreen),
);
} catch (error) {
if (IS_DEV_MODE) {
console.error('Error parsing mock_passport data or navigating:', error);
}
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
}
} else if (referrer && typeof referrer === 'string') {
useUserStore.getState().setDeepLinkReferrer(referrer);
- // Navigate to HomeScreen - it will show confirmation modal and then navigate to GratificationScreen
- navigationRef.reset({
- index: 0,
- routes: [{ name: 'Home' }],
- });
+ const currentRoute = navigationRef.getCurrentRoute();
+ if (currentRoute?.name === 'Home') {
+ // Already on Home, no navigation needed - the modal will show automatically
+ } else {
+ safeNavigate(createDeeplinkNavigationState('Home', 'Home'));
+ }
} else if (Platform.OS === 'web') {
// TODO: web handle links if we need to idk if we do
// For web, we can handle the URL some other way if we dont do this loading app in web always navigates to QRCodeTrouble
@@ -211,7 +235,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => {
'No sessionId, selfApp or valid OAuth parameters found in the data',
);
}
- navigationRef.reset(
+ safeNavigate(
createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen),
);
}
diff --git a/app/src/screens/account/settings/SettingsScreen.tsx b/app/src/screens/account/settings/SettingsScreen.tsx
index f9ea00ce2..cb53ccce3 100644
--- a/app/src/screens/account/settings/SettingsScreen.tsx
+++ b/app/src/screens/account/settings/SettingsScreen.tsx
@@ -187,22 +187,16 @@ const SettingsScreen: React.FC = () => {
const screenRoutes = useMemo(() => {
const baseRoutes = isDevMode ? [...routes, ...DEBUG_MENU] : routes;
-
- // Show all routes while loading or if user has a real document
- if (hasRealDocument === null || hasRealDocument === true) {
- return baseRoutes;
- }
-
const shouldHideCloudBackup = Platform.OS === 'android';
+ const hasConfirmedRealDocument = hasRealDocument === true;
- // Only filter out document-related routes if we've confirmed user has no real documents
return baseRoutes.filter(([, , route]) => {
if (DOCUMENT_DEPENDENT_ROUTES.includes(route)) {
- return false;
+ return hasConfirmedRealDocument;
}
if (shouldHideCloudBackup && route === CLOUD_BACKUP_ROUTE) {
- return false;
+ return hasConfirmedRealDocument;
}
return true;
diff --git a/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx b/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx
index 54b6adef8..33a0ae2bf 100644
--- a/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx
+++ b/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx
@@ -15,6 +15,7 @@ import Mnemonic from '@/components/Mnemonic';
import useMnemonic from '@/hooks/useMnemonic';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { useSettingStore } from '@/stores/settingStore';
+import { getRecoveryPhraseWarningMessage } from '@/utils/crypto/mnemonic';
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
function useCopyRecoveryPhrase(mnemonic: string[] | undefined) {
@@ -89,10 +90,7 @@ const ShowRecoveryPhraseScreen: React.FC & {
gap={20}
>
-
- This phrase is the only way to recover your account. Keep it secret,
- keep it safe.
-
+ {getRecoveryPhraseWarningMessage()}
);
diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx
index d40e48c4f..8eba89e13 100644
--- a/app/src/screens/dev/DevSettingsScreen.tsx
+++ b/app/src/screens/dev/DevSettingsScreen.tsx
@@ -33,7 +33,6 @@ import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
import BugIcon from '@/assets/icons/bug_icon.svg';
-import IdIcon from '@/assets/icons/id_icon.svg';
import WarningIcon from '@/assets/icons/warning.svg';
import type { RootStackParamList } from '@/navigation';
import { navigationScreens } from '@/navigation';
@@ -287,6 +286,110 @@ const ScreenSelector = ({}) => {
);
};
+const LogLevelSelector = ({
+ currentLevel,
+ onSelect,
+}: {
+ currentLevel: string;
+ onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void;
+}) => {
+ const [open, setOpen] = useState(false);
+
+ const logLevels = ['debug', 'info', 'warn', 'error'] as const;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Select log level
+
+
+
+
+ {logLevels.map(level => (
+ {
+ setOpen(false);
+ onSelect(level);
+ }}
+ >
+
+
+ {level.toUpperCase()}
+
+ {currentLevel === level && (
+
+ )}
+
+
+ ))}
+
+
+
+
+ >
+ );
+};
+
const DevSettingsScreen: React.FC = ({}) => {
const { clearDocumentCatalogForMigrationTesting } = usePassport();
const clearPointEvents = usePointEventStore(state => state.clearEvents);
@@ -547,57 +650,6 @@ const DevSettingsScreen: React.FC = ({}) => {
paddingTop="$4"
paddingBottom={paddingBottom}
>
- }
- title="Manage ID Documents"
- description="Register new IDs and generate test IDs"
- >
- {[
- {
- label: 'Manage available IDs',
- onPress: () => {
- navigation.navigate('ManageDocuments');
- },
- },
- {
- label: 'Generate Test ID',
- onPress: () => {
- navigation.navigate('CreateMock');
- },
- },
- {
- label: 'Scan new ID Document',
- onPress: () => {
- navigation.navigate('DocumentOnboarding');
- },
- },
- ].map(({ label, onPress }) => (
-
-
-
- ))}
-
-
}
title="Debug Shortcuts"
@@ -696,38 +748,10 @@ const DevSettingsScreen: React.FC = ({}) => {
title="Log Level"
description="Configure logging verbosity"
>
-
- {(['debug', 'info', 'warn', 'error'] as const).map(level => (
-
- ))}
-
+
{
+ const navigation =
+ useNavigation>();
const selfClient = useSelfClient();
const {
loadDocumentCatalog,
@@ -73,7 +75,20 @@ const PassportDataSelector = () => {
loadPassportDataInfo();
}, [loadPassportDataInfo]);
- const handleDocumentSelection = async (documentId: string) => {
+ const handleDocumentSelection = async (
+ documentId: string,
+ isRegistered: boolean | undefined,
+ ) => {
+ if (!isRegistered) {
+ Alert.alert(
+ 'Document not registered',
+ 'This document cannot be selected as active, because it is not registered. Click the add button next to it to register it first.',
+ [{ text: 'OK', style: 'cancel' }],
+ );
+
+ return;
+ }
+
await setSelectedDocument(documentId);
// Reload to update UI without loading state for quick operations
const catalog = await loadDocumentCatalog();
@@ -90,24 +105,40 @@ const PassportDataSelector = () => {
await loadPassportDataInfo();
};
- const handleDeleteButtonPress = (documentId: string) => {
- Alert.alert(
- '⚠️ Delete Document ⚠️',
- 'Are you sure you want to delete this document?\n\nThis document is already linked to your identity in Self Protocol and cannot be linked by another person.',
- [
- {
- text: 'Cancel',
- style: 'cancel',
+ const handleRegisterDocument = async (documentId: string) => {
+ try {
+ await setSelectedDocument(documentId);
+ navigation.navigate('ConfirmBelonging', {});
+ } catch {
+ Alert.alert(
+ 'Registration Error',
+ 'Failed to prepare document for registration. Please try again.',
+ [{ text: 'OK', style: 'cancel' }],
+ );
+ }
+ };
+
+ const handleDeleteButtonPress = (
+ documentId: string,
+ isRegistered: boolean | undefined,
+ ) => {
+ const message = isRegistered
+ ? 'Are you sure you want to delete this document?\n\nThis document is already linked to your identity in Self Protocol and cannot be linked by another person.'
+ : 'Are you sure you want to delete this document?';
+
+ Alert.alert('⚠️ Delete Document ⚠️', message, [
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ },
+ {
+ text: 'Delete',
+ style: 'destructive',
+ onPress: async () => {
+ await handleDeleteSpecific(documentId);
},
- {
- text: 'Delete',
- style: 'destructive',
- onPress: async () => {
- await handleDeleteSpecific(documentId);
- },
- },
- ],
- );
+ },
+ ]);
};
const getDisplayName = (documentType: string): string => {
@@ -156,6 +187,16 @@ const PassportDataSelector = () => {
}
};
+ const getDocumentBackgroundColor = (
+ isSelected: boolean,
+ isRegistered: boolean | undefined,
+ ): string => {
+ if (!isRegistered) {
+ return '#ffebee'; // Light red for unregistered documents
+ }
+ return isSelected ? '$gray2' : 'white';
+ };
+
if (loading) {
return (
@@ -196,6 +237,10 @@ const PassportDataSelector = () => {
);
}
+ const hasUnregisteredDocuments = documentCatalog.documents.some(
+ doc => !doc.isRegistered,
+ );
+
return (
{
>
Available Documents
+ {hasUnregisteredDocuments && (
+
+
+ ⚠️ We've detected some documents that are not registered. In order
+ to use them, you'll have to register them first by clicking the plus
+ icon next to them.
+
+
+ )}
{documentCatalog.documents.map((metadata: DocumentMetadata) => (
{
: borderColor
}
borderRadius="$3"
- backgroundColor={
- documentCatalog.selectedDocumentId === metadata.id
- ? '$gray2'
- : 'white'
+ backgroundColor={getDocumentBackgroundColor(
+ documentCatalog.selectedDocumentId === metadata.id,
+ metadata.isRegistered,
+ )}
+ onPress={() =>
+ handleDocumentSelection(metadata.id, metadata.isRegistered)
}
- onPress={() => handleDocumentSelection(metadata.id)}
pressStyle={{ opacity: 0.8 }}
>
{
}
borderColor={textBlack}
borderWidth={1}
- onPress={() => handleDocumentSelection(metadata.id)}
+ onPress={() =>
+ handleDocumentSelection(metadata.id, metadata.isRegistered)
+ }
>
{documentCatalog.selectedDocumentId === metadata.id && (
@@ -256,19 +319,36 @@ const PassportDataSelector = () => {
-
+
+ {metadata.isRegistered !== true && (
+
+ )}
+
+
))}
diff --git a/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx b/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx
index de62cca5e..6a732bb45 100644
--- a/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx
+++ b/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx
@@ -23,6 +23,7 @@ import useMnemonic from '@/hooks/useMnemonic';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { STORAGE_NAME } from '@/services/cloud-backup';
import { useSettingStore } from '@/stores/settingStore';
+import { getRecoveryPhraseWarningMessage } from '@/utils/crypto/mnemonic';
const SaveRecoveryPhraseScreen: React.FC = () => {
const [userHasSeenMnemonic, setUserHasSeenMnemonic] = useState(false);
@@ -55,8 +56,7 @@ const SaveRecoveryPhraseScreen: React.FC = () => {
Save your recovery phrase
- This phrase is the only way to recover your account. Keep it secret,
- keep it safe.
+ {getRecoveryPhraseWarningMessage()}
({
navigate: jest.fn(),
isReady: jest.fn(() => true),
reset: jest.fn(),
+ getCurrentRoute: jest.fn(),
},
}));
@@ -66,6 +67,10 @@ describe('deeplinks', () => {
setDeepLinkUserDetails,
});
mockPlatform.OS = 'ios';
+
+ // Setup default getCurrentRoute mock to return Splash (cold launch scenario)
+ const { navigationRef } = require('@/navigation');
+ navigationRef.getCurrentRoute.mockReturnValue({ name: 'Splash' });
});
describe('handleUrl', () => {
@@ -156,9 +161,10 @@ describe('deeplinks', () => {
const { navigationRef } = require('@/navigation');
// Should navigate to HomeScreen, which will show confirmation modal
+ // During cold launch (Splash screen), reset is called with full navigation state
expect(navigationRef.reset).toHaveBeenCalledWith({
- index: 0,
- routes: [{ name: 'Home' }],
+ index: 1,
+ routes: [{ name: 'Home' }, { name: 'Home' }],
});
});
@@ -598,7 +604,7 @@ describe('deeplinks', () => {
mockLinking.getInitialURL.mockResolvedValue(undefined as any);
mockLinking.addEventListener.mockReturnValue({ remove });
- const cleanup = setupUniversalLinkListenerInNavigation();
+ const cleanup = setupUniversalLinkListenerInNavigation({} as SelfClient);
expect(mockLinking.addEventListener).toHaveBeenCalled();
cleanup();
expect(remove).toHaveBeenCalled();
diff --git a/app/version.json b/app/version.json
index 1b9ef12ac..12d4ce934 100644
--- a/app/version.json
+++ b/app/version.json
@@ -1,7 +1,7 @@
{
"ios": {
- "build": 203,
- "lastDeployed": "2026-01-12T16:10:12.854Z"
+ "build": 205,
+ "lastDeployed": "2026-01-12T23:27:08.229Z"
},
"android": {
"build": 134,
diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json
index 2262c3301..a5a77be84 100644
--- a/packages/mobile-sdk-alpha/package.json
+++ b/packages/mobile-sdk-alpha/package.json
@@ -162,6 +162,7 @@
"zustand": "^4.5.2"
},
"devDependencies": {
+ "@openpassport/zk-kit-lean-imt": "^0.0.6",
"@testing-library/react": "^14.1.2",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
@@ -177,6 +178,7 @@
"eslint-plugin-sort-exports": "^0.9.1",
"jsdom": "^25.0.1",
"lottie-react-native": "7.2.2",
+ "poseidon-lite": "^0.3.0",
"prettier": "^3.5.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts
new file mode 100644
index 000000000..28b7b77d5
--- /dev/null
+++ b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts
@@ -0,0 +1,920 @@
+// 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 { poseidon2 } from 'poseidon-lite';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import type { AadhaarData, PassportData } from '@selfxyz/common';
+import {
+ generateCommitment,
+ genMockIdDoc,
+ getCircuitNameFromPassportData,
+ getLeafDscTree,
+ isMRZDocument,
+} from '@selfxyz/common/utils';
+import * as commonUtils from '@selfxyz/common/utils';
+import { generateCommitmentInAppAadhaar } from '@selfxyz/common/utils/passports/validate';
+import { AttestationIdHex } from '@selfxyz/common/utils/types';
+
+import { PassportEvents, ProofEvents } from '../../src/constants/analytics';
+import * as documentUtils from '../../src/documents/utils';
+import { useProvingStore } from '../../src/proving/provingMachine';
+import { fetchAllTreesAndCircuits } from '../../src/stores';
+import type { SelfClient } from '../../src/types/public';
+import { actorMock } from './actorMock';
+
+import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
+
+vi.mock('xstate', async () => {
+ const actual = await vi.importActual('xstate');
+ return {
+ ...actual,
+ createActor: vi.fn(() => actorMock),
+ };
+});
+
+vi.mock('../../src/documents/utils', async () => {
+ const actual = await vi.importActual('../../src/documents/utils');
+ return {
+ ...actual,
+ loadSelectedDocument: vi.fn(),
+ storePassportData: vi.fn(),
+ clearPassportData: vi.fn(),
+ reStorePassportDataWithRightCSCA: vi.fn(),
+ markCurrentDocumentAsRegistered: vi.fn(),
+ };
+});
+
+vi.mock('../../src/stores', async () => {
+ const actual = await vi.importActual('../../src/stores');
+ return {
+ ...actual,
+ fetchAllTreesAndCircuits: vi.fn(),
+ };
+});
+
+const createCommitmentTree = (commitments: string[]) => {
+ const tree = new LeanIMT((a, b) => poseidon2([a, b]));
+ if (commitments.length > 0) {
+ tree.insertMany(commitments.map(commitment => BigInt(commitment)));
+ }
+ return tree.export();
+};
+
+const createDscTree = (leaves: string[]) => createCommitmentTree(leaves);
+
+const buildPassportFixture = (): PassportData =>
+ ({
+ mrz: 'P;
+ publicKeys?: string[];
+}) => ({
+ passport: {
+ commitment_tree: commitmentTree,
+ dsc_tree: dscTree,
+ csca_tree: null,
+ deployed_circuits: deployedCircuits,
+ circuits_dns_mapping: null,
+ alternative_csca: alternativeCsca ?? {},
+ ofac_trees: null,
+ fetch_all: vi.fn(),
+ fetch_deployed_circuits: vi.fn(),
+ fetch_circuits_dns_mapping: vi.fn(),
+ fetch_csca_tree: vi.fn(),
+ fetch_dsc_tree: vi.fn(),
+ fetch_identity_tree: vi.fn(),
+ fetch_alternative_csca: vi.fn(),
+ fetch_ofac_trees: vi.fn(),
+ },
+ id_card: {
+ commitment_tree: commitmentTree,
+ dsc_tree: dscTree,
+ csca_tree: null,
+ deployed_circuits: deployedCircuits,
+ circuits_dns_mapping: null,
+ alternative_csca: alternativeCsca ?? {},
+ ofac_trees: null,
+ fetch_all: vi.fn(),
+ fetch_deployed_circuits: vi.fn(),
+ fetch_circuits_dns_mapping: vi.fn(),
+ fetch_csca_tree: vi.fn(),
+ fetch_dsc_tree: vi.fn(),
+ fetch_identity_tree: vi.fn(),
+ fetch_alternative_csca: vi.fn(),
+ fetch_ofac_trees: vi.fn(),
+ },
+ aadhaar: {
+ commitment_tree: commitmentTree,
+ dsc_tree: null,
+ csca_tree: null,
+ deployed_circuits: deployedCircuits,
+ circuits_dns_mapping: null,
+ public_keys: publicKeys ?? [],
+ ofac_trees: null,
+ fetch_all: vi.fn(),
+ fetch_deployed_circuits: vi.fn(),
+ fetch_circuits_dns_mapping: vi.fn(),
+ fetch_csca_tree: vi.fn(),
+ fetch_dsc_tree: vi.fn(),
+ fetch_identity_tree: vi.fn(),
+ fetch_alternative_csca: vi.fn(),
+ fetch_ofac_trees: vi.fn(),
+ },
+});
+
+const createSelfClient = (protocolState: ReturnType) =>
+ ({
+ trackEvent: vi.fn(),
+ logProofEvent: vi.fn(),
+ emit: vi.fn(),
+ getPrivateKey: vi.fn().mockResolvedValue('123456789'),
+ getProvingState: () => useProvingStore.getState(),
+ getSelfAppState: () => ({ selfApp: null }),
+ getProtocolState: () => protocolState,
+ }) as unknown as SelfClient;
+
+describe('parseIDDocument', () => {
+ const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument);
+ const storePassportDataMock = vi.mocked(documentUtils.storePassportData);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('parses passport data successfully and updates state with parsed result', async () => {
+ const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ const getSKIPEMSpy = vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({});
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ await useProvingStore.getState().parseIDDocument(selfClient);
+
+ const state = useProvingStore.getState();
+ expect(getSKIPEMSpy).toHaveBeenCalledWith('staging');
+ expect(storePassportDataMock).toHaveBeenCalledWith(selfClient, state.passportData);
+ if (state.passportData && isMRZDocument(state.passportData)) {
+ expect(state.passportData.passportMetadata).toBeDefined();
+ }
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' });
+ if (state.passportData && isMRZDocument(state.passportData)) {
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(
+ PassportEvents.PASSPORT_PARSED,
+ expect.objectContaining({
+ success: true,
+ country_code: state.passportData.passportMetadata?.countryCode,
+ }),
+ );
+ }
+ });
+
+ it('handles missing passport data with PARSE_ERROR and analytics event', async () => {
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any);
+
+ vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({});
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: null });
+
+ await useProvingStore.getState().parseIDDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, {
+ error: 'PassportData is not available',
+ });
+ });
+
+ it('surfaces parsing failures when the DSC cannot be parsed', async () => {
+ const passportData = {
+ ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData),
+ dsc: 'invalid-certificate',
+ } as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({});
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ await useProvingStore.getState().parseIDDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(
+ PassportEvents.PASSPORT_PARSE_FAILED,
+ expect.objectContaining({
+ error: expect.stringMatching(/asn\\.1|parsing/i),
+ }),
+ );
+ });
+
+ it('continues when DSC metadata cannot be read and logs empty dsc payload', async () => {
+ const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData;
+ let metadataProxy: PassportData['passportMetadata'];
+ Object.defineProperty(passportData, 'passportMetadata', {
+ get() {
+ return metadataProxy;
+ },
+ set(value) {
+ metadataProxy = new Proxy(value, {
+ get(target, prop) {
+ if (prop === 'dsc') {
+ throw new Error('dsc parse failed');
+ }
+ return target[prop as keyof typeof target];
+ },
+ });
+ },
+ configurable: true,
+ });
+
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({});
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ await useProvingStore.getState().parseIDDocument(selfClient);
+
+ const parsedEvent = vi
+ .mocked(selfClient.trackEvent)
+ .mock.calls.find(([event]) => event === PassportEvents.PASSPORT_PARSED)?.[1];
+
+ expect(parsedEvent).toEqual(
+ expect.objectContaining({
+ dsc: {},
+ }),
+ );
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' });
+ });
+
+ it('emits PARSE_ERROR when storing parsed passport data fails', async () => {
+ const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({});
+
+ storePassportDataMock.mockRejectedValue(new Error('storage unavailable'));
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ await useProvingStore.getState().parseIDDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, {
+ error: 'storage unavailable',
+ });
+ });
+});
+
+describe('startFetchingData', () => {
+ const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument);
+ const fetchAllTreesMock = vi.mocked(fetchAllTreesAndCircuits);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('fetches trees and circuits for passport documents', async () => {
+ const passportData = {
+ ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData),
+ dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any,
+ documentCategory: 'passport',
+ } as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, env: 'prod' });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'passport', 'prod', 'KEY123');
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_SUCCESS);
+ });
+
+ it('fetches trees and circuits for id cards', async () => {
+ const idCardData = {
+ ...(genMockIdDoc({ idType: 'mock_id_card' }) as PassportData),
+ dsc_parsed: { authorityKeyIdentifier: 'IDKEY' } as any,
+ documentCategory: 'id_card',
+ } as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: idCardData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: idCardData, env: 'stg' });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'id_card', 'stg', 'IDKEY');
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
+ });
+
+ it('fetches aadhaar protocol data via aadhaar fetcher', async () => {
+ const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: aadhaarData, env: 'prod' });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(protocolState.aadhaar.fetch_all).toHaveBeenCalledWith('prod');
+ expect(fetchAllTreesMock).not.toHaveBeenCalled();
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' });
+ });
+
+ it('emits FETCH_ERROR when passport data is missing', async () => {
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: null });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
+ message: 'PassportData is not available',
+ });
+ });
+
+ it('emits FETCH_ERROR when DSC data is missing for passports', async () => {
+ const passportData = {
+ ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData),
+ dsc_parsed: undefined,
+ documentCategory: 'passport',
+ } as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, env: 'stg' });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
+ message: 'Missing parsed DSC in passport data',
+ });
+ });
+
+ it('emits FETCH_ERROR when protocol fetch fails', async () => {
+ const passportData = {
+ ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData),
+ dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any,
+ documentCategory: 'passport',
+ } as PassportData;
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+ fetchAllTreesMock.mockRejectedValue(new Error('network down'));
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, env: 'prod' });
+
+ await useProvingStore.getState().startFetchingData(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, {
+ message: 'network down',
+ });
+ });
+});
+
+describe('validatingDocument', () => {
+ const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument);
+ const clearPassportDataMock = vi.mocked(documentUtils.clearPassportData);
+ const reStorePassportDataWithRightCSCMock = vi.mocked(documentUtils.reStorePassportDataWithRightCSCA);
+ const markCurrentDocumentAsRegisteredMock = vi.mocked(documentUtils.markCurrentDocumentAsRegistered);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('clears data and emits PASSPORT_NOT_SUPPORTED when document is unsupported', async () => {
+ const passportData = buildPassportFixture();
+ const unsupportedCircuits = {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ };
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: unsupportedCircuits,
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, secret: '123456789', circuitType: 'register' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(clearPassportDataMock).toHaveBeenCalledWith(selfClient);
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_NOT_SUPPORTED' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(
+ PassportEvents.COMING_SOON,
+ expect.objectContaining({ status: 'registration_circuit_not_supported' }),
+ );
+ });
+
+ it('validates disclose when the user is registered', async () => {
+ const passportData = buildPassportFixture();
+ const secret = '123456789';
+ const commitment = generateCommitment(secret, AttestationIdHex.passport, passportData);
+ const commitmentTree = createCommitmentTree([commitment]);
+
+ const registerCircuit = getCircuitNameFromPassportData(passportData, 'register');
+ const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc');
+ const deployedCircuits = {
+ REGISTER: [registerCircuit],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: ['register_aadhaar'],
+ DSC: [dscCircuit],
+ DSC_ID: [],
+ };
+
+ const protocolState = buildProtocolState({
+ commitmentTree,
+ dscTree: null,
+ deployedCircuits,
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'disclose');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, secret, circuitType: 'disclose' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_SUCCESS);
+ });
+
+ it('emits PASSPORT_DATA_NOT_FOUND when disclose document is not registered', async () => {
+ const passportData = buildPassportFixture();
+ const secret = '123456789';
+ const commitmentTree = createCommitmentTree([]);
+ const registerCircuit = getCircuitNameFromPassportData(passportData, 'register');
+ const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc');
+ const deployedCircuits = {
+ REGISTER: [registerCircuit],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: ['register_aadhaar'],
+ DSC: [dscCircuit],
+ DSC_ID: [],
+ };
+
+ const protocolState = buildProtocolState({
+ commitmentTree,
+ dscTree: null,
+ deployedCircuits,
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'disclose');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, secret, circuitType: 'disclose' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_DATA_NOT_FOUND' });
+ });
+
+ it('restores data when aadhaar is already registered with alternative keys', async () => {
+ const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData;
+ const secret = '123456789';
+ const { commitment_list: commitmentList } = generateCommitmentInAppAadhaar(
+ secret,
+ AttestationIdHex.aadhaar,
+ aadhaarData,
+ {
+ public_key_0: aadhaarData.publicKey,
+ },
+ );
+ const commitmentTree = createCommitmentTree([commitmentList[0]]);
+ const deployedCircuits = {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: ['register_aadhaar'],
+ DSC: [],
+ DSC_ID: [],
+ };
+
+ const protocolState = buildProtocolState({
+ commitmentTree,
+ dscTree: null,
+ deployedCircuits,
+ publicKeys: [aadhaarData.publicKey],
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: aadhaarData, secret, circuitType: 'register' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(reStorePassportDataWithRightCSCMock).toHaveBeenCalledWith(selfClient, aadhaarData, aadhaarData.publicKey);
+ expect(markCurrentDocumentAsRegisteredMock).toHaveBeenCalledWith(selfClient);
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'ALREADY_REGISTERED' });
+ });
+
+ it('routes to account recovery when nullifier is on chain', async () => {
+ const passportData = buildPassportFixture();
+ const secret = '123456789';
+ const registerCircuit = getCircuitNameFromPassportData(passportData, 'register');
+ const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc');
+ const deployedCircuits = {
+ REGISTER: [registerCircuit],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: ['register_aadhaar'],
+ DSC: [dscCircuit],
+ DSC_ID: [],
+ };
+
+ const protocolState = buildProtocolState({
+ commitmentTree: createCommitmentTree([]),
+ dscTree: null,
+ deployedCircuits,
+ alternativeCsca: {},
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: true }),
+ } as Response),
+ );
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, secret, circuitType: 'register' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'ACCOUNT_RECOVERY_CHOICE' });
+
+ globalThis.fetch = originalFetch;
+ });
+
+ it('switches to register circuit when DSC is already in the tree', async () => {
+ const passportData = buildPassportFixture();
+ const secret = '123456789';
+ const registerCircuit = getCircuitNameFromPassportData(passportData, 'register');
+ const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc');
+ const deployedCircuits = {
+ REGISTER: [registerCircuit],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: ['register_aadhaar'],
+ DSC: [dscCircuit],
+ DSC_ID: [],
+ };
+ const dscLeaf = getLeafDscTree(passportData.dsc_parsed!, passportData.csca_parsed!);
+ const dscTree = createDscTree([dscLeaf]);
+
+ const protocolState = buildProtocolState({
+ commitmentTree: createCommitmentTree([]),
+ dscTree,
+ deployedCircuits,
+ alternativeCsca: {},
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any);
+
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = vi.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ data: false }),
+ } as Response),
+ );
+
+ await useProvingStore.getState().init(selfClient, 'dsc');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData, secret, circuitType: 'dsc' });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(useProvingStore.getState().circuitType).toBe('register');
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' });
+
+ globalThis.fetch = originalFetch;
+ });
+
+ it('emits VALIDATION_ERROR when validation throws', async () => {
+ const protocolState = buildProtocolState({
+ commitmentTree: null,
+ dscTree: null,
+ deployedCircuits: {
+ REGISTER: [],
+ REGISTER_ID: [],
+ REGISTER_AADHAAR: [],
+ DSC: [],
+ DSC_ID: [],
+ },
+ });
+ const selfClient = createSelfClient(protocolState);
+
+ loadSelectedDocumentMock.mockResolvedValue({ data: buildPassportFixture() } as any);
+
+ await useProvingStore.getState().init(selfClient, 'register');
+ actorMock.send.mockClear();
+ vi.mocked(selfClient.trackEvent).mockClear();
+
+ useProvingStore.setState({ passportData: null });
+
+ await useProvingStore.getState().validatingDocument(selfClient);
+
+ expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_ERROR' });
+ expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_FAILED, {
+ message: 'PassportData is not available',
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index d9e270a75..85f78edea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8700,6 +8700,7 @@ __metadata:
resolution: "@selfxyz/mobile-sdk-alpha@workspace:packages/mobile-sdk-alpha"
dependencies:
"@babel/runtime": "npm:^7.28.3"
+ "@openpassport/zk-kit-lean-imt": "npm:^0.0.6"
"@selfxyz/common": "workspace:^"
"@selfxyz/euclid": "npm:^0.6.1"
"@testing-library/react": "npm:^14.1.2"
@@ -8719,6 +8720,7 @@ __metadata:
jsdom: "npm:^25.0.1"
lottie-react-native: "npm:7.2.2"
node-forge: "npm:^1.3.1"
+ poseidon-lite: "npm:^0.3.0"
prettier: "npm:^3.5.3"
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"