SELF-1262:Hotfix/hide unregistered document (#1423)

* hide unregistered documents

* redirect to 'Home' instead of 'Launch'

* RecoverWithPhraseScreen: wrap restoreAccount in try-catch

* Revert "RecoverWithPhraseScreen: wrap restoreAccount in try-catch"

This reverts commit e53b5630ca.

* RecoverWithPhraseScreen: wrap restoreAccount in try-catch

* update lock

* fix types

* bump version

* remove launch screen

* update bundle version

* add new events

* fix nested react requires

* fix heavy tests

* address fake mocks

* fix test

* address codex and coderabbit logic conceners

* fix linting

* remove last borked react test

---------

Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
This commit is contained in:
Seshanth.S
2025-11-19 05:34:09 +05:30
committed by GitHub
parent c50db06eee
commit e4cdb125b0
14 changed files with 106 additions and 338 deletions

View File

@@ -22,7 +22,7 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1183.0)
aws-partitions (1.1184.0)
aws-sdk-core (3.237.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)

View File

@@ -10,19 +10,12 @@ import type { DocumentCategory } from '@selfxyz/common/utils/types';
import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen';
import GratificationScreen from '@/screens/app/GratificationScreen';
import LaunchScreen from '@/screens/app/LaunchScreen';
import LoadingScreen from '@/screens/app/LoadingScreen';
import type { ModalNavigationParams } from '@/screens/app/ModalScreen';
import ModalScreen from '@/screens/app/ModalScreen';
import SplashScreen from '@/screens/app/SplashScreen';
const appScreens = {
Launch: {
screen: LaunchScreen,
options: {
header: () => <SystemBars style="light" />,
},
},
Loading: {
screen: LoadingScreen,
options: {

View File

@@ -156,14 +156,10 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
addListener(
SdkEvents.PROVING_REGISTER_ERROR_OR_FAILURE,
async ({ hasValidDocument }) => {
async ({ hasValidDocument: _hasValidDocument }) => {
setTimeout(() => {
if (navigationRef.isReady()) {
if (hasValidDocument) {
navigationRef.navigate({ name: 'Home', params: {} });
} else {
navigationRef.navigate({ name: 'Launch', params: undefined });
}
navigationRef.navigate({ name: 'Home', params: {} });
}
}, 3000);
},

View File

@@ -79,7 +79,7 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
if (!result) {
console.warn('Failed to restore account');
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN);
navigation.navigate('Launch');
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return false;
}
@@ -110,7 +110,7 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
'Secret provided did not match a registered ID. Please try again.',
);
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED);
navigation.navigate('Launch');
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return false;
}

View File

@@ -23,7 +23,7 @@ const { flush: flushAnalytics } = analytics();
const DocumentDataNotFoundScreen: React.FC = () => {
const selfClient = useSelfClient();
const navigateToLaunch = useHapticNavigation('Launch');
const navigateToCountryPicker = useHapticNavigation('CountryPicker');
const navigateToHome = useHapticNavigation('Home');
const onPress = async () => {
@@ -31,7 +31,7 @@ const DocumentDataNotFoundScreen: React.FC = () => {
if (hasValidDocument) {
navigateToHome();
} else {
navigateToLaunch();
navigateToCountryPicker();
}
};

View File

@@ -55,57 +55,88 @@ const RecoverWithPhraseScreen: React.FC = () => {
}, []);
const restoreAccount = useCallback(async () => {
setRestoring(true);
const slimMnemonic = mnemonic?.trim();
if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) {
setRestoring(false);
return;
}
const result = await restoreAccountFromMnemonic(slimMnemonic);
try {
setRestoring(true);
const slimMnemonic = mnemonic?.trim();
if (!slimMnemonic || !ethers.Mnemonic.isValidMnemonic(slimMnemonic)) {
setRestoring(false);
return;
}
const result = await restoreAccountFromMnemonic(slimMnemonic);
if (!result) {
console.warn('Failed to restore account');
navigation.navigate('Launch');
setRestoring(false);
return;
}
if (!result) {
console.warn('Failed to restore account');
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, {
mnemonicLength: slimMnemonic.split(' ').length,
});
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return;
}
const passportDataAndSecret = (await loadPassportDataAndSecret()) as string;
const { passportData, secret } = JSON.parse(passportDataAndSecret);
const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA(
passportData,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
const passportDataAndSecret = await loadPassportDataAndSecret();
if (!passportDataAndSecret) {
console.warn(
'No passport data found on device. Please scan or import your document.',
);
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, {
reason: 'no_passport_data',
});
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return;
}
const { passportData, secret } = JSON.parse(passportDataAndSecret);
const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA(
passportData,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys = useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},
);
if (!isRegistered) {
console.warn(
'Secret provided did not match a registered passport. Please try again.',
);
reStorePassportDataWithRightCSCA(passportData, csca as string);
navigation.navigate('Launch');
setRestoring(false);
return;
}
if (!isRegistered) {
console.warn(
'Secret provided did not match a registered passport. Please try again.',
);
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED, {
reason: 'document_not_registered',
hasCSCA: !!csca,
});
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return;
}
setRestoring(false);
await markCurrentDocumentAsRegistered(selfClient);
trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED);
navigation.navigate('AccountVerifiedSuccess');
if (csca) {
await reStorePassportDataWithRightCSCA(passportData, csca);
}
await markCurrentDocumentAsRegistered(selfClient);
setRestoring(false);
trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED);
navigation.navigate('AccountVerifiedSuccess');
} catch (error) {
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN, {
reason: 'unexpected_error',
error: error instanceof Error ? error.message : 'unknown',
});
setRestoring(false);
navigation.navigate({ name: 'Home', params: {} });
}
}, [
mnemonic,
navigation,

View File

@@ -1,214 +0,0 @@
// 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 { Pressable, StyleSheet, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Anchor, Text, YStack } from 'tamagui';
import { useTurnkey } from '@turnkey/react-native-wallet-kit';
import {
AbstractButton,
BodyText,
Caption,
} from '@selfxyz/mobile-sdk-alpha/components';
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { privacyUrl, termsUrl } from '@/consts/links';
import useConnectionModal from '@/hooks/useConnectionModal';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { useModal } from '@/hooks/useModal';
import IDCardPlaceholder from '@/images/icons/id_card_placeholder.svg';
import {
black,
red500,
slate300,
slate400,
white,
zinc800,
} from '@/utils/colors';
import { advercase, dinot } from '@/utils/fonts';
const LaunchScreen: React.FC = () => {
useConnectionModal();
const { handleGoogleOauth, fetchWallets } = useTurnkey();
const onPress = useHapticNavigation('CountryPicker');
const createMock = useHapticNavigation('CreateMock');
const { bottom } = useSafeAreaInsets();
const { showModal: showNoWalletsModal } = useModal({
titleText: 'No wallets found',
bodyText: 'No wallets found. Please sign in with Turnkey to continue.',
buttonText: 'OK',
onButtonPress: () => {},
onModalDismiss: () => {},
});
const onImportWalletPress = async () => {
try {
await handleGoogleOauth();
const fetchedWallets = await fetchWallets();
if (fetchedWallets.length === 0) {
showNoWalletsModal();
return;
}
onPress();
} catch {
console.error('handleGoogleOauth error');
}
};
const devModeTap = Gesture.Tap()
.numberOfTaps(5)
.onStart(() => {
createMock();
});
return (
<YStack backgroundColor={black} flex={1} alignItems="center">
<View style={styles.container}>
<YStack flex={1} justifyContent="center" alignItems="center">
<GestureDetector gesture={devModeTap}>
<YStack
backgroundColor={red500}
borderRadius={14}
overflow="hidden"
>
<IDCardPlaceholder width={300} height={180} />
</YStack>
</GestureDetector>
</YStack>
<Text
color={white}
fontSize={38}
fontFamily={advercase}
fontWeight="500"
textAlign="center"
marginBottom={16}
>
Take control of your digital identity
</Text>
<BodyText
style={{
color: slate300,
fontSize: 16,
textAlign: 'center',
marginHorizontal: 40,
marginBottom: 40,
}}
>
Self is the easiest way to verify your identity safely wherever you
are.
</BodyText>
</View>
<YStack
gap="$3"
width="100%"
alignItems="center"
paddingHorizontal={20}
paddingBottom={bottom}
paddingTop={30}
backgroundColor={zinc800}
>
<AbstractButton
trackEvent={AppEvents.GET_STARTED}
onPress={onPress}
bgColor={white}
color={black}
testID="launch-get-started-button"
>
Get Started
</AbstractButton>
<Pressable onPress={onImportWalletPress}>
<Text style={styles.disclaimer}>
<Text style={styles.haveAnAccount}>{`Have an account? `}</Text>
<Text style={styles.restore}>restore</Text>
</Text>
</Pressable>
<Caption style={styles.notice}>
By continuing, you agree to the&nbsp;
<Anchor style={styles.link} href={termsUrl}>
User Terms and Conditions
</Anchor>
&nbsp;and acknowledge the&nbsp;
<Anchor style={styles.link} href={privacyUrl}>
Privacy notice
</Anchor>
&nbsp;of Self provided by Self Inc.
</Caption>
</YStack>
</YStack>
);
};
export default LaunchScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '102%',
paddingTop: '30%',
},
card: {
width: '100%',
marginTop: '20%',
borderRadius: 16,
paddingVertical: 40,
paddingHorizontal: 20,
alignItems: 'center',
shadowColor: black,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 12,
elevation: 8,
marginBottom: 8,
},
logoSection: {
width: 60,
height: 60,
marginBottom: 24,
alignItems: 'center',
justifyContent: 'center',
},
logo: {
width: 40,
height: 40,
},
disclaimer: {
width: '100%',
fontSize: 11,
letterSpacing: 0.4,
textTransform: 'uppercase',
fontWeight: '500',
fontFamily: dinot,
textAlign: 'center',
},
haveAnAccount: {
color: '#6b7280',
},
restore: {
color: '#fff',
},
notice: {
fontFamily: dinot,
marginVertical: 10,
paddingBottom: 10,
color: slate400,
textAlign: 'center',
lineHeight: 22,
fontSize: 14,
},
link: {
fontFamily: dinot,
color: slate400,
lineHeight: 22,
textDecorationLine: 'underline',
},
});

