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:
Aaron DeRuvo
2025-12-01 16:08:09 +01:00
committed by GitHub
parent d5d0879045
commit 7899c239cc
20 changed files with 245 additions and 181 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 />;
}

View File

@@ -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",

View File

@@ -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'
);

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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;

View File

@@ -17,8 +17,10 @@ export type {
MRZValidation,
NFCScanResult,
NFCScannerAdapter,
NavigationAdapter,
NetworkAdapter,
Progress,
RouteName,
SelfClient,
StorageAdapter,
TrackEventParams,

View File

@@ -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

View File

@@ -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(),
};

View File

@@ -35,6 +35,10 @@ const createMockSelfClientWithDocumentsAdapter = (documentsAdapter: DocumentsAda
}),
},
},
navigation: {
goBack: () => {},
goTo: (_routeName: string, _params?: Record<string, any>) => {},
},
scanner: {
scan: async () => ({
passportData: {

View File

@@ -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

View File

@@ -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"
},

View File

@@ -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> {

View File

@@ -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: