From 0a1177ddff3bbfd89bae484f9e45ea7a1fc0f9fa Mon Sep 17 00:00:00 2001 From: Jiri Zbytovsky Date: Thu, 9 Jan 2025 13:42:15 +0100 Subject: [PATCH] feat(suite-native): add DeviceCompromisedModal for FW revision check --- suite-native/app/e2e/utils.ts | 3 +- suite-native/app/package.json | 1 + .../app/src/navigation/RootStackNavigator.tsx | 5 + suite-native/app/tsconfig.json | 3 + .../src/hooks/useHandleDeviceConnection.ts | 33 ++++++- suite-native/intl/src/en.ts | 13 +++ .../module-authenticity-checks/package.json | 22 +++++ .../module-authenticity-checks/src/index.ts | 1 + .../screens/DeviceCompromisedModalScreen.tsx | 93 +++++++++++++++++++ .../module-authenticity-checks/tsconfig.json | 13 +++ suite-native/navigation/src/navigators.ts | 1 + suite-native/navigation/src/routes.ts | 1 + yarn.lock | 15 +++ 13 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 suite-native/module-authenticity-checks/package.json create mode 100644 suite-native/module-authenticity-checks/src/index.ts create mode 100644 suite-native/module-authenticity-checks/src/screens/DeviceCompromisedModalScreen.tsx create mode 100644 suite-native/module-authenticity-checks/tsconfig.json diff --git a/suite-native/app/e2e/utils.ts b/suite-native/app/e2e/utils.ts index 599dfd42099..d3394f49f95 100644 --- a/suite-native/app/e2e/utils.ts +++ b/suite-native/app/e2e/utils.ts @@ -105,7 +105,8 @@ export const prepareTrezorEmulator = async ( // Prepare Trezor device for test scenario await TrezorUserEnvLink.disconnect(); await TrezorUserEnvLink.connect(); - await TrezorUserEnvLink.startEmu({ model: 'T3T1', wipe: true }); + // start with latest officially released firmware (necessary to pass the firmware checks) + await TrezorUserEnvLink.startEmu({ model: 'T3T1', version: '2-latest', wipe: true }); await TrezorUserEnvLink.setupEmu({ label: TREZOR_DEVICE_LABEL, mnemonic: seed, diff --git a/suite-native/app/package.json b/suite-native/app/package.json index dad471adb95..8587b3f44f7 100644 --- a/suite-native/app/package.json +++ b/suite-native/app/package.json @@ -59,6 +59,7 @@ "@suite-native/module-accounts-import": "workspace:*", "@suite-native/module-accounts-management": "workspace:*", "@suite-native/module-add-accounts": "workspace:*", + "@suite-native/module-authenticity-checks": "workspace:*", "@suite-native/module-authorize-device": "workspace:*", "@suite-native/module-connect-popup": "workspace:*", "@suite-native/module-dev-utils": "workspace:*", diff --git a/suite-native/app/src/navigation/RootStackNavigator.tsx b/suite-native/app/src/navigation/RootStackNavigator.tsx index 21e90b75323..5862d568ca4 100644 --- a/suite-native/app/src/navigation/RootStackNavigator.tsx +++ b/suite-native/app/src/navigation/RootStackNavigator.tsx @@ -9,6 +9,7 @@ import { AccountSettingsScreen, } from '@suite-native/module-accounts-management'; import { AddCoinAccountStackNavigator } from '@suite-native/module-add-accounts'; +import { DeviceCompromisedModalScreen } from '@suite-native/module-authenticity-checks'; import { AuthorizeDeviceStackNavigator } from '@suite-native/module-authorize-device'; import { ConnectPopupScreen } from '@suite-native/module-connect-popup'; import { DevUtilsStackNavigator } from '@suite-native/module-dev-utils'; @@ -80,6 +81,10 @@ export const RootStackNavigator = () => { name={RootStackRoutes.SettingsScreenStack} component={SettingsStackNavigator} /> + {/* Navigation flows that start by push from bottom animation on the first screen of its stack. */} { const isDeviceSetupSupported = useSelector(selectIsDeviceSetupSupported); const { isBiometricsOverlayVisible } = useIsBiometricsOverlayVisible(); + + const hasFirmwareAuthenticityCheckHardFailed = useSelector( + selectHasFirmwareAuthenticityCheckHardFailed, + ); + const isFirmwareAuthenticityCheckDismissed = useSelector( + selectIsFirmwareAuthenticityCheckDismissed, + ); + const shouldNavigateToDeviceCompromisedModal = + hasFirmwareAuthenticityCheckHardFailed && !isFirmwareAuthenticityCheckDismissed; + const navigation = useNavigation(); const dispatch = useDispatch(); @@ -58,6 +72,8 @@ export const useHandleDeviceConnection = () => { const isDeviceSettingsStackFocused = lastRoute === RootStackRoutes.DeviceSettingsStack; const isSendStackFocused = lastRoute === RootStackRoutes.SendStack; const shouldBlockSendReviewRedirect = isDeviceRemembered && isSendStackFocused; + const isDeviceCompromisedModalFocused = + lastRoute === RootStackRoutes.DeviceCompromisedModalScreen; // When is an uninitialized device model that supports device setup, navigate to device onboarding. useEffect(() => { @@ -103,7 +119,8 @@ export const useHandleDeviceConnection = () => { isOnboardingFinished && !isPortfolioTrackerDevice && !isDeviceConnectedAndAuthorized && - !isBiometricsOverlayVisible + !isBiometricsOverlayVisible && + !shouldNavigateToDeviceCompromisedModal ) { requestPrioritizedDeviceAccess({ deviceCallback: () => dispatch(authorizeDeviceThunk()), @@ -117,6 +134,9 @@ export const useHandleDeviceConnection = () => { }); } } + if (shouldNavigateToDeviceCompromisedModal) { + navigation.navigate(RootStackRoutes.DeviceCompromisedModalScreen); + } }, [ dispatch, isDeviceConnected, @@ -130,6 +150,7 @@ export const useHandleDeviceConnection = () => { shouldBlockSendReviewRedirect, isFirmwareInstallationRunning, isDeviceInitialized, + shouldNavigateToDeviceCompromisedModal, ]); // In case that the physical device is disconnected, redirect to the home screen and @@ -141,6 +162,13 @@ export const useHandleDeviceConnection = () => { if (shouldBlockSendReviewRedirect) { return; } + // DeviceCompromisedModal is persistent, so postpone navigating to away until it's dismissed + // TODO: this hook is getting very complex, and it's hard to understand the logic when it navigates there and back again. + // Ideally there'd be a single source of truth, a function returning "where we should be as per current state" + // rather than multiple useEffects with imperative instructions "go there when X changes" + if (isDeviceCompromisedModalFocused) { + return; + } navigation.navigate(RootStackRoutes.AppTabs, { screen: AppTabsRoutes.HomeStack, params: { @@ -154,6 +182,7 @@ export const useHandleDeviceConnection = () => { navigation, shouldBlockSendReviewRedirect, isFirmwareInstallationRunning, + isDeviceCompromisedModalFocused, ]); // When trezor gets locked, it is necessary to display a PIN matrix for T1 so that it can be unlocked diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index fe999074fe3..a6f6ab98faf 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -1225,6 +1225,19 @@ export const en = { }, }, }, + moduleAuthenticityChecks: { + deviceCompromised: { + title: 'Your device may have been compromised', + subtitle: + 'Contact our support to learn what’s going on with your device and what to do next.', + steps: { + disconnectDevice: 'Disconnect your device from your phone.', + avoidUsingDevice: 'Avoid using this device or sending any funds to it.', + contactSupport: 'Continue to Trezor support and use the Chat option.', + }, + buttonContactSupport: 'Contact Trezor Support', + }, + }, staking: { stakingDetailScreen: { title: 'Staking', diff --git a/suite-native/module-authenticity-checks/package.json b/suite-native/module-authenticity-checks/package.json new file mode 100644 index 00000000000..e7fff0df8b4 --- /dev/null +++ b/suite-native/module-authenticity-checks/package.json @@ -0,0 +1,22 @@ +{ + "name": "@suite-native/module-authenticity-checks", + "version": "1.0.0", + "private": true, + "license": "See LICENSE.md in repo root", + "sideEffects": false, + "main": "src/index", + "scripts": { + "depcheck": "yarn g:depcheck", + "lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'", + "type-check": "yarn g:tsc --build" + }, + "dependencies": { + "@react-navigation/native": "6.1.18", + "@suite-common/wallet-core": "workspace:*", + "@suite-native/atoms": "workspace:*", + "@suite-native/intl": "workspace:*", + "@suite-native/link": "workspace:*", + "@suite-native/navigation": "workspace:*", + "react-redux": "8.0.7" + } +} diff --git a/suite-native/module-authenticity-checks/src/index.ts b/suite-native/module-authenticity-checks/src/index.ts new file mode 100644 index 00000000000..731d37c6b30 --- /dev/null +++ b/suite-native/module-authenticity-checks/src/index.ts @@ -0,0 +1 @@ +export * from './screens/DeviceCompromisedModalScreen'; diff --git a/suite-native/module-authenticity-checks/src/screens/DeviceCompromisedModalScreen.tsx b/suite-native/module-authenticity-checks/src/screens/DeviceCompromisedModalScreen.tsx new file mode 100644 index 00000000000..34e4cdbce1f --- /dev/null +++ b/suite-native/module-authenticity-checks/src/screens/DeviceCompromisedModalScreen.tsx @@ -0,0 +1,93 @@ +import { useDispatch, useSelector } from 'react-redux'; + +import { useNavigation } from '@react-navigation/native'; + +import { deviceActions, selectSelectedDevice } from '@suite-common/wallet-core'; +import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms'; +import { Translation } from '@suite-native/intl'; +import { useOpenLink } from '@suite-native/link'; +import { + AppTabsRoutes, + HomeStackRoutes, + RootStackParamList, + RootStackRoutes, + Screen, + ScreenHeader, + StackToStackCompositeNavigationProps, +} from '@suite-native/navigation'; + +// TODO this page is for desktop; await creation of new page tailored to the suite-native UX +const TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL = + 'https://trezor.io/support/a/trezor-fw-revision-check-failed'; +const chatUrl = `${TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}#open-chat`; + +const InformativeList = () => ( + + + + + + + + + + + + + +); + +type NavigationProp = StackToStackCompositeNavigationProps< + RootStackParamList, + RootStackRoutes.AppTabs, + RootStackParamList +>; + +export const DeviceCompromisedModalScreen = () => { + const device = useSelector(selectSelectedDevice); + const dispatch = useDispatch(); + const openLink = useOpenLink(); + const navigation = useNavigation(); + + const dismissCheck = () => { + if (device?.id) { + dispatch(deviceActions.dismissFirmwareAuthenticityCheck(device.id)); + } + }; + + // After dismissCheck, an effect could fire in useHandleDeviceConnection to navigate away, but it's not guaranteed! + // To be sure we don't lock user on on this screen, we navigate home. + const handleClose = () => { + navigation.navigate(RootStackRoutes.AppTabs, { + screen: AppTabsRoutes.HomeStack, + params: { screen: HomeStackRoutes.Home }, + }); + dismissCheck(); + }; + + const handleContactSupportClick = () => openLink(chatUrl); + + return ( + }> + + } + subtitle={ + + } + /> + + + + + + + + ); +}; diff --git a/suite-native/module-authenticity-checks/tsconfig.json b/suite-native/module-authenticity-checks/tsconfig.json new file mode 100644 index 00000000000..cbb910b9cbb --- /dev/null +++ b/suite-native/module-authenticity-checks/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "outDir": "libDev" }, + "references": [ + { + "path": "../../suite-common/wallet-core" + }, + { "path": "../atoms" }, + { "path": "../intl" }, + { "path": "../link" }, + { "path": "../navigation" } + ] +} diff --git a/suite-native/navigation/src/navigators.ts b/suite-native/navigation/src/navigators.ts index f4e7b4eb52b..3190652b345 100644 --- a/suite-native/navigation/src/navigators.ts +++ b/suite-native/navigation/src/navigators.ts @@ -216,6 +216,7 @@ export type RootStackParamList = { parsedUrl: ParsedURL; }; [RootStackRoutes.SettingsScreenStack]: NavigatorScreenParams; + [RootStackRoutes.DeviceCompromisedModalScreen]: undefined; }; export type TradingStackParamList = { diff --git a/suite-native/navigation/src/routes.ts b/suite-native/navigation/src/routes.ts index 298fa5b139c..8c46c332b6f 100644 --- a/suite-native/navigation/src/routes.ts +++ b/suite-native/navigation/src/routes.ts @@ -16,6 +16,7 @@ export enum RootStackRoutes { CoinEnablingInit = 'CoinEnablingInit', ConnectPopup = 'ConnectPopup', SettingsScreenStack = 'SettingsScreenStack', + DeviceCompromisedModalScreen = 'DeviceCompromisedModalScreen', } export enum AppTabsRoutes { diff --git a/yarn.lock b/yarn.lock index a83b1962370..474fa68481a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10000,6 +10000,7 @@ __metadata: "@suite-native/module-accounts-import": "workspace:*" "@suite-native/module-accounts-management": "workspace:*" "@suite-native/module-add-accounts": "workspace:*" + "@suite-native/module-authenticity-checks": "workspace:*" "@suite-native/module-authorize-device": "workspace:*" "@suite-native/module-connect-popup": "workspace:*" "@suite-native/module-dev-utils": "workspace:*" @@ -10648,6 +10649,20 @@ __metadata: languageName: unknown linkType: soft +"@suite-native/module-authenticity-checks@workspace:*, @suite-native/module-authenticity-checks@workspace:suite-native/module-authenticity-checks": + version: 0.0.0-use.local + resolution: "@suite-native/module-authenticity-checks@workspace:suite-native/module-authenticity-checks" + dependencies: + "@react-navigation/native": "npm:6.1.18" + "@suite-common/wallet-core": "workspace:*" + "@suite-native/atoms": "workspace:*" + "@suite-native/intl": "workspace:*" + "@suite-native/link": "workspace:*" + "@suite-native/navigation": "workspace:*" + react-redux: "npm:8.0.7" + languageName: unknown + linkType: soft + "@suite-native/module-authorize-device@workspace:*, @suite-native/module-authorize-device@workspace:suite-native/module-authorize-device": version: 0.0.0-use.local resolution: "@suite-native/module-authorize-device@workspace:suite-native/module-authorize-device"