From abf01c82c07586f0b7a55bbef9f0bb4d35ba1bd7 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Thu, 12 Feb 2026 20:05:47 +0100 Subject: [PATCH] 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 --- app/scripts/bundle-analyze-ci.cjs | 4 +- app/src/components/homescreen/IdCard.tsx | 160 +++++++++++++--- .../proof-request/BottomVerifyBar.tsx | 3 + app/src/providers/passportDataProvider.tsx | 9 + app/src/screens/dev/DevSettingsScreen.tsx | 33 +++- .../dev/sections/DangerZoneSection.tsx | 7 + app/src/screens/home/HomeScreen.tsx | 25 +++ app/src/screens/verification/ProveScreen.tsx | 101 ++++++++++- app/src/utils/documents.ts | 22 +++ app/tests/src/utils/documents.test.ts | 132 ++++++++++++++ common/src/utils/aadhaar/mockData.ts | 14 +- common/src/utils/types.ts | 1 + .../buttons/HeldPrimaryButtonProveScreen.tsx | 21 ++- .../mobile-sdk-alpha/src/documents/utils.ts | 4 +- packages/mobile-sdk-alpha/src/types/ui.ts | 1 + .../tests/documents/utils.test.ts | 171 +++++++++++++++++- 16 files changed, 665 insertions(+), 43 deletions(-) create mode 100644 app/src/utils/documents.ts create mode 100644 app/tests/src/utils/documents.test.ts diff --git a/app/scripts/bundle-analyze-ci.cjs b/app/scripts/bundle-analyze-ci.cjs index f4f7dbe72..902aa4112 100755 --- a/app/scripts/bundle-analyze-ci.cjs +++ b/app/scripts/bundle-analyze-ci.cjs @@ -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) { diff --git a/app/src/components/homescreen/IdCard.tsx b/app/src/components/homescreen/IdCard.tsx index 2fc412239..faba049d7 100644 --- a/app/src/components/homescreen/IdCard.tsx +++ b/app/src/components/homescreen/IdCard.tsx @@ -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 = ({ 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 = ({ const truncatedId = getTruncatedId(); return ( - + // Container wrapper to handle shadow space properly + + {isInactive && ( + + + + + + + + Your document is inactive + + + Tap here to recover your ID + + + + + )} = ({ - {/* Security Badge */} - - + {isInactive && ( + + + INACTIVE + + + )} + {/* Security Badge */} + - {securityLevel} - + + {securityLevel} + + @@ -605,6 +717,10 @@ const styles = StyleSheet.create({ height: '90%', opacity: 0.6, }, + inactiveWarningContainer: { + width: '100%', + marginBottom: 16, + }, }); export default IdCardLayout; diff --git a/app/src/components/proof-request/BottomVerifyBar.tsx b/app/src/components/proof-request/BottomVerifyBar.tsx index 0106b7cce..72c425884 100644 --- a/app/src/components/proof-request/BottomVerifyBar.tsx +++ b/app/src/components/proof-request/BottomVerifyBar.tsx @@ -18,6 +18,7 @@ export interface BottomVerifyBarProps { isReadyToProve: boolean; isDocumentExpired: boolean; testID?: string; + hasCheckedForInactiveDocument: boolean; } export const BottomVerifyBar: React.FC = ({ @@ -28,6 +29,7 @@ export const BottomVerifyBar: React.FC = ({ isReadyToProve, isDocumentExpired, testID = 'bottom-verify-bar', + hasCheckedForInactiveDocument, }) => { const insets = useSafeAreaInsets(); @@ -46,6 +48,7 @@ export const BottomVerifyBar: React.FC = ({ isScrollable={isScrollable} isReadyToProve={isReadyToProve} isDocumentExpired={isDocumentExpired} + hasCheckedForInactiveDocument={hasCheckedForInactiveDocument} /> ); diff --git a/app/src/providers/passportDataProvider.tsx b/app/src/providers/passportDataProvider.tsx index 88f77ce6a..447330144 100644 --- a/app/src/providers/passportDataProvider.tsx +++ b/app/src/providers/passportDataProvider.tsx @@ -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 { diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index a484e67da..db5513d9f 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -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 ( @@ -109,6 +139,7 @@ const DevSettingsScreen: React.FC = () => { onResetBackupState={handleResetBackupStatePress} onClearBackupEvents={handleClearBackupEventsPress} onClearPendingKyc={handleClearPendingVerificationsPress} + onRemoveExpirationDateFlag={handleRemoveExpirationDateFlagPress} /> diff --git a/app/src/screens/dev/sections/DangerZoneSection.tsx b/app/src/screens/dev/sections/DangerZoneSection.tsx index 08c5d9af6..7a349d9a7 100644 --- a/app/src/screens/dev/sections/DangerZoneSection.tsx +++ b/app/src/screens/dev/sections/DangerZoneSection.tsx @@ -23,6 +23,7 @@ interface DangerZoneSectionProps { onResetBackupState: () => void; onClearBackupEvents: () => void; onClearPendingKyc: () => void; + onRemoveExpirationDateFlag: () => void; } export const DangerZoneSection: React.FC = ({ @@ -32,6 +33,7 @@ export const DangerZoneSection: React.FC = ({ onResetBackupState, onClearBackupEvents, onClearPendingKyc, + onRemoveExpirationDateFlag, }) => { const dangerActions = [ { @@ -64,6 +66,11 @@ export const DangerZoneSection: React.FC = ({ onPress: onClearPendingKyc, dangerTheme: true, }, + { + label: 'Remove expiration date flag', + onPress: onRemoveExpirationDateFlag, + dangerTheme: true, + }, ]; return ( diff --git a/app/src/screens/home/HomeScreen.tsx b/app/src/screens/home/HomeScreen.tsx index 6cfbff9cb..bb831ae8a 100644 --- a/app/src/screens/home/HomeScreen.tsx +++ b/app/src/screens/home/HomeScreen.tsx @@ -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 = () => { >