diff --git a/.changeset/fifty-garlics-marry.md b/.changeset/fifty-garlics-marry.md new file mode 100644 index 000000000000..480e79a86c98 --- /dev/null +++ b/.changeset/fifty-garlics-marry.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Fix for locked device error diff --git a/.changeset/little-parrots-swim.md b/.changeset/little-parrots-swim.md new file mode 100644 index 000000000000..6a45eb495a59 --- /dev/null +++ b/.changeset/little-parrots-swim.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/live-common": patch +"@ledgerhq/errors": patch +--- + +Fix for locked device error diff --git a/.changeset/selfish-lobsters-enjoy.md b/.changeset/selfish-lobsters-enjoy.md new file mode 100644 index 000000000000..180d780ec64e --- /dev/null +++ b/.changeset/selfish-lobsters-enjoy.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Fix for locked device error diff --git a/apps/ledger-live-desktop/src/renderer/components/CustomImage/CustomImageDeviceAction.tsx b/apps/ledger-live-desktop/src/renderer/components/CustomImage/CustomImageDeviceAction.tsx index fd0ab0930925..1e9a45dffc08 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/CustomImageDeviceAction.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/CustomImageDeviceAction.tsx @@ -96,7 +96,9 @@ const CustomImageDeviceAction: React.FC = withRemountableWrapper(props => ) : isError ? ( {renderError({ + t, error, + device: device ?? undefined, ...(isRefusedOnStaxError ? { Icon: Icons.CircledAlertMedium, iconColor: "warning.c100" } : {}), diff --git a/apps/ledger-live-desktop/src/renderer/components/DeviceAction/index.tsx b/apps/ledger-live-desktop/src/renderer/components/DeviceAction/index.tsx index 52286421f542..b229538e9720 100644 --- a/apps/ledger-live-desktop/src/renderer/components/DeviceAction/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/DeviceAction/index.tsx @@ -34,6 +34,7 @@ import { renderSecureTransferDeviceConfirmation, renderAllowLanguageInstallation, renderInstallingLanguage, + renderLockedDeviceError, } from "./rendering"; type Props = { @@ -73,6 +74,7 @@ export const DeviceActionDefaultRendering = ({ appAndVersion, device, unresponsive, + isLocked, error, isLoading, allowManagerRequestedWording, @@ -241,6 +243,7 @@ export const DeviceActionDefaultRendering = ({ if (inWrongDeviceForAccount) { return renderInWrongAppForAccount({ + t, onRetry, accountName: inWrongDeviceForAccount.accountName, }); @@ -253,6 +256,7 @@ export const DeviceActionDefaultRendering = ({ error instanceof UpdateYourApp ) { return renderError({ + t, error, managerAppName: error.managerAppName, }); @@ -260,6 +264,7 @@ export const DeviceActionDefaultRendering = ({ if (error instanceof LatestFirmwareVersionRequired) { return renderError({ + t, error, requireFirmwareUpdate: true, }); @@ -272,6 +277,7 @@ export const DeviceActionDefaultRendering = ({ (error instanceof TransportStatusError && error.message.includes("0x6d06")) ) { return renderError({ + t, error: new DeviceNotOnboarded(), withOnboardingCTA: true, info: true, @@ -280,6 +286,7 @@ export const DeviceActionDefaultRendering = ({ if (error instanceof NoSuchAppOnProvider) { return renderError({ + t, error, withOpenManager: true, withExportLogs: true, @@ -287,12 +294,19 @@ export const DeviceActionDefaultRendering = ({ } return renderError({ + t, error, onRetry, withExportLogs: true, + device: device ?? undefined, }); } + // Renders an error as long as LLD is using the "event" implementation of device actions + if (isLocked) { + return renderLockedDeviceError({ t, device, onRetry }); + } + if ((!isLoading && !device) || unresponsive) { return renderConnectYourDevice({ modelId, diff --git a/apps/ledger-live-desktop/src/renderer/components/DeviceAction/rendering.tsx b/apps/ledger-live-desktop/src/renderer/components/DeviceAction/rendering.tsx index 1009498029a3..74887e70ecd5 100644 --- a/apps/ledger-live-desktop/src/renderer/components/DeviceAction/rendering.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/DeviceAction/rendering.tsx @@ -9,7 +9,7 @@ import { TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types"; import { ExchangeRate, Exchange } from "@ledgerhq/live-common/exchange/swap/types"; import { getProviderName } from "@ledgerhq/live-common/exchange/swap/utils/index"; -import { WrongDeviceForAccount, UpdateYourApp } from "@ledgerhq/errors"; +import { WrongDeviceForAccount, UpdateYourApp, LockedDeviceError } from "@ledgerhq/errors"; import { LatestFirmwareVersionRequired } from "@ledgerhq/live-common/errors"; import { DeviceModelId, getDeviceModel } from "@ledgerhq/devices"; import { Device } from "@ledgerhq/live-common/hw/actions/types"; @@ -47,7 +47,8 @@ import { SWAP_VERSION } from "~/renderer/screens/exchange/Swap2/utils/index"; import { context } from "~/renderer/drawers/Provider"; import { track } from "~/renderer/analytics/segment"; import { DrawerFooter } from "~/renderer/screens/exchange/Swap2/Form/DrawerFooter"; -import { Flex, Icons, Text as TextV3, Log, ProgressLoader } from "@ledgerhq/react-ui"; +import { Flex, Icons, Text as TextV3, Log, ProgressLoader, BoxedIcon } from "@ledgerhq/react-ui"; +import { LockAltMedium } from "@ledgerhq/react-ui/assets/icons"; import { withV3StyleProvider } from "~/renderer/styles/StyleProviderV3"; import FramedImage from "../CustomImage/FramedImage"; @@ -483,8 +484,49 @@ export const renderWarningOutdated = ({ ); +// Quick fix: the error LockedDeviceError should be catched +// inside all the device actions and mapped to an event of type "lockedDevice". +// With this fix, we can catch all the device action error that were not catched upstream. +// If LockedDeviceError is thrown from outside a device action and renderError was not called +// it is still handled by GenericErrorView. +export const renderLockedDeviceError = ({ + t, + device, + onRetry, +}: { + t: TFunction; + device?: Device; + onRetry?: () => void; +}) => { + const productName = device ? getDeviceModel(device.modelId).productName : null; + + return ( + + + + + {t("errors.LockedDeviceError.title")} + + {productName + ? t("errors.LockedDeviceError.descriptionWithProductName", { + productName, + }) + : t("errors.LockedDeviceError.description")} + + + {onRetry ? ( + + ) : null} + + + ); +}; + export const renderError = ({ error, + t, withOpenManager, onRetry, withExportLogs, @@ -495,8 +537,10 @@ export const renderError = ({ managerAppName, requireFirmwareUpdate, withOnboardingCTA, + device, }: { error: Error; + t: TFunction; withOpenManager?: boolean; onRetry?: () => void; withExportLogs?: boolean; @@ -507,67 +551,79 @@ export const renderError = ({ managerAppName?: string; requireFirmwareUpdate?: boolean; withOnboardingCTA?: boolean; -}) => ( - - - - - - - - - - - {list ? ( + device?: Device; +}) => { + // Redirects from renderError and not from DeviceActionDefaultRendering because renderError + // can be used directly by other component + if (error instanceof LockedDeviceError) { + return renderLockedDeviceError({ t, onRetry, device }); + } + + return ( + + + + + + + -
    - -
+
- ) : null} - - {managerAppName || requireFirmwareUpdate ? ( - - ) : ( - <> - {supportLink ? ( - } url={supportLink} /> - ) : null} - {withExportLogs ? ( - } - small={false} - primary={false} - outlineGrey - mx={1} - /> - ) : null} - {withOpenManager ? ( - - ) : onRetry ? ( - - ) : null} - {withOnboardingCTA ? : null} - - )} - -
-); + {list ? ( + +
    + +
+
+ ) : null} + + {managerAppName || requireFirmwareUpdate ? ( + + ) : ( + <> + {supportLink ? ( + + ) : null} + {withExportLogs ? ( + + ) : null} + {withOpenManager ? ( + + ) : onRetry ? ( + + ) : null} + {withOnboardingCTA ? : null} + + )} + +
+ ); +}; export const renderInWrongAppForAccount = ({ + t, onRetry, accountName, }: { + t: TFunction; onRetry: () => void; accountName: string; }) => renderError({ + t, error: new WrongDeviceForAccount(null, { accountName }), withExportLogs: true, onRetry, diff --git a/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.js b/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.js deleted file mode 100644 index 628124d291c8..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { renderError } from "~/renderer/components/DeviceAction/rendering"; - -export type ErrorDisplayProps = { - error: Error, - onRetry?: () => void, - withExportLogs?: boolean, - list?: boolean, - supportLink?: string, - warning?: boolean, -}; - -const ErrorDisplay = ({ - error, - onRetry, - withExportLogs, - list, - supportLink, - warning, -}: ErrorDisplayProps) => - renderError({ error, onRetry, withExportLogs, list, supportLink, warning }); - -export default ErrorDisplay; diff --git a/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx b/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx new file mode 100644 index 000000000000..7e4e63cd2aec --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/components/ErrorDisplay.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from "react-i18next"; +import { renderError } from "~/renderer/components/DeviceAction/rendering"; + +export type ErrorDisplayProps = { + error: Error; + onRetry?: () => void; + withExportLogs?: boolean; + list?: boolean; + supportLink?: string; + warning?: boolean; +}; + +const ErrorDisplay = ({ + error, + onRetry, + withExportLogs, + list, + supportLink, + warning, +}: ErrorDisplayProps) => { + const { t } = useTranslation(); + + return renderError({ t, error, onRetry, withExportLogs, list, supportLink, warning }); +}; + +export default ErrorDisplay; diff --git a/apps/ledger-live-desktop/src/renderer/modals/SellDeviceConfirm/index.jsx b/apps/ledger-live-desktop/src/renderer/modals/SellDeviceConfirm/index.jsx index 4a8e5d874cdb..4b2d6c929b82 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/SellDeviceConfirm/index.jsx +++ b/apps/ledger-live-desktop/src/renderer/modals/SellDeviceConfirm/index.jsx @@ -61,6 +61,8 @@ const Result = ({ const Root = ({ data, onClose }: Props) => { const { account, parentAccount, getCoinifyContext, onResult, onCancel } = data; + const { t } = useTranslation(); + const tokenCurrency = account && account.type === "TokenAccount" && account.token; // state @@ -138,6 +140,7 @@ const Root = ({ data, onClose }: Props) => { return ( {renderError({ + t, error, })} diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 2ddc2c28cf8c..80e071160c3c 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -5309,6 +5309,11 @@ "ImageLoadRefusedOnDevice": { "title": "Image load refused on device (TODO: final wording)", "description": "If this image wasn't the right fit, you can try again with another image." + }, + "LockedDeviceError": { + "title": "Your device is locked", + "description": "Unlock your device and try again.", + "descriptionWithProductName": "Unlock your {{ productName }} and try again." } }, "cryptoOrg": { diff --git a/apps/ledger-live-mobile/src/components/CustomImageDeviceAction.tsx b/apps/ledger-live-mobile/src/components/CustomImageDeviceAction.tsx index 0d7633753e9e..a23a8a6e1a1b 100644 --- a/apps/ledger-live-mobile/src/components/CustomImageDeviceAction.tsx +++ b/apps/ledger-live-mobile/src/components/CustomImageDeviceAction.tsx @@ -82,6 +82,7 @@ const CustomImageDeviceAction: React.FC void }> = ({ {renderError({ t, error, + device, ...(isRefusedOnStaxError ? { Icon: Icons.CircledAlertMedium, iconColor: "warning.c100" } : {}), diff --git a/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx b/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx index fc6734b8b2a9..d45c8dd9b0d5 100644 --- a/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx +++ b/apps/ledger-live-mobile/src/components/DeviceAction/index.tsx @@ -446,6 +446,7 @@ export function DeviceActionDefaultRendering({ onRetry, colors, theme, + device: device ?? undefined, }); } diff --git a/apps/ledger-live-mobile/src/components/DeviceAction/rendering.tsx b/apps/ledger-live-mobile/src/components/DeviceAction/rendering.tsx index c2cd5905c4c6..32b923ffe805 100644 --- a/apps/ledger-live-mobile/src/components/DeviceAction/rendering.tsx +++ b/apps/ledger-live-mobile/src/components/DeviceAction/rendering.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Platform, ScrollView } from "react-native"; import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components/native"; -import { WrongDeviceForAccount } from "@ledgerhq/errors"; +import { LockedDeviceError, WrongDeviceForAccount } from "@ledgerhq/errors"; import { TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { Transaction } from "@ledgerhq/live-common/generated/types"; import { getDeviceModel } from "@ledgerhq/devices"; @@ -19,6 +19,7 @@ import { Log, BoxedIcon, } from "@ledgerhq/native-ui"; +import { LockAltMedium } from "@ledgerhq/native-ui/assets/icons"; import BigNumber from "bignumber.js"; import { ExchangeRate, @@ -535,6 +536,67 @@ export function renderInWrongAppForAccount({ }); } +// Quick fix: the error LockedDeviceError should be catched +// inside all the device actions and mapped to an event of type "lockedDevice". +// With this fix, we can catch all the device action error that were not catched upstream. +// If LockedDeviceError is thrown from outside a device action and renderError was not called +// it is still handled by GenericErrorView. +export function renderLockedDeviceError({ + t, + onRetry, + device, +}: RawProps & { + onRetry?: (() => void) | null; + device?: Device; +}) { + const productName = device + ? getDeviceModel(device.modelId).productName + : null; + + return ( + + + + + + + + {t("errors.LockedDeviceError.title")} + + + {productName + ? t("errors.LockedDeviceError.descriptionWithProductName", { + productName, + }) + : t("errors.LockedDeviceError.description")} + + {onRetry ? ( + + + + ) : null} + + + ); +} + export function renderError({ t, error, @@ -543,6 +605,7 @@ export function renderError({ navigation, Icon, iconColor, + device, }: RawProps & { navigation?: StackNavigationProp; error: Error; @@ -550,6 +613,7 @@ export function renderError({ managerAppName?: string; Icon?: React.ComponentProps["Icon"]; iconColor?: string; + device?: Device; }) { const onPress = () => { if (managerAppName && navigation) { @@ -564,6 +628,13 @@ export function renderError({ onRetry(); } }; + + // Redirects from renderError and not from DeviceActionDefaultRendering because renderError + // can be used directly by other component + if (error instanceof LockedDeviceError) { + return renderLockedDeviceError({ t, onRetry, device }); + } + return ( { - if ( - e && - e instanceof TransportStatusError && - // @ts-expect-error typescript not checking agains the instanceof - e.statusCode === StatusCodes.LOCKED_DEVICE - ) { + if (e instanceof LockedDeviceError) { return of({ type: "lockedDevice", } as ConnectManagerEvent); diff --git a/libs/ledger-live-common/src/hw/getDeviceRunningMode.test.ts b/libs/ledger-live-common/src/hw/getDeviceRunningMode.test.ts index 704f239121be..d00995916e7d 100644 --- a/libs/ledger-live-common/src/hw/getDeviceRunningMode.test.ts +++ b/libs/ledger-live-common/src/hw/getDeviceRunningMode.test.ts @@ -1,10 +1,11 @@ import { from, Observable, of, timer } from "rxjs"; import { delay } from "rxjs/operators"; -import Transport, { - StatusCodes, - TransportStatusError, -} from "@ledgerhq/hw-transport"; -import { CantOpenDevice, DisconnectedDevice } from "@ledgerhq/errors"; +import Transport from "@ledgerhq/hw-transport"; +import { + CantOpenDevice, + DisconnectedDevice, + LockedDeviceError, +} from "@ledgerhq/errors"; import { DeviceInfo } from "@ledgerhq/types-live"; import getDeviceInfo from "./getDeviceInfo"; import { getDeviceRunningMode } from "./getDeviceRunningMode"; @@ -180,11 +181,9 @@ describe("getDeviceRunningMode", () => { }); }); - describe("And the device responds with a LOCKED_DEVICE error", () => { + describe("And the device responds with a locked device error", () => { it("pushes an event lockedDevice", (done) => { - mockedGetDeviceInfo.mockRejectedValue( - new TransportStatusError(StatusCodes.LOCKED_DEVICE) - ); + mockedGetDeviceInfo.mockRejectedValue(new LockedDeviceError()); getDeviceRunningMode({ deviceId: A_DEVICE_ID, diff --git a/libs/ledger-live-common/src/hw/getDeviceRunningMode.ts b/libs/ledger-live-common/src/hw/getDeviceRunningMode.ts index 8ed8271888eb..9e636b6d7b4a 100644 --- a/libs/ledger-live-common/src/hw/getDeviceRunningMode.ts +++ b/libs/ledger-live-common/src/hw/getDeviceRunningMode.ts @@ -1,8 +1,4 @@ -import { - TransportStatusError, - StatusCodes, - CantOpenDevice, -} from "@ledgerhq/errors"; +import { CantOpenDevice, LockedDeviceError } from "@ledgerhq/errors"; import { DeviceInfo } from "@ledgerhq/types-live"; import { from, Observable, TimeoutError } from "rxjs"; import { retryWhen, timeout } from "rxjs/operators"; @@ -104,11 +100,5 @@ export const getDeviceRunningMode = ({ }); const isLockedDeviceError = (e: Error) => { - return ( - (e && - e instanceof TransportStatusError && - // @ts-expect-error typescript not checking agains the instanceof - e.statusCode === StatusCodes.LOCKED_DEVICE) || - e instanceof TimeoutError - ); + return e && (e instanceof TimeoutError || e instanceof LockedDeviceError); }; diff --git a/libs/ledgerjs/packages/errors/src/index.ts b/libs/ledgerjs/packages/errors/src/index.ts index 688f1ad53ab1..d13b2f2e1430 100644 --- a/libs/ledgerjs/packages/errors/src/index.ts +++ b/libs/ledgerjs/packages/errors/src/index.ts @@ -52,6 +52,7 @@ export const DeviceSocketFail = createCustomErrorClass("DeviceSocketFail"); export const DeviceSocketNoBulkStatus = createCustomErrorClass( "DeviceSocketNoBulkStatus" ); +export const LockedDeviceError = createCustomErrorClass("LockedDeviceError"); export const DisconnectedDevice = createCustomErrorClass("DisconnectedDevice"); export const DisconnectedDeviceDuringOperation = createCustomErrorClass( "DisconnectedDeviceDuringOperation" @@ -319,13 +320,20 @@ export function getAltStatusMessage(code: number): string | undefined | null { * the error.statusCode is one of the `StatusCodes` exported by this library. */ export function TransportStatusError(statusCode: number): void { - this.name = "TransportStatusError"; const statusText = Object.keys(StatusCodes).find((k) => StatusCodes[k] === statusCode) || "UNKNOWN_ERROR"; const smsg = getAltStatusMessage(statusCode) || statusText; const statusCodeStr = statusCode.toString(16); - this.message = `Ledger device: ${smsg} (0x${statusCodeStr})`; + const message = `Ledger device: ${smsg} (0x${statusCodeStr})`; + + // Maps to a LockedDeviceError + if (statusCode === StatusCodes.LOCKED_DEVICE) { + throw new LockedDeviceError(message); + } + + this.name = "TransportStatusError"; + this.message = message; this.stack = new Error().stack; this.statusCode = statusCode; this.statusText = statusText;