mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
Three/country picker (#1448)
* use 3.0 country picker * get blurview working in app add navigation adapter to sdk render * fix fonts and double view registration issues * dont need this script as we use peer deps now * fix our package installs * prayed to the false idol of claude to resolve installing anon-aadhar from a specific commit from a monorepo * fix route types * add peer deps to demo --------- Co-authored-by: Leszek Stachowski <leszek.stachowski@self.xyz> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
2
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
2
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -31,6 +31,7 @@ on:
|
||||
|
||||
jobs:
|
||||
android-e2e:
|
||||
name: Android E2E Tests Demo App
|
||||
# Currently build-only for Android. E2E steps are preserved but skipped (if: false).
|
||||
# To re-enable full E2E: change `if: false` to `if: true` on emulator steps.
|
||||
concurrency:
|
||||
@@ -192,6 +193,7 @@ jobs:
|
||||
ios-e2e:
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest-large
|
||||
name: iOS E2E Tests Demo App
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-ios-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -2201,7 +2201,7 @@ DEPENDENCIES:
|
||||
- lottie-ios
|
||||
- lottie-react-native (from `../node_modules/lottie-react-native`)
|
||||
- Mixpanel-swift (~> 5.0.0)
|
||||
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
|
||||
- NFCPassportReader (from `https://github.com/selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)
|
||||
- QKMRZScanner
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
@@ -2340,7 +2340,7 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/lottie-react-native"
|
||||
NFCPassportReader:
|
||||
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
|
||||
:git: "git@github.com:selfxyz/NFCPassportReader.git"
|
||||
:git: https://github.com/selfxyz/NFCPassportReader.git
|
||||
RCT-Folly:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
RCTDeprecation:
|
||||
@@ -2517,15 +2517,15 @@ EXTERNAL SOURCES:
|
||||
CHECKOUT OPTIONS:
|
||||
NFCPassportReader:
|
||||
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
|
||||
:git: "git@github.com:selfxyz/NFCPassportReader.git"
|
||||
:git: https://github.com/selfxyz/NFCPassportReader.git
|
||||
SwiftQRScanner:
|
||||
:commit: c71ff91297640a944de4bca61434155c3f9b0979
|
||||
:git: https://github.com/vinodiOS/SwiftQRScanner
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 7605ea4810e0e10ae4815292433c09bf4324ba45
|
||||
Firebase: 91fefd38712feb9186ea8996af6cbdef41473442
|
||||
@@ -2540,7 +2540,7 @@ SPEC CHECKSUMS:
|
||||
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
|
||||
FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e
|
||||
fmt: 01b82d4ca6470831d1cc0852a1af644be019e8f6
|
||||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
||||
glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
|
||||
GoogleAppMeasurement: f3abf08495ef2cba7829f15318c373b8d9226491
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
|
||||
@@ -74,10 +74,14 @@
|
||||
"web:preview": "vite preview"
|
||||
},
|
||||
"resolutions": {
|
||||
"punycode": "npm:punycode.js@2.3.1"
|
||||
"punycode": "npm:punycode.js@2.3.1",
|
||||
"react-native-blur-effect": "1.1.3",
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"overrides": {
|
||||
"punycode": "npm:punycode.js@2.3.1"
|
||||
"punycode": "npm:punycode.js@2.3.1",
|
||||
"react-native-blur-effect": "1.1.3",
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
@@ -137,6 +141,7 @@
|
||||
"react-native": "0.76.9",
|
||||
"react-native-app-auth": "^8.0.3",
|
||||
"react-native-biometrics": "^3.0.1",
|
||||
"react-native-blur-effect": "^1.1.3",
|
||||
"react-native-check-version": "^1.3.0",
|
||||
"react-native-cloud-storage": "^2.2.2",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
|
||||
@@ -89,6 +89,23 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
},
|
||||
},
|
||||
documents: selfClientDocumentsAdapter,
|
||||
navigation: {
|
||||
goBack: () => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.goBack();
|
||||
}
|
||||
},
|
||||
goTo: (routeName, params) => {
|
||||
if (navigationRef.isReady()) {
|
||||
if (params !== undefined) {
|
||||
// @ts-expect-error
|
||||
navigationRef.navigate(routeName, params);
|
||||
} else {
|
||||
navigationRef.navigate(routeName as never);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
crypto: {
|
||||
async hash(
|
||||
data: Uint8Array,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { StaticScreenProps } from '@react-navigation/native';
|
||||
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
advercase,
|
||||
dinot,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
|
||||
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
|
||||
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha/browser';
|
||||
import {
|
||||
black,
|
||||
slate400,
|
||||
|
||||
@@ -2,17 +2,8 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import { slate100 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
||||
|
||||
import { DocumentFlowNavBar } from '@/components/navbar/DocumentFlowNavBar';
|
||||
|
||||
export default function CountryPickerScreen() {
|
||||
return (
|
||||
<YStack flex={1} backgroundColor={slate100}>
|
||||
<DocumentFlowNavBar title="GETTING STARTED" />
|
||||
<SDKCountryPickerScreen />
|
||||
</YStack>
|
||||
);
|
||||
return <SDKCountryPickerScreen />;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@zk-email/zk-regex-circom": "^1.2.1",
|
||||
"@zk-kit/binary-merkle-root.circom": "2.0.0",
|
||||
"@zk-kit/circuits": "^1.0.0-beta",
|
||||
"anon-aadhaar-circuits": "https://gitpkg.vercel.app/selfxyz/anon-aadhaar/packages/circuits?main",
|
||||
"anon-aadhaar-circuits": "https://github.com/selfxyz/anon-aadhaar.git#commit=1b9efa501cff3cf25dc260b060bf611229e316a4&workspace=@anon-aadhaar/circuits",
|
||||
"asn1": "^0.2.6",
|
||||
"asn1.js": "^5.4.1",
|
||||
"asn1js": "^3.0.5",
|
||||
|
||||
@@ -17,9 +17,28 @@ import nameAndYobAadhaarjson from '../consts/ofac/nameAndYobAadhaarSMT.json' wit
|
||||
import fs from 'fs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// const privateKeyPath = path.join(__dirname, '../../../node_modules/anon-aadhaar-circuits/assets/testPrivateKey.pem');
|
||||
|
||||
// Dynamically resolve the anon-aadhaar-circuits package location
|
||||
function resolvePackagePath(packageName: string, subpath: string): string {
|
||||
try {
|
||||
// Try to resolve the package's package.json
|
||||
const packageJsonPath = require.resolve(`${packageName}/package.json`, {
|
||||
paths: [__dirname],
|
||||
});
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
return path.join(packageDir, subpath);
|
||||
} catch (error) {
|
||||
// Fallback to traditional node_modules search
|
||||
const modulePath = path.join(__dirname, '../../node_modules', packageName, subpath);
|
||||
if (fs.existsSync(modulePath)) {
|
||||
return modulePath;
|
||||
}
|
||||
throw new Error(`Could not resolve ${packageName}/${subpath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const privateKeyPem = fs.readFileSync(
|
||||
path.join(__dirname, '../../node_modules/anon-aadhaar-circuits/assets/testPrivateKey.pem'),
|
||||
resolvePackagePath('anon-aadhaar-circuits', 'assets/testPrivateKey.pem'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-passkey": "3.3.1"
|
||||
"react-native-blur-effect": "1.1.3",
|
||||
"react-native-passkey": "3.3.1",
|
||||
"react-native-webview": "13.16.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"@selfxyz/euclid": "^0.4.1",
|
||||
"@xstate/react": "^5.0.5",
|
||||
"node-forge": "^1.3.1",
|
||||
"react-native-nfc-manager": "^3.17.1",
|
||||
@@ -191,9 +192,11 @@
|
||||
"lottie-react-native": "7.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-blur-effect": "^1.1.3",
|
||||
"react-native-haptic-feedback": "*",
|
||||
"react-native-localize": "*",
|
||||
"react-native-svg": "*"
|
||||
"react-native-svg": "*",
|
||||
"react-native-webview": "^13.16.0"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -42,7 +42,7 @@ const optionalDefaults: Required<Pick<Adapters, 'clock' | 'logger'>> = {
|
||||
},
|
||||
};
|
||||
|
||||
const REQUIRED_ADAPTERS = ['auth', 'scanner', 'network', 'crypto', 'documents'] as const;
|
||||
const REQUIRED_ADAPTERS = ['auth', 'scanner', 'network', 'crypto', 'documents', 'navigation'] as const;
|
||||
|
||||
export const createListenersMap = (): {
|
||||
map: Map<SDKEvent, Set<(p: any) => void>>;
|
||||
@@ -212,7 +212,12 @@ export function createSelfClient({
|
||||
getMRZState: () => {
|
||||
return useMRZStore.getState();
|
||||
},
|
||||
|
||||
goBack: () => {
|
||||
adapters.navigation.goBack();
|
||||
},
|
||||
goTo: (routeName, params) => {
|
||||
adapters.navigation.goTo(routeName, params);
|
||||
},
|
||||
// for reactivity (if needed)
|
||||
useProvingStore,
|
||||
useSelfAppStore,
|
||||
|
||||
@@ -2,67 +2,26 @@
|
||||
// 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, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { commonNames } from '@selfxyz/common/constants/countries';
|
||||
import { CountryPickerScreen as CountryPickerUI } from '@selfxyz/euclid';
|
||||
|
||||
import { BodyText, RoundFlag, XStack, YStack } from '../../components';
|
||||
import { black, slate100, slate500 } from '../../constants/colors';
|
||||
import { advercase, dinot } from '../../constants/fonts';
|
||||
import { RoundFlag } from '../../components';
|
||||
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={styles.countryItemContainer}>
|
||||
<XStack style={styles.countryItemContent}>
|
||||
<RoundFlag countryCode={countryCode} size={FLAG_SIZE} />
|
||||
<BodyText style={styles.countryItemText}>{countryName}</BodyText>
|
||||
</XStack>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
CountryItem.displayName = 'CountryItem';
|
||||
|
||||
const Loading = () => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" />
|
||||
</View>
|
||||
);
|
||||
Loading.displayName = 'Loading';
|
||||
|
||||
const CountryPickerScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { countryData, countryList, loading, userCountryCode, showSuggestion } = useCountries();
|
||||
|
||||
const onPressCountry = useCallback(
|
||||
const onCountrySelect = 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);
|
||||
@@ -87,105 +46,34 @@ const CountryPickerScreen: React.FC = () => {
|
||||
[countryData, selfClient],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: CountryListItem }) => <CountryItem countryCode={item.countryCode} onSelect={onPressCountry} />,
|
||||
[onPressCountry],
|
||||
);
|
||||
const renderFlag = useCallback((countryCode: string, size: number) => {
|
||||
return <RoundFlag countryCode={countryCode} size={size} />;
|
||||
}, []);
|
||||
|
||||
const keyExtractor = useCallback((item: CountryListItem) => item.countryCode, []);
|
||||
const getCountryName = useCallback((countryCode: string) => {
|
||||
return commonNames[countryCode as keyof typeof commonNames] || countryCode;
|
||||
}, []);
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_data: ArrayLike<CountryListItem> | null | undefined, index: number) => ({
|
||||
length: ITEM_HEIGHT,
|
||||
offset: ITEM_HEIGHT * index,
|
||||
index,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const onSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<YStack flex={1} paddingTop="$4" paddingHorizontal="$4" backgroundColor={slate100}>
|
||||
<YStack marginTop="$4" marginBottom="$6">
|
||||
<BodyText style={styles.titleText}>Select the country that issued your ID</BodyText>
|
||||
<BodyText style={styles.subtitleText}>
|
||||
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={styles.sectionLabel}>SUGGESTION</BodyText>
|
||||
<CountryItem
|
||||
countryCode={userCountryCode as string /*safe due to showSuggestion*/}
|
||||
onSelect={onPressCountry}
|
||||
/>
|
||||
<BodyText style={styles.sectionLabelBottom}>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>
|
||||
<CountryPickerUI
|
||||
isLoading={loading}
|
||||
countries={countryList}
|
||||
onCountrySelect={onCountrySelect}
|
||||
suggestionCountryCode={userCountryCode ?? undefined}
|
||||
showSuggestion={!!showSuggestion}
|
||||
renderFlag={renderFlag}
|
||||
getCountryName={getCountryName}
|
||||
searchValue={searchValue}
|
||||
onClose={selfClient.goBack}
|
||||
onInfoPress={() => console.log('Info pressed TODO: Implement')}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CountryPickerScreen.displayName = 'CountryPickerScreen';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
countryItemContainer: {
|
||||
paddingVertical: 13,
|
||||
},
|
||||
countryItemContent: {
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
countryItemText: {
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
titleText: {
|
||||
fontSize: 29,
|
||||
fontFamily: advercase,
|
||||
color: black,
|
||||
},
|
||||
subtitleText: {
|
||||
fontSize: 16,
|
||||
color: slate500,
|
||||
marginTop: 20,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
sectionLabelBottom: {
|
||||
fontSize: 16,
|
||||
color: black,
|
||||
fontFamily: dinot,
|
||||
letterSpacing: 0.8,
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default CountryPickerScreen;
|
||||
|
||||
@@ -17,8 +17,10 @@ export type {
|
||||
MRZValidation,
|
||||
NFCScanResult,
|
||||
NFCScannerAdapter,
|
||||
NavigationAdapter,
|
||||
NetworkAdapter,
|
||||
Progress,
|
||||
RouteName,
|
||||
SelfClient,
|
||||
StorageAdapter,
|
||||
TrackEventParams,
|
||||
|
||||
@@ -199,6 +199,36 @@ export interface Adapters {
|
||||
auth: AuthAdapter;
|
||||
/** Required document persistence layer. Implementations must be idempotent. */
|
||||
documents: DocumentsAdapter;
|
||||
/** Required navigation adapter for handling screen transitions. */
|
||||
navigation: NavigationAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map these route names to your navigation configuration.
|
||||
* Includes all screens that the SDK may navigate to across host applications.
|
||||
*/
|
||||
export type RouteName =
|
||||
// Document acquisition flow
|
||||
| 'DocumentCamera'
|
||||
| 'DocumentOnboarding'
|
||||
| 'CountryPicker'
|
||||
| 'IDPicker'
|
||||
| 'DocumentNFCScan'
|
||||
| 'ManageDocuments'
|
||||
// Account/onboarding flow
|
||||
| 'Home'
|
||||
| 'AccountVerifiedSuccess'
|
||||
| 'AccountRecoveryChoice'
|
||||
| 'SaveRecoveryPhrase'
|
||||
// Error/fallback screens
|
||||
| 'ComingSoon'
|
||||
| 'DocumentDataNotFound'
|
||||
// Settings
|
||||
| 'Settings';
|
||||
|
||||
export interface NavigationAdapter {
|
||||
goBack(): void;
|
||||
goTo(routeName: RouteName, params?: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,6 +314,8 @@ export interface SelfClient {
|
||||
scanNFC(opts: NFCScanOpts & { signal?: AbortSignal }): Promise<NFCScanResult>;
|
||||
/** Parses MRZ text and returns structured fields plus checksum metadata. */
|
||||
extractMRZInfo(mrz: string): MRZInfo;
|
||||
goBack(): void;
|
||||
goTo(routeName: RouteName, params?: Record<string, unknown>): void;
|
||||
|
||||
/**
|
||||
* Convenience wrapper around {@link AnalyticsAdapter.trackEvent}. Calls are
|
||||
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, NFCScannerAdapter } from '../src';
|
||||
import { createListenersMap, createSelfClient, SdkEvents } from '../src/index';
|
||||
import type { AuthAdapter } from '../src/types/public';
|
||||
import type { AuthAdapter, NavigationAdapter } from '../src/types/public';
|
||||
|
||||
describe('createSelfClient', () => {
|
||||
// Test eager validation during client creation
|
||||
@@ -27,21 +27,21 @@ describe('createSelfClient', () => {
|
||||
|
||||
it('throws when network adapter missing during creation', () => {
|
||||
// @ts-expect-error -- missing adapters
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, crypto, documents, auth } })).toThrow(
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, crypto, documents, auth, navigation } })).toThrow(
|
||||
'network adapter not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when crypto adapter missing during creation', () => {
|
||||
// @ts-expect-error -- missing adapters
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, network, documents, auth } })).toThrow(
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, network, documents, auth, navigation } })).toThrow(
|
||||
'crypto adapter not provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when documents adapter missing during creation', () => {
|
||||
// @ts-expect-error -- missing adapters
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, network, crypto, auth } })).toThrow(
|
||||
expect(() => createSelfClient({ config: {}, adapters: { scanner, network, crypto, auth, navigation } })).toThrow(
|
||||
'documents adapter not provided',
|
||||
);
|
||||
});
|
||||
@@ -49,7 +49,7 @@ describe('createSelfClient', () => {
|
||||
it('creates client successfully with all required adapters', () => {
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner, network, crypto, documents, auth },
|
||||
adapters: { scanner, network, crypto, documents, auth, navigation },
|
||||
listeners: new Map(),
|
||||
});
|
||||
expect(client).toBeTruthy();
|
||||
@@ -59,7 +59,7 @@ describe('createSelfClient', () => {
|
||||
const scanMock = vi.fn().mockResolvedValue({ passportData: { mock: true } });
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth },
|
||||
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth, navigation },
|
||||
listeners: new Map(),
|
||||
});
|
||||
const result = await client.scanNFC({
|
||||
@@ -85,7 +85,7 @@ describe('createSelfClient', () => {
|
||||
const scanMock = vi.fn().mockRejectedValue(err);
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth },
|
||||
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth, navigation },
|
||||
listeners: new Map(),
|
||||
});
|
||||
await expect(
|
||||
@@ -106,7 +106,7 @@ describe('createSelfClient', () => {
|
||||
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner, network, crypto, documents, auth },
|
||||
adapters: { scanner, network, crypto, documents, auth, navigation },
|
||||
listeners: listeners.map,
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('createSelfClient', () => {
|
||||
it('parses MRZ via client', () => {
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner, network, crypto, documents, auth },
|
||||
adapters: { scanner, network, crypto, documents, auth, navigation },
|
||||
listeners: new Map(),
|
||||
});
|
||||
const sample = `P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10`;
|
||||
@@ -149,6 +149,7 @@ describe('createSelfClient', () => {
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: {
|
||||
navigation,
|
||||
scanner,
|
||||
network,
|
||||
crypto,
|
||||
@@ -171,7 +172,7 @@ describe('createSelfClient', () => {
|
||||
const getPrivateKey = vi.fn(() => Promise.resolve('stubbed-private-key'));
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner, network, crypto, documents, auth: { getPrivateKey } },
|
||||
adapters: { scanner, network, crypto, documents, navigation, auth: { getPrivateKey } },
|
||||
listeners: new Map(),
|
||||
});
|
||||
|
||||
@@ -181,7 +182,7 @@ describe('createSelfClient', () => {
|
||||
const getPrivateKey = vi.fn(() => Promise.resolve('stubbed-private-key'));
|
||||
const client = createSelfClient({
|
||||
config: {},
|
||||
adapters: { scanner, network, crypto, documents, auth: { getPrivateKey } },
|
||||
adapters: { scanner, network, crypto, documents, navigation, auth: { getPrivateKey } },
|
||||
listeners: new Map(),
|
||||
});
|
||||
await expect(client.hasPrivateKey()).resolves.toBe(true);
|
||||
@@ -222,3 +223,8 @@ const documents: DocumentsAdapter = {
|
||||
saveDocument: async () => {},
|
||||
deleteDocument: async () => {},
|
||||
};
|
||||
|
||||
const navigation: NavigationAdapter = {
|
||||
goBack: vi.fn(),
|
||||
goTo: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -35,6 +35,10 @@ const createMockSelfClientWithDocumentsAdapter = (documentsAdapter: DocumentsAda
|
||||
}),
|
||||
},
|
||||
},
|
||||
navigation: {
|
||||
goBack: () => {},
|
||||
goTo: (_routeName: string, _params?: Record<string, any>) => {},
|
||||
},
|
||||
scanner: {
|
||||
scan: async () => ({
|
||||
passportData: {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
/* eslint-disable sort-exports/sort-exports */
|
||||
import type { NavigationAdapter } from 'src/types/public';
|
||||
|
||||
import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, NFCScannerAdapter } from '../../src';
|
||||
|
||||
// Shared test data
|
||||
@@ -60,12 +62,18 @@ const mockAuth = {
|
||||
getPrivateKey: async () => 'stubbed-private-key',
|
||||
};
|
||||
|
||||
const mockNavigation: NavigationAdapter = {
|
||||
goBack: vi.fn(),
|
||||
goTo: vi.fn(),
|
||||
};
|
||||
|
||||
export const mockAdapters = {
|
||||
scanner: mockScanner,
|
||||
network: mockNetwork,
|
||||
crypto: mockCrypto,
|
||||
documents: mockDocuments,
|
||||
auth: mockAuth,
|
||||
navigation: mockNavigation,
|
||||
};
|
||||
|
||||
// Shared test expectations
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"lottie-react-native": "7.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-blur-effect": "1.1.3",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-keychain": "^10.0.0",
|
||||
@@ -49,6 +50,7 @@
|
||||
"react-native-safe-area-context": "^5.6.1",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-vector-icons": "^10.3.0",
|
||||
"react-native-webview": "13.16.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.5"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createListenersMap,
|
||||
SdkEvents,
|
||||
type Adapters,
|
||||
type RouteName,
|
||||
type TrackEventParams,
|
||||
type WsConn,
|
||||
reactNativeScannerAdapter,
|
||||
@@ -18,8 +19,40 @@ import {
|
||||
|
||||
import { persistentDocumentsAdapter } from '../utils/documentStore';
|
||||
import { getOrCreateSecret } from '../utils/secureStorage';
|
||||
import type { ScreenName } from '../navigation/NavigationProvider';
|
||||
import { useNavigation } from '../navigation/NavigationProvider';
|
||||
|
||||
/**
|
||||
* Maps SDK RouteName values to demo app ScreenName values.
|
||||
* Routes not in this map are not supported in the demo app.
|
||||
*/
|
||||
const ROUTE_TO_SCREEN_MAP: Partial<Record<RouteName, ScreenName>> = {
|
||||
'Home': 'Home',
|
||||
'CountryPicker': 'CountrySelection',
|
||||
'IDPicker': 'IDSelection',
|
||||
'DocumentCamera': 'MRZ',
|
||||
'DocumentNFCScan': 'NFC',
|
||||
'ManageDocuments': 'Documents',
|
||||
'AccountVerifiedSuccess': 'Success',
|
||||
// Routes not implemented in demo app:
|
||||
// 'DocumentOnboarding': null,
|
||||
// 'SaveRecoveryPhrase': null,
|
||||
// 'AccountRecoveryChoice': null,
|
||||
// 'ComingSoon': null,
|
||||
// 'DocumentDataNotFound': null,
|
||||
// 'Settings': null,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Translates SDK RouteName to demo app ScreenName.
|
||||
*
|
||||
* @param routeName - The route name from the SDK
|
||||
* @returns The corresponding demo app screen name, or null if not supported
|
||||
*/
|
||||
function translateRouteToScreen(routeName: RouteName): ScreenName | null {
|
||||
return ROUTE_TO_SCREEN_MAP[routeName] ?? null;
|
||||
}
|
||||
|
||||
const createFetch = () => {
|
||||
const fetchImpl = globalThis.fetch;
|
||||
if (!fetchImpl) {
|
||||
@@ -129,6 +162,21 @@ export function SelfClientProvider({ children, onNavigate }: SelfClientProviderP
|
||||
},
|
||||
ws: createWsAdapter(),
|
||||
},
|
||||
navigation: {
|
||||
goBack: () => {
|
||||
navigation.goBack();
|
||||
},
|
||||
goTo: (routeName, params) => {
|
||||
const screenName = translateRouteToScreen(routeName);
|
||||
if (screenName) {
|
||||
// SDK passes generic Record<string, unknown>, but demo navigation expects specific types
|
||||
// This is safe because we control the route mapping
|
||||
navigation.navigate(screenName, params as any);
|
||||
} else {
|
||||
console.warn(`[SelfClientProvider] SDK route "${routeName}" is not mapped to a demo screen. Ignoring navigation request.`);
|
||||
}
|
||||
},
|
||||
},
|
||||
documents: persistentDocumentsAdapter,
|
||||
crypto: {
|
||||
async hash(data: Uint8Array): Promise<Uint8Array> {
|
||||
|
||||
40
yarn.lock
40
yarn.lock
@@ -7579,7 +7579,7 @@ __metadata:
|
||||
"@zk-email/zk-regex-circom": "npm:^1.2.1"
|
||||
"@zk-kit/binary-merkle-root.circom": "npm:2.0.0"
|
||||
"@zk-kit/circuits": "npm:^1.0.0-beta"
|
||||
anon-aadhaar-circuits: "https://gitpkg.vercel.app/selfxyz/anon-aadhaar/packages/circuits?main"
|
||||
anon-aadhaar-circuits: "https://github.com/selfxyz/anon-aadhaar.git#commit=1b9efa501cff3cf25dc260b060bf611229e316a4&workspace=@anon-aadhaar/circuits"
|
||||
asn1: "npm:^0.2.6"
|
||||
asn1.js: "npm:^5.4.1"
|
||||
asn1js: "npm:^3.0.5"
|
||||
@@ -7750,6 +7750,19 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@selfxyz/euclid@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "@selfxyz/euclid@npm:0.4.1"
|
||||
peerDependencies:
|
||||
react: ">=18.2.0"
|
||||
react-native: ">=0.72.0"
|
||||
react-native-blur-effect: ^1.1.3
|
||||
react-native-svg: ">=15.14.0"
|
||||
react-native-webview: ^13.16.0
|
||||
checksum: 10c0/f25a30b936d5ab1c154008296c64e0b4f97d91cf16e420b9bc3d2f4d9196ae426d1c2b28af653e36a9a580f78a42953f6bca3e9e9fbb36a15860636b9a0cb5fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@selfxyz/mobile-app@workspace:app":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@selfxyz/mobile-app@workspace:app"
|
||||
@@ -7862,6 +7875,7 @@ __metadata:
|
||||
react-native: "npm:0.76.9"
|
||||
react-native-app-auth: "npm:^8.0.3"
|
||||
react-native-biometrics: "npm:^3.0.1"
|
||||
react-native-blur-effect: "npm:^1.1.3"
|
||||
react-native-check-version: "npm:^1.3.0"
|
||||
react-native-cloud-storage: "npm:^2.2.2"
|
||||
react-native-device-info: "npm:^14.0.4"
|
||||
@@ -7909,6 +7923,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.28.3"
|
||||
"@selfxyz/common": "workspace:^"
|
||||
"@selfxyz/euclid": "npm:^0.4.1"
|
||||
"@testing-library/react": "npm:^14.1.2"
|
||||
"@types/react": "npm:^18.3.4"
|
||||
"@types/react-dom": "npm:^18.3.0"
|
||||
@@ -7946,9 +7961,11 @@ __metadata:
|
||||
lottie-react-native: 7.2.2
|
||||
react: ^18.3.1
|
||||
react-native: 0.76.9
|
||||
react-native-blur-effect: ^1.1.3
|
||||
react-native-haptic-feedback: "*"
|
||||
react-native-localize: "*"
|
||||
react-native-svg: "*"
|
||||
react-native-webview: ^13.16.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -14505,13 +14522,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"anon-aadhaar-circuits@https://gitpkg.vercel.app/selfxyz/anon-aadhaar/packages/circuits?main":
|
||||
"anon-aadhaar-circuits@https://github.com/selfxyz/anon-aadhaar.git#commit=1b9efa501cff3cf25dc260b060bf611229e316a4&workspace=@anon-aadhaar/circuits":
|
||||
version: 2.4.3
|
||||
resolution: "anon-aadhaar-circuits@https://gitpkg.vercel.app/selfxyz/anon-aadhaar/packages/circuits?main"
|
||||
resolution: "anon-aadhaar-circuits@https://github.com/selfxyz/anon-aadhaar.git#workspace=%40anon-aadhaar%2Fcircuits&commit=1b9efa501cff3cf25dc260b060bf611229e316a4"
|
||||
dependencies:
|
||||
"@anon-aadhaar/core": "npm:^2.4.3"
|
||||
"@zk-email/circuits": "npm:^6.1.1"
|
||||
checksum: 10c0/93138d1c251988402482f1719ed37764b962250a51deb67bf5b855b91a6f89df2776ffe6135e8accc7a0d57dd13e7c210fc02fc6562af249ea4305f24d7d55f4
|
||||
checksum: 10c0/1e092f002e6a413fd034016320eedfb789158996f707d0c8c2055450baa35660fd90657e34e05c4a23094ed397e9088b6e9feb3463287bf9a0b272cc1fde592f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -25613,6 +25630,7 @@ __metadata:
|
||||
react: "npm:^18.3.1"
|
||||
react-dom: "npm:^18.3.1"
|
||||
react-native: "npm:0.76.9"
|
||||
react-native-blur-effect: "npm:1.1.3"
|
||||
react-native-get-random-values: "npm:^1.11.0"
|
||||
react-native-haptic-feedback: "npm:^2.3.3"
|
||||
react-native-keychain: "npm:^10.0.0"
|
||||
@@ -25621,6 +25639,7 @@ __metadata:
|
||||
react-native-svg: "npm:15.12.1"
|
||||
react-native-svg-transformer: "npm:^1.5.1"
|
||||
react-native-vector-icons: "npm:^10.3.0"
|
||||
react-native-webview: "npm:13.16.0"
|
||||
stream-browserify: "npm:^3.0.0"
|
||||
typescript: "npm:^5.9.2"
|
||||
util: "npm:^0.12.5"
|
||||
@@ -28332,6 +28351,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-blur-effect@npm:1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "react-native-blur-effect@npm:1.1.3"
|
||||
peerDependencies:
|
||||
react: ^17.0.2
|
||||
react-native: ^0.66.4
|
||||
react-native-webview: ^13.6.2
|
||||
checksum: 10c0/5036214ac36fd430c7cea41bf0f14b2aa18338ae7f3e5df4142775dd4462f26ea3bc53710397bfe01c3a2c4450c219978f86dbc5d1989deefa39ca3c4ac80bb6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-check-version@npm:^1.3.0":
|
||||
version: 1.4.0
|
||||
resolution: "react-native-check-version@npm:1.4.0"
|
||||
@@ -28716,7 +28746,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-webview@npm:^13.16.0":
|
||||
"react-native-webview@npm:13.16.0":
|
||||
version: 13.16.0
|
||||
resolution: "react-native-webview@npm:13.16.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user