diff --git a/packages/connect/src/device/checkFirmwareRevision.ts b/packages/connect/src/device/checkFirmwareRevision.ts index 206d0ff31bb..672d9979001 100644 --- a/packages/connect/src/device/checkFirmwareRevision.ts +++ b/packages/connect/src/device/checkFirmwareRevision.ts @@ -6,13 +6,21 @@ import { FirmwareRelease, VersionArray } from '../types'; import { calculateRevisionForDevice } from './calculateRevisionForDevice'; import { FirmwareRevisionCheckError, FirmwareRevisionCheckResult } from '../types/device'; -/* - * error names that signify unavailable internet connection, see https://github.com/node-fetch/node-fetch/blob/main/docs/ERROR-HANDLING.md - * Only works in Suite Desktop, where `cross-fetch` uses `node-fetch` (nodeJS environment) - * In Suite Web, the errors are unfortunately indistinguishable from other errors, because they are all lumped as CORS errors +const isNodeJSNetworkError = (e: Error) => ['FetchError', 'AbortError'].includes(e.name); +const isReactNativeNetworkError = (e: Error) => + e.name === 'TypeError' && e.message.includes('Network request failed'); + +/** + * Check if an error signifies a missing fetch response (meaning network connection loss or unavailable host). + * This can only by correctly identified in nodeJS or React native runtimes (i.e. Suite Desktop main process, or Suite Lite). + * In browser runtime (Suite Web), all fetch errors are lumped together as CORS errors, therefore indistinguishable. * (even a request that had no response is CORS error, since a non-existent response does not have CORS headers) */ -const NODE_FETCH_OFFLINE_ERROR_NAMES = ['FetchError', 'AbortError'] as const; +const isNetworkError = (e: unknown): boolean => { + if (!(e instanceof Error)) return false; + + return isNodeJSNetworkError(e) || isReactNativeNetworkError(e); +}; type GetOnlineReleaseMetadataParams = { firmwareVersion: VersionArray; @@ -94,12 +102,10 @@ export const checkFirmwareRevision = async ({ } return { success: true }; - } catch (e) { - if (NODE_FETCH_OFFLINE_ERROR_NAMES.includes(e.name)) { - return failFirmwareRevisionCheck('cannot-perform-check-offline'); - } - - return failFirmwareRevisionCheck('other-error'); + } catch (e: unknown) { + return isNetworkError(e) + ? failFirmwareRevisionCheck('cannot-perform-check-offline') + : failFirmwareRevisionCheck('other-error'); } } diff --git a/packages/suite/src/components/suite/SecurityCheck/useReportDeviceCompromised.ts b/packages/suite/src/components/suite/SecurityCheck/useReportDeviceCompromised.ts index f4f0a912ebf..574f3b2ee2d 100644 --- a/packages/suite/src/components/suite/SecurityCheck/useReportDeviceCompromised.ts +++ b/packages/suite/src/components/suite/SecurityCheck/useReportDeviceCompromised.ts @@ -5,7 +5,7 @@ import { FIRMWARE } from '@trezor/connect'; import { getFirmwareVersion } from '@trezor/device-utils'; import { isArrayMember } from '@trezor/utils'; -import { hashCheckErrorScenarios } from 'src/constants/suite/firmware'; +import { hashCheckErrorScenarios, revisionCheckErrorScenarios } from 'src/constants/suite/firmware'; import { useDevice, useSelector } from 'src/hooks/suite'; import { selectFirmwareRevisionCheckError } from 'src/reducers/suite/suiteReducer'; import { captureSentryMessage, withSentryScope } from 'src/utils/suite/sentry'; @@ -60,7 +60,7 @@ const useReportRevisionCheck = () => { const errorType = useSelector(selectFirmwareRevisionCheckError); useEffect(() => { - if (errorType !== null) { + if (errorType !== null && revisionCheckErrorScenarios[errorType].shouldReport) { reportCheckFail('Firmware revision', { ...commonData, errorType }); } }, [commonData, errorType]); diff --git a/packages/suite/src/constants/suite/firmware.ts b/packages/suite/src/constants/suite/firmware.ts index ee2904e3a83..366c0eec795 100644 --- a/packages/suite/src/constants/suite/firmware.ts +++ b/packages/suite/src/constants/suite/firmware.ts @@ -11,7 +11,7 @@ type BehaviorBaseType = { shouldReport: boolean; debugOnly?: boolean }; // will be ignored completely type SkippedBehavior = BehaviorBaseType & { type: 'skipped' }; // display a warning banner -type SoftWarningBehavior = BehaviorBaseType & { type: 'softWarning'; shouldReport: true }; +type SoftWarningBehavior = BehaviorBaseType & { type: 'softWarning' }; // display "Device Compromised" modal, after closing it display a warning banner, block receiving address type HardModalBehavior = BehaviorBaseType & { type: 'hardModal'; shouldReport: true }; @@ -24,7 +24,7 @@ type HashCheckErrorScenarios = Record export const revisionCheckErrorScenarios = { 'revision-mismatch': { type: 'hardModal', shouldReport: true }, 'firmware-version-unknown': { type: 'hardModal', shouldReport: true }, - 'cannot-perform-check-offline': { type: 'softWarning', shouldReport: true }, + 'cannot-perform-check-offline': { type: 'softWarning', shouldReport: false }, 'other-error': { type: 'softWarning', shouldReport: true }, } satisfies RevisionCheckErrorScenarios; diff --git a/suite-native/device/src/config/firmware.ts b/suite-native/device/src/config/firmware.ts index 4dc57f0bda3..ddf1098c883 100644 --- a/suite-native/device/src/config/firmware.ts +++ b/suite-native/device/src/config/firmware.ts @@ -6,7 +6,7 @@ import { FirmwareRevisionCheckError } from '@trezor/connect'; */ // display a warning banner -type SoftWarningBehavior = { type: 'softWarning'; shouldReport: true }; +type SoftWarningBehavior = { type: 'softWarning'; shouldReport: boolean }; // display "Device Compromised" modal, after closing it dispaly a warning banner, block receiving address type HardModalBehavior = { type: 'hardModal'; shouldReport: true }; @@ -16,6 +16,6 @@ type RevisionCheckErrorScenarios = Record { @@ -31,7 +32,10 @@ export const useReportDeviceCompromised = () => { const revisionCheckError = useSelector(selectFirmwareRevisionCheckError); useEffect(() => { - if (revisionCheckError !== null) { + if ( + revisionCheckError !== null && + revisionCheckErrorScenarios[revisionCheckError].shouldReport + ) { reportCheckFail('Firmware revision', { ...commonData, revisionCheckError }); } }, [commonData, revisionCheckError]);