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"