Move Country and ID Picker and more (#1286)

* update fonts

* move haptics

* fix sorting isues

* dynamic import of peer dependency seems wise


* moves Country Picker Screen

* remove unused and incorrect countryname from doc pick event.


Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* svg works


Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Aaron DeRuvo
2025-10-16 16:20:58 +02:00
committed by GitHub
parent b3c5603d74
commit 5d04870b36
37 changed files with 844 additions and 619 deletions

View File

@@ -2,199 +2,17 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { memo, useCallback } from 'react';
import {
ActivityIndicator,
FlatList,
TouchableOpacity,
View,
} from 'react-native';
import { commonNames } from '@selfxyz/common/constants/countries';
import {
SdkEvents,
useCountries,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
RoundFlag,
XStack,
YStack,
} from '@selfxyz/mobile-sdk-alpha/components';
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
import { black, slate100, slate500 } from '@/utils/colors';
import { advercase, dinot } from '@/utils/fonts';
import { buttonTap } from '@/utils/haptic';
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 style={{ fontSize: 16, color: black, flex: 1 }}>
{countryName}
</BodyText>
</XStack>
</TouchableOpacity>
);
});
CountryItem.displayName = 'CountryItem';
const CountryPickerScreen: React.FC = () => {
const selfClient = useSelfClient();
const { countryData, countryList, loading, userCountryCode, showSuggestion } =
useCountries();
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,
});
} else {
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {
countryCode: countryCode,
documentCategory: null,
});
}
},
[countryData, selfClient],
);
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' }}>
<ActivityIndicator size="small" />
</View>
);
const getItemLayout = useCallback(
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[],
);
import { slate100 } from '@/utils/colors';
export default function CountryPickerScreen() {
return (
<YStack flex={1} backgroundColor={slate100}>
<DocumentFlowNavBar title="GETTING STARTED" />
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4">
<YStack marginTop="$4" marginBottom="$6">
<BodyText style={{ fontSize: 29, fontFamily: advercase }}>
Select the country that issued your ID
</BodyText>
<BodyText style={{ fontSize: 16, color: slate500, marginTop: 20 }}>
Self has support for over 300 ID types. You can select the type of
ID in the next step
</BodyText>
</YStack>
{loading ? (
renderLoadingState()
) : (
<YStack flex={1}>
{showSuggestion && (
<YStack marginBottom="$2">
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginBottom: 8,
}}
>
SUGGESTION
</BodyText>
<CountryItem
countryCode={
userCountryCode as string /*safe due to showSuggestion*/
}
onSelect={onPressCountry}
/>
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginTop: 20,
}}
>
SELECT AN ISSUING COUNTRY
</BodyText>
</YStack>
)}
<FlatList
data={countryList}
renderItem={renderItem}
keyExtractor={keyExtractor}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
getItemLayout={getItemLayout}
/>
</YStack>
)}
</YStack>
<SDKCountryPickerScreen />
</YStack>
);
};
export default CountryPickerScreen;
}

View File

@@ -7,98 +7,20 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import type { RouteProp } from '@react-navigation/native';
import { useRoute } from '@react-navigation/native';
import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
RoundFlag,
View,
XStack,
YStack,
} from '@selfxyz/mobile-sdk-alpha/components';
import AadhaarLogo from '@selfxyz/mobile-sdk-alpha/svgs/icons/aadhaar.svg';
import EPassportLogoRounded from '@selfxyz/mobile-sdk-alpha/svgs/icons/epassport_rounded.svg';
import PlusIcon from '@selfxyz/mobile-sdk-alpha/svgs/icons/plus.svg';
import SelfLogo from '@selfxyz/mobile-sdk-alpha/svgs/logo.svg';
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
import IDSelection from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen';
import { DocumentFlowNavBar } from '@/components/NavBar/DocumentFlowNavBar';
import type { RootStackParamList } from '@/navigation';
import { black, slate100, slate300, slate400, white } from '@/utils/colors';
import { slate100 } 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 onSelectDocumentType = (docType: string) => {
buttonTap();
const countryName = getDocumentName(docType);
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
documentType: docType,
documentName: getDocumentNameForEvent(docType),
countryCode: countryCode,
countryName: countryName,
});
};
return (
<YStack
@@ -107,94 +29,7 @@ const IDPickerScreen: React.FC = () => {
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
style={{
marginTop: 48,
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={{
transform: [{ scale: 0.97 }],
backgroundColor: slate100,
}}
onPress={() => onSelectDocumentType(docType)}
>
<XStack alignItems="center" gap={'$3'} flex={1}>
{getDocumentLogo(docType)}
<YStack gap={'$1'}>
<BodyText
style={{ fontSize: 24, fontFamily: dinot, color: black }}
>
{getDocumentName(docType)}
</BodyText>
<BodyText
style={{
fontSize: 14,
fontFamily: dinot,
color: slate400,
}}
>
{getDocumentDescription(docType)}
</BodyText>
</YStack>
</XStack>
</XStack>
))}
<BodyText
style={{
fontSize: 18,
fontFamily: dinot,
color: slate400,
textAlign: 'center',
}}
>
Be sure your document is ready to scan
</BodyText>
</YStack>
</YStack>
<IDSelection countryCode={countryCode} documentTypes={documentTypes} />
</YStack>
);
};

