Skip to content

Commit

Permalink
feat(suite): redesign FW hash check other-error UI (on dev only)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lemonexe committed Jan 24, 2025
1 parent 53c913f commit 66e82b0
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 19 deletions.
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
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 are used
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
Expand Up @@ -16,3 +16,16 @@ export const hardFailureChecklistItems: SecurityChecklistItem[] = [
content: <Translation id="TR_USE_CHAT" values={{ b: chunks => <b>{chunks}</b> }} />,
},
];

export const softFailureChecklistItems: SecurityChecklistItem[] = [
{
icon: 'numberOne',
content: <Translation id="TR_DISCONNECT_YOUR_TREZOR" />,
subtitle: <Translation id="TR_DISCONNECT_YOUR_TREZOR_SUBTITLE" />,
},
{
icon: 'numberTwo',
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_FAILED_VERIFY_DEVICE_HEADING',
};

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
32 changes: 32 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_HASH_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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useTheme } from 'styled-components';

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

import { SecurityChecklistItem } from './types';
Expand All @@ -21,7 +21,14 @@ export const SecurityChecklist = ({ items }: SecurityChecklistProps) => {
{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>
<Box>
<Paragraph variant="tertiary">{item.content}</Paragraph>
{item.subtitle ? (
<Paragraph typographyStyle="hint" variant="tertiary">
{item.subtitle}
</Paragraph>
) : null}
</Box>
</Row>
))}
</Column>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import { IconName } from '@trezor/components';
export type SecurityChecklistItem = {
icon: IconName;
content: ReactNode;
subtitle?: ReactNode;
};

0 comments on commit 66e82b0

Please sign in to comment.