diff --git a/packages/connect-examples/expo-example/App.tsx b/packages/connect-examples/expo-example/App.tsx index 367f0354a..b4047d40e 100644 --- a/packages/connect-examples/expo-example/App.tsx +++ b/packages/connect-examples/expo-example/App.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { enableFreeze, enableScreens } from 'react-native-screens'; -import { View } from 'react-native'; import HomeScreen from './src/views/HomeScreen'; import MockScreen from './src/views/MockConnect'; import { getHardwareSDKInstance } from './src/utils/hardwareInstance'; import PassphraseTestScreen from './src/views/PassphraseTestScreen'; import SDKProvider from './src/provider/SDKProvider'; +import AddressTestScreen from './src/views/AddressTestScreen'; getHardwareSDKInstance(); @@ -19,6 +18,7 @@ export default function App() { + diff --git a/packages/connect-examples/expo-example/package.json b/packages/connect-examples/expo-example/package.json index a5fadb5b2..7a81d3192 100644 --- a/packages/connect-examples/expo-example/package.json +++ b/packages/connect-examples/expo-example/package.json @@ -45,7 +45,8 @@ "react-native-screens": "~3.22.0", "react-native-web": "~0.19.6", "ripple-keypairs": "^1.1.4", - "stream-browserify": "^3.0.0" + "stream-browserify": "^3.0.0", + "use-context-selector": "^1.4.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/packages/connect-examples/expo-example/src/components/AddressTest/TestAddress.tsx b/packages/connect-examples/expo-example/src/components/AddressTest/TestAddress.tsx new file mode 100644 index 000000000..77b8bd2db --- /dev/null +++ b/packages/connect-examples/expo-example/src/components/AddressTest/TestAddress.tsx @@ -0,0 +1,394 @@ +import { Text, View, Button, StyleSheet } from 'react-native'; +import { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { CoreMessage, UI_EVENT, UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core'; +import { Picker } from '@react-native-picker/picker'; +import { isEmpty, isNil } from 'lodash'; +import { createContext, useContextSelector } from 'use-context-selector'; +import HardwareSDKContext from '../../provider/HardwareSDKContext'; +import { useDevice } from '../../provider/DeviceProvider'; +import { baseChainParams, testCases } from './data'; +import { TestCaseData } from './data/types'; + +type VerifyState = 'none' | 'pending' | 'skip' | 'success' | 'fail'; +type TestCaseDataWithKey = TestCaseData & { $key: string }; + +const TestAddressContext = createContext<{ + itemValues: TestCaseDataWithKey[]; + setItemValues?: React.Dispatch>; +}>({ + itemValues: [], +}); + +function TestAddressProvider({ children }: { children: React.ReactNode }) { + const [itemValues, setItemValues] = useState([]); + + const value = useMemo( + () => ({ + itemValues, + setItemValues, + }), + [itemValues] + ); + + return {children}; +} + +const TestAddressVerifyContext = createContext<{ + setItemVerifyState: (key: string, newState: { verify: VerifyState; error?: string }) => void; + itemVerifyState: { [key: string]: { verify: VerifyState; error?: string } }; + clearItemVerifyState: () => void; +}>({ + setItemVerifyState: () => {}, + itemVerifyState: {}, + clearItemVerifyState: () => {}, +}); + +function TestAddressVerifyProvider({ children }: { children: React.ReactNode }) { + const [itemVerifyState, setItemVerifyStateInternal] = useState<{ + [key: string]: { verify: VerifyState; error?: string }; + }>({}); + + const setItemVerifyState = useCallback( + (key: string, newState: { verify: VerifyState; error?: string }) => { + setItemVerifyStateInternal(prevState => ({ + ...prevState, + [key]: newState, + })); + }, + [] + ); + + const clearItemVerifyState = useCallback(() => { + setItemVerifyStateInternal({}); + }, []); + + const value = useMemo( + () => ({ + setItemVerifyState, + clearItemVerifyState, + itemVerifyState, + }), + [clearItemVerifyState, itemVerifyState, setItemVerifyState] + ); + + return ( + {children} + ); +} + +const TestItemView = ({ item }: { item: TestCaseDataWithKey }) => { + const itemVerifyState = useContextSelector( + TestAddressVerifyContext, + v => v.itemVerifyState?.[item.$key] + ); + + const verifyState = useMemo(() => itemVerifyState?.verify ?? 'none', [itemVerifyState]); + const errorState = useMemo(() => itemVerifyState?.error ?? '', [itemVerifyState]); + + const errorStateViewMemo = useMemo(() => { + if (!errorState) return null; + return error: {errorState}; + }, [errorState]); + + const verifyStateViewMemo = useMemo(() => { + let color = 'gray'; + if (verifyState === 'pending') { + color = 'blue'; + } else if (verifyState === 'skip') { + color = 'gray'; + } else if (verifyState === 'success') { + color = 'green'; + } else if (verifyState === 'fail') { + color = 'red'; + } + + return ( + + {verifyState} + + ); + }, [verifyState]); + + return ( + + {verifyStateViewMemo} + + {item.method} + Expected:{item.expectedAddress} + {errorStateViewMemo} + + + ); +}; + +const TestItemViewMemo = memo(TestItemView); + +function OptionsView() { + const { sdk: SDK } = useContext(HardwareSDKContext); + const { selectedDevice } = useDevice(); + + const [testCaseList, setTestCaseList] = useState([]); + const [currentTestCase, setCurrentTestCase] = useState(); + const [testDescription, setTestDescription] = useState(); + const [passphrase, setPassphrase] = useState(); + + const setItemValues = useContextSelector(TestAddressContext, v => v.setItemValues); + const setItemVerifyState = useContextSelector( + TestAddressVerifyContext, + v => v.setItemVerifyState + ); + const clearItemVerifyState = useContextSelector( + TestAddressVerifyContext, + v => v.clearItemVerifyState + ); + + const running = useRef(false); + + const currentPassphrase = useRef(''); + + useEffect(() => { + const testCaseList: string[] = []; + testCases.forEach(testCase => { + testCaseList.push(testCase.name); + }); + setTestCaseList(testCaseList); + setCurrentTestCase(testCaseList[0]); + }, []); + + useEffect(() => { + const testCase = testCases.find(testCase => testCase.name === currentTestCase); + if (!testCase) return; + setTestDescription(testCase.description); + setPassphrase(testCase.passphrase); + }, [currentTestCase]); + + const stopTest = useCallback(() => { + running.current = false; + setItemValues?.([]); + clearItemVerifyState?.(); + if (SDK) { + SDK.cancel(); + SDK.removeAllListeners(UI_EVENT); + } + }, [SDK, setItemValues, clearItemVerifyState]); + + const beginTest = useCallback(async () => { + if (!SDK) return; + SDK.removeAllListeners(UI_EVENT); + + const testCase = testCases.find(testCase => testCase.name === currentTestCase); + if (!testCase) return; + + const currentTestCases = testCase?.data?.map((item, index) => { + const key = `${item.method}-${index}`; + + return { + ...item, + $key: key, + } as unknown as TestCaseDataWithKey; + }); + + setItemValues?.(currentTestCases); + clearItemVerifyState?.(); + running.current = true; + + SDK.on(UI_EVENT, (message: CoreMessage) => { + console.log('TopLEVEL EVENT ===>>>>: ', message); + if (message.type === UI_REQUEST.REQUEST_PIN) { + SDK.uiResponse({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + } + if (message.type === UI_REQUEST.REQUEST_PASSPHRASE) { + setTimeout(() => { + SDK.uiResponse({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: currentPassphrase.current ?? '', + }, + }); + }, 200); + } + }); + + const connectId = selectedDevice?.connectId ?? ''; + const featuresRes = await SDK.getFeatures(connectId); + const deviceId = featuresRes.payload?.device_id ?? ''; + + if (featuresRes.payload?.passphrase_protection === true && testCase.passphrase == null) { + await SDK.deviceSettings(connectId, { + usePassphrase: false, + }); + } + if (!featuresRes.payload?.passphrase_protection && testCase.passphrase != null) { + await SDK.deviceSettings(connectId, { + usePassphrase: true, + }); + } + + currentPassphrase.current = testCase.passphrase; + const { passphraseState } = testCase; + + for (const item of currentTestCases) { + try { + if (isNil(item.expectedAddress) || isEmpty(item.expectedAddress)) { + setItemVerifyState?.(item.$key, { + verify: 'skip', + }); + } else { + const { method, params } = item; + // @ts-expect-error + const commonParams = baseChainParams[method]; + const requestParams = { + ...commonParams, + ...params, + passphraseState, + useEmptyPassphrase: !passphraseState, + }; + setItemVerifyState?.(item.$key, { + verify: 'pending', + }); + + // @ts-expect-error + const res = await SDK[`${method}` as keyof typeof sdk]( + connectId, + deviceId, + requestParams + ); + + if (!running.current) return; + let verifyState: VerifyState = 'none'; + let error: string | undefined; + + if (!res.success) { + if (res.payload?.code === 802 || res.payload?.code === 803) { + verifyState = 'skip'; + } else { + verifyState = 'fail'; + error = res.payload?.error; + } + } else if (res.payload?.address === item.expectedAddress) { + verifyState = 'success'; + } else { + verifyState = 'fail'; + error = `actual: ${res.payload?.address}, expected: ${item.expectedAddress}`; + } + + setItemVerifyState?.(item.$key, { + verify: verifyState, + error, + }); + } + } catch (e) { + setItemVerifyState?.(item.$key, { + verify: 'fail', + // @ts-expect-error + error: e?.message ?? '', + }); + } + } + + SDK.removeAllListeners(UI_EVENT); + }, [ + SDK, + setItemValues, + clearItemVerifyState, + selectedDevice?.connectId, + currentTestCase, + setItemVerifyState, + ]); + + const contentMemo = useMemo( + () => ( + <> + {testDescription} + {!!passphrase && ( + + Passphrase:「{passphrase}」 + + )} + + setCurrentTestCase(itemValue)} + > + {testCaseList.map(testCase => ( + + ))} + +