View File

@@ -2,9 +2,4 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform } from 'react-native';
export const advercase = 'Advercase-Regular';
export const dinot = 'DINOT-Medium';
export const plexMono =
Platform.OS === 'ios' ? 'IBM Plex Mono' : 'IBMPlexMono-Regular';
export { advercase, dinot, plexMono } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,140 +2,21 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform, Vibration } from 'react-native';
import { triggerFeedback } from '@/utils/haptic/trigger';
// Keep track of the loading screen interval
let loadingScreenInterval: ReturnType<typeof setInterval> | null = null;
// Define the base functions first
export const impactLight = () => triggerFeedback('impactLight');
export const impactMedium = () => triggerFeedback('impactMedium');
export const selectionChange = () => triggerFeedback('selection');
// Then define the aliases
export const buttonTap = impactLight;
export const cancelTap = selectionChange;
export const confirmTap = impactMedium;
// consistent light feedback at a steady interval
export const feedbackProgress = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Three light impacts at 750ms intervals
triggerFeedback('custom', {
pattern: [
0,
50, // First light impact
750,
50, // Second light impact
750,
50, // Third light impact
],
});
return;
}
// Match the timing of the light impacts in the Android pattern
setTimeout(() => {
triggerFeedback('impactLight');
}, 750); // First light impact
setTimeout(() => {
triggerFeedback('impactLight');
}, 1500); // Second light impact (750ms after first)
setTimeout(() => {
triggerFeedback('impactLight');
}, 2250); // Third light impact (750ms after second)
};
// light -> medium -> heavy intensity in sequence
export const feedbackSuccess = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Increasing intensity sequence: light -> medium -> heavy
triggerFeedback('custom', {
pattern: [
500,
50, // Initial delay, then light impact
200,
100, // Medium impact
150,
150, // Heavy impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactLight');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
// heavy -> medium -> light intensity in sequence
export const feedbackUnsuccessful = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Decreasing intensity sequence: heavy -> medium -> light
triggerFeedback('custom', {
pattern: [
500,
150, // Initial delay, then heavy impact
100,
100, // Medium impact
150,
50, // Light impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactLight');
}, 1000);
};
/**
* Haptic actions
*/
// Custom feedback events
export const loadingScreenProgress = (shouldVibrate: boolean = true) => {
// Clear any existing interval
if (loadingScreenInterval) {
clearInterval(loadingScreenInterval);
loadingScreenInterval = null;
}
// If we shouldn't vibrate, just stop here
if (!shouldVibrate) {
Vibration.cancel();
return;
}
triggerFeedback('impactHeavy');
loadingScreenInterval = setInterval(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
export const notificationError = () => triggerFeedback('notificationError');
export const notificationSuccess = () => triggerFeedback('notificationSuccess');
export const notificationWarning = () => triggerFeedback('notificationWarning');
export { triggerFeedback } from '@/utils/haptic/trigger';
// Re-export all haptic functionality from the mobile SDK
export {
buttonTap,
cancelTap,
confirmTap,
feedbackProgress,
feedbackSuccess,
feedbackUnsuccessful,
impactLight,
impactMedium,
loadingScreenProgress,
notificationError,
notificationSuccess,
notificationWarning,
selectionChange,
triggerFeedback,
} from '@selfxyz/mobile-sdk-alpha';
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,25 +2,6 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export type HapticOptions = {
enableVibrateFallback?: boolean;
ignoreAndroidSystemSettings?: boolean;
pattern?: number[];
increaseIosIntensity?: boolean;
};
export type HapticType =
| 'selection'
| 'impactLight'
| 'impactMedium'
| 'impactHeavy'
| 'notificationSuccess'
| 'notificationWarning'
| 'notificationError';
export const defaultOptions: HapticOptions = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
pattern: [50, 100, 50],
increaseIosIntensity: true,
};
// Re-export types and defaults from the mobile SDK
export type { HapticOptions, HapticType } from '@selfxyz/mobile-sdk-alpha';
export { defaultOptions } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,39 +2,5 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { Platform, Vibration } from 'react-native';
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
import { defaultOptions } from '@/utils/haptic/shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type.
* @param options - Custom options (optional).
*/
export const triggerFeedback = (
type: HapticType | 'custom',
options: HapticOptions = {},
) => {
const mergedOptions = { ...defaultOptions, ...options };
if (Platform.OS === 'ios' && type !== 'custom') {
if (mergedOptions.increaseIosIntensity) {
if (type === 'impactLight') {
type = 'impactMedium';
} else if (type === 'impactMedium') {
type = 'impactHeavy';
}
}
ReactNativeHapticFeedback.trigger(type, {
enableVibrateFallback: mergedOptions.enableVibrateFallback,
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
});
} else {
if (mergedOptions.pattern) {
Vibration.vibrate(mergedOptions.pattern, false);
} else {
Vibration.vibrate(100);
}
}
};
// Re-export triggerFeedback from the mobile SDK
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,29 +2,5 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { HapticOptions, HapticType } from '@/utils/haptic/shared';
import { defaultOptions } from '@/utils/haptic/shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type. (only here for compatibility, not used in web)
* @param options - Custom options (optional).
*/
export const triggerFeedback = (
_type: HapticType | 'custom',
options: HapticOptions = {},
) => {
const mergedOptions = { ...defaultOptions, ...options };
// Check if Vibration API is available
if (!navigator.vibrate) {
console.warn('Vibration API not supported in this browser');
return;
}
if (mergedOptions.pattern) {
navigator.vibrate(mergedOptions.pattern);
} else {
navigator.vibrate(100);
}
};
// Re-export triggerFeedback from the mobile SDK
export { triggerFeedback } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -108,6 +108,13 @@ module.exports = {
'sort-exports/sort-exports': 'off',
},
},
{
// Disable export sorting for files with dependency issues
files: ['src/haptic/index.ts'],
rules: {
'sort-exports/sort-exports': 'off',
},
},
{
// Allow require imports only in the NFC decoder shim that conditionally imports node:util
files: ['src/processing/nfc.ts'],

View File

@@ -209,8 +209,8 @@ selfClient.on(SdkEvents.DOCUMENT_COUNTRY_SELECTED, payload => {
```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}`);
// payload: { documentType: string, documentName: string, countryCode: string }
console.log(`Document selected: ${payload.documentName} from ${payload.countryCode}`);
});
```

View File

@@ -70,14 +70,14 @@
"require": "./dist/cjs/hooks/index.cjs"
},
"./svgs/*.svg": {
"react-native": "./svgs/*.svg",
"import": "./svgs/*.svg",
"require": "./svgs/*.svg"
"react-native": "./dist/svgs/*.svg",
"import": "./dist/svgs/*.svg",
"require": "./dist/svgs/*.svg"
},
"./svgs/icons/*.svg": {
"react-native": "./svgs/icons/*.svg",
"import": "./svgs/icons/*.svg",
"require": "./svgs/icons/*.svg"
"react-native": "./dist/svgs/icons/*.svg",
"import": "./dist/svgs/icons/*.svg",
"require": "./dist/svgs/icons/*.svg"
}
},
"main": "./dist/cjs/index.cjs",
@@ -141,6 +141,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-native": "0.76.9",
"react-native-haptic-feedback": "^2.3.3",
"react-native-localize": "^3.5.2",
"react-native-web": "^0.21.1",
"tsup": "^8.0.1",
@@ -150,6 +151,7 @@
"peerDependencies": {
"react": "^18.3.1",
"react-native": "0.76.9",
"react-native-haptic-feedback": "*",
"react-native-localize": "*",
"react-native-svg": "*"
},

View File

@@ -0,0 +1,36 @@
#!/usr/bin/env node
// 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 { cpSync, existsSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '..');
function copyAssets() {
const sourceDir = join(rootDir, 'svgs');
const targetSvgDir = join(rootDir, 'dist/svgs');
if (!existsSync(sourceDir)) {
console.log('No svgs directory found, skipping asset copy');
return;
}
// Create target directory if it doesn't exist
mkdirSync(targetSvgDir, { recursive: true });
// Copy SVGs to single shared location in dist
try {
cpSync(sourceDir, targetSvgDir, { recursive: true });
console.log('✅ SVG assets copied to dist/svgs');
} catch (error) {
console.error('❌ Failed to copy SVG assets:', error.message);
process.exit(1);
}
}
copyAssets();

View File

@@ -27,18 +27,39 @@ export type {
} from './types/public';
export type { DG1, DG2, ParsedNFCResponse } from './nfc';
export type { HapticOptions, HapticType } from './haptic/shared';
export type { MRZScanOptions } from './mrz';
export type { PassportValidationCallbacks } from './validation/document';
export type { SDKEvent, SDKEventMap } from './types/events';
export type { SdkErrorCategory } from './errors';
export { type ProvingStateType } from './proving/provingMachine';
export { type ProvingStateType } from './proving/provingMachine';
export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors';
export { SdkEvents } from './types/events';
export { SelfClientContext, SelfClientProvider, useSelfClient } from './context';
export { advercase, dinot, plexMono } from './constants/fonts';
export {
buttonTap,
cancelTap,
confirmTap,
feedbackProgress,
feedbackSuccess,
feedbackUnsuccessful,
impactLight,
impactMedium,
loadingScreenProgress,
notificationError,
notificationSuccess,
notificationWarning,
selectionChange,
triggerFeedback,
} from './haptic';
export {
clearPassportData,
getAllDocuments,
@@ -56,6 +77,7 @@ export { defaultConfig } from './config/defaults';
export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz';
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
// Core functions
export { isPassportDataValid } from './validation/document';
@@ -64,6 +86,5 @@ export { mergeConfig } from './config/merge';
export { parseNFCResponse, scanNFC } from './nfc';
export { reactNativeScannerAdapter } from './adapters/react-native/nfc-scanner';
export { useCountries } from './documents/useCountries';
export { webNFCScannerShim } from './adapters/web/shims';

View File

@@ -6,8 +6,8 @@ import type React from 'react';
import type { GestureResponderEvent, LayoutChangeEvent, PressableProps, ViewStyle } from 'react-native';
import { Platform, Pressable, StyleSheet, Text } from 'react-native';
import { dinot } from '../../constants/fonts';
import { useSelfClient } from '../../context';
import { dinot } from '../../utils/fonts';
import { pressedStyle } from './pressedStyle';
export interface ButtonProps extends PressableProps {

View File

@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import { slate400 } from '../../constants/colors';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
type AdditionalProps = TextProps;

View File

@@ -7,7 +7,7 @@ import type { TextProps } from 'react-native';
import { Text } from 'react-native';
import { slate500 } from '../../constants/colors';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
export const BodyText: React.FC<TextProps> = ({ style, ...props }) => (
<Text style={[{ fontFamily: dinot, color: slate500 }, style]} {...props} />

View File

@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import { slate700 } from '../../constants/colors';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
type CautionProps = TextProps;

View File

@@ -6,7 +6,7 @@ import type { TextProps } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import { slate500 } from '../../constants/colors';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
type DescriptionProps = TextProps & {
color?: string;

View File

@@ -6,7 +6,7 @@ import type React from 'react';
import type { TextProps } from 'react-native';
import { Text } from 'react-native';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
export const DescriptionTitle: React.FC<TextProps> = ({ style, ...props }) => (
<Text

View File

@@ -6,7 +6,7 @@ import type React from 'react';
import type { TextProps } from 'react-native';
import { Text } from 'react-native';
import { dinot } from '../../utils/fonts';
import { dinot } from '../../constants/fonts';
export const SubHeader: React.FC<TextProps> = ({ style, ...props }) => (
<Text

View File

@@ -8,7 +8,7 @@ import type { StyleProp, TextProps, TextStyle } from 'react-native';
import { StyleSheet, Text } from 'react-native';
import { black } from '../../constants/colors';
import { advercase } from '../../utils/fonts';
import { advercase } from '../../constants/fonts';
type TitleProps = TextProps & {
size?: 'large';

View File

@@ -0,0 +1,172 @@
// 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 } from 'react';
import { ActivityIndicator, FlatList, TouchableOpacity, View } from 'react-native';
import { commonNames } from '@selfxyz/common/constants/countries';
import { BodyText, RoundFlag, XStack, YStack } from '../../components';
import { black, slate100, slate500 } from '../../constants/colors';
import { advercase, dinot } from '../../constants/fonts';
import { useSelfClient } from '../../context';
import { useCountries } from '../../documents/useCountries';
import { buttonTap } from '../../haptic';
import { SdkEvents } from '../../types/events';
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 style={{ fontSize: 16, color: black, flex: 1 }}>{countryName}</BodyText>
</XStack>
</TouchableOpacity>
);
});
CountryItem.displayName = 'CountryItem';
const Loading = () => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="small" />
</View>
);
Loading.displayName = 'Loading';
const CountryPickerScreen: React.FC = () => {
const selfClient = useSelfClient();
const { countryData, countryList, loading, userCountryCode, showSuggestion } = useCountries();
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,
});
} else {
selfClient.emit(SdkEvents.PROVING_PASSPORT_NOT_SUPPORTED, {
countryCode: countryCode,
documentCategory: null,
});
}
},
[countryData, selfClient],
);
const renderItem = useCallback(
({ item }: { item: CountryListItem }) => <CountryItem countryCode={item.countryCode} onSelect={onPressCountry} />,
[onPressCountry],
);
const keyExtractor = useCallback((item: CountryListItem) => item.countryCode, []);
const getItemLayout = useCallback(
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[],
);
return (
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4" backgroundColor={slate100}>
<YStack marginTop="$4" marginBottom="$6">
<BodyText style={{ fontSize: 29, fontFamily: advercase }}>Select the country that issued your ID</BodyText>
<BodyText style={{ fontSize: 16, color: slate500, marginTop: 20 }}>
Self has support for over 300 ID types. You can select the type of ID in the next step
</BodyText>
</YStack>
{loading ? (
<Loading />
) : (
<YStack flex={1}>
{showSuggestion && (
<YStack marginBottom="$2">
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginBottom: 8,
}}
>
SUGGESTION
</BodyText>
<CountryItem
countryCode={userCountryCode as string /*safe due to showSuggestion*/}
onSelect={onPressCountry}
/>
<BodyText
style={{
fontSize: 16,
color: black,
fontFamily: dinot,
letterSpacing: 0.8,
marginTop: 20,
}}
>
SELECT AN ISSUING COUNTRY
</BodyText>
</YStack>
)}
<FlatList
data={countryList}
renderItem={renderItem}
keyExtractor={keyExtractor}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={10}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
getItemLayout={getItemLayout}
/>
</YStack>
)}
</YStack>
);
};
CountryPickerScreen.displayName = 'CountryPickerScreen';
export default CountryPickerScreen;

View File

@@ -0,0 +1,169 @@
// 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 React from 'react';
import AadhaarLogo from '../../../svgs/icons/aadhaar.svg';
import EPassportLogoRounded from '../../../svgs/icons/epassport_rounded.svg';
import PlusIcon from '../../../svgs/icons/plus.svg';
import SelfLogo from '../../../svgs/logo.svg';
import { BodyText, RoundFlag, View, XStack, YStack } from '../../components';
import { black, slate100, slate300, slate400, white } from '../../constants/colors';
import { advercase, dinot } from '../../constants/fonts';
import { useSelfClient } from '../../context';
import { buttonTap } from '../../haptic';
import { SdkEvents } from '../../types/events';
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;
}
};
type IDSelectionScreenProps = {
countryCode: string;
documentTypes: string[];
};
const IDSelectionScreen: React.FC<IDSelectionScreenProps> = props => {
const { countryCode = '', documentTypes = [] } = props;
const selfClient = useSelfClient();
const onSelectDocumentType = (docType: string) => {
buttonTap();
selfClient.emit(SdkEvents.DOCUMENT_TYPE_SELECTED, {
documentType: docType,
documentName: getDocumentNameForEvent(docType),
countryCode: countryCode,
});
};
return (
<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
style={{
marginTop: 48,
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={{
transform: [{ scale: 0.97 }],
backgroundColor: slate100,
}}
onPress={() => onSelectDocumentType(docType)}
>
<XStack alignItems="center" gap={'$3'} flex={1}>
{getDocumentLogo(docType)}
<YStack gap={'$1'}>
<BodyText style={{ fontSize: 24, fontFamily: dinot, color: black }}>
{getDocumentName(docType)}
</BodyText>
<BodyText
style={{
fontSize: 14,
fontFamily: dinot,
color: slate400,
}}
>
{getDocumentDescription(docType)}
</BodyText>
</YStack>
</XStack>
</XStack>
))}
<BodyText
style={{
fontSize: 18,
fontFamily: dinot,
color: slate400,
textAlign: 'center',
}}
>
Be sure your document is ready to scan
</BodyText>
</YStack>
</YStack>
);
};
export default IDSelectionScreen;

View File

@@ -0,0 +1,144 @@
// 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 { Platform, Vibration } from 'react-native';
import { triggerFeedback } from './trigger';
// Keep track of the loading screen interval
let loadingScreenInterval: ReturnType<typeof setInterval> | null = null;
// consistent light feedback at a steady interval
export const feedbackProgress = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Three light impacts at 750ms intervals
triggerFeedback('custom', {
pattern: [
0,
50, // First light impact
750,
50, // Second light impact
750,
50, // Third light impact
],
});
return;
}
// Match the timing of the light impacts in the Android pattern
setTimeout(() => {
triggerFeedback('impactLight');
}, 750); // First light impact
setTimeout(() => {
triggerFeedback('impactLight');
}, 1500); // Second light impact (750ms after first)
setTimeout(() => {
triggerFeedback('impactLight');
}, 2250); // Third light impact (750ms after second)
};
// light -> medium -> heavy intensity in sequence
export const feedbackSuccess = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Increasing intensity sequence: light -> medium -> heavy
triggerFeedback('custom', {
pattern: [
500,
50, // Initial delay, then light impact
200,
100, // Medium impact
150,
150, // Heavy impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactLight');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
// heavy -> medium -> light intensity in sequence
export const feedbackUnsuccessful = () => {
if (Platform.OS === 'android') {
// Pattern: [delay, duration, delay, duration, ...]
// Decreasing intensity sequence: heavy -> medium -> light
triggerFeedback('custom', {
pattern: [
500,
150, // Initial delay, then heavy impact
100,
100, // Medium impact
150,
50, // Light impact
],
});
return;
}
setTimeout(() => {
triggerFeedback('impactHeavy');
}, 500);
setTimeout(() => {
triggerFeedback('impactMedium');
}, 750);
setTimeout(() => {
triggerFeedback('impactLight');
}, 1000);
};
// Define the base functions first
export const impactLight = () => triggerFeedback('impactLight');
export const impactMedium = () => triggerFeedback('impactMedium');
export const selectionChange = () => triggerFeedback('selection');
// Then define the aliases
export const buttonTap = impactLight;
export const cancelTap = selectionChange;
export const confirmTap = impactMedium;
/**
* Haptic actions
*/
// Custom feedback events
export const loadingScreenProgress = (shouldVibrate: boolean = true) => {
// Clear any existing interval
if (loadingScreenInterval) {
clearInterval(loadingScreenInterval);
loadingScreenInterval = null;
}
// If we shouldn't vibrate, just stop here
if (!shouldVibrate) {
Vibration.cancel();
return;
}
triggerFeedback('impactHeavy');
loadingScreenInterval = setInterval(() => {
triggerFeedback('impactHeavy');
}, 1000);
};
export const notificationError = () => triggerFeedback('notificationError');
export const notificationSuccess = () => triggerFeedback('notificationSuccess');
export const notificationWarning = () => triggerFeedback('notificationWarning');
export { triggerFeedback } from './trigger';

View File

@@ -0,0 +1,26 @@
// 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.
export type HapticOptions = {
enableVibrateFallback?: boolean;
ignoreAndroidSystemSettings?: boolean;
pattern?: number[];
increaseIosIntensity?: boolean;
};
export type HapticType =
| 'selection'
| 'impactLight'
| 'impactMedium'
| 'impactHeavy'
| 'notificationSuccess'
| 'notificationWarning'
| 'notificationError';
export const defaultOptions: HapticOptions = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
pattern: [50, 100, 50],
increaseIosIntensity: true,
};

View File

@@ -0,0 +1,52 @@
// 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 { Platform, Vibration } from 'react-native';
import type { HapticOptions, HapticType } from './shared';
import { defaultOptions } from './shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type.
* @param options - Custom options (optional).
*/
export const triggerFeedback = (type: HapticType | 'custom', options: HapticOptions = {}) => {
const mergedOptions = { ...defaultOptions, ...options };
if (Platform.OS === 'ios' && type !== 'custom') {
if (mergedOptions.increaseIosIntensity) {
if (type === 'impactLight') {
type = 'impactMedium';
} else if (type === 'impactMedium') {
type = 'impactHeavy';
}
}
// Use dynamic import to avoid loading the module on Android or if its not installed
(async () => {
try {
const trigger = await import('react-native-haptic-feedback').then(mod => mod.trigger);
trigger(type, {
enableVibrateFallback: mergedOptions.enableVibrateFallback,
ignoreAndroidSystemSettings: mergedOptions.ignoreAndroidSystemSettings,
});
} catch {
standardVibration(mergedOptions);
}
})();
} else {
standardVibration(mergedOptions);
}
};
function standardVibration(mergedOptions: {
enableVibrateFallback?: boolean;
ignoreAndroidSystemSettings?: boolean;
pattern?: number[];
increaseIosIntensity?: boolean;
}) {
if (mergedOptions.pattern) {
Vibration.vibrate(mergedOptions.pattern, false);
} else {
Vibration.vibrate(100);
}
}

View File

@@ -0,0 +1,27 @@
// 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 { HapticOptions, HapticType } from './shared';
import { defaultOptions } from './shared';
/**
* Triggers haptic feedback or vibration based on platform.
* @param type - The haptic feedback type. (only here for compatibility, not used in web)
* @param options - Custom options (optional).
*/
export const triggerFeedback = (_type: HapticType | 'custom', options: HapticOptions = {}) => {
const mergedOptions = { ...defaultOptions, ...options };
// Check if Vibration API is available
if (!navigator.vibrate) {
console.warn('Vibration API not supported in this browser');
return;
}
if (mergedOptions.pattern) {
navigator.vibrate(mergedOptions.pattern);
} else {
navigator.vibrate(100);
}
};

View File

@@ -36,6 +36,8 @@ export type { DG1, DG2, ParsedNFCResponse } from './nfc';
export type { DocumentData, DocumentMetadata, PassportCameraProps, ScreenProps } from './types/ui';
export type { HapticOptions, HapticType } from './haptic/shared';
export type { MRZScanOptions } from './mrz';
// QR module
@@ -45,7 +47,6 @@ export type { SDKEvent, SDKEventMap } from './types/events';
// Error handling
export type { SdkErrorCategory } from './errors';
// Screen Components (React Native-based)
export type { provingMachineCircuitType } from './proving/provingMachine';
export {
@@ -59,17 +60,39 @@ export {
sdkError,
} from './errors';
export { NFCScannerScreen } from './components/screens/NFCScannerScreen';
export { PassportCameraScreen } from './components/screens/PassportCameraScreen';
// Context and Client
export { type ProvingStateType } from './proving/provingMachine';
export { PassportCameraScreen } from './components/screens/PassportCameraScreen';
export { QRCodeScreen } from './components/screens/QRCodeScreen';
export { type ProvingStateType } from './proving/provingMachine';
// Components
export { SdkEvents } from './types/events';
export { QRCodeScreen } from './components/screens/QRCodeScreen';
// Documents utils
export { SdkEvents } from './types/events';
export { SelfClientContext, SelfClientProvider, useSelfClient } from './context';
// Haptic feedback utilities
export { advercase, dinot, plexMono } from './constants/fonts';
export {
buttonTap,
cancelTap,
confirmTap,
feedbackProgress,
feedbackSuccess,
feedbackUnsuccessful,
impactLight,
impactMedium,
loadingScreenProgress,
notificationError,
notificationSuccess,
notificationWarning,
selectionChange,
triggerFeedback,
} from './haptic';
/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */
export {
clearPassportData,
getAllDocuments,
@@ -79,20 +102,21 @@ export {
reStorePassportDataWithRightCSCA,
} from './documents/utils';
/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */
export { createListenersMap, createSelfClient } from './client';
// Document utils
export { defaultConfig } from './config/defaults';
// Document utils
export { defaultOptions } from './haptic/shared';
export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz';
// Core functions
export { extractNameFromDocument } from './documents/utils';
// Core functions
// Document validation
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
// Document validation
export { isPassportDataValid } from './validation/document';
export { mergeConfig } from './config/merge';
@@ -100,7 +124,5 @@ export { mergeConfig } from './config/merge';
export { parseNFCResponse, scanNFC } from './nfc';
export { reactNativeScannerAdapter } from './adapters/react-native/nfc-scanner';
export { useCountries } from './documents/useCountries';
export { webNFCScannerShim } from './adapters/web/shims';

View File

@@ -160,7 +160,6 @@ export interface SDKEventMap {
documentType: string;
documentName: string;
countryCode: string;
countryName: string;
};
[SdkEvents.PROVING_BEGIN_GENERATION]: {
uuid: string;

View File

@@ -0,0 +1,11 @@
// 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.
declare module '*.svg' {
import type React from 'react';
import type { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}

View File

@@ -23,8 +23,8 @@ function findFlowFiles(dir: string, basePath = ''): Record<string, string> {
if (item.isDirectory()) {
Object.assign(entries, findFlowFiles(itemPath, relativePath));
} else if (item.isFile() && item.name.endsWith('.ts')) {
const key = path.join('flows', relativePath).replace(/\.ts$/, '');
} else if (item.isFile() && (item.name.endsWith('.ts') || item.name.endsWith('.tsx'))) {
const key = path.join('flows', relativePath).replace(/\.tsx?$/, '');
entries[key] = path.join('src', 'flows', relativePath);
}
}
@@ -71,6 +71,8 @@ export default defineConfig([
'react-native-keychain',
'react-native-sqlite-storage',
// State management (xstate included in bundle)
// SVG files should be handled by React Native's SVG transformer
/\.svg$/,
],
esbuildOptions(options) {
options.supported = {
@@ -98,6 +100,7 @@ export default defineConfig([
splitting: true,
clean: false,
outDir: 'dist/cjs',
onSuccess: 'node ./scripts/copy-assets.mjs',
tsconfig: './tsconfig.cjs.json',
target: 'es2020',
external: [
@@ -116,6 +119,8 @@ export default defineConfig([
'react-native-keychain',
'react-native-sqlite-storage',
// State management (xstate included in bundle)
// SVG files should be handled by React Native's SVG transformer
/\.svg$/,
],
outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.js' }),
esbuildOptions(options) {

View File

@@ -42,6 +42,7 @@
"react-native": "0.76.9",
"react-native-get-random-values": "^1.11.0",
"react-native-keychain": "^10.0.0",
"react-native-localize": "^3.5.4",
"react-native-safe-area-context": "^5.6.1",
"react-native-svg": "15.12.1",
"react-native-vector-icons": "^10.3.0",

View File

@@ -13,6 +13,7 @@ import {
type TrackEventParams,
type WsConn,
webNFCScannerShim,
SdkEvents,
} from '@selfxyz/mobile-sdk-alpha';
import { persistentDocumentsAdapter } from '../utils/documentStore';
@@ -153,7 +154,11 @@ export function SelfClientProvider({ children }: PropsWithChildren) {
);
const listeners = useMemo(() => {
const { map } = createListenersMap();
const { map, addListener } = createListenersMap();
addListener(SdkEvents.DOCUMENT_COUNTRY_SELECTED, event => {
console.info('go to id picker', event);
});
return map;
}, []);

View File

@@ -0,0 +1,11 @@
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
import ScreenLayout from '../components/ScreenLayout';
export default function CountrySelection({ onBack }: { onBack: () => void }) {
return (
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
<SDKCountryPickerScreen />
</ScreenLayout>
);
}

View File

@@ -0,0 +1,11 @@
import IDSelectionScreen from '@selfxyz/mobile-sdk-alpha/onboarding/id-selection-screen';
import ScreenLayout from '../components/ScreenLayout';
export default function IDSelection({ onBack }: { onBack: () => void }) {
return (
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
<IDSelectionScreen countryCode="USA" documentTypes={['p']} />
</ScreenLayout>
);
}

View File

@@ -6,7 +6,15 @@ import type { ComponentType } from 'react';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
export type ScreenId = 'generate' | 'register' | 'prove' | 'camera' | 'nfc' | 'documents';
export type ScreenId =
| 'generate'
| 'register'
| 'prove'
| 'camera'
| 'nfc'
| 'documents'
| 'country-selection'
| 'id-selection';
export type ScreenContext = {
navigate: (next: ScreenRoute) => void;
@@ -89,6 +97,30 @@ export const screenDescriptors: ScreenDescriptor[] = [
catalog: documentCatalog,
}),
},
{
id: 'country-selection',
title: 'Country Selection',
subtitle: 'Select the country that issued your ID',
sectionTitle: '📋 Selection',
status: 'working',
load: () => require('./CountrySelection').default,
getProps: ({ navigate, documentCatalog }) => ({
onBack: () => navigate('home'),
catalog: documentCatalog,
}),
},
{
id: 'id-selection',
title: 'ID Selection',
subtitle: 'Choose the type of ID you want to verify',
sectionTitle: '📋 Selection',
status: 'working',
load: () => require('./IDSelection').default,
getProps: ({ navigate, documentCatalog }) => ({
onBack: () => navigate('home'),
catalog: documentCatalog,
}),
},
];
export const screenMap = screenDescriptors.reduce<Record<ScreenId, ScreenDescriptor>>(

View File

@@ -7462,6 +7462,7 @@ __metadata:
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-native: "npm:0.76.9"
react-native-haptic-feedback: "npm:^2.3.3"
react-native-localize: "npm:^3.5.2"
react-native-svg-circle-country-flags: "npm:^0.2.2"
react-native-web: "npm:^0.21.1"
@@ -7475,6 +7476,7 @@ __metadata:
peerDependencies:
react: ^18.3.1
react-native: 0.76.9
react-native-haptic-feedback: "*"
react-native-localize: "*"
react-native-svg: "*"
languageName: unknown
@@ -23823,6 +23825,7 @@ __metadata:
react-native: "npm:0.76.9"
react-native-get-random-values: "npm:^1.11.0"
react-native-keychain: "npm:^10.0.0"
react-native-localize: "npm:^3.5.4"
react-native-safe-area-context: "npm:^5.6.1"
react-native-svg: "npm:15.12.1"
react-native-svg-transformer: "npm:^1.5.1"
@@ -26378,6 +26381,23 @@ __metadata:
languageName: node
linkType: hard
"react-native-localize@npm:^3.5.4":
version: 3.5.4
resolution: "react-native-localize@npm:3.5.4"
peerDependencies:
"@expo/config-plugins": ^9.0.0 || ^10.0.0
react: "*"
react-native: "*"
react-native-macos: "*"
peerDependenciesMeta:
"@expo/config-plugins":
optional: true
react-native-macos:
optional: true
checksum: 10c0/b1366c2cce740163f5337e842b537a7e5132e0914d9023f1cf07861ea141b0a6d4e7b360b6bb6d8af16860ef56f1d9ca7a59508dcc4619f42991123298207f1b
languageName: node
linkType: hard
"react-native-logs@npm:^5.3.0":
version: 5.5.0
resolution: "react-native-logs@npm:5.5.0"