mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Merge pull request #1602 from selfxyz/release/staging-2026-01-13
Mobile Release 2.9.11
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.9.10</string>
|
||||
<string>2.9.11</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@selfxyz/mobile-app",
|
||||
"version": "2.9.10",
|
||||
"version": "2.9.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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<typeof createDeeplinkNavigationState>,
|
||||
): 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<typeof navigationRef.navigate>[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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Mnemonic words={mnemonic} onRevealWords={loadMnemonic} />
|
||||
<Description>
|
||||
This phrase is the only way to recover your account. Keep it secret,
|
||||
keep it safe.
|
||||
</Description>
|
||||
<Description>{getRecoveryPhraseWarningMessage()}</Description>
|
||||
</ExpandableBottomLayout.BottomSection>
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={() => setOpen(true)}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{currentLevel.toUpperCase()}
|
||||
</Text>
|
||||
<ChevronDown color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
|
||||
<Sheet
|
||||
modal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
snapPoints={[50]}
|
||||
animation="medium"
|
||||
dismissOnSnapToBottom
|
||||
>
|
||||
<Sheet.Overlay />
|
||||
<Sheet.Frame
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius="$9"
|
||||
borderTopRightRadius="$9"
|
||||
>
|
||||
<YStack padding="$4">
|
||||
<XStack
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
marginBottom="$4"
|
||||
>
|
||||
<Text fontSize="$8" fontFamily={dinot}>
|
||||
Select log level
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => setOpen(false)}
|
||||
padding="$2"
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
<ChevronDown
|
||||
color={slate500}
|
||||
strokeWidth={2.5}
|
||||
style={{ transform: [{ rotate: '180deg' }] }}
|
||||
/>
|
||||
</Button>
|
||||
</XStack>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{logLevels.map(level => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
onPress={() => {
|
||||
setOpen(false);
|
||||
onSelect(level);
|
||||
}}
|
||||
>
|
||||
<XStack
|
||||
paddingVertical="$3"
|
||||
paddingHorizontal="$2"
|
||||
borderBottomWidth={1}
|
||||
borderBottomColor={slate200}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{currentLevel === level && (
|
||||
<Check color={slate600} size={20} />
|
||||
)}
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</YStack>
|
||||
</Sheet.Frame>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
const { clearDocumentCatalogForMigrationTesting } = usePassport();
|
||||
const clearPointEvents = usePointEventStore(state => state.clearEvents);
|
||||
@@ -547,57 +650,6 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
paddingTop="$4"
|
||||
paddingBottom={paddingBottom}
|
||||
>
|
||||
<ParameterSection
|
||||
icon={<IdIcon />}
|
||||
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 }) => (
|
||||
<YStack gap="$2" key={label}>
|
||||
<Button
|
||||
style={{ backgroundColor: 'white' }}
|
||||
borderColor={slate200}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
padding={0}
|
||||
onPress={onPress}
|
||||
>
|
||||
<XStack
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
paddingVertical="$3"
|
||||
paddingLeft="$4"
|
||||
paddingRight="$1.5"
|
||||
>
|
||||
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
|
||||
{label}
|
||||
</Text>
|
||||
<ChevronRight color={slate500} strokeWidth={2.5} />
|
||||
</XStack>
|
||||
</Button>
|
||||
</YStack>
|
||||
))}
|
||||
</ParameterSection>
|
||||
|
||||
<ParameterSection
|
||||
icon={<BugIcon />}
|
||||
title="Debug Shortcuts"
|
||||
@@ -696,38 +748,10 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
|
||||
title="Log Level"
|
||||
description="Configure logging verbosity"
|
||||
>
|
||||
<YStack gap="$2">
|
||||
{(['debug', 'info', 'warn', 'error'] as const).map(level => (
|
||||
<Button
|
||||
key={level}
|
||||
backgroundColor={
|
||||
loggingSeverity === level ? '$green9' : slate200
|
||||
}
|
||||
borderRadius="$2"
|
||||
height="$5"
|
||||
onPress={() => {
|
||||
setLoggingSeverity(level);
|
||||
}}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
paddingHorizontal="$4"
|
||||
pressStyle={{
|
||||
opacity: 0.8,
|
||||
scale: 0.98,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={loggingSeverity === level ? white : slate600}
|
||||
fontSize="$5"
|
||||
fontFamily={dinot}
|
||||
fontWeight="600"
|
||||
>
|
||||
{level.toUpperCase()}
|
||||
</Text>
|
||||
{loggingSeverity === level && <Check color={white} size={20} />}
|
||||
</Button>
|
||||
))}
|
||||
</YStack>
|
||||
<LogLevelSelector
|
||||
currentLevel={loggingSeverity}
|
||||
onSelect={setLoggingSeverity}
|
||||
/>
|
||||
</ParameterSection>
|
||||
|
||||
<ParameterSection
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Button, ScrollView, Spinner, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { Check, Eraser } from '@tamagui/lucide-icons';
|
||||
import { Check, Eraser, HousePlus } from '@tamagui/lucide-icons';
|
||||
|
||||
import type {
|
||||
DocumentCatalog,
|
||||
@@ -33,6 +33,8 @@ import { usePassport } from '@/providers/passportDataProvider';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
const PassportDataSelector = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
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 (
|
||||
<YStack gap="$3" alignItems="center" padding="$4">
|
||||
@@ -196,6 +237,10 @@ const PassportDataSelector = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const hasUnregisteredDocuments = documentCatalog.documents.some(
|
||||
doc => !doc.isRegistered,
|
||||
);
|
||||
|
||||
return (
|
||||
<YStack gap="$3" width="100%">
|
||||
<Text
|
||||
@@ -206,6 +251,21 @@ const PassportDataSelector = () => {
|
||||
>
|
||||
Available Documents
|
||||
</Text>
|
||||
{hasUnregisteredDocuments && (
|
||||
<YStack
|
||||
padding="$3"
|
||||
backgroundColor="#fff3cd"
|
||||
borderRadius="$3"
|
||||
borderWidth={1}
|
||||
borderColor="#ffc107"
|
||||
>
|
||||
<Text color="#856404" fontSize="$3" textAlign="center">
|
||||
⚠️ 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.
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
{documentCatalog.documents.map((metadata: DocumentMetadata) => (
|
||||
<YStack
|
||||
key={metadata.id}
|
||||
@@ -217,12 +277,13 @@ const PassportDataSelector = () => {
|
||||
: 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 }}
|
||||
>
|
||||
<XStack
|
||||
@@ -241,7 +302,9 @@ const PassportDataSelector = () => {
|
||||
}
|
||||
borderColor={textBlack}
|
||||
borderWidth={1}
|
||||
onPress={() => handleDocumentSelection(metadata.id)}
|
||||
onPress={() =>
|
||||
handleDocumentSelection(metadata.id, metadata.isRegistered)
|
||||
}
|
||||
>
|
||||
{documentCatalog.selectedDocumentId === metadata.id && (
|
||||
<Check size={12} color="white" />
|
||||
@@ -256,19 +319,36 @@ const PassportDataSelector = () => {
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
<Button
|
||||
backgroundColor="white"
|
||||
justifyContent="center"
|
||||
borderColor={borderColor}
|
||||
borderWidth={1}
|
||||
size="$3"
|
||||
onPress={e => {
|
||||
e.stopPropagation();
|
||||
handleDeleteButtonPress(metadata.id);
|
||||
}}
|
||||
>
|
||||
<Eraser color={textBlack} size={16} />
|
||||
</Button>
|
||||
<XStack gap="$3">
|
||||
{metadata.isRegistered !== true && (
|
||||
<Button
|
||||
backgroundColor="white"
|
||||
justifyContent="center"
|
||||
borderColor={borderColor}
|
||||
borderWidth={1}
|
||||
size="$3"
|
||||
onPress={e => {
|
||||
e.stopPropagation();
|
||||
handleRegisterDocument(metadata.id);
|
||||
}}
|
||||
>
|
||||
<HousePlus color={textBlack} size={16} />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
backgroundColor="white"
|
||||
justifyContent="center"
|
||||
borderColor={borderColor}
|
||||
borderWidth={1}
|
||||
size="$3"
|
||||
onPress={e => {
|
||||
e.stopPropagation();
|
||||
handleDeleteButtonPress(metadata.id, metadata.isRegistered);
|
||||
}}
|
||||
>
|
||||
<Eraser color={textBlack} size={16} />
|
||||
</Button>
|
||||
</XStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
</Title>
|
||||
<Description style={{ paddingBottom: 10 }}>
|
||||
This phrase is the only way to recover your account. Keep it secret,
|
||||
keep it safe.
|
||||
{getRecoveryPhraseWarningMessage()}
|
||||
</Description>
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import { STORAGE_NAME } from '@/services/cloud-backup';
|
||||
import type { Mnemonic } from '@/types/mnemonic';
|
||||
|
||||
/**
|
||||
* Gets the recovery phrase warning message based on the current platform.
|
||||
* The message mentions cloud backup options available for the OS.
|
||||
* @returns The recovery phrase warning message
|
||||
*/
|
||||
export function getRecoveryPhraseWarningMessage(): string {
|
||||
const cloudBackupName = STORAGE_NAME;
|
||||
return `Using this phrase or ${cloudBackupName} backup are the only ways to recover your account. Keep it secret, keep it safe.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if an object is a valid Mnemonic
|
||||
* @param obj The object to check
|
||||
|
||||
@@ -36,6 +36,7 @@ jest.mock('@/navigation', () => ({
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<typeof import('xstate')>('xstate');
|
||||
return {
|
||||
...actual,
|
||||
createActor: vi.fn(() => actorMock),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../src/documents/utils', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../src/documents/utils')>('../../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<typeof import('../../src/stores')>('../../src/stores');
|
||||
return {
|
||||
...actual,
|
||||
fetchAllTreesAndCircuits: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const createCommitmentTree = (commitments: string[]) => {
|
||||
const tree = new LeanIMT<bigint>((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<GBRSMITH<<BILL<<<<<<<<<<<<<<<<<<<<<<<<<<<<7475772739GBR6911111M1601013<<<<<<<<<<<<<<00',
|
||||
dsc: '-----BEGIN CERTIFICATE-----\nMIIBvTCCASagAwIBAgIJAJc1qz3hVp5NMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV\nBAMMCFRlc3QgQ0EgMQ==\n-----END CERTIFICATE-----',
|
||||
eContent: [1, 2, 3, 4, 5],
|
||||
signedAttr: [6, 7, 8, 9, 10],
|
||||
encryptedDigest: [11, 12, 13, 14, 15],
|
||||
documentType: 'passport',
|
||||
documentCategory: 'passport',
|
||||
mock: true,
|
||||
passportMetadata: {
|
||||
dataGroups: '1,2,3',
|
||||
dg1Size: 88,
|
||||
dg1HashSize: 32,
|
||||
dg1HashFunction: 'sha256',
|
||||
dg1HashOffset: 0,
|
||||
dgPaddingBytes: 0,
|
||||
eContentSize: 5,
|
||||
eContentHashFunction: 'sha256',
|
||||
eContentHashOffset: 0,
|
||||
signedAttrSize: 5,
|
||||
signedAttrHashFunction: 'sha256',
|
||||
signatureAlgorithm: 'rsa',
|
||||
saltLength: 0,
|
||||
curveOrExponent: '65537',
|
||||
signatureAlgorithmBits: 2048,
|
||||
countryCode: 'GBR',
|
||||
cscaFound: true,
|
||||
cscaHashFunction: 'sha256',
|
||||
cscaSignatureAlgorithm: 'rsa',
|
||||
cscaSaltLength: 0,
|
||||
cscaCurveOrExponent: '65537',
|
||||
cscaSignatureAlgorithmBits: 2048,
|
||||
dsc: '-----BEGIN CERTIFICATE-----\nMIIBvTCCASagAwIBAgIJAJc1qz3hVp5NMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV\nBAMMCFRlc3QgQ0EgMQ==\n-----END CERTIFICATE-----',
|
||||
csca: '-----BEGIN CERTIFICATE-----\nMIIBvTCCASagAwIBAgIJAJc1qz3hVp5NMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV\nBAMMCFRlc3QgQ0EgMQ==\n-----END CERTIFICATE-----',
|
||||
},
|
||||
dsc_parsed: {
|
||||
id: 'test123',
|
||||
issuer: 'UTO',
|
||||
validity: {
|
||||
notBefore: '2020-01-01',
|
||||
notAfter: '2030-01-01',
|
||||
},
|
||||
subjectKeyIdentifier: 'test123456789',
|
||||
authorityKeyIdentifier: 'test987654321',
|
||||
signatureAlgorithm: 'rsa',
|
||||
hashAlgorithm: 'sha256',
|
||||
publicKeyDetails: undefined,
|
||||
tbsBytes: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
tbsBytesLength: '10',
|
||||
rawPem:
|
||||
'-----BEGIN CERTIFICATE-----\nMIIBvTCCASagAwIBAgIJAJc1qz3hVp5NMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV\nBAMMCFRlc3QgQ0EgMQ==\n-----END CERTIFICATE-----',
|
||||
rawTxt: '',
|
||||
publicKeyAlgoOID: '',
|
||||
},
|
||||
csca_parsed: {
|
||||
id: 'csca123',
|
||||
issuer: 'UTO',
|
||||
validity: {
|
||||
notBefore: '2020-01-01',
|
||||
notAfter: '2030-01-01',
|
||||
},
|
||||
subjectKeyIdentifier: 'csca123456789',
|
||||
authorityKeyIdentifier: 'csca987654321',
|
||||
signatureAlgorithm: 'rsa',
|
||||
hashAlgorithm: 'sha256',
|
||||
publicKeyDetails: undefined,
|
||||
tbsBytes: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
||||
tbsBytesLength: '10',
|
||||
rawPem:
|
||||
'-----BEGIN CERTIFICATE-----\nMIIBvTCCASagAwIBAgIJAJc1qz3hVp5NMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV\nBAMMCFRlc3QgQ0EgMQ==\n-----END CERTIFICATE-----',
|
||||
rawTxt: '',
|
||||
publicKeyAlgoOID: '',
|
||||
},
|
||||
}) as PassportData;
|
||||
|
||||
const buildProtocolState = ({
|
||||
commitmentTree,
|
||||
dscTree,
|
||||
deployedCircuits,
|
||||
alternativeCsca,
|
||||
publicKeys,
|
||||
}: {
|
||||
commitmentTree: string | null;
|
||||
dscTree: string | null;
|
||||
deployedCircuits: {
|
||||
REGISTER: string[];
|
||||
REGISTER_ID: string[];
|
||||
REGISTER_AADHAAR: string[];
|
||||
DSC: string[];
|
||||
DSC_ID: string[];
|
||||
};
|
||||
alternativeCsca?: Record<string, string>;
|
||||
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<typeof buildProtocolState>) =>
|
||||
({
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user