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
Show file tree
Hide file tree
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
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment could be a bit more descriptive, e.g.:

// revision check has precedence over hash check, because it does not have the ambiguous other-error state

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) {
Expand All @@ -24,6 +64,7 @@ export const DeviceCompromised = () => {
<SecurityCheckFail
goBack={goToSuite}
supportUrl={TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}
{...securityCheckFailProps}
/>
</Card>
</WelcomeLayout>
Expand Down
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';
Expand Down Expand Up @@ -28,6 +30,7 @@ export type SecurityCheckFailProps = {
text?: TranslationKey;
supportUrl: Url;
checklistItems?: SecurityChecklistItem[];
supportButtonVariant?: ComponentProps<typeof Button>[`variant`];
};

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

Expand Down Expand Up @@ -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>
Expand Down
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';
Expand All @@ -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 => {
Expand All @@ -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>
Expand Down
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
Expand Down Expand Up @@ -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<
Expand Down
37 changes: 37 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
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';
Expand All @@ -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
<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;
};
Loading