Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(suite): redesign FW checks UI #16599

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -3,14 +3,54 @@ import { Card } from '@trezor/components';
import { TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL } from '@trezor/urls';

import { WelcomeLayout } from 'src/components/suite';
import { useDevice, useDispatch } from 'src/hooks/suite';
import { useDevice, useDispatch, useSelector } from 'src/hooks/suite';
import {
selectFirmwareHashCheckErrorIfEnabled,
selectFirmwareRevisionCheckErrorIfEnabled,
} from 'src/reducers/suite/suiteReducer';

import { SecurityCheckFail } from './SecurityCheckFail';
import { SecurityCheckFail, SecurityCheckFailProps } from './SecurityCheckFail';
import { hardFailureChecklistItems, softFailureChecklistItems } from './checklistItems';

const useSecurityCheckFailProps = (): Partial<SecurityCheckFailProps> => {
const revisionCheckError = useSelector(selectFirmwareRevisionCheckErrorIfEnabled);
const hashCheckError = useSelector(selectFirmwareHashCheckErrorIfEnabled);

// revision check has precedence over hash check, because it is unimpeachable
Lemonexe marked this conversation as resolved.
Show resolved Hide resolved
if (revisionCheckError !== null) {
return {
heading: 'TR_DEVICE_COMPROMISED_HEADING',
text: 'TR_DEVICE_COMPROMISED_FW_REVISION_CHECK_TEXT',
checklistItems: hardFailureChecklistItems,
};
}
// hash check other-error shall display softer wording than standard hash check errors
if (hashCheckError === 'other-error') {
return {
heading: 'TR_FAILED_VERIFY_DEVICE_HEADING',
text: 'TR_FAILED_VERIFY_DEVICE_TEXT',
checklistItems: softFailureChecklistItems,
supportButtonVariant: 'warning',
};
}
if (hashCheckError !== null) {
return {
heading: 'TR_DEVICE_COMPROMISED_HEADING',
text: 'TR_DEVICE_COMPROMISED_FW_HASH_CHECK_TEXT',
checklistItems: hardFailureChecklistItems,
};
}

// should not happen, but default props will be used with no problem
return {};
};

