diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts index a1e43788611..f29c1b50502 100644 --- a/packages/urls/src/urls.ts +++ b/packages/urls/src/urls.ts @@ -33,6 +33,8 @@ export const TREZOR_SUPPORT_FW_ALREADY_INSTALLED: Url = 'https://trezor.io/support/a/firmware-is-already-installed'; export const TREZOR_SUPPORT_IS_MY_DEVICE_SAFE: Url = 'https://trezor.io/support/a/is-my-device-safe-to-use'; +export const TREZOR_SUPPORT_DIFFERENT_PACKAGING: Url = + 'https://trezor.io/support/a/why-is-my-box-different-from-what-is-shown-on-the-website'; export const HELP_CENTER_PIN_URL: Url = 'https://trezor.io/learn/a/pin-protection-on-trezor-devices'; diff --git a/suite-native/atoms/src/IconListItem.tsx b/suite-native/atoms/src/IconListItem.tsx index acac47b1395..f9887825883 100644 --- a/suite-native/atoms/src/IconListItem.tsx +++ b/suite-native/atoms/src/IconListItem.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import { FlexAlignType } from 'react-native'; import { IconName, IconSize } from '@suite-native/icons'; -import { Color } from '@trezor/theme'; +import { Color, TypographyStyle } from '@trezor/theme'; import { Box } from './Box'; import { OrderedListIcon } from './OrderedListIcon'; @@ -42,6 +42,10 @@ type IconListItemProps = { verticalAlign?: FlexAlignType; }; +type IconListTextItemProps = IconListItemProps & { + textVariant?: TypographyStyle; +}; + export const IconListItem = ({ icon, children, @@ -59,8 +63,12 @@ export const IconListItem = ({ ); }; -export const IconListTextItem = ({ children, ...rest }: IconListItemProps) => ( +export const IconListTextItem = ({ + children, + textVariant = 'hint', + ...rest +}: IconListTextItemProps) => ( - {children} + {children} ); diff --git a/suite-native/device/src/hooks/useHandleDeviceConnection.ts b/suite-native/device/src/hooks/useHandleDeviceConnection.ts index a6b15ddd7ac..edc38abe307 100644 --- a/suite-native/device/src/hooks/useHandleDeviceConnection.ts +++ b/suite-native/device/src/hooks/useHandleDeviceConnection.ts @@ -41,6 +41,19 @@ type NavigationProp = StackToStackCompositeNavigationProps< RootStackParamList >; +// We encourage user to disconnect device when he is redirected to suspicious device screen. +// We should not redirect him away so he can read the screen content and decide what to do. +// If the device is connected again, he still should stay on that screen. +const isSuspiciousDeviceScreenFocused = (navigation: NavigationProp) => { + const previousRoute = navigation.getState()?.routes.at(-1); + const innerStackRoute = previousRoute?.state?.routes.at(-1); + + return ( + previousRoute?.name === RootStackRoutes.OnboardingStack && + innerStackRoute?.name === OnboardingStackRoutes.SuspiciousDevice + ); +}; + export const useHandleDeviceConnection = () => { const isNoPhysicalDeviceConnected = useSelector(selectIsNoPhysicalDeviceConnected); const isPortfolioTrackerDevice = useSelector(selectIsPortfolioTrackerDevice); @@ -77,6 +90,7 @@ export const useHandleDeviceConnection = () => { // When is an uninitialized device model that supports device setup, navigate to device onboarding. useEffect(() => { + if (isSuspiciousDeviceScreenFocused(navigation)) return; if ( isDeviceSetupSupported && !isDeviceInitialized && @@ -111,7 +125,7 @@ export const useHandleDeviceConnection = () => { // At the moment when unauthorized physical device is selected, // redirect to the Connecting screen where is handled the connection logic. useEffect(() => { - if (isFirmwareInstallationRunning) return; + if (isFirmwareInstallationRunning || isSuspiciousDeviceScreenFocused(navigation)) return; if ( isDeviceInitialized && @@ -166,7 +180,7 @@ export const useHandleDeviceConnection = () => { // 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) { + if (isDeviceCompromisedModalFocused || isSuspiciousDeviceScreenFocused(navigation)) { return; } navigation.navigate(RootStackRoutes.AppTabs, { diff --git a/suite-native/intl/src/en.ts b/suite-native/intl/src/en.ts index e1b89ec6bbc..a386799a44e 100644 --- a/suite-native/intl/src/en.ts +++ b/suite-native/intl/src/en.ts @@ -843,6 +843,15 @@ export const en = { }, lookDifferentLabel: 'My device looks different', }, + suspiciousDeviceScreen: { + title: 'Let’s play it safe', + subtitle: + 'We want to be sure your device is in the best shape before you start using it.', + bullet1: 'Disconnect your device from your phone.', + bullet2: 'Avoid using this device or sending any funds to it.', + bullet3: 'Go to the support page linked below and use the Chat option on the website.', + contactSupportButton: 'Contact Trezor Support', + }, }, moduleAccountManagement: { accountSettingsScreen: { diff --git a/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx b/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx index 08cf84e32a4..b7492f24e9d 100644 --- a/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx +++ b/suite-native/module-onboarding/src/navigation/OnboardingStackNavigator.tsx @@ -8,6 +8,7 @@ import { import { AnalyticsConsentScreen } from '../screens/AnalyticsConsentScreen'; import { BiometricsScreen } from '../screens/BiometricsScreen'; +import { SuspiciousDeviceScreen } from '../screens/SuspiciousDeviceScreen'; import { UninitializedDeviceLandingScreen } from '../screens/UninitializedDeviceLandingScreen'; import { WelcomeScreen } from '../screens/WelcomeScreen'; @@ -31,5 +32,9 @@ export const OnboardingStackNavigator = () => ( name={OnboardingStackRoutes.UninitializedDeviceLanding} component={UninitializedDeviceLandingScreen} /> + ); diff --git a/suite-native/module-onboarding/src/screens/SuspiciousDeviceScreen.tsx b/suite-native/module-onboarding/src/screens/SuspiciousDeviceScreen.tsx new file mode 100644 index 00000000000..b38b64f4899 --- /dev/null +++ b/suite-native/module-onboarding/src/screens/SuspiciousDeviceScreen.tsx @@ -0,0 +1,126 @@ +import { useSelector } from 'react-redux'; + +import { selectIsNoPhysicalDeviceConnected } 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, + DeviceSuspicionCause, + HomeStackRoutes, + OnboardingStackParamList, + OnboardingStackRoutes, + RootStackParamList, + RootStackRoutes, + Screen, + ScreenHeader, + StackToStackCompositeScreenProps, +} from '@suite-native/navigation'; +import { + TREZOR_SUPPORT_DIFFERENT_PACKAGING, + TREZOR_SUPPORT_FW_ALREADY_INSTALLED, + TREZOR_SUPPORT_IS_MY_DEVICE_SAFE, + Url, +} from '@trezor/urls'; + +const causeToLinkMap = { + deviceLooksDifferent: TREZOR_SUPPORT_IS_MY_DEVICE_SAFE, + firmwareAlreadyInstalled: TREZOR_SUPPORT_FW_ALREADY_INSTALLED, + untrustedReseller: TREZOR_SUPPORT_IS_MY_DEVICE_SAFE, + securitySeal: TREZOR_SUPPORT_IS_MY_DEVICE_SAFE, + packaging: TREZOR_SUPPORT_DIFFERENT_PACKAGING, +} as const satisfies Record; + +export const SuspiciousDeviceScreen = ({ + route, + navigation, +}: StackToStackCompositeScreenProps< + OnboardingStackParamList, + OnboardingStackRoutes.SuspiciousDevice, + RootStackParamList +>) => { + const { suspicionCause } = route.params; + const openLink = useOpenLink(); + const isNoPhysicalDeviceConnected = useSelector(selectIsNoPhysicalDeviceConnected); + + const supportLink = causeToLinkMap[suspicionCause]; + + const handleContactSupportButtonPress = () => { + openLink(`${supportLink}/#open-chat`); + }; + + const handleBackButtonPress = () => { + if (isNoPhysicalDeviceConnected) { + // Exit the onboarding flow if device was disconnected while was user on this screen. + navigation.navigate(RootStackRoutes.AppTabs, { + screen: AppTabsRoutes.HomeStack, + params: { + screen: HomeStackRoutes.Home, + }, + }); + + return; + } + + navigation.goBack(); + }; + + return ( + }> + + + } + subtitle={ + + } + /> + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx b/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx index 543ce5deb25..50e408d1aa7 100644 --- a/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx +++ b/suite-native/module-onboarding/src/screens/UninitializedDeviceLandingScreen.tsx @@ -4,7 +4,13 @@ import { selectDeviceModel, selectHasDeviceFirmwareInstalled } from '@suite-comm import { Box, Button, Image, Text, TextButton, TitleHeader, VStack } from '@suite-native/atoms'; import { SetupSupportingDeviceModel } from '@suite-native/device'; import { Translation } from '@suite-native/intl'; -import { Screen, ScreenHeader } from '@suite-native/navigation'; +import { + OnboardingStackParamList, + OnboardingStackRoutes, + Screen, + ScreenHeader, + StackProps, +} from '@suite-native/navigation'; import { useToast } from '@suite-native/toasts'; import { DeviceModelInternal } from '@trezor/connect'; import { getScreenHeight } from '@trezor/env-utils'; @@ -67,7 +73,9 @@ const UninitializedDeviceLandingScreenContent = () => { ); }; -export const UninitializedDeviceLandingScreen = () => { +export const UninitializedDeviceLandingScreen = ({ + navigation, +}: StackProps) => { const { showToast } = useToast(); const hasDeviceFirmwareInstalled = useSelector(selectHasDeviceFirmwareInstalled); @@ -76,9 +84,16 @@ export const UninitializedDeviceLandingScreen = () => { showToast({ variant: 'warning', message: 'TODO: implement next screen' }); }; - const handleDeclineButtonPress = () => { - // TODO: navigate to screen where user can see more info regarding tampered devices. - showToast({ variant: 'warning', message: 'TODO: implement next screen' }); + const handleNeverUsedThisDeviceButtonPress = () => { + navigation.navigate(OnboardingStackRoutes.SuspiciousDevice, { + suspicionCause: 'firmwareAlreadyInstalled', + }); + }; + + const handleDeviceLooksDifferentButtonPress = () => { + navigation.navigate(OnboardingStackRoutes.SuspiciousDevice, { + suspicionCause: 'deviceLooksDifferent', + }); }; return ( @@ -89,7 +104,7 @@ export const UninitializedDeviceLandingScreen = () => { @@ -105,7 +120,7 @@ export const UninitializedDeviceLandingScreen = () => { {hasDeviceFirmwareInstalled && (