View File

@@ -95,8 +95,8 @@ const SplashScreen: React.FC = ({}) => {
}
} catch (error) {
console.error(`Error in SplashScreen data loading: ${error}`);
setDeeplinkParentScreen('Launch');
setNextScreen('Launch');
setDeeplinkParentScreen('Home');
setNextScreen('Home');
}
};

View File

@@ -7,11 +7,7 @@ import { StyleSheet } from 'react-native';
import { View, XStack, YStack } from 'tamagui';
import { useIsFocused } from '@react-navigation/native';
import {
DelayedLottieView,
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import {
Additional,
Description,
@@ -33,27 +29,18 @@ import { black, slate400, slate800, white } from '@/utils/colors';
import { dinot } from '@/utils/fonts';
const DocumentCameraScreen: React.FC = () => {
const client = useSelfClient();
const isFocused = useIsFocused();
// Add a ref to track when the camera screen is mounted
const scanStartTimeRef = useRef(Date.now());
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
const navigateToLaunch = useHapticNavigation('Launch', {
action: 'cancel',
});
const navigateToHome = useHapticNavigation('Home', {
action: 'cancel',
});
const onCancelPress = async () => {
const hasValidDocument = await hasAnyValidRegisteredDocument(client);
if (hasValidDocument) {
navigateToHome();
} else {
navigateToLaunch();
}
navigateToHome();
};
return (

View File

@@ -32,11 +32,7 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { CircleHelp } from '@tamagui/lucide-icons';
import type { PassportData } from '@selfxyz/common/types';
import {
hasAnyValidRegisteredDocument,
sanitizeErrorMessage,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { sanitizeErrorMessage, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
ButtonsContainer,
@@ -447,9 +443,6 @@ const DocumentNFCScanScreen: React.FC = () => {
trackEvent,
]);
const navigateToLaunch = useHapticNavigation('Launch', {
action: 'cancel',
});
const navigateToHome = useHapticNavigation('Home', {
action: 'cancel',
});
@@ -457,12 +450,7 @@ const DocumentNFCScanScreen: React.FC = () => {
const onCancelPress = async () => {
flushAllAnalytics();
logNFCEvent('info', 'scan_cancelled', { ...baseContext, stage: 'cancel' });
const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient);
if (hasValidDocument) {
navigateToHome();
} else {
navigateToLaunch();
}
navigateToHome();
};
useFocusEffect(

View File

@@ -5,10 +5,6 @@
import React from 'react';
import { Image } from 'tamagui';
import {
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
ButtonsContainer,
@@ -24,21 +20,12 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { black, slate100, white } from '@/utils/colors';
const DocumentNFCScanScreen: React.FC = () => {
const selfClient = useSelfClient();
const navigateToLaunch = useHapticNavigation('Launch', {
action: 'cancel',
});
const navigateToHome = useHapticNavigation('Home', {
action: 'cancel',
});
const onCancelPress = async () => {
const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient);
if (hasValidDocument) {
navigateToHome();
} else {
navigateToLaunch();
}
navigateToHome();
};
return (

View File

@@ -2,7 +2,13 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Dimensions, Image, Pressable } from 'react-native';
import { Button, ScrollView, Text, View, XStack, YStack } from 'tamagui';
import {
@@ -122,6 +128,10 @@ const HomeScreen: React.FC = () => {
// Prevents back navigation
usePreventRemove(true, () => {});
const hasValidRegisteredDocument = useMemo(() => {
return documentCatalog.documents.some(doc => doc.isRegistered === true);
}, [documentCatalog]);
// Calculate bottom padding to prevent button bleeding into system navigation
const bottomPadding = useSafeBottomPadding(20);
@@ -186,7 +196,7 @@ const HomeScreen: React.FC = () => {
paddingBottom: 35, // Add extra bottom padding for shadow
}}
>
{documentCatalog.documents.length === 0 ? (
{!hasValidRegisteredDocument ? (
<Pressable
onPress={() => {
navigation.navigate('CountryPicker');
@@ -218,7 +228,7 @@ const HomeScreen: React.FC = () => {
const isSelected =
documentCatalog.selectedDocumentId === metadata.id;
if (!documentData) {
if (!documentData || !documentData.metadata.isRegistered) {
return null;
}

View File

@@ -7,10 +7,6 @@ import { XStack, YStack } from 'tamagui';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { countryCodes } from '@selfxyz/common/constants';
import {
hasAnyValidRegisteredDocument,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
PrimaryButton,
@@ -36,8 +32,6 @@ type ComingSoonScreenProps = NativeStackScreenProps<
>;
const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
const selfClient = useSelfClient();
const navigateToLaunch = useHapticNavigation('Launch');
const navigateToHome = useHapticNavigation('Home');
const { countryName, countryCode, documentTypeText } = useMemo(() => {
@@ -82,12 +76,7 @@ const ComingSoonScreen: React.FC<ComingSoonScreenProps> = ({ route }) => {
}, [route.params?.documentCategory, route.params?.countryCode]);
const onDismiss = async () => {
const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient);
if (hasValidDocument) {
navigateToHome();
} else {
navigateToLaunch();
}
navigateToHome();
};
const onNotifyMe = async () => {

View File

@@ -69,6 +69,7 @@ export const BackupEvents = {
CLOUD_BACKUP_ENABLED_DONE: 'Backup: Cloud Backup Enabled Done',
CLOUD_BACKUP_ENABLE_STARTED: 'Backup: Cloud Backup Enable Started',
CLOUD_BACKUP_STARTED: 'Backup: Cloud Backup Started',
CLOUD_RESTORE_FAILED_AUTH: 'Backup: Cloud Restore Failed: Authentication Failed',
CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED: 'Backup: Cloud Restore Failed: Passport Not Registered',
CLOUD_RESTORE_FAILED_UNKNOWN: 'Backup: Cloud Restore Failed: Unknown Error',
CLOUD_RESTORE_SUCCESS: 'Backup: Cloud Restore Success',