export const DeviceCompromised = () => {
const dispatch = useDispatch();
const { device } = useDevice();

const securityCheckFailProps = useSecurityCheckFailProps();

const goToSuite = () => {
// Condition to satisfy TypeScript, device.id is always defined at this point.
if (device?.id) {
@@ -24,6 +64,7 @@ export const DeviceCompromised = () => {
<SecurityCheckFail
goBack={goToSuite}
supportUrl={TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}
{...securityCheckFailProps}
/>
</Card>
</WelcomeLayout>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ComponentProps } from 'react';

import styled from 'styled-components';

import { TranslationKey } from '@suite-common/intl-types';
@@ -28,6 +30,7 @@ export type SecurityCheckFailProps = {
text?: TranslationKey;
supportUrl: Url;
checklistItems?: SecurityChecklistItem[];
supportButtonVariant?: ComponentProps<typeof Button>[`variant`];
};

export const SecurityCheckFail = ({
@@ -36,6 +39,7 @@ export const SecurityCheckFail = ({
text = 'TR_DEVICE_COMPROMISED_TEXT',
supportUrl,
checklistItems = hardFailureChecklistItems,
supportButtonVariant = 'primary',
}: SecurityCheckFailProps) => {
const chatUrl = `${supportUrl}#open-chat`;

@@ -63,7 +67,7 @@ export const SecurityCheckFail = ({
</Button>
)}
<Flex>
<Button href={chatUrl} isFullWidth size="large">
<Button href={chatUrl} isFullWidth size="large" variant={supportButtonVariant}>
<Translation id="TR_CONTACT_TREZOR_SUPPORT" />
</Button>
</Flex>
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
import styled from 'styled-components';

import { Icon } from '@trezor/components';
import { borders, spacingsPx } from '@trezor/theme';

import { SecurityChecklistItem } from 'src/views/onboarding/steps/SecurityCheck/types';

import { Translation } from '../Translation';

const IconBackground = styled.div`
border-radius: ${borders.radii.full};
background-color: ${({ theme }) => theme.backgroundTertiaryDefaultOnElevation0};
padding: ${spacingsPx.xs};
`;

export const hardFailureChecklistItems: SecurityChecklistItem[] = [
{
icon: 'plugs',
icon: <Icon size={24} variant="default" name="plugs" />,
content: <Translation id="TR_DISCONNECT_DEVICE" />,
},
{
icon: 'hand',
icon: <Icon size={24} variant="default" name="hand" />,
content: <Translation id="TR_AVOID_USING_DEVICE" />,
},
{
icon: 'chat',
icon: <Icon size={24} variant="default" name="chat" />,
content: <Translation id="TR_USE_CHAT" values={{ b: chunks => <b>{chunks}</b> }} />,
},
];

export const softFailureChecklistItems: SecurityChecklistItem[] = [
{
icon: (
<IconBackground>
<Icon size={20} variant="default" name="numberOne" />
</IconBackground>
),
content: <Translation id="TR_DISCONNECT_YOUR_TREZOR" />,
subtitle: <Translation id="TR_DISCONNECT_YOUR_TREZOR_SUBTITLE" />,
},
{
icon: (
<IconBackground>
<Icon size={20} variant="default" name="numberTwo" />
</IconBackground>
),
content: <Translation id="TR_PROBLEM_PERSISTS" />,
subtitle: <Translation id="TR_PROBLEM_PERSISTS_SUBTITLE" />,
},
];
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { TranslationKey } from '@suite-common/intl-types';
import { Banner } from '@trezor/components';
import { Banner, Row } from '@trezor/components';
import { FirmwareHashCheckError, FirmwareRevisionCheckError } from '@trezor/connect';
import { HELP_CENTER_FIRMWARE_REVISION_CHECK } from '@trezor/urls';
import {
HELP_CENTER_FIRMWARE_REVISION_CHECK,
TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL,
} from '@trezor/urls';
import { spacings } from '@trezor/theme';

import { Translation, TrezorLink } from 'src/components/suite';
import { useSelector } from 'src/hooks/suite';
@@ -23,6 +27,7 @@ const hashCheckMessages: Record<
TranslationKey
> = {
'hash-mismatch': 'TR_DEVICE_FIRMWARE_HASH_CHECK_HASH_MISMATCH',
'other-error': 'TR_DEVICE_FIRMWARE_HASH_CHECK_OTHER_ERROR',
};

const useAuthenticityCheckMessage = (): TranslationKey | null => {
@@ -39,26 +44,38 @@ const useAuthenticityCheckMessage = (): TranslationKey | null => {
return null;
};

const urlWithChatBox = `${TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}#open-chat`;

const BannerButtons = () => (
<Row gap={spacings.sm}>
<TrezorLink variant="nostyle" href={urlWithChatBox}>
<Banner.Button>
<Translation id="TR_CONTACT_TREZOR_SUPPORT" />
</Banner.Button>
</TrezorLink>
<TrezorLink variant="nostyle" href={HELP_CENTER_FIRMWARE_REVISION_CHECK}>
<Banner.Button isSubtle>
<Translation id="TR_LEARN_MORE" />
</Banner.Button>
</TrezorLink>
</Row>
);

export const FirmwareAuthenticityCheckBanner = () => {
const firmwareRevisionError = useSelector(selectFirmwareRevisionCheckErrorIfEnabled);
const firmwareHashError = useSelector(selectFirmwareHashCheckErrorIfEnabled);
const wasOffline = firmwareRevisionError === 'cannot-perform-check-offline';
const isHashCheckOtherError =
firmwareRevisionError === null && firmwareHashError === 'other-error';

const message = useAuthenticityCheckMessage();
if (message === null) return null;

return (
<Banner
icon
variant="destructive"
rightContent={
!wasOffline && (
<TrezorLink variant="nostyle" href={HELP_CENTER_FIRMWARE_REVISION_CHECK}>
<Banner.Button iconAlignment="right">
<Translation id="TR_LEARN_MORE" />
</Banner.Button>
</TrezorLink>
)
}
variant={isHashCheckOtherError ? 'warning' : 'destructive'}
rightContent={wasOffline ? null : <BannerButtons />}
>
<Translation id={message} />
</Banner>
5 changes: 3 additions & 2 deletions packages/suite/src/constants/suite/firmware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FirmwareHashCheckError, FirmwareRevisionCheckError } from '@trezor/connect';
import { FilterPropertiesByType } from '@trezor/type-utils';
import { isDevEnv } from '@suite-common/suite-utils';

/*
* Various scenarios how firmware authenticity check errors are handled
@@ -31,8 +32,8 @@ export const hashCheckErrorScenarios = {
'check-unsupported': { type: 'skipped', shouldReport: false },
// could mean counterfeit firmware, but it's also caught by revision check, which handles edge-cases better
'unknown-release': { type: 'skipped', shouldReport: false },
// TODO fix FW hash check unreliability & reenable
'other-error': { type: 'skipped', shouldReport: true },
// TODO fix FW hash check unreliability & reenable on production
'other-error': { type: isDevEnv ? 'hardModal' : 'skipped', shouldReport: true },
} satisfies HashCheckErrorScenarios;

export type SkippedHashCheckError = keyof FilterPropertiesByType<
37 changes: 37 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
@@ -6839,6 +6839,22 @@ export default defineMessages({
defaultMessage:
"Contact Trezor Support to figure out what's going on with your device and what to do next.",
},
TR_FAILED_VERIFY_DEVICE_HEADING: {
id: 'TR_FAILED_VERIFY_DEVICE_HEADING',
defaultMessage: 'Failed to verify device',
},
TR_FAILED_VERIFY_DEVICE_TEXT: {
id: 'TR_FAILED_VERIFY_DEVICE_TEXT',
defaultMessage: 'Avoid using this device or sending any funds to it.',
},
TR_DEVICE_COMPROMISED_FW_HASH_CHECK_TEXT: {
id: 'TR_DEVICE_COMPROMISED_FW_HASH_CHECK_TEXT',
defaultMessage: 'Your device firmware hash check failed.',
},
TR_DEVICE_COMPROMISED_FW_REVISION_CHECK_TEXT: {
id: 'TR_DEVICE_COMPROMISED_FW_REVISION_CHECK_TEXT',
defaultMessage: 'Your device firmware revision check failed.',
},
TR_PLAY_IT_SAFE: {
id: 'TR_PLAY_IT_SAFE',
defaultMessage: "Let's play it safe",
@@ -6860,6 +6876,22 @@ export default defineMessages({
id: 'TR_USE_CHAT',
defaultMessage: 'Click below and use the <b>Chat</b> option on the next page.',
},
TR_DISCONNECT_YOUR_TREZOR: {
id: 'TR_DISCONNECT_YOUR_TREZOR',
defaultMessage: 'Reconnect the device',
},
TR_DISCONNECT_YOUR_TREZOR_SUBTITLE: {
id: 'TR_DISCONNECT_YOUR_TREZOR_SUBTITLE',
defaultMessage: 'This usually solves the issue.',
},
TR_PROBLEM_PERSISTS: {
id: 'TR_PROBLEM_PERSISTS',
defaultMessage: 'If the problem persists, contact Trezor Support',
},
TR_PROBLEM_PERSISTS_SUBTITLE: {
id: 'TR_PROBLEM_PERSISTS_SUBTITLE',
defaultMessage: 'Figure out what’s going on with your device and what to do next.',
},
TR_CONTACT_TREZOR_SUPPORT: {
id: 'TR_CONTACT_TREZOR_SUPPORT',
defaultMessage: 'Contact Trezor Support',
@@ -7027,6 +7059,11 @@ export default defineMessages({
id: 'TR_DEVICE_FIRMWARE_HASH_CHECK_HASH_MISMATCH',
defaultMessage: 'Firmware hash check failed. Your Trezor might be counterfeit.',
},
TR_DEVICE_FIRMWARE_HASH_CHECK_OTHER_ERROR: {
id: 'TR_DEVICE_FIRMWARE_HASH_CHECK_OTHER_ERROR',
defaultMessage:
"Failed to verify device. Don't send any funds to it and reconnect your device. If the problem persists after reconnecting, contact Trezor Support.",
},
TR_ONBOARDING_COINS_STEP: {
id: 'TR_ONBOARDING_COINS_STEP',
defaultMessage: 'Activate coins',
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useTheme } from 'styled-components';

import { Column, Icon, Row, Text } from '@trezor/components';
import { Box, Column, Paragraph, Row } from '@trezor/components';
import { spacings } from '@trezor/theme';

import { SecurityChecklistItem } from './types';
@@ -9,21 +7,24 @@ type SecurityChecklistProps = {
items: readonly SecurityChecklistItem[];
};

export const SecurityChecklist = ({ items }: SecurityChecklistProps) => {
const theme = useTheme();

return (
<Column
alignItems="flex-start"
gap={spacings.xl}
margin={{ top: spacings.xl, bottom: spacings.xxxxl }}
>
{items.map(item => (
<Row key={item.icon} gap={spacings.xl}>
<Icon size={24} name={item.icon} color={theme.legacy.TYPE_DARK_GREY} />
<Text variant="tertiary">{item.content}</Text>
</Row>
))}
</Column>
);
};
export const SecurityChecklist = ({ items }: SecurityChecklistProps) => (
<Column
alignItems="flex-start"
gap={spacings.xl}
margin={{ top: spacings.xl, bottom: spacings.xxxxl }}
>
{items.map((item, index) => (
<Row key={index} gap={spacings.xl}>
{item.icon}
<Box>
Lemonexe marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this caused problem and TS check didn't see it for some reason. In SecurityCheck there is checklistItems varaible, where icon is string. That's likely the reason. Coz string is a ReactNode.

Before, string was kinda correct here as it was the IconName.

<Icon size={24} name={item.icon} color={theme.legacy.TYPE_DARK_GREY} />

My proposal: enforce the Icon element here. I am not sure how to do it correctly. I can check if <Icon /> is its own Component type and require just it, or to remove string from the ReactNode type here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am figuring out proper fix here: #16709

Copy link
Contributor Author

@Lemonexe Lemonexe Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I missed all those checklist definitions 🤦
Yeah, exclude string from ReactNode could work, but would need comment to explain why...
EDIT: ReactElement is more suitable..

<Paragraph variant="tertiary">{item.content}</Paragraph>
{item.subtitle ? (
<Paragraph typographyStyle="hint" variant="tertiary">
{item.subtitle}
</Paragraph>
) : null}
</Box>
</Row>
))}
</Column>
);
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ReactNode } from 'react';

import { IconName } from '@trezor/components';

export type SecurityChecklistItem = {
icon: IconName;
icon: ReactNode;
content: ReactNode;
subtitle?: ReactNode;
};