diff --git a/.changeset/mighty-doors-visit.md b/.changeset/mighty-doors-visit.md new file mode 100644 index 000000000000..3f9d4c3bdfc9 --- /dev/null +++ b/.changeset/mighty-doors-visit.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Fix Stax onboarding early security checks: infinite loading state for locked device diff --git a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/index.tsx b/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/index.tsx index 2abc9d479b76..22d2296f13b9 100644 --- a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/index.tsx @@ -33,7 +33,9 @@ import TroubleshootingDrawer, { Props as TroubleshootingDrawerProps, } from "./TroubleshootingDrawer"; import LockedDeviceDrawer, { Props as LockedDeviceDrawerProps } from "./LockedDeviceDrawer"; -import { LockedDeviceError } from "@ledgerhq/errors"; +import { LockedDeviceError, UnexpectedBootloader } from "@ledgerhq/errors"; +import ErrorDrawer from "./EarlySecurityChecks/ErrorDrawer"; +import logger from "~/renderer/logger"; const POLLING_PERIOD_MS = 1000; const DESYNC_TIMEOUT_MS = 20000; @@ -67,7 +69,7 @@ const SyncOnboardingScreen: React.FC = ({ const deviceModelId = stringToDeviceModelId(strDeviceModelId, DeviceModelId.stax); const [mustRecoverIfBootloader, setMustRecoverIfBootloader] = useState(true); - const [isBootloader, setIsBootloader] = useState(null); + const [isBootloader, setIsBootloader] = useState(false); // Needed because `device` object can be null or changed if disconnected/reconnected const [lastSeenDevice, setLastSeenDevice] = useState(device ?? null); useEffect(() => { @@ -92,7 +94,7 @@ const SyncOnboardingScreen: React.FC = ({ const { onboardingState, allowedError, fatalError, lockedDevice } = useOnboardingStatePolling({ device: lastSeenDevice, pollingPeriodMs: POLLING_PERIOD_MS, - stopPolling: !isPollingOn || isBootloader === null || isBootloader, + stopPolling: !isPollingOn || isBootloader, }); const { state: toggleOnboardingEarlyCheckState } = useToggleOnboardingEarlyCheck({ @@ -100,18 +102,45 @@ const SyncOnboardingScreen: React.FC = ({ toggleType: toggleOnboardingEarlyCheckType, }); - const refreshIsBootloaderMode = useCallback(() => { - if (!device) return; - withDevice(device.deviceId)(transport => from(getDeviceInfo(transport))) - .toPromise() - .then((deviceInfo: DeviceInfo) => { - setIsBootloader(deviceInfo?.isBootloader); - }); - }, [device]); - useEffect(() => { + let dead = false; + function refreshIsBootloaderMode() { + if (!device) return; + withDevice(device.deviceId)(transport => from(getDeviceInfo(transport))) + .toPromise() + .then(({ isBootloader }: DeviceInfo) => { + if (dead) return; + setIsBootloader(isBootloader); + }) + .catch(error => { + if (dead) return; + if (error instanceof LockedDeviceError) { + // Here we just want to know if the device is in bootloader mode. + // It can't be locked in bootloader mode so we can just ignore the + // error, another LockedDeviceError error will be handled in the + // polling hook. + setIsBootloader(false); + return; + } + logger.error(error); + setDrawer( + ErrorDrawer, + { + onClickRetry: () => { + setDrawer(); + refreshIsBootloaderMode(); + }, + error, + }, + { preventBackdropClick: true, forceDisableFocusTrap: true }, + ); + }); + } refreshIsBootloaderMode(); - }, [device, refreshIsBootloaderMode]); + return () => { + dead = true; + }; + }, [device]); // Called when the ESC is complete const notifyOnboardingEarlyCheckEnded = useCallback(() => { @@ -186,7 +215,9 @@ const SyncOnboardingScreen: React.FC = ({ // A fatal error during polling triggers directly an error message useEffect(() => { - if (fatalError) { + if ((fatalError as unknown) instanceof UnexpectedBootloader) { + setIsBootloader(true); + } else if (fatalError) { setIsPollingOn(false); setTroubleshootingDrawerOpen(true); } @@ -307,8 +338,8 @@ const SyncOnboardingScreen: React.FC = ({ ); } - const onDeviceActionResult = useCallback((result: Result) => { - setIsBootloader(result.deviceInfo.isBootloader); + const onDeviceActionResult = useCallback(({ deviceInfo: { isBootloader } }: Result) => { + setIsBootloader(isBootloader); }, []); return (