diff --git a/packages/suite/src/components/suite/SecurityCheck/DeviceCompromised.tsx b/packages/suite/src/components/suite/SecurityCheck/DeviceCompromised.tsx index a30e66e9bab..70da1e519cc 100644 --- a/packages/suite/src/components/suite/SecurityCheck/DeviceCompromised.tsx +++ b/packages/suite/src/components/suite/SecurityCheck/DeviceCompromised.tsx @@ -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 => { + 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 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 = () => { diff --git a/packages/suite/src/components/suite/SecurityCheck/SecurityCheckFail.tsx b/packages/suite/src/components/suite/SecurityCheck/SecurityCheckFail.tsx index 8b1e1fc8b2a..5168ef83efb 100644 --- a/packages/suite/src/components/suite/SecurityCheck/SecurityCheckFail.tsx +++ b/packages/suite/src/components/suite/SecurityCheck/SecurityCheckFail.tsx @@ -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[`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 = ({ )} - diff --git a/packages/suite/src/components/suite/SecurityCheck/checklistItems.tsx b/packages/suite/src/components/suite/SecurityCheck/checklistItems.tsx index 168d8daeb6c..3db5792b8df 100644 --- a/packages/suite/src/components/suite/SecurityCheck/checklistItems.tsx +++ b/packages/suite/src/components/suite/SecurityCheck/checklistItems.tsx @@ -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: , content: , }, { - icon: 'hand', + icon: , content: , }, { - icon: 'chat', + icon: , content: {chunks} }} />, }, ]; + +export const softFailureChecklistItems: SecurityChecklistItem[] = [ + { + icon: ( + + + + ), + content: , + subtitle: , + }, + { + icon: ( + + + + ), + content: , + subtitle: , + }, +]; diff --git a/packages/suite/src/components/suite/banners/SuiteBanners/FirmwareAuthenticityCheckBanner.tsx b/packages/suite/src/components/suite/banners/SuiteBanners/FirmwareAuthenticityCheckBanner.tsx index be3c2fd3ae5..c238240b430 100644 --- a/packages/suite/src/components/suite/banners/SuiteBanners/FirmwareAuthenticityCheckBanner.tsx +++ b/packages/suite/src/components/suite/banners/SuiteBanners/FirmwareAuthenticityCheckBanner.tsx @@ -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,9 +44,29 @@ const useAuthenticityCheckMessage = (): TranslationKey | null => { return null; }; +const urlWithChatBox = `${TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}#open-chat`; + +const BannerButtons = () => ( + + + + + + + + + + + + +); + 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; @@ -49,16 +74,8 @@ export const FirmwareAuthenticityCheckBanner = () => { return ( - - - - - ) - } + variant={isHashCheckOtherError ? 'warning' : 'destructive'} + rightContent={wasOffline ? null : } > diff --git a/packages/suite/src/constants/suite/firmware.ts b/packages/suite/src/constants/suite/firmware.ts index c8ff5c09c20..53520af72d8 100644 --- a/packages/suite/src/constants/suite/firmware.ts +++ b/packages/suite/src/constants/suite/firmware.ts @@ -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< diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 101a86d82c0..fead8ba853b 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -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 Chat 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', diff --git a/packages/suite/src/views/onboarding/steps/SecurityCheck/SecurityChecklist.tsx b/packages/suite/src/views/onboarding/steps/SecurityCheck/SecurityChecklist.tsx index 0b4234544b8..a5078bcff3e 100644 --- a/packages/suite/src/views/onboarding/steps/SecurityCheck/SecurityChecklist.tsx +++ b/packages/suite/src/views/onboarding/steps/SecurityCheck/SecurityChecklist.tsx @@ -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 ( - - {items.map(item => ( - - - {item.content} - - ))} - - ); -}; +export const SecurityChecklist = ({ items }: SecurityChecklistProps) => ( + + {items.map((item, index) => ( + + {item.icon} + + {item.content} + {item.subtitle ? ( + + {item.subtitle} + + ) : null} + + + ))} + +); diff --git a/packages/suite/src/views/onboarding/steps/SecurityCheck/types.ts b/packages/suite/src/views/onboarding/steps/SecurityCheck/types.ts index 68cf3a6b660..9e8936a9258 100644 --- a/packages/suite/src/views/onboarding/steps/SecurityCheck/types.ts +++ b/packages/suite/src/views/onboarding/steps/SecurityCheck/types.ts @@ -1,8 +1,7 @@ import { ReactNode } from 'react'; -import { IconName } from '@trezor/components'; - export type SecurityChecklistItem = { - icon: IconName; + icon: ReactNode; content: ReactNode; + subtitle?: ReactNode; };