Skip to content

Commit

Permalink
feat(suite-native): onboarding suspicious device screen
Browse files Browse the repository at this point in the history
  • Loading branch information
PeKne committed Feb 6, 2025
1 parent e329be8 commit 4e69e04
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 12 deletions.
2 changes: 2 additions & 0 deletions packages/urls/src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
14 changes: 11 additions & 3 deletions suite-native/atoms/src/IconListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +42,10 @@ type IconListItemProps = {
verticalAlign?: FlexAlignType;
};

type IconListTextItemProps = IconListItemProps & {
textVariant?: TypographyStyle;
};

export const IconListItem = ({
icon,
children,
Expand All @@ -59,8 +63,12 @@ export const IconListItem = ({
);
};

export const IconListTextItem = ({ children, ...rest }: IconListItemProps) => (
export const IconListTextItem = ({
children,
textVariant = 'hint',
...rest
}: IconListTextItemProps) => (
<IconListItem {...rest}>
<Text variant="hint">{children}</Text>
<Text variant={textVariant}>{children}</Text>
</IconListItem>
);
18 changes: 16 additions & 2 deletions suite-native/device/src/hooks/useHandleDeviceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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, {
Expand Down
9 changes: 9 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -31,5 +32,9 @@ export const OnboardingStackNavigator = () => (
name={OnboardingStackRoutes.UninitializedDeviceLanding}
component={UninitializedDeviceLandingScreen}
/>
<OnboardingStack.Screen
name={OnboardingStackRoutes.SuspiciousDevice}
component={SuspiciousDeviceScreen}
/>
</OnboardingStack.Navigator>
);
126 changes: 126 additions & 0 deletions suite-native/module-onboarding/src/screens/SuspiciousDeviceScreen.tsx
Original file line number Diff line number Diff line change
@@ -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<DeviceSuspicionCause, Url>;

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 (
<Screen header={<ScreenHeader closeAction={handleBackButtonPress} />}>
<VStack justifyContent="space-between" flex={1} paddingTop="sp16">
<VStack spacing="sp32">
<TitleHeader
titleVariant="titleMedium"
title={<Translation id="moduleOnboarding.suspiciousDeviceScreen.title" />}
subtitle={
<Translation id="moduleOnboarding.suspiciousDeviceScreen.subtitle" />
}
/>
<VStack spacing="sp24">
<IconListTextItem
iconSize="large"
variant="yellow"
textVariant="highlight"
icon="plugs"
>
<Translation id="moduleOnboarding.suspiciousDeviceScreen.bullet1" />
</IconListTextItem>
<IconListTextItem
iconSize="large"
variant="yellow"
textVariant="highlight"
icon="handPalm"
>
<Translation id="moduleOnboarding.suspiciousDeviceScreen.bullet2" />
</IconListTextItem>
<IconListTextItem
iconSize="large"
variant="yellow"
textVariant="highlight"
icon="chatCircle"
>
<Translation id="moduleOnboarding.suspiciousDeviceScreen.bullet3" />
</IconListTextItem>
</VStack>
</VStack>
<VStack spacing="sp12">
<Button
testID="@onboarding/SuspiciousDeviceScreen/contactSupportBtn"
colorScheme="yellowBold"
onPress={handleContactSupportButtonPress}
>
<Translation id="moduleOnboarding.suspiciousDeviceScreen.contactSupportButton" />
</Button>

<Button
colorScheme="yellowElevation0"
onPress={handleBackButtonPress}
testID="@onboarding/SuspiciousDeviceScreen/backBtn"
>
<Translation id="generic.buttons.back" />
</Button>
</VStack>
</VStack>
</Screen>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,7 +73,9 @@ const UninitializedDeviceLandingScreenContent = () => {
);
};

export const UninitializedDeviceLandingScreen = () => {
export const UninitializedDeviceLandingScreen = ({
navigation,
}: StackProps<OnboardingStackParamList, OnboardingStackRoutes.UninitializedDeviceLanding>) => {
const { showToast } = useToast();
const hasDeviceFirmwareInstalled = useSelector(selectHasDeviceFirmwareInstalled);

Expand All @@ -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 (
Expand All @@ -89,7 +104,7 @@ export const UninitializedDeviceLandingScreen = () => {
<UninitializedDeviceLandingScreenContent />
<TextButton
isUnderlined
onPress={handleDeclineButtonPress}
onPress={handleDeviceLooksDifferentButtonPress}
testID="@onboarding/UninitializedDeviceLandingScreen/deviceLooksDifferentBtn"
>
<Translation id="moduleOnboarding.uninitializedDeviceLandingScreen.lookDifferentLabel" />
Expand All @@ -105,7 +120,7 @@ export const UninitializedDeviceLandingScreen = () => {
{hasDeviceFirmwareInstalled && (
<Button
colorScheme="tertiaryElevation0"
onPress={handleDeclineButtonPress}
onPress={handleNeverUsedThisDeviceButtonPress}
testID="@onboarding/UninitializedDeviceLandingScreen/declineBtn"
>
<Translation id="moduleOnboarding.uninitializedDeviceLandingScreen.firmware.noButton" />
Expand Down
9 changes: 9 additions & 0 deletions suite-native/navigation/src/navigators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ type AddCoinFlowParams = RequireAllOrNone<
>;

export type CloseActionType = 'back' | 'close';
export type DeviceSuspicionCause =
| 'deviceLooksDifferent'
| 'firmwareAlreadyInstalled'
| 'untrustedReseller'
| 'securitySeal'
| 'packaging';

type AccountDetailParams = {
accountKey?: AccountKey;
Expand Down Expand Up @@ -113,6 +119,9 @@ export type OnboardingStackParamList = {
[OnboardingStackRoutes.AnalyticsConsent]: undefined;
[OnboardingStackRoutes.Biometrics]: undefined;
[OnboardingStackRoutes.UninitializedDeviceLanding]: undefined;
[OnboardingStackRoutes.SuspiciousDevice]: {
suspicionCause: DeviceSuspicionCause;
};
};

export type AccountsImportStackParamList = {
Expand Down
1 change: 1 addition & 0 deletions suite-native/navigation/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum OnboardingStackRoutes {
AnalyticsConsent = 'AnalyticsConsent',
Biometrics = 'Biometrics',
UninitializedDeviceLanding = 'UninitializedDeviceLanding',
SuspiciousDevice = 'SuspiciousDevice',
}

export enum AccountsImportStackRoutes {
Expand Down

0 comments on commit 4e69e04

Please sign in to comment.