diff --git a/suite-common/wallet-core/src/device/deviceReducer.ts b/suite-common/wallet-core/src/device/deviceReducer.ts index ff8859a6067..f7b8a4812c6 100644 --- a/suite-common/wallet-core/src/device/deviceReducer.ts +++ b/suite-common/wallet-core/src/device/deviceReducer.ts @@ -847,3 +847,10 @@ export const selectIsDeviceInViewOnlyMode = (state: DeviceRootState) => { return !isDeviceConnected && isDeviceRemembered; }; + +export const selectIsDeviceUsingPassphrase = (state: DeviceRootState) => { + const isDeviceProtectedByPassphrase = selectIsDeviceProtectedByPassphrase(state); + const device = selectDevice(state); + + return isDeviceProtectedByPassphrase && device?.useEmptyPassphrase === false; +}; diff --git a/suite-native/app/src/navigation/AppTabNavigator.tsx b/suite-native/app/src/navigation/AppTabNavigator.tsx index dfcfd7639af..bc6844e0d38 100644 --- a/suite-native/app/src/navigation/AppTabNavigator.tsx +++ b/suite-native/app/src/navigation/AppTabNavigator.tsx @@ -5,13 +5,16 @@ import { HomeStackNavigator } from '@suite-native/module-home'; import { AccountsStackNavigator } from '@suite-native/module-accounts-management'; import { SettingsStackNavigator } from '@suite-native/module-settings'; import { AppTabsParamList, AppTabsRoutes, TabBar } from '@suite-native/navigation'; +import { useHandleDeviceRequestsPassphrase } from '@suite-native/device'; import { rootTabsOptions } from './routes'; const Tab = createBottomTabNavigator(); -export const AppTabNavigator = () => ( - <> +export const AppTabNavigator = () => { + useHandleDeviceRequestsPassphrase(); + + return ( ( - -); + ); +}; diff --git a/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx b/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx index d7030e59178..920818253c8 100644 --- a/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx +++ b/suite-native/device-manager/src/components/AddHiddenWalletButton.tsx @@ -1,36 +1,23 @@ -import { useNavigation } from '@react-navigation/native'; +import { useDispatch, useSelector } from 'react-redux'; import { Button } from '@suite-native/atoms'; import { Translation } from '@suite-native/intl'; -import { - PassphraseStackParamList, - PassphraseStackRoutes, - RootStackRoutes, - StackToStackCompositeNavigationProps, - RootStackParamList, -} from '@suite-native/navigation'; +import { createDeviceInstance, selectDevice } from '@suite-common/wallet-core'; import { useDeviceManager } from '../hooks/useDeviceManager'; -type NavigationProp = StackToStackCompositeNavigationProps< - PassphraseStackParamList, - PassphraseStackRoutes.PassphraseForm, - RootStackParamList ->; - export const AddHiddenWalletButton = () => { - const navigation = useNavigation(); + const dispatch = useDispatch(); + + const device = useSelector(selectDevice); const { setIsDeviceManagerVisible } = useDeviceManager(); const handleAddHiddenWallet = () => { - setIsDeviceManagerVisible(false); + if (!device) return; - navigation.navigate(RootStackRoutes.PassphraseStack, { - screen: PassphraseStackRoutes.PassphraseForm, - }); - - // await dispatch(createDeviceInstance({ device: instance })); + setIsDeviceManagerVisible(false); + dispatch(createDeviceInstance({ device })); }; return ( diff --git a/suite-native/device/src/hooks/useHandleDeviceConnection.ts b/suite-native/device/src/hooks/useHandleDeviceConnection.ts index 4bddb2280df..29f93263896 100644 --- a/suite-native/device/src/hooks/useHandleDeviceConnection.ts +++ b/suite-native/device/src/hooks/useHandleDeviceConnection.ts @@ -18,6 +18,7 @@ import { selectDeviceRequestedPin, selectIsDeviceConnectedAndAuthorized, selectIsNoPhysicalDeviceConnected, + selectIsDeviceUsingPassphrase, } from '@suite-common/wallet-core'; import { selectIsOnboardingFinished } from '@suite-native/module-settings'; import { requestPrioritizedDeviceAccess } from '@suite-native/device-mutex'; @@ -36,7 +37,7 @@ export const useHandleDeviceConnection = () => { const isDeviceConnectedAndAuthorized = useSelector(selectIsDeviceConnectedAndAuthorized); const hasDeviceRequestedPin = useSelector(selectDeviceRequestedPin); const { isBiometricsOverlayVisible } = useIsBiometricsOverlayVisible(); - + const isDeviceUsingPassphrase = useSelector(selectIsDeviceUsingPassphrase); const navigation = useNavigation(); const dispatch = useDispatch(); @@ -50,9 +51,14 @@ export const useHandleDeviceConnection = () => { !isBiometricsOverlayVisible ) { requestPrioritizedDeviceAccess(() => dispatch(authorizeDevice())); - navigation.navigate(RootStackRoutes.ConnectDeviceStack, { - screen: ConnectDeviceStackRoutes.ConnectingDevice, - }); + + // Note: Passphrase protected device (excluding empty passphrase, e. g. standard wallet with passphrase protection on device), + // post auth navigation is handled in @suite-native/module-passphrase for custom UX flow. + if (!isDeviceUsingPassphrase) { + navigation.navigate(RootStackRoutes.ConnectDeviceStack, { + screen: ConnectDeviceStackRoutes.ConnectingDevice, + }); + } } }, [ dispatch, @@ -62,6 +68,7 @@ export const useHandleDeviceConnection = () => { isDeviceConnectedAndAuthorized, isBiometricsOverlayVisible, navigation, + isDeviceUsingPassphrase, ]); // In case that the physical device is disconnected, redirect to the home screen and diff --git a/suite-native/device/src/hooks/useHandleDeviceRequestsPassphrase.ts b/suite-native/device/src/hooks/useHandleDeviceRequestsPassphrase.ts new file mode 100644 index 00000000000..b33d0f3bdf4 --- /dev/null +++ b/suite-native/device/src/hooks/useHandleDeviceRequestsPassphrase.ts @@ -0,0 +1,34 @@ +import { useCallback, useEffect } from 'react'; + +import { useNavigation } from '@react-navigation/native'; + +import TrezorConnect from '@trezor/connect'; +import { + PassphraseStackParamList, + PassphraseStackRoutes, + RootStackParamList, + RootStackRoutes, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; + +type NavigationProp = StackToStackCompositeNavigationProps< + PassphraseStackParamList, + PassphraseStackRoutes.PassphraseForm, + RootStackParamList +>; + +export const useHandleDeviceRequestsPassphrase = () => { + const navigation = useNavigation(); + + const handleNavigateToPassphraseForm = useCallback(() => { + navigation.navigate(RootStackRoutes.PassphraseStack, { + screen: PassphraseStackRoutes.PassphraseForm, + }); + }, [navigation]); + + useEffect(() => { + TrezorConnect.on('ui-request_passphrase', handleNavigateToPassphraseForm); + + return () => TrezorConnect.off('ui-request_passphrase', handleNavigateToPassphraseForm); + }, [handleNavigateToPassphraseForm]); +}; diff --git a/suite-native/device/src/index.ts b/suite-native/device/src/index.ts index 4e7c723f174..7d54bc88294 100644 --- a/suite-native/device/src/index.ts +++ b/suite-native/device/src/index.ts @@ -3,6 +3,7 @@ export * from './middlewares/buttonRequestMiddleware'; export * from './hooks/useHandleDeviceConnection'; export * from './hooks/useDetectDeviceError'; export * from './hooks/useDelayedNavigation'; +export * from './hooks/useHandleDeviceRequestsPassphrase'; export * from './hooks/useReportDeviceConnectToAnalytics'; export * from './screens/DeviceInfoModalScreen'; export * from './components/ConnectDeviceAnimation'; diff --git a/suite-native/module-passphrase/package.json b/suite-native/module-passphrase/package.json index 6f133aaa487..03514815756 100644 --- a/suite-native/module-passphrase/package.json +++ b/suite-native/module-passphrase/package.json @@ -12,18 +12,23 @@ "dependencies": { "@react-navigation/native": "6.1.10", "@react-navigation/native-stack": "6.9.18", + "@reduxjs/toolkit": "1.9.5", "@suite-common/icons": "workspace:*", + "@suite-common/redux-utils": "workspace:*", "@suite-common/validators": "workspace:*", "@suite-common/wallet-constants": "workspace:*", + "@suite-common/wallet-core": "workspace:*", "@suite-native/atoms": "workspace:*", "@suite-native/forms": "workspace:*", "@suite-native/intl": "workspace:*", "@suite-native/navigation": "workspace:*", "@suite-native/theme": "workspace:*", + "@trezor/connect": "workspace:*", "@trezor/styles": "workspace:*", "react": "18.2.0", "react-native": "0.73.6", "react-native-reanimated": "3.8.1", - "react-native-svg": "14.1.0" + "react-native-svg": "14.1.0", + "react-redux": "8.0.7" } } diff --git a/suite-native/module-passphrase/redux.d.ts b/suite-native/module-passphrase/redux.d.ts new file mode 100644 index 00000000000..df9a0c3f969 --- /dev/null +++ b/suite-native/module-passphrase/redux.d.ts @@ -0,0 +1,7 @@ +import { AsyncThunkAction } from '@reduxjs/toolkit'; + +declare module 'redux' { + export interface Dispatch { + >(thunk: TThunk): ReturnType; + } +} diff --git a/suite-native/module-passphrase/src/components/ConfirmOnDeviceBottomSheet.tsx b/suite-native/module-passphrase/src/components/ConfirmOnDeviceBottomSheet.tsx deleted file mode 100644 index 2c8e199bb7a..00000000000 --- a/suite-native/module-passphrase/src/components/ConfirmOnDeviceBottomSheet.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { BottomSheet, Box, Button, CenteredTitleHeader, VStack } from '@suite-native/atoms'; -import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { Translation } from '@suite-native/intl'; - -import { DeviceTS3Svg } from '../assets/DeviceTS3Svg'; - -const buttonStyle = prepareNativeStyle(_ => ({ - width: '100%', -})); - -type ConfirmOnDeviceBottomSheetProps = { - isVisible: boolean; -}; - -export const ConfirmOnDeviceBottomSheet = ({ isVisible }: ConfirmOnDeviceBottomSheetProps) => { - const { applyStyle } = useNativeStyles(); - - const handleClose = () => { - // TODO Close bottom sheet and trigger connnect cancel event - }; - - return ( - null} isCloseDisplayed={false}> - - - } - subtitle={} - /> - - - - - - ); -}; diff --git a/suite-native/module-passphrase/src/components/PassphraseForm.tsx b/suite-native/module-passphrase/src/components/PassphraseForm.tsx index 91daedd82bb..ebbc42ebd0e 100644 --- a/suite-native/module-passphrase/src/components/PassphraseForm.tsx +++ b/suite-native/module-passphrase/src/components/PassphraseForm.tsx @@ -1,4 +1,8 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { useDispatch, useSelector } from 'react-redux'; +import { useEffect } from 'react'; + +import { useNavigation } from '@react-navigation/native'; import { Form, TextInputField, useForm } from '@suite-native/forms'; import { @@ -9,6 +13,18 @@ import { import { Button, VStack } from '@suite-native/atoms'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; import { Translation, useTranslate } from '@suite-native/intl'; +import { + deviceActions, + onPassphraseSubmit, + selectDevice, + selectDeviceButtonRequestsCodes, +} from '@suite-common/wallet-core'; +import { + PassphraseStackParamList, + PassphraseStackRoutes, + RootStackParamList, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; type PassphraseFormProps = { onFocus: () => void; @@ -19,11 +35,24 @@ const formStyle = prepareNativeStyle(utils => ({ borderRadius: utils.borders.radii.large, })); +type NavigationProp = StackToStackCompositeNavigationProps< + PassphraseStackParamList, + PassphraseStackRoutes.PassphraseForm, + RootStackParamList +>; + export const PassphraseForm = ({ onFocus }: PassphraseFormProps) => { + const dispatch = useDispatch(); + const { applyStyle } = useNativeStyles(); const { translate } = useTranslate(); + const device = useSelector(selectDevice); + const buttonRequestCodes = useSelector(selectDeviceButtonRequestsCodes); + + const navigation = useNavigation(); + const form = useForm({ validation: passphraseFormSchema, defaultValues: { @@ -31,11 +60,18 @@ export const PassphraseForm = ({ onFocus }: PassphraseFormProps) => { }, }); + useEffect(() => { + if (buttonRequestCodes.includes('ButtonRequest_Other')) { + navigation.navigate(PassphraseStackRoutes.PassphraseConfirmOnDevice); + dispatch(deviceActions.removeButtonRequests({ device })); + } + }, [buttonRequestCodes, device, dispatch, navigation]); + const { handleSubmit, watch } = form; - const handleCreateHiddenWallet = handleSubmit(values => { - console.warn(values); - // TODO create wallet + const handleCreateHiddenWallet = handleSubmit(({ passphrase }) => { + dispatch(deviceActions.removeButtonRequests({ device })); + dispatch(onPassphraseSubmit({ value: passphrase, passphraseOnDevice: false })); }); const inputHasValue = !!watch('passphrase').length; diff --git a/suite-native/module-passphrase/src/components/PassphraseScreenHeader.tsx b/suite-native/module-passphrase/src/components/PassphraseScreenHeader.tsx index d3aa06c2e11..85fb0eefdb1 100644 --- a/suite-native/module-passphrase/src/components/PassphraseScreenHeader.tsx +++ b/suite-native/module-passphrase/src/components/PassphraseScreenHeader.tsx @@ -1,10 +1,51 @@ -import { ScreenHeaderWrapper } from '@suite-native/atoms'; -import { GoBackIcon } from '@suite-native/navigation'; +import { useDispatch } from 'react-redux'; + +import { useNavigation } from '@react-navigation/native'; + +import { IconButton, ScreenHeaderWrapper } from '@suite-native/atoms'; +import { + AppTabsRoutes, + HomeStackRoutes, + PassphraseStackParamList, + PassphraseStackRoutes, + RootStackParamList, + RootStackRoutes, + StackToTabCompositeProps, +} from '@suite-native/navigation'; + +import { cancelPassphraseAndSelectStandardDeviceThunk } from '../passphraseThunks'; + +type NavigationProp = StackToTabCompositeProps< + PassphraseStackParamList, + PassphraseStackRoutes, + RootStackParamList +>; export const PassphraseScreenHeader = () => { + const navigation = useNavigation(); + + const dispatch = useDispatch(); + + const handleClose = () => { + dispatch(cancelPassphraseAndSelectStandardDeviceThunk()); + navigation.navigate(RootStackRoutes.AppTabs, { + screen: AppTabsRoutes.HomeStack, + params: { + screen: HomeStackRoutes.Home, + }, + }); + }; + return ( - + ); }; diff --git a/suite-native/module-passphrase/src/navigation/PassphraseStackNavigator.tsx b/suite-native/module-passphrase/src/navigation/PassphraseStackNavigator.tsx index e2f87cac521..08c0cbb3b03 100644 --- a/suite-native/module-passphrase/src/navigation/PassphraseStackNavigator.tsx +++ b/suite-native/module-passphrase/src/navigation/PassphraseStackNavigator.tsx @@ -7,6 +7,8 @@ import { } from '@suite-native/navigation'; import { PassphraseFormScreen } from '../screens/PassphraseFormScreen'; +import { PassphraseLoadingScreen } from '../screens/PassphraseLoadingScreen'; +import { PassphraseConfirmOnDeviceScreen } from '../screens/PassphraseConfirmOnDeviceScreen'; export const PassphraseStack = createNativeStackNavigator(); @@ -17,6 +19,14 @@ export const PassphraseStackNavigator = () => { name={PassphraseStackRoutes.PassphraseForm} component={PassphraseFormScreen} /> + + ); }; diff --git a/suite-native/module-passphrase/src/passphraseThunks.ts b/suite-native/module-passphrase/src/passphraseThunks.ts new file mode 100644 index 00000000000..93ba2c27db5 --- /dev/null +++ b/suite-native/module-passphrase/src/passphraseThunks.ts @@ -0,0 +1,31 @@ +import { createThunk } from '@suite-common/redux-utils'; +import { + deviceActions, + selectDevice, + selectDeviceThunk, + selectDevices, +} from '@suite-common/wallet-core'; +import TrezorConnect from '@trezor/connect'; + +const PASSPHRASE_MODULE_PREFIX = '@suite-native/device'; + +export const cancelPassphraseAndSelectStandardDeviceThunk = createThunk( + `${PASSPHRASE_MODULE_PREFIX}/cancelPassphraseFlow`, + (_, { getState, dispatch }) => { + const devices = selectDevices(getState()); + const device = selectDevice(getState()); + + if (!device) return; + + // Select standard wallet (e.g. empty passphrase) that has the same device ID. + const standardWalletDeviceIndex = devices.findIndex( + d => d.id === device.id && d.instance === 1, + ); + + TrezorConnect.cancel(); + dispatch(selectDeviceThunk(devices[standardWalletDeviceIndex])); + + // Remove device on which the passphrase flow was canceled + dispatch(deviceActions.forgetDevice(device)); + }, +); diff --git a/suite-native/module-passphrase/src/screens/PassphraseConfirmOnDeviceScreen.tsx b/suite-native/module-passphrase/src/screens/PassphraseConfirmOnDeviceScreen.tsx new file mode 100644 index 00000000000..6d0cfdcadef --- /dev/null +++ b/suite-native/module-passphrase/src/screens/PassphraseConfirmOnDeviceScreen.tsx @@ -0,0 +1,86 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { useEffect } from 'react'; + +import { useNavigation } from '@react-navigation/native'; + +import { + AppTabsRoutes, + HomeStackRoutes, + PassphraseStackParamList, + PassphraseStackRoutes, + RootStackParamList, + RootStackRoutes, + Screen, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; +import { Box, Button, CenteredTitleHeader, VStack } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; +import { selectIsDeviceConnectedAndAuthorized } from '@suite-common/wallet-core'; + +import { PassphraseScreenHeader } from '../components/PassphraseScreenHeader'; +import { DeviceTS3Svg } from '../assets/DeviceTS3Svg'; +import { cancelPassphraseAndSelectStandardDeviceThunk } from '../passphraseThunks'; + +const buttonStyle = prepareNativeStyle(_ => ({ + width: '100%', +})); + +type NavigationProp = StackToStackCompositeNavigationProps< + PassphraseStackParamList, + PassphraseStackRoutes.PassphraseConfirmOnDevice, + RootStackParamList +>; + +export const PassphraseConfirmOnDeviceScreen = () => { + const { applyStyle } = useNativeStyles(); + + const isDeviceConnectedAndAuthorized = useSelector(selectIsDeviceConnectedAndAuthorized); + + const navigation = useNavigation(); + + const dispatch = useDispatch(); + + useEffect(() => { + if (isDeviceConnectedAndAuthorized) { + navigation.navigate(PassphraseStackRoutes.PassphraseLoading); + } + }, [isDeviceConnectedAndAuthorized, navigation]); + + const handleClose = () => { + dispatch(cancelPassphraseAndSelectStandardDeviceThunk()); + navigation.navigate(RootStackRoutes.AppTabs, { + screen: AppTabsRoutes.HomeStack, + params: { + screen: HomeStackRoutes.Home, + }, + }); + }; + + return ( + }> + + + } + subtitle={} + /> + + + + + + ); +}; diff --git a/suite-native/module-passphrase/src/screens/PassphraseLoadingScreen.tsx b/suite-native/module-passphrase/src/screens/PassphraseLoadingScreen.tsx new file mode 100644 index 00000000000..7ed0f40c7c0 --- /dev/null +++ b/suite-native/module-passphrase/src/screens/PassphraseLoadingScreen.tsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; + +import { useNavigation } from '@react-navigation/native'; + +import { VStack, Loader } from '@suite-native/atoms'; +import { + AppTabsRoutes, + HomeStackRoutes, + RootStackParamList, + RootStackRoutes, + Screen, + StackNavigationProps, +} from '@suite-native/navigation'; + +import { PassphraseScreenHeader } from '../components/PassphraseScreenHeader'; + +type NavigationProp = StackNavigationProps; + +export const PassphraseLoadingScreen = () => { + const navigation = useNavigation(); + + useEffect(() => { + // NOTE: This is just for demo purposes. Proper loading screen will be implemented in a follow-up. + const timeout = setTimeout(() => { + navigation.navigate(RootStackRoutes.AppTabs, { + screen: AppTabsRoutes.HomeStack, + params: { + screen: HomeStackRoutes.Home, + }, + }); + }, 2000); + + return () => clearTimeout(timeout); + }, [navigation]); + + return ( + }> + + + + + ); +}; diff --git a/suite-native/module-passphrase/tsconfig.json b/suite-native/module-passphrase/tsconfig.json index 4389c9a750b..471be8ac1c4 100644 --- a/suite-native/module-passphrase/tsconfig.json +++ b/suite-native/module-passphrase/tsconfig.json @@ -3,17 +3,24 @@ "compilerOptions": { "outDir": "libDev" }, "references": [ { "path": "../../suite-common/icons" }, + { + "path": "../../suite-common/redux-utils" + }, { "path": "../../suite-common/validators" }, { "path": "../../suite-common/wallet-constants" }, + { + "path": "../../suite-common/wallet-core" + }, { "path": "../atoms" }, { "path": "../forms" }, { "path": "../intl" }, { "path": "../navigation" }, { "path": "../theme" }, + { "path": "../../packages/connect" }, { "path": "../../packages/styles" } ] } diff --git a/suite-native/navigation/src/navigators.ts b/suite-native/navigation/src/navigators.ts index 2114fdf3a3d..56c7665da25 100644 --- a/suite-native/navigation/src/navigators.ts +++ b/suite-native/navigation/src/navigators.ts @@ -123,6 +123,8 @@ export type ConnectDeviceStackParamList = { export type PassphraseStackParamList = { [PassphraseStackRoutes.PassphraseForm]: undefined; + [PassphraseStackRoutes.PassphraseConfirmOnDevice]: undefined; + [PassphraseStackRoutes.PassphraseLoading]: undefined; }; export type RootStackParamList = { diff --git a/suite-native/navigation/src/routes.ts b/suite-native/navigation/src/routes.ts index 75db84301e6..3837755a3ff 100644 --- a/suite-native/navigation/src/routes.ts +++ b/suite-native/navigation/src/routes.ts @@ -45,6 +45,8 @@ export enum ConnectDeviceStackRoutes { export enum PassphraseStackRoutes { PassphraseForm = 'PassphraseForm', + PassphraseConfirmOnDevice = 'PassphraseConfirmOnDevice', + PassphraseLoading = 'PassphraseLoading', } export enum DevUtilsStackRoutes { diff --git a/yarn.lock b/yarn.lock index b0d75ebc7d3..b6dca1f6a68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9411,19 +9411,24 @@ __metadata: dependencies: "@react-navigation/native": "npm:6.1.10" "@react-navigation/native-stack": "npm:6.9.18" + "@reduxjs/toolkit": "npm:1.9.5" "@suite-common/icons": "workspace:*" + "@suite-common/redux-utils": "workspace:*" "@suite-common/validators": "workspace:*" "@suite-common/wallet-constants": "workspace:*" + "@suite-common/wallet-core": "workspace:*" "@suite-native/atoms": "workspace:*" "@suite-native/forms": "workspace:*" "@suite-native/intl": "workspace:*" "@suite-native/navigation": "workspace:*" "@suite-native/theme": "workspace:*" + "@trezor/connect": "workspace:*" "@trezor/styles": "workspace:*" react: "npm:18.2.0" react-native: "npm:0.73.6" react-native-reanimated: "npm:3.8.1" react-native-svg: "npm:14.1.0" + react-redux: "npm:8.0.7" languageName: unknown linkType: soft