diff --git a/app/src/components/NavBar/BaseNavBar.tsx b/app/src/components/NavBar/BaseNavBar.tsx index ee92c5707..9746bcc51 100644 --- a/app/src/components/NavBar/BaseNavBar.tsx +++ b/app/src/components/NavBar/BaseNavBar.tsx @@ -98,7 +98,6 @@ const Container: React.FC = ({ { + const navigation = useNavigation(); + const { top } = useSafeAreaInsets(); + + return ( + + navigation.goBack()} /> + + {title} + + } + onPress={() => { + /* Handle help action, button is transparent for now as we dont have the help screen ready */ + }} + /> + + ); +}; diff --git a/app/src/components/flag/RoundFlag.tsx b/app/src/components/flag/RoundFlag.tsx new file mode 100644 index 000000000..97a50223f --- /dev/null +++ b/app/src/components/flag/RoundFlag.tsx @@ -0,0 +1,77 @@ +// 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 getCountryISO2 from 'country-iso-3-to-2'; +import React from 'react'; +import { View } from 'react-native'; +import * as CountryFlags from 'react-native-svg-circle-country-flags'; + +import { slate300 } from '@/utils/colors'; + +type CountryFlagComponent = React.ComponentType<{ + width: number; + height: number; +}>; + +type CountryFlagsRecord = Record; + +interface RoundFlagProps { + countryCode: string; + size: number; +} + +const findFlagComponent = (formattedCode: string) => { + const patterns = [ + formattedCode, + formattedCode.toLowerCase(), + formattedCode.charAt(0).toUpperCase() + + formattedCode.charAt(1).toLowerCase(), + ]; + + for (const pattern of patterns) { + const component = (CountryFlags as unknown as CountryFlagsRecord)[pattern]; + if (component) { + return component; + } + } + return null; +}; + +const getCountryFlag = (countryCode: string): CountryFlagComponent | null => { + try { + const normalizedCountryCode = countryCode === 'D<<' ? 'DEU' : countryCode; + const iso2 = getCountryISO2(normalizedCountryCode); + if (!iso2) { + return null; + } + + const formattedCode = iso2.toUpperCase(); + return findFlagComponent(formattedCode); + } catch (error) { + console.error('Error getting country flag:', error); + return null; + } +}; + +export const RoundFlag: React.FC = ({ countryCode, size }) => { + const CountryFlagComponent = getCountryFlag(countryCode); + + if (!CountryFlagComponent) { + return ( + + ); + } + + return ( + + + + ); +}; diff --git a/app/src/images/icons/epassport_rounded.svg b/app/src/images/icons/epassport_rounded.svg new file mode 100644 index 000000000..6d8d61984 --- /dev/null +++ b/app/src/images/icons/epassport_rounded.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/images/icons/id_card_placeholder.svg b/app/src/images/icons/id_card_placeholder.svg new file mode 100644 index 000000000..a0b88bdf3 --- /dev/null +++ b/app/src/images/icons/id_card_placeholder.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/images/icons/plus.svg b/app/src/images/icons/plus.svg new file mode 100644 index 000000000..713d113a6 --- /dev/null +++ b/app/src/images/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/navigation/document.ts b/app/src/navigation/document.ts index 8de50f32b..26a2f96d2 100644 --- a/app/src/navigation/document.ts +++ b/app/src/navigation/document.ts @@ -4,13 +4,15 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; +import ComingSoonScreen from '@/screens/document/ComingSoonScreen'; +import CountryPickerScreen from '@/screens/document/CountryPickerScreen'; import DocumentCameraScreen from '@/screens/document/DocumentCameraScreen'; import DocumentCameraTroubleScreen from '@/screens/document/DocumentCameraTroubleScreen'; import DocumentNFCMethodSelectionScreen from '@/screens/document/DocumentNFCMethodSelectionScreen'; import DocumentNFCScanScreen from '@/screens/document/DocumentNFCScanScreen'; import DocumentNFCTroubleScreen from '@/screens/document/DocumentNFCTroubleScreen'; import DocumentOnboardingScreen from '@/screens/document/DocumentOnboardingScreen'; -import UnsupportedDocumentScreen from '@/screens/document/UnsupportedDocumentScreen'; +import IDPickerScreen from '@/screens/document/IDPickerScreen'; const documentScreens = { DocumentCamera: { @@ -56,8 +58,8 @@ const documentScreens = { headerShown: false, } as NativeStackNavigationOptions, }, - UnsupportedDocument: { - screen: UnsupportedDocumentScreen, + ComingSoon: { + screen: ComingSoonScreen, options: { headerShown: false, } as NativeStackNavigationOptions, @@ -73,6 +75,22 @@ const documentScreens = { animation: 'slide_from_bottom', } as NativeStackNavigationOptions, }, + CountryPicker: { + screen: CountryPickerScreen, + options: { + headerShown: false, + } as NativeStackNavigationOptions, + }, + IDPicker: { + screen: IDPickerScreen, + options: { + headerShown: false, + } as NativeStackNavigationOptions, + initialParams: { + countryCode: '', + documentTypes: [], + }, + }, }; export default documentScreens; diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index 7b615b2b9..c91514daf 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -79,7 +79,7 @@ const NavigationWithTracking = () => { return () => { cleanup(); }; - }, []); + }, [selfClient]); return ( diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index c6570fca8..c981395c7 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -136,7 +136,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, ({ countryCode, documentCategory }) => { if (navigationRef.isReady()) { - navigationRef.navigate('UnsupportedDocument', { + navigationRef.navigate('ComingSoon', { countryCode, documentCategory, } as any); diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index d5533dcbf..f88a40052 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -124,6 +124,7 @@ function ParameterSection({ const items = [ 'DevSettings', + 'CountryPicker', 'AadhaarUpload', 'DevFeatureFlags', 'DevHapticFeedback', @@ -149,7 +150,7 @@ const items = [ 'RecoverWithPhrase', 'ShowRecoveryPhrase', 'CloudBackupSettings', - 'UnsupportedDocument', + 'ComingSoon', 'DocumentCameraTrouble', 'DocumentNFCTrouble', ] satisfies (keyof RootStackParamList)[]; diff --git a/app/src/screens/document/UnsupportedDocumentScreen.tsx b/app/src/screens/document/ComingSoonScreen.tsx similarity index 62% rename from app/src/screens/document/UnsupportedDocumentScreen.tsx rename to app/src/screens/document/ComingSoonScreen.tsx index c0157dc31..8c5862523 100644 --- a/app/src/screens/document/UnsupportedDocumentScreen.tsx +++ b/app/src/screens/document/ComingSoonScreen.tsx @@ -2,10 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import getCountryISO2 from 'country-iso-3-to-2'; import React, { useEffect, useMemo } from 'react'; -import { View } from 'react-native'; -import * as CountryFlags from 'react-native-svg-circle-country-flags'; import { XStack, YStack } from 'tamagui'; import type { RouteProp } from '@react-navigation/native'; @@ -19,6 +16,7 @@ import { PassportEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { PrimaryButton } from '@/components/buttons/PrimaryButton'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; +import { RoundFlag } from '@/components/flag/RoundFlag'; import { BodyText } from '@/components/typography/BodyText'; import { Title } from '@/components/typography/Title'; import useHapticNavigation from '@/hooks/useHapticNavigation'; @@ -30,88 +28,66 @@ import { notificationError } from '@/utils/haptic'; const { flush: flushAnalytics } = analytics(); -type CountryFlagComponent = React.ComponentType<{ - width: number; - height: number; -}>; -type CountryFlagsRecord = Record; - -type UnsupportedDocumentScreenRouteProp = RouteProp< +type ComingSoonScreenRouteProp = RouteProp< { - UnsupportedDocument: { - countryCode: string | null; - documentCategory: DocumentCategory | null; + ComingSoon: { + countryCode: string; + documentCategory?: DocumentCategory; }; }, - 'UnsupportedDocument' + 'ComingSoon' >; -interface UnsupportedDocumentScreenProps { - route: UnsupportedDocumentScreenRouteProp; +interface ComingSoonScreenProps { + route: ComingSoonScreenRouteProp; } -const UnsupportedDocumentScreen: React.FC = ({ - route, -}) => { +const ComingSoonScreen: React.FC = ({ route }) => { const selfClient = useSelfClient(); const navigateToLaunch = useHapticNavigation('Launch'); const navigateToHome = useHapticNavigation('Home'); - const { countryName, country2AlphaCode, documentTypeText } = useMemo(() => { + const { countryName, countryCode, documentTypeText } = useMemo(() => { try { - const countryCode = route.params?.countryCode; - if (countryCode) { + const routeCountryCode = route.params?.countryCode; + if (routeCountryCode) { // Handle Germany corner case where country code is "D<<" instead of "DEU" - let normalizedCountryCode = countryCode; - if (countryCode === 'D<<') { - normalizedCountryCode = 'DEU'; - } - - const iso2 = getCountryISO2(normalizedCountryCode); - const extractedCode = iso2 - ? iso2.charAt(0).toUpperCase() + iso2.charAt(1).toLowerCase() - : 'Unknown'; + const normalizedCountryCode = + routeCountryCode === 'D<<' ? 'DEU' : routeCountryCode; const name = countryCodes[normalizedCountryCode as keyof typeof countryCodes]; - const docType = - route.params?.documentCategory === 'id_card' - ? 'ID Cards' - : 'Passports'; + + let docType = ''; + if (route.params?.documentCategory === 'id_card') { + docType = 'ID Cards'; + } else if (route.params?.documentCategory === 'passport') { + docType = 'Passports'; + } + return { countryName: name, - country2AlphaCode: extractedCode, + countryCode: normalizedCountryCode, documentTypeText: docType, }; } } catch (error) { console.error('Error extracting country from passport data:', error); } - const docType = - route.params?.documentCategory === 'id_card' ? 'ID Cards' : 'Passports'; + + let docType = ''; + if (route.params?.documentCategory === 'id_card') { + docType = 'ID Cards'; + } else if (route.params?.documentCategory === 'passport') { + docType = 'Passports'; + } + return { countryName: 'Unknown', - country2AlphaCode: 'Unknown', + countryCode: 'Unknown', documentTypeText: docType, }; }, [route.params?.documentCategory, route.params?.countryCode]); - // Get country flag component dynamically - const getCountryFlag = (code: string) => { - try { - const FlagComponent = (CountryFlags as unknown as CountryFlagsRecord)[ - code.toUpperCase() - ]; - if (FlagComponent) { - return FlagComponent; - } - } catch (error) { - console.error('Error getting country flag:', error); - return null; - } - }; - - const CountryFlagComponent = getCountryFlag(country2AlphaCode); - const onDismiss = async () => { const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { @@ -125,9 +101,8 @@ const UnsupportedDocumentScreen: React.FC = ({ try { await sendCountrySupportNotification({ countryName, - countryCode: - country2AlphaCode !== 'Unknown' ? country2AlphaCode : undefined, - documentCategory: route.params?.documentCategory ?? '', + countryCode: countryCode !== 'Unknown' ? countryCode : '', + documentCategory: route.params?.documentCategory, }); } catch (error) { console.error('Failed to open email client:', error); @@ -155,10 +130,8 @@ const UnsupportedDocumentScreen: React.FC = ({ marginBottom={20} gap={12} > - {CountryFlagComponent && ( - - - + {countryCode !== 'Unknown' && ( + )} = ({ marginBottom={10} paddingHorizontal={10} > - We're working to roll out support for {documentTypeText} in{' '} - {countryName}. + {documentTypeText + ? `We're working to roll out support for ${documentTypeText} in ${countryName}.` + : `We're working to roll out support in ${countryName}.`} </BodyText> <BodyText fontSize={17} @@ -198,12 +172,12 @@ const UnsupportedDocumentScreen: React.FC<UnsupportedDocumentScreenProps> = ({ > <PrimaryButton onPress={onNotifyMe} - trackEvent={PassportEvents.NOTIFY_UNSUPPORTED_PASSPORT} + trackEvent={PassportEvents.NOTIFY_COMING_SOON} > Sign up for updates </PrimaryButton> <SecondaryButton - trackEvent={PassportEvents.DISMISS_UNSUPPORTED_PASSPORT} + trackEvent={PassportEvents.DISMISS_COMING_SOON} onPress={onDismiss} > Dismiss @@ -213,4 +187,4 @@ const UnsupportedDocumentScreen: React.FC<UnsupportedDocumentScreenProps> = ({ ); }; -export default UnsupportedDocumentScreen; +export default ComingSoonScreen; diff --git a/app/src/screens/document/CountryPickerScreen.tsx b/app/src/screens/document/CountryPickerScreen.tsx new file mode 100644 index 000000000..8afa3e25e --- /dev/null +++ b/app/src/screens/document/CountryPickerScreen.tsx @@ -0,0 +1,197 @@ +// 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 { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { FlatList, TouchableOpacity, View } from 'react-native'; +import { Spinner, XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { commonNames } from '@selfxyz/common/constants/countries'; +import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + +import { RoundFlag } from '@/components/flag/RoundFlag'; +import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar'; +import { BodyText } from '@/components/typography/BodyText'; +import type { RootStackParamList } from '@/navigation'; +import { black, slate100, slate500 } from '@/utils/colors'; +import { advercase } from '@/utils/fonts'; +import { buttonTap } from '@/utils/haptic'; + +interface CountryData { + [countryCode: string]: string[]; +} + +interface CountryListItem { + key: string; + countryCode: string; +} + +const ITEM_HEIGHT = 65; +const FLAG_SIZE = 32; + +const CountryItem = memo<{ + countryCode: string; + onSelect: (code: string) => void; +}>(({ countryCode, onSelect }) => { + const countryName = commonNames[countryCode as keyof typeof commonNames]; + + if (!countryName) return null; + + return ( + <TouchableOpacity + onPress={() => onSelect(countryCode)} + style={{ + paddingVertical: 13, + }} + > + <XStack alignItems="center" gap={16}> + <RoundFlag countryCode={countryCode} size={FLAG_SIZE} /> + <BodyText fontSize={16} color={black} flex={1}> + {countryName} + </BodyText> + </XStack> + </TouchableOpacity> + ); +}); + +CountryItem.displayName = 'CountryItem'; + +const CountryPickerScreen: React.FC = () => { + const [countryData, setCountryData] = useState<CountryData>({}); + const [loading, setLoading] = useState(true); + const navigation = + useNavigation<NativeStackNavigationProp<RootStackParamList>>(); + const selfClient = useSelfClient(); + + const onPressCountry = useCallback( + (countryCode: string) => { + buttonTap(); + if (__DEV__) { + console.log('Selected country code:', countryCode); + console.log('Current countryData:', countryData); + console.log('Available country codes:', Object.keys(countryData)); + } + const documentTypes = countryData[countryCode]; + if (__DEV__) { + console.log('documentTypes for', countryCode, ':', documentTypes); + } + + if (documentTypes && documentTypes.length > 0) { + const countryName = + commonNames[countryCode as keyof typeof commonNames] || countryCode; + + // Emit the country selection event + selfClient.emit(SdkEvents.DOCUMENT_COUNTRY_SELECTED, { + countryCode: countryCode, + countryName: countryName, + documentTypes: documentTypes, + }); + + navigation.navigate('IDPicker', { countryCode, documentTypes }); + } else { + navigation.navigate('ComingSoon', { countryCode }); + } + }, + [countryData, navigation, selfClient], + ); + + useEffect(() => { + const fetchCountryData = async () => { + try { + const response = await fetch('https://api.staging.self.xyz/id-picker'); + const result = await response.json(); + + if (result.status === 'success') { + setCountryData(result.data); + if (__DEV__) { + console.log('Set country data:', result.data); + } + } else { + console.error('API returned non-success status:', result.status); + } + } catch (error) { + console.error('Error fetching country data:', error); + } finally { + setLoading(false); + } + }; + + fetchCountryData(); + }, []); + + const countryList = useMemo( + () => + Object.keys(countryData).map(countryCode => ({ + key: countryCode, + countryCode, + })), + [countryData], + ); + + const renderItem = useCallback( + ({ item }: { item: CountryListItem }) => ( + <CountryItem countryCode={item.countryCode} onSelect={onPressCountry} /> + ), + [onPressCountry], + ); + + const keyExtractor = useCallback( + (item: CountryListItem) => item.countryCode, + [], + ); + + const renderLoadingState = () => ( + <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> + <Spinner size="small" /> + </View> + ); + + const getItemLayout = useCallback( + ( + _data: ReadonlyArray<CountryListItem> | null | undefined, + index: number, + ) => ({ + length: ITEM_HEIGHT, + offset: ITEM_HEIGHT * index, + index, + }), + [], + ); + + return ( + <YStack flex={1} backgroundColor={slate100}> + <DocumentFlowNavBar title="GETTING STARTED" /> + <YStack flex={1} paddingTop="$4" paddingHorizontal="$4"> + <YStack marginTop="$4" marginBottom="$6"> + <BodyText fontSize={29} fontFamily={advercase}> + Select the country that issued your ID + </BodyText> + <BodyText fontSize={16} color={slate500} marginTop="$3"> + Self has support for over 300 ID types. You can select the type of + ID in the next step + </BodyText> + </YStack> + {loading ? ( + renderLoadingState() + ) : ( + <FlatList + data={countryList} + renderItem={renderItem} + keyExtractor={keyExtractor} + showsVerticalScrollIndicator={false} + removeClippedSubviews={true} + maxToRenderPerBatch={10} + windowSize={10} + initialNumToRender={10} + updateCellsBatchingPeriod={50} + getItemLayout={getItemLayout} + /> + )} + </YStack> + </YStack> + ); +}; + +export default CountryPickerScreen; diff --git a/app/src/screens/document/DocumentOnboardingScreen.tsx b/app/src/screens/document/DocumentOnboardingScreen.tsx index 606fd6fa1..f46f7945a 100644 --- a/app/src/screens/document/DocumentOnboardingScreen.tsx +++ b/app/src/screens/document/DocumentOnboardingScreen.tsx @@ -48,7 +48,7 @@ const DocumentOnboardingScreen: React.FC = () => { onAnimationFinish={() => { setTimeout(() => { animationRef.current?.play(); - }, 5000); // Pause 5 seconds before playing again + }, 220); }} source={passportOnboardingAnimation} style={styles.animation} diff --git a/app/src/screens/document/IDPickerScreen.tsx b/app/src/screens/document/IDPickerScreen.tsx new file mode 100644 index 000000000..8a299e23d --- /dev/null +++ b/app/src/screens/document/IDPickerScreen.tsx @@ -0,0 +1,207 @@ +// 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 React from 'react'; +import { View, XStack, YStack } from 'tamagui'; +import type { RouteProp } from '@react-navigation/native'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + +import { RoundFlag } from '@/components/flag/RoundFlag'; +import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar'; +import { BodyText } from '@/components/typography/BodyText'; +import AadhaarLogo from '@/images/icons/aadhaar.svg'; +import EPassportLogoRounded from '@/images/icons/epassport_rounded.svg'; +import PlusIcon from '@/images/icons/plus.svg'; +import SelfLogo from '@/images/logo.svg'; +import { useSafeAreaInsets } from '@/mocks/react-native-safe-area-context'; +import type { RootStackParamList } from '@/navigation'; +import { black, slate100, slate300, slate400, white } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; +import { advercase, dinot } from '@/utils/fonts'; +import { buttonTap } from '@/utils/haptic'; + +type IDPickerScreenRouteProp = RouteProp<RootStackParamList, 'IDPicker'>; + +const getDocumentName = (docType: string): string => { + switch (docType) { + case 'p': + return 'Passport'; + case 'i': + return 'ID card'; + case 'a': + return 'Aadhaar'; + default: + return 'Unknown Document'; + } +}; + +const getDocumentNameForEvent = (docType: string): string => { + switch (docType) { + case 'p': + return 'passport'; + case 'i': + return 'id_card'; + case 'a': + return 'aadhaar'; + default: + return 'unknown_document'; + } +}; + +const getDocumentDescription = (docType: string): string => { + switch (docType) { + case 'p': + return 'Verified Biometric Passport'; + case 'i': + return 'Verified Biometric ID card'; + case 'a': + return 'Verified mAadhaar QR code'; + default: + return 'Unknown Document'; + } +}; + +const getDocumentLogo = (docType: string): React.ReactNode => { + switch (docType) { + case 'p': + return <EPassportLogoRounded />; + case 'i': + return <EPassportLogoRounded />; + case 'a': + return <AadhaarLogo />; + default: + return null; + } +}; + +const IDPickerScreen: React.FC = () => { + const route = useRoute<IDPickerScreenRouteProp>(); + const { countryCode = '', documentTypes = [] } = route.params || {}; + const bottom = useSafeAreaInsets().bottom; + const selfClient = useSelfClient(); + const navigation = + useNavigation<NativeStackNavigationProp<RootStackParamList>>(); + + const onSelectDocumentType = (docType: string) => { + buttonTap(); + + const countryName = getDocumentName(docType); + + selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, { + documentType: docType, + documentName: getDocumentNameForEvent(docType), + countryCode: countryCode, + countryName: countryName, + }); + switch (docType) { + case 'p': + navigation.navigate('DocumentOnboarding'); + break; + case 'i': + navigation.navigate('DocumentOnboarding'); + break; + case 'a': + navigation.navigate('AadhaarUpload', { countryCode } as never); + break; + default: + navigation.navigate('ComingSoon', { countryCode } as never); + break; + } + + // TODO: Navigate to the next screen based on document type + if (__DEV__) { + console.log( + `Selected document type: ${docType} for country: ${countryCode}`, + ); + } + }; + + return ( + <YStack + flex={1} + backgroundColor={slate100} + paddingBottom={bottom + extraYPadding + 24} + > + <DocumentFlowNavBar title="GETTING STARTED" /> + <YStack + flex={1} + paddingTop="$4" + paddingHorizontal="$4" + justifyContent="center" + > + <YStack marginTop="$4" marginBottom="$6"> + <XStack + justifyContent="center" + alignItems="center" + borderRadius={'$2'} + gap={'$2.5'} + > + <View width={48} height={48}> + <RoundFlag countryCode={countryCode} size={48} /> + </View> + <PlusIcon width={18} height={18} color={slate400} /> + <YStack + backgroundColor={black} + borderRadius={'$2'} + height={48} + width={48} + justifyContent="center" + alignItems="center" + > + <SelfLogo width={24} height={24} /> + </YStack> + </XStack> + <BodyText + marginTop="$6" + fontSize={29} + fontFamily={advercase} + textAlign="center" + > + Select an ID type + </BodyText> + </YStack> + <YStack gap="$3"> + {documentTypes.map((docType: string) => ( + <XStack + key={docType} + backgroundColor={white} + borderWidth={1} + borderColor={slate300} + elevation={4} + borderRadius={'$5'} + padding={'$3'} + pressStyle={{ scale: 0.97, backgroundColor: slate100 }} + onPress={() => onSelectDocumentType(docType)} + > + <XStack alignItems="center" gap={'$3'} flex={1}> + {getDocumentLogo(docType)} + <YStack gap={'$1'}> + <BodyText fontSize={24} fontFamily={dinot} color={black}> + {getDocumentName(docType)} + </BodyText> + <BodyText fontSize={14} fontFamily={dinot} color="#9193A2"> + {getDocumentDescription(docType)} + </BodyText> + </YStack> + </XStack> + </XStack> + ))} + <BodyText + fontSize={18} + fontFamily={dinot} + color={slate400} + textAlign="center" + > + Be sure your document is ready to scan + </BodyText> + </YStack> + </YStack> + </YStack> + ); +}; + +export default IDPickerScreen; diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index b56d89cb8..e8f12ee53 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import LottieView from 'lottie-react-native'; +import LottieView, { type LottieViewProps } from 'lottie-react-native'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Linking, StyleSheet, View } from 'react-native'; import { SystemBars } from 'react-native-edge-to-edge'; @@ -48,7 +48,8 @@ const SuccessScreen: React.FC = () => { const isFocused = useIsFocused(); - const [animationSource, setAnimationSource] = useState<any>(loadingAnimation); + const [animationSource, setAnimationSource] = + useState<LottieViewProps['source']>(loadingAnimation); const [countdown, setCountdown] = useState<number | null>(null); const [countdownStarted, setCountdownStarted] = useState(false); const timerRef = useRef<NodeJS.Timeout | null>(null); diff --git a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx index 130fbd0bf..83f8e5613 100644 --- a/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/recovery/AccountRecoveryChoiceScreen.tsx @@ -111,7 +111,7 @@ const AccountRecoveryChoiceScreen: React.FC = () => { onRestoreFromCloudNext, navigation, toggleCloudBackupEnabled, - selfClient, + useProtocolStore, ]); const handleManualRecoveryPress = useCallback(() => { diff --git a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx index 8d63ad2a6..d3062b47e 100644 --- a/app/src/screens/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/recovery/RecoverWithPhraseScreen.tsx @@ -102,7 +102,7 @@ const RecoverWithPhraseScreen: React.FC = () => { navigation, restoreAccountFromMnemonic, trackEvent, - selfClient, + useProtocolStore, ]); return ( diff --git a/app/src/screens/settings/ManageDocumentsScreen.tsx b/app/src/screens/settings/ManageDocumentsScreen.tsx index 52d6ad6a6..f0e030aa7 100644 --- a/app/src/screens/settings/ManageDocumentsScreen.tsx +++ b/app/src/screens/settings/ManageDocumentsScreen.tsx @@ -280,10 +280,10 @@ const ManageDocumentsScreen: React.FC = () => { trackEvent(DocumentEvents.MANAGE_SCREEN_OPENED); }, [trackEvent]); - const handleScanDocument = () => { + const handleAddDocument = () => { impactLight(); trackEvent(DocumentEvents.ADD_NEW_SCAN_SELECTED); - navigation.navigate('DocumentOnboarding'); + navigation.navigate('CountryPicker'); }; const handleGenerateMock = () => { @@ -292,12 +292,6 @@ const ManageDocumentsScreen: React.FC = () => { navigation.navigate('CreateMock'); }; - const handleAddAadhaar = () => { - impactLight(); - trackEvent(DocumentEvents.ADD_NEW_AADHAAR_SELECTED); - navigation.navigate('AadhaarUpload'); - }; - return ( <YStack flex={1} @@ -322,11 +316,8 @@ const ManageDocumentsScreen: React.FC = () => { </Text> <ButtonsContainer> - <PrimaryButton onPress={handleScanDocument}> - Scan New ID Document - </PrimaryButton> - <PrimaryButton onPress={handleAddAadhaar}> - Add Aadhaar + <PrimaryButton onPress={handleAddDocument}> + Add New Document </PrimaryButton> <SecondaryButton onPress={handleGenerateMock}> Generate Mock Document diff --git a/app/src/screens/system/LaunchScreen.tsx b/app/src/screens/system/LaunchScreen.tsx index fcc8f1895..91f6423eb 100644 --- a/app/src/screens/system/LaunchScreen.tsx +++ b/app/src/screens/system/LaunchScreen.tsx @@ -3,7 +3,7 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import React from 'react'; -import { Linking, StyleSheet, View } from 'react-native'; +import { 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'; @@ -13,18 +13,23 @@ import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import AbstractButton from '@/components/buttons/AbstractButton'; import { BodyText } from '@/components/typography/BodyText'; import { Caption } from '@/components/typography/Caption'; -import { privacyUrl, supportedBiometricIdsUrl, termsUrl } from '@/consts/links'; +import { privacyUrl, termsUrl } from '@/consts/links'; import useConnectionModal from '@/hooks/useConnectionModal'; import useHapticNavigation from '@/hooks/useHapticNavigation'; -import Logo from '@/images/logo.svg'; -import { black, slate400, white, zinc800, zinc900 } from '@/utils/colors'; -import { extraYPadding } from '@/utils/constants'; +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 onStartPress = useHapticNavigation('DocumentOnboarding'); - const onAadhaarPress = useHapticNavigation('AadhaarUpload'); + const onPress = useHapticNavigation('CountryPicker'); const createMock = useHapticNavigation('CreateMock'); const { bottom } = useSafeAreaInsets(); @@ -35,75 +40,59 @@ const LaunchScreen: React.FC = () => { }); return ( - <YStack - backgroundColor={black} - flex={1} - alignItems="center" - paddingHorizontal={20} - paddingBottom={bottom + extraYPadding} - > + <YStack backgroundColor={black} flex={1} alignItems="center"> <View style={styles.container}> - <View style={styles.card}> + <YStack flex={1} justifyContent="center" alignItems="center"> <GestureDetector gesture={devModeTap}> - <View style={styles.logoSection}> - <Logo style={styles.logo} /> - </View> + <YStack + backgroundColor={red500} + borderRadius={14} + overflow="hidden" + > + <IDCardPlaceholder width={300} height={180} /> + </YStack> </GestureDetector> - - <Text style={styles.title}>Get started</Text> - - <BodyText style={styles.description}> - Register with Self using your passport, biometric ID or Aadhaar card - to prove your identity across the web without revealing your - personal information. - </BodyText> - </View> + </YStack> + <Text + color={white} + fontSize={38} + fontFamily={advercase} + fontWeight="500" + textAlign="center" + marginBottom={16} + > + Take control of your digital identity + </Text> + <BodyText + 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" - marginBottom={20} - marginTop={24} + paddingHorizontal={20} + paddingBottom={bottom} + paddingTop={30} + backgroundColor={zinc800} > - <YStack gap="$3" width="100%"> - <AbstractButton - bgColor={black} - borderColor={zinc800} - borderWidth={1} - color={white} - trackEvent={AppEvents.SUPPORTED_BIOMETRIC_IDS} - onPress={async () => { - try { - await Linking.openURL(supportedBiometricIdsUrl); - } catch (error) { - console.warn('Failed to open supported IDs URL:', error); - } - }} - > - List of Supported Biometric IDs - </AbstractButton> - - <AbstractButton - trackEvent={AppEvents.GET_STARTED_BIOMETRIC} - onPress={onStartPress} - bgColor={white} - color={black} - testID="launch-get-started-button" - > - I have a Passport or Biometric ID - </AbstractButton> - <AbstractButton - trackEvent={AppEvents.GET_STARTED_AADHAAR} - onPress={onAadhaarPress} - bgColor={white} - color={black} - testID="launch-get-started-button" - > - I have an Aadhaar Card - </AbstractButton> - </YStack> + <AbstractButton + trackEvent={AppEvents.GET_STARTED} + onPress={onPress} + bgColor={white} + color={black} + testID="launch-get-started-button" + > + Get Started + </AbstractButton> <Caption style={styles.notice}> By continuing, you agree to the  @@ -125,8 +114,8 @@ export default LaunchScreen; const styles = StyleSheet.create({ container: { - flex: 0, - justifyContent: 'flex-start', + flex: 1, + justifyContent: 'center', alignItems: 'center', width: '102%', paddingTop: '30%', @@ -138,7 +127,6 @@ const styles = StyleSheet.create({ paddingVertical: 40, paddingHorizontal: 20, alignItems: 'center', - backgroundColor: zinc900, shadowColor: black, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, @@ -157,25 +145,11 @@ const styles = StyleSheet.create({ width: 40, height: 40, }, - title: { - fontFamily: advercase, - fontSize: 38, - fontWeight: '500', - color: white, - textAlign: 'center', - marginBottom: 16, - }, - description: { - color: white, - fontSize: 16, - lineHeight: 22, - textAlign: 'center', - marginBottom: 8, - }, + notice: { fontFamily: dinot, + paddingVertical: 10, paddingHorizontal: 20, - paddingVertical: 16, color: slate400, textAlign: 'center', lineHeight: 18, diff --git a/app/src/screens/system/SplashScreen.tsx b/app/src/screens/system/SplashScreen.tsx index f3d347935..18d3128c9 100644 --- a/app/src/screens/system/SplashScreen.tsx +++ b/app/src/screens/system/SplashScreen.tsx @@ -113,7 +113,7 @@ const SplashScreen: React.FC = ({}) => { }); } } - }, [isAnimationFinished, nextScreen, queuedDeepLink, navigation]); + }, [isAnimationFinished, nextScreen, queuedDeepLink, navigation, selfClient]); return ( <LottieView diff --git a/app/src/utils/email.ts b/app/src/utils/email.ts index 412f15e59..0ebe00f67 100644 --- a/app/src/utils/email.ts +++ b/app/src/utils/email.ts @@ -41,11 +41,15 @@ export const sendCountrySupportNotification = async ({ ['documentCategory', documentCategory || 'Unknown'], ['tz', getTimeZone()], ['ts', new Date().toISOString()], - ['origin', 'unsupported_passport_screen'], + ['origin', 'coming_soon_screen'], ] as [string, string][]; const documentTypeText = - documentCategory === 'id_card' ? 'ID cards' : 'passports'; + documentCategory === 'id_card' + ? 'ID cards' + : documentCategory === 'passport' + ? 'passports' + : 'documents'; const body = `Hi SELF Team, diff --git a/app/tests/e2e/launch.android.flow.yaml b/app/tests/e2e/launch.android.flow.yaml index 3d841eed9..5900d5f93 100644 --- a/app/tests/e2e/launch.android.flow.yaml +++ b/app/tests/e2e/launch.android.flow.yaml @@ -2,5 +2,5 @@ appId: com.proofofpassportapp --- - launchApp - extendedWaitUntil: - visible: "I have a Passport or Biometric ID" + visible: "Get Started" timeout: ${LAUNCH_WAIT_MS:60000} diff --git a/app/tests/e2e/launch.ios.flow.yaml b/app/tests/e2e/launch.ios.flow.yaml index 09bcfb987..befe82e85 100644 --- a/app/tests/e2e/launch.ios.flow.yaml +++ b/app/tests/e2e/launch.ios.flow.yaml @@ -2,5 +2,5 @@ appId: com.warroom.proofofpassport --- - launchApp - extendedWaitUntil: - visible: "I have a Passport or Biometric ID" + visible: "Get Started" timeout: ${LAUNCH_WAIT_MS:60000} diff --git a/app/tests/src/navigation.test.ts b/app/tests/src/navigation.test.ts index 005873378..194bada06 100644 --- a/app/tests/src/navigation.test.ts +++ b/app/tests/src/navigation.test.ts @@ -14,7 +14,9 @@ describe('navigation', () => { 'AccountRecoveryChoice', 'AccountVerifiedSuccess', 'CloudBackupSettings', + 'ComingSoon', 'ConfirmBelonging', + 'CountryPicker', 'CreateMock', 'DeferredLinkingInfo', 'DevFeatureFlags', @@ -31,6 +33,7 @@ describe('navigation', () => { 'DocumentNFCTrouble', 'DocumentOnboarding', 'Home', + 'IDPicker', 'IdDetails', 'Launch', 'Loading', @@ -48,7 +51,6 @@ describe('navigation', () => { 'Settings', 'ShowRecoveryPhrase', 'Splash', - 'UnsupportedDocument', ]); }); }); diff --git a/packages/mobile-sdk-alpha/README.md b/packages/mobile-sdk-alpha/README.md index d393e2a17..e49f32a29 100644 --- a/packages/mobile-sdk-alpha/README.md +++ b/packages/mobile-sdk-alpha/README.md @@ -14,7 +14,7 @@ Alpha SDK for registering and proving. Adapters-first, React Native-first with w - `createSelfClient({ config, adapters })` - `scanDocument(opts)`, `validateDocument(input)`, `checkRegistration(input)`, `generateProof(req, { signal, onProgress, timeoutMs })` -- Eventing: `on(event, cb)` +- Eventing: `on(event, cb)`, `emit(event, payload)` - Web shim: `webScannerShim` (QR stub only) ## Environment shims @@ -37,6 +37,48 @@ const sdk = createSelfClient({ }); ``` +## SDK Events + +The SDK emits events throughout the verification lifecycle. Subscribe using `selfClient.on(event, callback)`. + +### Document Selection Events + +**`SdkEvents.DOCUMENT_COUNTRY_SELECTED`** - Emitted when user selects a country during document flow + +```ts +selfClient.on(SdkEvents.DOCUMENT_COUNTRY_SELECTED, payload => { + // payload: { countryCode: string, countryName: string, documentTypes: string[] } + console.log(`Country selected: ${payload.countryName} (${payload.countryCode})`); + console.log(`Available types: ${payload.documentTypes.join(', ')}`); +}); +``` + +**`SdkEvents.DOCUMENT_TYPE_SELECTED`** - Emitted when user selects a document type + +```ts +selfClient.on(SdkEvents.DOCUMENT_TYPE_SELECTED, payload => { + // payload: { documentType: string, documentName: string, countryCode: string, countryName: string } + console.log(`Document selected: ${payload.documentName} from ${payload.countryName}`); +}); +``` + +### Verification Flow Events + +- **`PROVING_PASSPORT_DATA_NOT_FOUND`** - No passport data found; navigate to scanning screen +- **`PROVING_ACCOUNT_VERIFIED_SUCCESS`** - Identity verification successful +- **`PROVING_REGISTER_ERROR_OR_FAILURE`** - Registration failed; check `hasValidDocument` flag +- **`PROVING_PASSPORT_NOT_SUPPORTED`** - Unsupported country/document; includes `countryCode` and `documentCategory` +- **`PROVING_ACCOUNT_RECOVERY_REQUIRED`** - Document registered with different credentials + +### System Events + +- **`ERROR`** - SDK operation errors and timeouts +- **`PROGRESS`** - Long-running operation progress updates +- **`PROOF_EVENT`** - Detailed proof generation events (for debugging) +- **`NFC_EVENT`** - NFC scanning lifecycle events (for debugging) + +See `SdkEvents` enum and `SDKEventMap` in `src/types/events.ts` for complete payload definitions. + ## Processing utilities ```ts diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 05bdaa1b9..ea2a34876 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -32,11 +32,12 @@ export type { DG1, DG2, NFCScanOptions, ParsedNFCResponse } from './nfc'; export type { MRZScanOptions } from './mrz'; export type { PassportValidationCallbacks } from './validation/document'; export type { QRProofOptions } from './qr'; -export type { SdkErrorCategory } from './errors'; +export type { SDKEvent, SDKEventMap } from './types/events'; +export type { SdkErrorCategory } from './errors'; export { type ProvingStateType } from './proving/provingMachine'; -export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; +export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; export { SdkEvents } from './types/events'; export { SelfClientContext, SelfClientProvider, usePrepareDocumentProof, useSelfClient } from './context'; diff --git a/packages/mobile-sdk-alpha/src/constants/analytics.ts b/packages/mobile-sdk-alpha/src/constants/analytics.ts index fae69d751..56307c97f 100644 --- a/packages/mobile-sdk-alpha/src/constants/analytics.ts +++ b/packages/mobile-sdk-alpha/src/constants/analytics.ts @@ -3,46 +3,40 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. export const AadhaarEvents = { - UPLOAD_SCREEN_OPENED: 'Aadhaar: Upload Screen Opened', - QR_UPLOAD_REQUESTED: 'Aadhaar: QR Upload Requested', - QR_UPLOAD_SUCCESS: 'Aadhaar: QR Upload Success', - QR_UPLOAD_FAILED: 'Aadhaar: QR Upload Failed', - PERMISSION_MODAL_OPENED: 'Aadhaar: Permission Modal Opened', + CONTINUE_TO_REGISTRATION_PRESSED: 'Aadhaar: Continue to Registration Pressed', + DATA_STORAGE_STARTED: 'Aadhaar: Data Storage Started', + DATA_STORAGE_SUCCESS: 'Aadhaar: Data Storage Success', + ERROR_SCREEN_NAVIGATED: 'Aadhaar: Error Screen Navigated', + HELP_BUTTON_PRESSED: 'Aadhaar: Help Button Pressed', PERMISSION_MODAL_DISMISSED: 'Aadhaar: Permission Modal Dismissed', + PERMISSION_MODAL_OPENED: 'Aadhaar: Permission Modal Opened', PERMISSION_SETTINGS_OPENED: 'Aadhaar: Permission Settings Opened', + PHOTO_LIBRARY_UNAVAILABLE: 'Aadhaar: Photo Library Unavailable', PROCESSING_STARTED: 'Aadhaar: Processing Started', - // Error-specific events QR_CODE_EXPIRED: 'Aadhaar: QR Code Expired', QR_CODE_INVALID_FORMAT: 'Aadhaar: QR Code Invalid Format', QR_CODE_MISSING_FIELDS: 'Aadhaar: QR Code Missing Required Fields', QR_CODE_PARSE_FAILED: 'Aadhaar: QR Code Parse Failed', - PHOTO_LIBRARY_UNAVAILABLE: 'Aadhaar: Photo Library Unavailable', - USER_CANCELLED_SELECTION: 'Aadhaar: User Cancelled Photo Selection', - // Validation events - TIMESTAMP_VALIDATION_STARTED: 'Aadhaar: Timestamp Validation Started', - TIMESTAMP_VALIDATION_FAILED: 'Aadhaar: Timestamp Validation Failed', - TIMESTAMP_VALIDATION_SUCCESS: 'Aadhaar: Timestamp Validation Success', - // Data processing events QR_DATA_EXTRACTION_STARTED: 'Aadhaar: QR Data Extraction Started', QR_DATA_EXTRACTION_SUCCESS: 'Aadhaar: QR Data Extraction Success', - DATA_STORAGE_STARTED: 'Aadhaar: Data Storage Started', - DATA_STORAGE_SUCCESS: 'Aadhaar: Data Storage Success', - // Screen interaction events + QR_UPLOAD_FAILED: 'Aadhaar: QR Upload Failed', + QR_UPLOAD_REQUESTED: 'Aadhaar: QR Upload Requested', + QR_UPLOAD_SUCCESS: 'Aadhaar: QR Upload Success', + RETRY_BUTTON_PRESSED: 'Aadhaar: Retry Button Pressed', + TIMESTAMP_VALIDATION_FAILED: 'Aadhaar: Timestamp Validation Failed', + TIMESTAMP_VALIDATION_STARTED: 'Aadhaar: Timestamp Validation Started', + TIMESTAMP_VALIDATION_SUCCESS: 'Aadhaar: Timestamp Validation Success', UPLOAD_BUTTON_DISABLED: 'Aadhaar: Upload Button Disabled', UPLOAD_BUTTON_ENABLED: 'Aadhaar: Upload Button Enabled', - // Error recovery events - ERROR_SCREEN_NAVIGATED: 'Aadhaar: Error Screen Navigated', - RETRY_BUTTON_PRESSED: 'Aadhaar: Retry Button Pressed', - HELP_BUTTON_PRESSED: 'Aadhaar: Help Button Pressed', - // Success screen events - CONTINUE_TO_REGISTRATION_PRESSED: 'Aadhaar: Continue to Registration Pressed', + UPLOAD_SCREEN_OPENED: 'Aadhaar: Upload Screen Opened', + USER_CANCELLED_SELECTION: 'Aadhaar: User Cancelled Photo Selection', }; export const AppEvents = { DISMISS_PRIVACY_DISCLAIMER: 'App: Dismiss Privacy Disclaimer', GET_STARTED: 'App: Get Started', - GET_STARTED_BIOMETRIC: 'App: Get Started - Biometric ID', GET_STARTED_AADHAAR: 'App: Get Started - Aadhaar', + GET_STARTED_BIOMETRIC: 'App: Get Started - Biometric ID', SUPPORTED_BIOMETRIC_IDS: 'App: Supported Biometric IDs', UPDATE_MODAL_CLOSED: 'App: Update Modal Closed', UPDATE_MODAL_OPENED: 'App: Update Modal Opened', @@ -83,18 +77,18 @@ export const BackupEvents = { }; export const DocumentEvents = { + ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar', ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock', ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan', - ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar', DOCUMENT_DELETED: 'Document: Document Deleted', DOCUMENT_SELECTED: 'Document: Document Selected', + DOCUMENT_VALIDATED: 'Document: Document Validated', DOCUMENTS_FETCHED: 'Document: Documents Fetched', MANAGE_SCREEN_OPENED: 'Document: Manage Documents Screen Opened', NO_DOCUMENTS_FOUND: 'Document: No Documents Found', PASSPORT_INFO_OPENED: 'Document: Passport Info Screen Opened', PASSPORT_METADATA_LOADED: 'Document: Passport Metadata Loaded', VALIDATE_DOCUMENT_FAILED: 'Document: Validate Document Failed', - DOCUMENT_VALIDATED: 'Document: Document Validated', }; export const MockDataEvents = { @@ -126,19 +120,19 @@ export const PassportEvents = { CAMERA_SCAN_SUCCESS: 'Passport: Camera Scan Success', CAMERA_SCREEN_CLOSED: 'Passport: Camera View Closed', CANCEL_PASSPORT_NFC: 'Passport: Cancel Passport NFC', + COMING_SOON: 'Passport: Passport Not Supported', DATA_LOAD_ERROR: 'Passport: Passport Data Load Error', - DISMISS_UNSUPPORTED_PASSPORT: 'Passport: Dismiss Unsupported Passport', - NOTIFY_UNSUPPORTED_PASSPORT: 'Passport: Notify Unsupported Passport', + DISMISS_COMING_SOON: 'Passport: Dismiss Unsupported Passport', NFC_RESPONSE_PARSE_FAILED: 'Passport: Parsing NFC Response Unsuccessful', NFC_SCAN_FAILED: 'Passport: NFC Scan Failed', NFC_SCAN_SUCCESS: 'Passport: NFC Scan Success', + NOTIFY_COMING_SOON: 'Passport: Notify Unsupported Passport', OPEN_NFC_SETTINGS: 'Passport: Open NFC Settings', OWNERSHIP_CONFIRMED: 'Passport: Passport Ownership Confirmed', PASSPORT_DATA_NOT_FOUND: 'Passport: Passport Data Not Found', PASSPORT_PARSE_FAILED: 'Passport: Passport Parse Failed', PASSPORT_PARSED: 'Passport: Passport Parsed', START_PASSPORT_NFC: 'Passport: Start Passport NFC', - UNSUPPORTED_PASSPORT: 'Passport: Passport Not Supported', }; export const ProofEvents = { @@ -147,6 +141,7 @@ export const ProofEvents = { ATTESTATION_VERIFIED: 'Proof: Attestation Verified', CLEANUP_COMPLETED: 'Proof: Connections Cleanup Completed', CLEANUP_STARTED: 'Proof: Connections Cleanup Started', + CONNECTION_UUID_GENERATED: 'Proof: Connection UUID Generated', DEVICE_TOKEN_REG_FAILED: 'Proof: Device Token Registration Failed', DEVICE_TOKEN_REG_STARTED: 'Proof: Device Token Registration Started', DEVICE_TOKEN_REG_SUCCESS: 'Proof: Device Token Registration Succeeded', @@ -170,11 +165,11 @@ export const ProofEvents = { PROOF_DISCLOSURES_SCROLLED: 'Proof: Proof Disclosures Scrolled', PROOF_FAILED: 'Proof: Proof Failed', PROOF_RESULT_ACKNOWLEDGED: 'Proof: Proof Result Acknowledged', - PROVING_PROCESS_STARTED: 'Proof: Proving Process Started', - PROOF_VERIFY_LONG_PRESS: 'Proof: Verify Button Long Pressed', PROOF_VERIFY_CONFIRMATION_ACCEPTED: 'Proof: Verify Confirmation Accepted', + PROOF_VERIFY_LONG_PRESS: 'Proof: Verify Button Long Pressed', PROVING_INIT: 'Proof: Proving Machine Init', PROVING_PROCESS_ERROR: 'Proof: Proving Process Error', + PROVING_PROCESS_STARTED: 'Proof: Proving Process Started', PROVING_STATE_CHANGE: 'Proof: Proving State Change', QR_SCAN_CANCELLED: 'Proof: QR Scan Cancelled', QR_SCAN_FAILED: 'Proof: QR Scan Failed', @@ -200,7 +195,6 @@ export const ProofEvents = { VALIDATION_SUCCESS: 'Proof: Validation Succeeded', WS_HELLO_ACK: 'Proof: WS Hello Acknowledged', WS_HELLO_SENT: 'Proof: WS Hello Sent', - CONNECTION_UUID_GENERATED: 'Proof: Connection UUID Generated', }; export const SettingsEvents = { diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index d41dde32f..aa63493b7 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -45,9 +45,12 @@ export type { PassportValidationCallbacks } from './validation/document'; export type { QRProofOptions } from './qr'; +export type { SDKEvent, SDKEventMap } from './types/events'; + // Error handling export type { SdkErrorCategory } from './errors'; +// Screen Components (React Native-based) export { InitError, LivenessError, @@ -58,13 +61,11 @@ export { notImplemented, sdkError, } from './errors'; - -// Screen Components (React Native-based) export { NFCScannerScreen } from './components/screens/NFCScannerScreen'; export { PassportCameraScreen } from './components/screens/PassportCameraScreen'; -export { QRCodeScreen } from './components/screens/QRCodeScreen'; // Context and Client +export { QRCodeScreen } from './components/screens/QRCodeScreen'; export { SdkEvents } from './types/events'; // Components diff --git a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts index 98f1cefb9..0eeff062d 100644 --- a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts +++ b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts @@ -972,7 +972,7 @@ export const useProvingStore = create<ProvingState>((set, get) => { duration_ms: Date.now() - startTime, }); console.error('Passport not supported:', isSupported.status, isSupported.details); - selfClient.trackEvent(PassportEvents.UNSUPPORTED_PASSPORT, { + selfClient.trackEvent(PassportEvents.COMING_SOON, { status: isSupported.status, details: isSupported.details, }); diff --git a/packages/mobile-sdk-alpha/src/types/events.ts b/packages/mobile-sdk-alpha/src/types/events.ts index 473451bbf..008465e82 100644 --- a/packages/mobile-sdk-alpha/src/types/events.ts +++ b/packages/mobile-sdk-alpha/src/types/events.ts @@ -69,6 +69,22 @@ export enum SdkEvents { */ PROVING_ACCOUNT_RECOVERY_REQUIRED = 'PROVING_ACCOUNT_RECOVERY_REQUIRED', + /** + * Emitted when a user selects a country in the document flow. + * + * **Recommended:** Use this event to track user selection patterns and analytics. + * The event includes the selected country code and available document types. + */ + DOCUMENT_COUNTRY_SELECTED = 'DOCUMENT_COUNTRY_SELECTED', + + /** + * Emitted when a user selects a document type for verification. + * + * **Recommended:** Use this event to track document type preferences and analytics. + * The event includes the selected document type, country code, and document name. + */ + DOCUMENT_TYPE_SELECTED = 'DOCUMENT_TYPE_SELECTED', + /** * Emitted when the proving generation process begins. * @@ -105,6 +121,17 @@ export interface SDKEventMap { documentCategory: DocumentCategory | null; }; [SdkEvents.PROVING_ACCOUNT_RECOVERY_REQUIRED]: undefined; + [SdkEvents.DOCUMENT_COUNTRY_SELECTED]: { + countryCode: string; + countryName: string; + documentTypes: string[]; + }; + [SdkEvents.DOCUMENT_TYPE_SELECTED]: { + documentType: string; + documentName: string; + countryCode: string; + countryName: string; + }; [SdkEvents.PROVING_BEGIN_GENERATION]: { uuid: string; isMock: boolean;