mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
Show badge for inactive documents (#1487)
* Show badge for inactive documents * fix * refactor to use the new flag * add inactive check to ProveScreen * lint * fix for proving button not working * use new qrHashlogic * increase bundle size threshold to 46MB * remove commented out line * add kyc related changes --------- Co-authored-by: seshanthS <seshanth@protonmail.com>
This commit is contained in:
committed by
GitHub
parent
ec8b8fc419
commit
abf01c82c0
@@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
|
||||
// Bundle size thresholds in MB - easy to update!
|
||||
const BUNDLE_THRESHOLDS_MB = {
|
||||
// TODO: fix temporary bundle bump
|
||||
ios: 45,
|
||||
android: 45,
|
||||
ios: 46,
|
||||
android: 46,
|
||||
};
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet } from 'react-native';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Dimensions, Image, Pressable, StyleSheet } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { Text, XStack, YStack } from 'tamagui';
|
||||
import { Separator, Text, XStack, YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import type { AadhaarData } from '@selfxyz/common';
|
||||
import type { PassportData } from '@selfxyz/common/types/passport';
|
||||
@@ -16,8 +17,18 @@ import {
|
||||
isKycDocument,
|
||||
isMRZDocument,
|
||||
} from '@selfxyz/common/utils/types';
|
||||
import { WarningTriangleIcon } from '@selfxyz/euclid/dist/components/icons/WarningTriangleIcon';
|
||||
import { RoundFlag } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import {
|
||||
black,
|
||||
red600,
|
||||
slate100,
|
||||
slate300,
|
||||
slate400,
|
||||
slate500,
|
||||
white,
|
||||
yellow500,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot, plexMono } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import CardBackgroundId1 from '@/assets/images/card_background_id1.png';
|
||||
@@ -28,14 +39,21 @@ import CardBackgroundId5 from '@/assets/images/card_background_id5.png';
|
||||
import CardBackgroundId6 from '@/assets/images/card_background_id6.png';
|
||||
import DevCardLogo from '@/assets/images/dev_card_logo.svg';
|
||||
import DevCardWave from '@/assets/images/dev_card_wave.svg';
|
||||
import LogoGray from '@/assets/images/logo_gray.svg';
|
||||
import SelfLogoPending from '@/assets/images/self_logo_pending.svg';
|
||||
import WaveOverlay from '@/assets/images/wave_overlay.png';
|
||||
import { getSecurityLevel } from '@/components/homescreen/cardSecurityBadge';
|
||||
import { cardStyles } from '@/components/homescreen/cardStyles';
|
||||
import KycIdCard from '@/components/homescreen/KycIdCard';
|
||||
import { SvgXml } from '@/components/homescreen/SvgXmlWrapper';
|
||||
import { useCardDimensions } from '@/hooks/useCardDimensions';
|
||||
import { getBackgroundIndex } from '@/utils/cardBackgroundSelector';
|
||||
import { getDocumentAttributes } from '@/utils/documentAttributes';
|
||||
import {
|
||||
formatDateFromYYMMDD,
|
||||
getDocumentAttributes,
|
||||
getNameAndSurname,
|
||||
} from '@/utils/documentAttributes';
|
||||
import { registerModalCallbacks } from '@/utils/modalCallbackRegistry';
|
||||
|
||||
const CARD_BACKGROUNDS = [
|
||||
CardBackgroundId1,
|
||||
@@ -279,6 +297,7 @@ interface IdCardLayoutAttributes {
|
||||
idDocument: PassportData | AadhaarData | KycData | null;
|
||||
selected: boolean;
|
||||
hidden: boolean;
|
||||
isInactive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,7 +312,38 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
idDocument,
|
||||
selected,
|
||||
hidden,
|
||||
isInactive = false,
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
const navigateToDocumentOnboarding = useCallback(() => {
|
||||
switch (idDocument?.documentCategory) {
|
||||
case 'passport':
|
||||
case 'id_card':
|
||||
navigation.navigate('DocumentOnboarding');
|
||||
break;
|
||||
case 'aadhaar':
|
||||
navigation.navigate('AadhaarUpload', { countryCode: 'IND' });
|
||||
break;
|
||||
}
|
||||
}, [idDocument?.documentCategory, navigation]);
|
||||
|
||||
const handleInactivePress = useCallback(() => {
|
||||
const callbackId = registerModalCallbacks({
|
||||
onButtonPress: navigateToDocumentOnboarding,
|
||||
onModalDismiss: () => {},
|
||||
});
|
||||
|
||||
navigation.navigate('Modal', {
|
||||
titleText: 'Your ID needs to be reactivated to continue',
|
||||
bodyText:
|
||||
'Make sure that you have your document and recovery method ready.',
|
||||
buttonText: 'Continue',
|
||||
secondaryButtonText: 'Not now',
|
||||
callbackId,
|
||||
});
|
||||
}, [navigateToDocumentOnboarding, navigation]);
|
||||
|
||||
// Early return if document is null
|
||||
// Call hooks at the top, before any conditional returns
|
||||
const {
|
||||
cardWidth,
|
||||
@@ -399,7 +449,47 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
const truncatedId = getTruncatedId();
|
||||
|
||||
return (
|
||||
<YStack width="100%" alignItems="center" justifyContent="center">
|
||||
// Container wrapper to handle shadow space properly
|
||||
<YStack
|
||||
width="100%" // Add space for horizontal margins
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isInactive && (
|
||||
<Pressable
|
||||
style={styles.inactiveWarningContainer}
|
||||
onPress={handleInactivePress}
|
||||
>
|
||||
<XStack
|
||||
backgroundColor={red600}
|
||||
borderRadius={8}
|
||||
padding={16}
|
||||
gap={16}
|
||||
>
|
||||
<YStack padding={8} backgroundColor={white} borderRadius={8}>
|
||||
<WarningTriangleIcon color={yellow500} />
|
||||
</YStack>
|
||||
<YStack gap={4}>
|
||||
<Text
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontSize={16}
|
||||
fontWeight="500"
|
||||
>
|
||||
Your document is inactive
|
||||
</Text>
|
||||
<Text
|
||||
color={white}
|
||||
fontFamily={dinot}
|
||||
fontSize={14}
|
||||
fontWeight="400"
|
||||
>
|
||||
Tap here to recover your ID
|
||||
</Text>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
)}
|
||||
<YStack
|
||||
width={cardWidth}
|
||||
height={cardHeight}
|
||||
@@ -566,23 +656,45 @@ const IdCardLayout: FC<IdCardLayoutAttributes> = ({
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
{/* Security Badge */}
|
||||
<YStack
|
||||
backgroundColor="rgba(0, 0, 0, 0.5)"
|
||||
borderRadius={30}
|
||||
paddingHorizontal={padding * 0.6}
|
||||
paddingVertical={padding * 0.3}
|
||||
>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={fontSize.badge}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
textTransform="uppercase"
|
||||
letterSpacing={0.6}
|
||||
{/* Bottom Right: Badges */}
|
||||
<YStack alignItems="flex-end" gap={4}>
|
||||
{isInactive && (
|
||||
<YStack
|
||||
backgroundColor={red600}
|
||||
borderRadius={30}
|
||||
paddingHorizontal={padding * 0.6}
|
||||
paddingVertical={padding * 0.3}
|
||||
>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={fontSize.badge}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
textTransform="uppercase"
|
||||
letterSpacing={0.6}
|
||||
>
|
||||
INACTIVE
|
||||
</Text>
|
||||
</YStack>
|
||||
)}
|
||||
{/* Security Badge */}
|
||||
<YStack
|
||||
backgroundColor="rgba(0, 0, 0, 0.5)"
|
||||
borderRadius={30}
|
||||
paddingHorizontal={padding * 0.6}
|
||||
paddingVertical={padding * 0.3}
|
||||
>
|
||||
{securityLevel}
|
||||
</Text>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={fontSize.badge}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
textTransform="uppercase"
|
||||
letterSpacing={0.6}
|
||||
>
|
||||
{securityLevel}
|
||||
</Text>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</XStack>
|
||||
</YStack>
|
||||
@@ -605,6 +717,10 @@ const styles = StyleSheet.create({
|
||||
height: '90%',
|
||||
opacity: 0.6,
|
||||
},
|
||||
inactiveWarningContainer: {
|
||||
width: '100%',
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default IdCardLayout;
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface BottomVerifyBarProps {
|
||||
isReadyToProve: boolean;
|
||||
isDocumentExpired: boolean;
|
||||
testID?: string;
|
||||
hasCheckedForInactiveDocument: boolean;
|
||||
}
|
||||
|
||||
export const BottomVerifyBar: React.FC<BottomVerifyBarProps> = ({
|
||||
@@ -28,6 +29,7 @@ export const BottomVerifyBar: React.FC<BottomVerifyBarProps> = ({
|
||||
isReadyToProve,
|
||||
isDocumentExpired,
|
||||
testID = 'bottom-verify-bar',
|
||||
hasCheckedForInactiveDocument,
|
||||
}) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@@ -46,6 +48,7 @@ export const BottomVerifyBar: React.FC<BottomVerifyBarProps> = ({
|
||||
isScrollable={isScrollable}
|
||||
isReadyToProve={isReadyToProve}
|
||||
isDocumentExpired={isDocumentExpired}
|
||||
hasCheckedForInactiveDocument={hasCheckedForInactiveDocument}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
import {
|
||||
brutforceSignatureAlgorithmDsc,
|
||||
calculateContentHash,
|
||||
inferDocumentCategory,
|
||||
} from '@selfxyz/common/utils';
|
||||
import { parseCertificateSimple } from '@selfxyz/common/utils/certificate_parsing/parseCertificateSimple';
|
||||
import type {
|
||||
@@ -869,6 +870,12 @@ export async function storeDocumentWithDeduplication(
|
||||
// Store new document using contentHash as service name
|
||||
await storeDocumentDirectlyToKeychain(contentHash, passportData);
|
||||
|
||||
const documentCategory =
|
||||
passportData.documentCategory ||
|
||||
inferDocumentCategory(
|
||||
(passportData as PassportData | AadhaarData).documentType,
|
||||
);
|
||||
|
||||
// Add to catalog
|
||||
let dataField: string;
|
||||
if (isMRZDocument(passportData)) {
|
||||
@@ -886,6 +893,8 @@ export async function storeDocumentWithDeduplication(
|
||||
data: dataField,
|
||||
mock: passportData.mock || false,
|
||||
isRegistered: false,
|
||||
hasExpirationDate:
|
||||
documentCategory === 'id_card' || documentCategory === 'passport',
|
||||
...(isKycDocument(passportData)
|
||||
? (() => {
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Alert, ScrollView } from 'react-native';
|
||||
import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
@@ -13,6 +13,10 @@ import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
|
||||
import BugIcon from '@/assets/icons/bug_icon.svg';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import {
|
||||
loadDocumentCatalogDirectlyFromKeychain,
|
||||
saveDocumentCatalogDirectlyToKeychain,
|
||||
} from '@/providers/passportDataProvider';
|
||||
import { ErrorInjectionSelector } from '@/screens/dev/components/ErrorInjectionSelector';
|
||||
import { LogLevelSelector } from '@/screens/dev/components/LogLevelSelector';
|
||||
import { ParameterSection } from '@/screens/dev/components/ParameterSection';
|
||||
@@ -52,6 +56,32 @@ const DevSettingsScreen: React.FC = () => {
|
||||
handleClearPendingVerificationsPress,
|
||||
} = useDangerZoneActions();
|
||||
|
||||
const handleRemoveExpirationDateFlagPress = () => {
|
||||
Alert.alert(
|
||||
'Remove Expiration Date Flag',
|
||||
'Are you sure you want to remove the expiration date flag for the current (selected) document?.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Remove',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const catalog = await loadDocumentCatalogDirectlyFromKeychain();
|
||||
const selectedDocumentId = catalog.selectedDocumentId;
|
||||
const selectedDocument = catalog.documents.find(
|
||||
document => document.id === selectedDocumentId,
|
||||
);
|
||||
|
||||
if (selectedDocument) {
|
||||
delete selectedDocument.hasExpirationDate;
|
||||
|
||||
await saveDocumentCatalogDirectlyToKeychain(catalog);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
@@ -109,6 +139,7 @@ const DevSettingsScreen: React.FC = () => {
|
||||
onResetBackupState={handleResetBackupStatePress}
|
||||
onClearBackupEvents={handleClearBackupEventsPress}
|
||||
onClearPendingKyc={handleClearPendingVerificationsPress}
|
||||
onRemoveExpirationDateFlag={handleRemoveExpirationDateFlagPress}
|
||||
/>
|
||||
</YStack>
|
||||
</ScrollView>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DangerZoneSectionProps {
|
||||
onResetBackupState: () => void;
|
||||
onClearBackupEvents: () => void;
|
||||
onClearPendingKyc: () => void;
|
||||
onRemoveExpirationDateFlag: () => void;
|
||||
}
|
||||
|
||||
export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
|
||||
@@ -32,6 +33,7 @@ export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
|
||||
onResetBackupState,
|
||||
onClearBackupEvents,
|
||||
onClearPendingKyc,
|
||||
onRemoveExpirationDateFlag,
|
||||
}) => {
|
||||
const dangerActions = [
|
||||
{
|
||||
@@ -64,6 +66,11 @@ export const DangerZoneSection: React.FC<DangerZoneSectionProps> = ({
|
||||
onPress: onClearPendingKyc,
|
||||
dangerTheme: true,
|
||||
},
|
||||
{
|
||||
label: 'Remove expiration date flag',
|
||||
onPress: onRemoveExpirationDateFlag,
|
||||
dangerTheme: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
checkDocumentExpiration,
|
||||
getDocumentAttributes,
|
||||
} from '@/utils/documentAttributes';
|
||||
import { isDocumentInactive } from '@/utils/documents';
|
||||
|
||||
const HomeScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
@@ -80,6 +81,9 @@ const HomeScreen: React.FC = () => {
|
||||
>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const hasIncrementedOnFocus = useRef(false);
|
||||
const [isSelectedDocumentInactive, setIsSelectedDocumentInactive] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
|
||||
const { pendingVerifications, removeExpiredVerifications } =
|
||||
usePendingKycStore();
|
||||
@@ -126,12 +130,28 @@ const HomeScreen: React.FC = () => {
|
||||
|
||||
const loadDocuments = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const catalog = await loadDocumentCatalog();
|
||||
const docs = await getAllDocuments();
|
||||
|
||||
setDocumentCatalog(catalog);
|
||||
setAllDocuments(docs);
|
||||
|
||||
if (catalog.selectedDocumentId) {
|
||||
const documentData = docs[catalog.selectedDocumentId];
|
||||
|
||||
if (documentData) {
|
||||
try {
|
||||
setIsSelectedDocumentInactive(
|
||||
isDocumentInactive(documentData.metadata),
|
||||
);
|
||||
} catch (error) {
|
||||
// we don't want to block the home screen from loading
|
||||
console.warn('Failed to check if document is inactive:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load documents:', error);
|
||||
}
|
||||
@@ -307,6 +327,11 @@ const HomeScreen: React.FC = () => {
|
||||
>
|
||||
<IdCardLayout
|
||||
idDocument={documentData.data}
|
||||
isInactive={
|
||||
isSelected &&
|
||||
isSelectedDocumentInactive === true &&
|
||||
!metadata.mock
|
||||
}
|
||||
selected={isSelected}
|
||||
hidden={true}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import type { DocumentMetadata } from '@selfxyz/common';
|
||||
import { isMRZDocument } from '@selfxyz/common';
|
||||
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
@@ -51,10 +52,12 @@ import {
|
||||
} from '@/services/points';
|
||||
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
||||
import { ProofStatus } from '@/stores/proofTypes';
|
||||
import { registerModalCallbacks } from '@/utils';
|
||||
import {
|
||||
checkDocumentExpiration,
|
||||
getDocumentAttributes,
|
||||
} from '@/utils/documentAttributes';
|
||||
import { isDocumentInactive } from '@/utils/documents';
|
||||
import { getDocumentTypeName } from '@/utils/documentUtils';
|
||||
|
||||
const ProveScreen: React.FC = () => {
|
||||
@@ -85,6 +88,9 @@ const ProveScreen: React.FC = () => {
|
||||
const scrollViewRef = useRef<ScrollViewType>(null);
|
||||
const hasInitializedScrollStateRef = useRef(false);
|
||||
|
||||
const [hasCheckedForInactiveDocument, setHasCheckedForInactiveDocument] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const isContentShorterThanScrollView = useMemo(
|
||||
() => scrollViewContentHeight <= scrollViewHeight + 50,
|
||||
[scrollViewContentHeight, scrollViewHeight],
|
||||
@@ -114,8 +120,70 @@ const ProveScreen: React.FC = () => {
|
||||
|
||||
const { addProofHistory } = useProofHistoryStore();
|
||||
const { loadDocumentCatalog } = usePassport();
|
||||
const navigateToDocumentOnboarding = useCallback(
|
||||
(documentMetadata: DocumentMetadata) => {
|
||||
switch (documentMetadata.documentCategory) {
|
||||
case 'passport':
|
||||
case 'id_card':
|
||||
navigate('DocumentOnboarding');
|
||||
break;
|
||||
case 'aadhaar':
|
||||
navigate('AadhaarUpload', { countryCode: 'IND' });
|
||||
break;
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't check twice
|
||||
if (hasCheckedForInactiveDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkForInactiveDocument = async () => {
|
||||
const catalog = await loadDocumentCatalog();
|
||||
const selectedDocumentId = catalog.selectedDocumentId;
|
||||
|
||||
for (const documentMetadata of catalog.documents) {
|
||||
if (
|
||||
documentMetadata.id === selectedDocumentId &&
|
||||
isDocumentInactive(documentMetadata)
|
||||
) {
|
||||
const callbackId = registerModalCallbacks({
|
||||
onButtonPress: () => navigateToDocumentOnboarding(documentMetadata),
|
||||
onModalDismiss: () => navigate('Home' as never),
|
||||
});
|
||||
|
||||
navigate('Modal', {
|
||||
titleText: 'Your ID needs to be reactivated to continue',
|
||||
bodyText:
|
||||
'Make sure that you have your document and recovery method ready.',
|
||||
buttonText: 'Continue',
|
||||
secondaryButtonText: 'Not now',
|
||||
callbackId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setHasCheckedForInactiveDocument(true);
|
||||
};
|
||||
|
||||
checkForInactiveDocument();
|
||||
}, [
|
||||
loadDocumentCatalog,
|
||||
navigateToDocumentOnboarding,
|
||||
navigate,
|
||||
hasCheckedForInactiveDocument,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCheckedForInactiveDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addHistory = async () => {
|
||||
if (provingStore.uuid && selectedApp) {
|
||||
const catalog = await loadDocumentCatalog();
|
||||
@@ -137,9 +205,19 @@ const ProveScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
addHistory();
|
||||
}, [addProofHistory, loadDocumentCatalog, provingStore.uuid, selectedApp]);
|
||||
}, [
|
||||
addProofHistory,
|
||||
provingStore.uuid,
|
||||
selectedApp,
|
||||
loadDocumentCatalog,
|
||||
hasCheckedForInactiveDocument,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCheckedForInactiveDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for actual measurements before determining initial scroll state
|
||||
// Both start at 0, causing false-positive on first render
|
||||
const hasMeasurements = scrollViewContentHeight > 0 && scrollViewHeight > 0;
|
||||
@@ -161,10 +239,11 @@ const ProveScreen: React.FC = () => {
|
||||
isContentShorterThanScrollView,
|
||||
scrollViewContentHeight,
|
||||
scrollViewHeight,
|
||||
hasCheckedForInactiveDocument,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused || !selectedApp) {
|
||||
if (!isFocused || !selectedApp || !hasCheckedForInactiveDocument) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,12 +308,21 @@ const ProveScreen: React.FC = () => {
|
||||
//removed provingStore from dependencies because it causes infinite re-render on longpressing the button
|
||||
//as it sets provingStore.setUserConfirmed()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedApp?.sessionId, isFocused, selfClient]);
|
||||
}, [
|
||||
selectedApp?.sessionId,
|
||||
isFocused,
|
||||
selfClient,
|
||||
hasCheckedForInactiveDocument,
|
||||
]);
|
||||
|
||||
// Enhance selfApp with user's points address if not already set
|
||||
useEffect(() => {
|
||||
console.log('useEffect selectedApp', selectedApp);
|
||||
if (!selectedApp || selectedApp.selfDefinedData) {
|
||||
if (
|
||||
!selectedApp ||
|
||||
selectedApp.selfDefinedData ||
|
||||
!hasCheckedForInactiveDocument
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,11 +365,11 @@ const ProveScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
enhanceApp();
|
||||
}, [selectedApp, selfClient]);
|
||||
}, [selectedApp, selfClient, hasCheckedForInactiveDocument]);
|
||||
|
||||
function onVerify() {
|
||||
provingStore.setUserConfirmed(selfClient);
|
||||
buttonTap();
|
||||
provingStore.setUserConfirmed(selfClient);
|
||||
trackEvent(ProofEvents.PROOF_VERIFY_CONFIRMATION_ACCEPTED, {
|
||||
appName: selectedApp?.appName,
|
||||
sessionId: provingStore.uuid,
|
||||
@@ -388,6 +476,7 @@ const ProveScreen: React.FC = () => {
|
||||
isReadyToProve={isReadyToProve}
|
||||
isDocumentExpired={isDocumentExpired}
|
||||
testID="prove-screen-verify-bar"
|
||||
hasCheckedForInactiveDocument={hasCheckedForInactiveDocument}
|
||||
/>
|
||||
|
||||
{formattedUserId && selectedApp?.userId && (
|
||||
|
||||
22
app/src/utils/documents.ts
Normal file
22
app/src/utils/documents.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 type { DocumentMetadata } from '@selfxyz/common';
|
||||
|
||||
export const isDocumentInactive = (metadata: DocumentMetadata): boolean => {
|
||||
if (
|
||||
metadata.documentCategory === 'id_card' ||
|
||||
metadata.documentCategory === 'passport' ||
|
||||
metadata.documentCategory === 'kyc'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//for aadhaar migration
|
||||
if (metadata.hasExpirationDate === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
132
app/tests/src/utils/documents.test.ts
Normal file
132
app/tests/src/utils/documents.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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 type { DocumentMetadata } from '@selfxyz/common';
|
||||
|
||||
import { isDocumentInactive } from '@/utils/documents';
|
||||
|
||||
const createMockMetadata = (
|
||||
overrides: Partial<DocumentMetadata> = {},
|
||||
): DocumentMetadata =>
|
||||
({
|
||||
id: 'test-doc-id',
|
||||
documentType: 'aadhaar',
|
||||
documentCategory: 'aadhaar',
|
||||
data: 'test-data',
|
||||
mock: false,
|
||||
isRegistered: true,
|
||||
registeredAt: Date.now(),
|
||||
...overrides,
|
||||
}) as DocumentMetadata;
|
||||
|
||||
describe('isDocumentInactive', () => {
|
||||
describe('registered pre-document expiration', () => {
|
||||
describe('when hasExpirationDate is undefined', () => {
|
||||
it('returns true for aadhaar document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'aadhaar',
|
||||
hasExpirationDate: undefined,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for passport document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'passport',
|
||||
hasExpirationDate: undefined,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for id_card document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'id_card',
|
||||
hasExpirationDate: undefined,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered post-document expiration', () => {
|
||||
describe('when hasExpirationDate is true', () => {
|
||||
it('returns false for aadhaar document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'aadhaar',
|
||||
hasExpirationDate: true,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for passport document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'passport',
|
||||
hasExpirationDate: true,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for id_card document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'id_card',
|
||||
hasExpirationDate: true,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when hasExpirationDate is false', () => {
|
||||
it('returns false for aadhaar document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'aadhaar',
|
||||
hasExpirationDate: false,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for passport document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'passport',
|
||||
hasExpirationDate: false,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for id_card document', () => {
|
||||
const metadata = createMockMetadata({
|
||||
documentCategory: 'id_card',
|
||||
hasExpirationDate: false,
|
||||
});
|
||||
|
||||
const result = isDocumentInactive(metadata);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -574,13 +574,13 @@ export function processQRDataSimple(qrData: string) {
|
||||
const extractedFields = extractQRDataFields(qrDataBytes);
|
||||
|
||||
// Calculate qrHash exclude timestamp (positions 9-25, 17 bytes)
|
||||
// const qrDataWithoutTimestamp = [
|
||||
// ...Array.from(qrDataPadded.slice(0, 9)),
|
||||
// ...Array.from(qrDataPadded.slice(9, 26)).map((x) => 0),
|
||||
// ...Array.from(qrDataPadded.slice(26)),
|
||||
// ];
|
||||
// const qrHash = packBytesAndPoseidon(qrDataWithoutTimestamp);
|
||||
const qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
|
||||
const qrDataWithoutTimestamp = [
|
||||
...Array.from(qrDataPadded.slice(0, 9)),
|
||||
...Array.from(qrDataPadded.slice(9, 26)).map((x) => 0),
|
||||
...Array.from(qrDataPadded.slice(26)),
|
||||
];
|
||||
const qrHash = packBytesAndPoseidon(qrDataWithoutTimestamp);
|
||||
// const qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
|
||||
const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1);
|
||||
|
||||
const photoHash = packBytesAndPoseidon(photo.bytes.map(Number));
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface DocumentMetadata {
|
||||
mock: boolean; // whether this is a mock document
|
||||
isRegistered?: boolean; // whether the document is registered onChain
|
||||
registeredAt?: number; // timestamp (epoch ms) when document was registered
|
||||
hasExpirationDate?: boolean; // whether the document has an expiration date
|
||||
idType?: string; // for KYC documents: the ID type used (e.g. "passport", "drivers_licence")
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ interface HeldPrimaryButtonProveScreenProps {
|
||||
isScrollable: boolean;
|
||||
isReadyToProve: boolean;
|
||||
isDocumentExpired: boolean;
|
||||
hasCheckedForInactiveDocument: boolean;
|
||||
}
|
||||
|
||||
interface ButtonContext {
|
||||
@@ -29,6 +30,7 @@ interface ButtonContext {
|
||||
isReadyToProve: boolean;
|
||||
onVerify: () => void;
|
||||
isDocumentExpired: boolean;
|
||||
hasCheckedForInactiveDocument: boolean;
|
||||
}
|
||||
|
||||
type ButtonEvent =
|
||||
@@ -38,6 +40,7 @@ type ButtonEvent =
|
||||
hasScrolledToBottom: boolean;
|
||||
isReadyToProve: boolean;
|
||||
isDocumentExpired: boolean;
|
||||
hasCheckedForInactiveDocument: boolean;
|
||||
}
|
||||
| { type: 'VERIFY' };
|
||||
|
||||
@@ -56,6 +59,7 @@ const buttonMachine = createMachine(
|
||||
isReadyToProve: false,
|
||||
onVerify: input.onVerify,
|
||||
isDocumentExpired: false,
|
||||
hasCheckedForInactiveDocument: false,
|
||||
}),
|
||||
on: {
|
||||
PROPS_UPDATED: {
|
||||
@@ -177,13 +181,15 @@ const buttonMachine = createMachine(
|
||||
context.selectedAppSessionId !== event.selectedAppSessionId ||
|
||||
context.hasScrolledToBottom !== event.hasScrolledToBottom ||
|
||||
context.isReadyToProve !== event.isReadyToProve ||
|
||||
context.isDocumentExpired !== event.isDocumentExpired
|
||||
context.isDocumentExpired !== event.isDocumentExpired ||
|
||||
context.hasCheckedForInactiveDocument !== event.hasCheckedForInactiveDocument
|
||||
) {
|
||||
return {
|
||||
selectedAppSessionId: event.selectedAppSessionId,
|
||||
hasScrolledToBottom: event.hasScrolledToBottom,
|
||||
isReadyToProve: event.isReadyToProve,
|
||||
isDocumentExpired: event.isDocumentExpired,
|
||||
hasCheckedForInactiveDocument: event.hasCheckedForInactiveDocument,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -203,6 +209,7 @@ export const HeldPrimaryButtonProveScreen: React.FC<HeldPrimaryButtonProveScreen
|
||||
isScrollable,
|
||||
isReadyToProve,
|
||||
isDocumentExpired,
|
||||
hasCheckedForInactiveDocument,
|
||||
}) => {
|
||||
const [state, send] = useMachine(buttonMachine, {
|
||||
input: { onVerify },
|
||||
@@ -215,10 +222,18 @@ export const HeldPrimaryButtonProveScreen: React.FC<HeldPrimaryButtonProveScreen
|
||||
hasScrolledToBottom,
|
||||
isReadyToProve,
|
||||
isDocumentExpired,
|
||||
hasCheckedForInactiveDocument,
|
||||
});
|
||||
}, [selectedAppSessionId, hasScrolledToBottom, isReadyToProve, isDocumentExpired, send]);
|
||||
}, [
|
||||
selectedAppSessionId,
|
||||
hasScrolledToBottom,
|
||||
isReadyToProve,
|
||||
isDocumentExpired,
|
||||
hasCheckedForInactiveDocument,
|
||||
send,
|
||||
]);
|
||||
|
||||
const isDisabled = !state.matches('ready') && !state.matches('verifying');
|
||||
const isDisabled = (!state.matches('ready') && !state.matches('verifying')) || !hasCheckedForInactiveDocument;
|
||||
|
||||
const LoadingContent: React.FC<{ text: string }> = ({ text }) => (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
|
||||
@@ -236,13 +236,15 @@ export async function storeDocumentWithDeduplication(
|
||||
|
||||
// Add to catalog
|
||||
const docType = passportData.documentType;
|
||||
const documentCategory = passportData.documentCategory || inferDocumentCategory(docType);
|
||||
const metadata: DocumentMetadata = {
|
||||
id: contentHash,
|
||||
documentType: docType,
|
||||
documentCategory: passportData.documentCategory || inferDocumentCategory(docType),
|
||||
documentCategory,
|
||||
data: isMRZDocument(passportData) ? passportData.mrz : (passportData as AadhaarData).qrData || '',
|
||||
mock: passportData.mock || false,
|
||||
isRegistered: false,
|
||||
hasExpirationDate: documentCategory === 'id_card' || documentCategory === 'passport',
|
||||
};
|
||||
|
||||
catalog.documents.push(metadata);
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface DocumentMetadata {
|
||||
mock: boolean;
|
||||
isRegistered?: boolean;
|
||||
registeredAt?: number; // timestamp (epoch ms) when document was registered
|
||||
hasExpirationDate?: boolean; // whether the document has an expiration date
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { DocumentCatalog } from '@selfxyz/common/types';
|
||||
import type { AadhaarData, DocumentCatalog } from '@selfxyz/common';
|
||||
import type { PassportData } from '@selfxyz/common/types/passport';
|
||||
|
||||
import type { DocumentsAdapter, SelfClient } from '../../src';
|
||||
import { createSelfClient, defaultConfig, loadSelectedDocument } from '../../src';
|
||||
import { storeDocumentWithDeduplication } from '../../src/documents/utils';
|
||||
|
||||
const createMockSelfClientWithDocumentsAdapter = (documentsAdapter: DocumentsAdapter): SelfClient => {
|
||||
return createSelfClient({
|
||||
@@ -160,3 +161,171 @@ describe('loadSelectedDocument', () => {
|
||||
expect(saveDocumentCatalogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeDocumentWithDeduplication', () => {
|
||||
const passportDocument = {
|
||||
mrz: 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<',
|
||||
eContent: [1, 2, 3],
|
||||
documentType: 'passport',
|
||||
documentCategory: 'passport',
|
||||
} as PassportData;
|
||||
|
||||
const idCardDocument = {
|
||||
mrz: 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<',
|
||||
eContent: [1, 2, 3],
|
||||
documentType: 'id_card',
|
||||
documentCategory: 'id_card',
|
||||
} as PassportData;
|
||||
|
||||
const aadhaarDocument = {
|
||||
qrData: 'test-qr-data',
|
||||
documentType: 'aadhaar',
|
||||
documentCategory: 'aadhaar',
|
||||
} as AadhaarData;
|
||||
|
||||
it('sets hasExpirationDate to true for passport documents', async () => {
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const loadDocumentCatalogSpy = vi.fn().mockResolvedValue(emptyCatalog);
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
const saveDocumentSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: loadDocumentCatalogSpy,
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: saveDocumentSpy,
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, passportDocument);
|
||||
|
||||
expect(saveDocumentCatalogSpy).toHaveBeenCalledTimes(1);
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
|
||||
expect(savedCatalog.documents).toHaveLength(1);
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('passport');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(true);
|
||||
});
|
||||
|
||||
it('sets hasExpirationDate to true for ID card documents', async () => {
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const loadDocumentCatalogSpy = vi.fn().mockResolvedValue(emptyCatalog);
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
const saveDocumentSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: loadDocumentCatalogSpy,
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: saveDocumentSpy,
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, idCardDocument);
|
||||
|
||||
expect(saveDocumentCatalogSpy).toHaveBeenCalledTimes(1);
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
|
||||
expect(savedCatalog.documents).toHaveLength(1);
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('id_card');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(true);
|
||||
});
|
||||
|
||||
it('sets hasExpirationDate to false for Aadhaar documents', async () => {
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const loadDocumentCatalogSpy = vi.fn().mockResolvedValue(emptyCatalog);
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
const saveDocumentSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: loadDocumentCatalogSpy,
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: saveDocumentSpy,
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, aadhaarDocument);
|
||||
|
||||
expect(saveDocumentCatalogSpy).toHaveBeenCalledTimes(1);
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
|
||||
expect(savedCatalog.documents).toHaveLength(1);
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('aadhaar');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(false);
|
||||
});
|
||||
|
||||
it('infers passport category and sets hasExpirationDate when documentCategory is missing', async () => {
|
||||
const docWithoutCategory = {
|
||||
mrz: 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<',
|
||||
eContent: [1, 2, 3],
|
||||
documentType: 'passport',
|
||||
} as PassportData;
|
||||
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: vi.fn().mockResolvedValue(emptyCatalog),
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: vi.fn(),
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, docWithoutCategory);
|
||||
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('passport');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(true);
|
||||
});
|
||||
|
||||
it('infers id_card category and sets hasExpirationDate when documentCategory is missing', async () => {
|
||||
const docWithoutCategory = {
|
||||
mrz: 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<',
|
||||
eContent: [1, 2, 3],
|
||||
documentType: 'id_card',
|
||||
} as PassportData;
|
||||
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: vi.fn().mockResolvedValue(emptyCatalog),
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: vi.fn(),
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, docWithoutCategory);
|
||||
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('id_card');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(true);
|
||||
});
|
||||
|
||||
it('infers aadhaar category and sets hasExpirationDate to false when documentCategory is missing', async () => {
|
||||
const docWithoutCategory = {
|
||||
qrData: 'test-qr-data',
|
||||
documentType: 'aadhaar',
|
||||
} as AadhaarData;
|
||||
|
||||
const emptyCatalog: DocumentCatalog = { documents: [] };
|
||||
const saveDocumentCatalogSpy = vi.fn();
|
||||
|
||||
const client = createMockSelfClientWithDocumentsAdapter({
|
||||
loadDocumentCatalog: vi.fn().mockResolvedValue(emptyCatalog),
|
||||
loadDocumentById: vi.fn(),
|
||||
saveDocumentCatalog: saveDocumentCatalogSpy,
|
||||
saveDocument: vi.fn(),
|
||||
deleteDocument: vi.fn(),
|
||||
});
|
||||
|
||||
await storeDocumentWithDeduplication(client, docWithoutCategory);
|
||||
|
||||
const savedCatalog = saveDocumentCatalogSpy.mock.calls[0][0] as DocumentCatalog;
|
||||
expect(savedCatalog.documents[0].documentCategory).toBe('aadhaar');
|
||||
expect(savedCatalog.documents[0].hasExpirationDate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user