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:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user