From c259edd23bbb2dee5ea64ea5c3f8e861bd5361e5 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Thu, 28 Jun 2018 15:32:09 +0200 Subject: [PATCH] plugged update actions to modals --- src/commands/installFinalFirmware.js | 8 +- src/commands/installMcu.js | 8 +- src/commands/installOsuFirmware.js | 2 +- .../ManagerPage/FirmwareFinalUpdate.js | 67 -------- src/components/ManagerPage/FirmwareUpdate.js | 64 +++++--- src/components/ManagerPage/FlashMcu.js | 50 ------ .../ManagerPage/UpdateFirmwareButton.js | 6 +- src/components/ManagerPage/index.js | 4 - src/components/modals/ReleaseNotes.js | 2 +- .../modals/UpdateFirmware/Disclaimer.js | 12 +- src/components/modals/UpdateFirmware/index.js | 97 ++++++++---- .../steps/01-step-install-full-firmware.js | 80 +++++++--- .../steps/01-step-osu-installer.js | 57 ++++--- .../UpdateFirmware/steps/02-step-flash-mcu.js | 149 ++++++++++++++---- .../devices/getLatestFirmwareForDevice.js | 21 ++- src/helpers/firmware/getMcus.js | 19 +++ .../{devices => firmware}/getNextMCU.js | 0 src/helpers/firmware/installFinalFirmware.js | 6 +- src/helpers/firmware/installMcu.js | 12 +- src/helpers/firmware/installOsuFirmware.js | 24 ++- src/helpers/urls.js | 1 + static/images/logos/bootloaderMode.png | Bin 0 -> 14499 bytes static/images/logos/unplugDevice.png | Bin 10289 -> 11201 bytes 23 files changed, 407 insertions(+), 282 deletions(-) delete mode 100644 src/components/ManagerPage/FirmwareFinalUpdate.js delete mode 100644 src/components/ManagerPage/FlashMcu.js create mode 100644 src/helpers/firmware/getMcus.js rename src/helpers/{devices => firmware}/getNextMCU.js (100%) create mode 100755 static/images/logos/bootloaderMode.png diff --git a/src/commands/installFinalFirmware.js b/src/commands/installFinalFirmware.js index 54fda5c5..42671301 100644 --- a/src/commands/installFinalFirmware.js +++ b/src/commands/installFinalFirmware.js @@ -3,23 +3,19 @@ import { createCommand, Command } from 'helpers/ipc' import { fromPromise } from 'rxjs/observable/fromPromise' import { withDevice } from 'helpers/deviceAccess' -import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' import installFinalFirmware from 'helpers/firmware/installFinalFirmware' type Input = { devicePath: string, - deviceInfo: DeviceInfo, } type Result = { success: boolean, } -const cmd: Command = createCommand( - 'installFinalFirmware', - ({ devicePath, deviceInfo }) => - fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, deviceInfo))), +const cmd: Command = createCommand('installFinalFirmware', ({ devicePath }) => + fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport))), ) export default cmd diff --git a/src/commands/installMcu.js b/src/commands/installMcu.js index 7e49ea16..7d85e40b 100644 --- a/src/commands/installMcu.js +++ b/src/commands/installMcu.js @@ -8,16 +8,12 @@ import installMcu from 'helpers/firmware/installMcu' type Input = { devicePath: string, - targetId: string | number, - version: string, } type Result = * -const cmd: Command = createCommand( - 'installMcu', - ({ devicePath, targetId, version }) => - fromPromise(withDevice(devicePath)(transport => installMcu(transport, { targetId, version }))), +const cmd: Command = createCommand('installMcu', ({ devicePath }) => + fromPromise(withDevice(devicePath)(transport => installMcu(transport))), ) export default cmd diff --git a/src/commands/installOsuFirmware.js b/src/commands/installOsuFirmware.js index 02767210..fd990d54 100644 --- a/src/commands/installOsuFirmware.js +++ b/src/commands/installOsuFirmware.js @@ -11,7 +11,7 @@ import type { LedgerScriptParams } from 'helpers/common' type Input = { devicePath: string, targetId: string | number, - firmware: LedgerScriptParams, + firmware: LedgerScriptParams & { shouldUpdateMcu: boolean }, } type Result = * diff --git a/src/components/ManagerPage/FirmwareFinalUpdate.js b/src/components/ManagerPage/FirmwareFinalUpdate.js deleted file mode 100644 index 19e9b76f..00000000 --- a/src/components/ManagerPage/FirmwareFinalUpdate.js +++ /dev/null @@ -1,67 +0,0 @@ -// @flow - -import React, { PureComponent } from 'react' -import { translate } from 'react-i18next' -import logger from 'logger' -import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' - -import type { Device, T } from 'types/common' - -import installFinalFirmware from 'commands/installFinalFirmware' - -import Box, { Card } from 'components/base/Box' -import Button from 'components/base/Button' - -type Props = { - t: T, - device: Device, - deviceInfo: DeviceInfo, -} - -type State = {} - -class FirmwareFinalUpdate extends PureComponent { - componentDidMount() {} - - componentWillUnmount() { - this._unmounting = true - } - - _unmounting = false - - installFinalFirmware = async () => { - try { - const { device, deviceInfo } = this.props - const { success } = await installFinalFirmware - .send({ - devicePath: device.path, - deviceInfo, - }) - .toPromise() - if (success) { - this.setState() - } - } catch (err) { - logger.log(err) - } - } - - render() { - const { t, ...props } = this.props - - return ( - - - {t('app:manager.firmware.update')} - - - - - - - - ) - } -} - -export default translate()(FirmwareFinalUpdate) diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index 60afcc15..103e1c89 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -1,7 +1,7 @@ // @flow /* eslint-disable react/jsx-no-literals */ // FIXME -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import { translate } from 'react-i18next' import isEqual from 'lodash/isEqual' import isEmpty from 'lodash/isEmpty' @@ -14,6 +14,8 @@ import type { LedgerScriptParams } from 'helpers/common' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import installOsuFirmware from 'commands/installOsuFirmware' +import installFinalFirmware from 'commands/installFinalFirmware' +import installMcu from 'commands/installMcu' import DisclaimerModal from 'components/modals/UpdateFirmware/Disclaimer' import UpdateModal from 'components/modals/UpdateFirmware' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' @@ -40,7 +42,7 @@ type Props = { } type State = { - latestFirmware: ?LedgerScriptParams, + latestFirmware: ?LedgerScriptParams & ?{ shouldUpdateMcu: boolean }, modal: ModalStatus, } @@ -78,26 +80,42 @@ class FirmwareUpdate extends PureComponent { } } - installFirmware = async () => { + installOsuFirmware = async () => { try { const { latestFirmware } = this.state - const { deviceInfo } = this.props - invariant(latestFirmware, 'did not find a new firmware or firmware is not set') const { + deviceInfo, device: { path: devicePath }, } = this.props + invariant(latestFirmware, 'did not find a new firmware or firmware is not set') + this.setState({ modal: 'install' }) const { success } = await installOsuFirmware .send({ devicePath, firmware: latestFirmware, targetId: deviceInfo.targetId }) .toPromise() - if (success) { - this.fetchLatestFirmware() - } + return success } catch (err) { logger.log(err) + throw err } } + installFinalFirmware = async () => { + try { + const { device } = this.props + const { success } = await installFinalFirmware.send({ devicePath: device.path }).toPromise() + return success + } catch (err) { + logger.log(err) + throw err + } + } + + flashMCU = async () => { + const { device } = this.props + await installMcu.send({ devicePath: device.path }).toPromise() + } + handleCloseModal = () => this.setState({ modal: 'closed' }) handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' }) @@ -130,19 +148,27 @@ class FirmwareUpdate extends PureComponent { })} - + {modal !== 'closed' ? : null} - - + {latestFirmware && ( + + + + + )} ) } diff --git a/src/components/ManagerPage/FlashMcu.js b/src/components/ManagerPage/FlashMcu.js deleted file mode 100644 index 76a9b77d..00000000 --- a/src/components/ManagerPage/FlashMcu.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow -import React, { PureComponent } from 'react' - -import type { Device } from 'types/common' -import installMcu from 'commands/installMcu' - -import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' - -type Props = { - device: Device, - deviceInfo: DeviceInfo, -} - -type State = { - flashing: boolean, -} - -class FlashMcu extends PureComponent { - state = { - flashing: false, - } - - flashMCU = async () => { - const { device, deviceInfo } = this.props - const { flashing } = this.state - - if (!flashing) { - this.setState({ flashing: true }) - await installMcu - .send({ - devicePath: device.path, - targetId: deviceInfo.targetId, - version: deviceInfo.seVersion, - }) - .toPromise() - this.setState({ flashing: false }) - } - } - - render() { - return ( -
-

{'Flashing MCU'}

- -
- ) - } -} - -export default FlashMcu diff --git a/src/components/ManagerPage/UpdateFirmwareButton.js b/src/components/ManagerPage/UpdateFirmwareButton.js index cdfead05..e3a58aac 100644 --- a/src/components/ManagerPage/UpdateFirmwareButton.js +++ b/src/components/ManagerPage/UpdateFirmwareButton.js @@ -18,16 +18,16 @@ type FirmwareInfos = { type Props = { t: T, firmware: ?FirmwareInfos, - installFirmware: () => void, + onClick: () => void, } -const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) => +const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) => firmware ? ( {t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })} - diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index e0b3f76e..9eba78a4 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -40,10 +40,6 @@ class ManagerPage extends PureComponent { invariant(device, 'Inexistant device considered genuine') invariant(deviceInfo, 'Inexistant device infos for genuine device') - // TODO - // renderFinalUpdate - // renderMcuUpdate - return } } diff --git a/src/components/modals/ReleaseNotes.js b/src/components/modals/ReleaseNotes.js index 2d4950e3..d80999d8 100644 --- a/src/components/modals/ReleaseNotes.js +++ b/src/components/modals/ReleaseNotes.js @@ -25,7 +25,7 @@ type State = { markdown: ?string, } -const Notes = styled(Box).attrs({ +export const Notes = styled(Box).attrs({ ff: 'Open Sans', fontSize: 4, color: 'smoke', diff --git a/src/components/modals/UpdateFirmware/Disclaimer.js b/src/components/modals/UpdateFirmware/Disclaimer.js index 379c8928..bb462158 100644 --- a/src/components/modals/UpdateFirmware/Disclaimer.js +++ b/src/components/modals/UpdateFirmware/Disclaimer.js @@ -3,6 +3,7 @@ import React, { PureComponent, Fragment } from 'react' import { translate, Trans } from 'react-i18next' +import ReactMarkdown from 'react-markdown' import type { T } from 'types/common' @@ -11,11 +12,12 @@ import Text from 'components/base/Text' import Button from 'components/base/Button' import GrowScroll from 'components/base/GrowScroll' import GradientBox from 'components/GradientBox' - -import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' +import { Notes } from 'components/modals/ReleaseNotes' import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' +import { getCleanVersion } from 'components/ManagerPage/FirmwareUpdate' + type FirmwareInfos = { name: string, notes: string, @@ -58,9 +60,9 @@ class DisclaimerModal extends PureComponent { - - {firmware.notes} - + + {firmware.notes} + diff --git a/src/components/modals/UpdateFirmware/index.js b/src/components/modals/UpdateFirmware/index.js index abb71c8b..a86b8bef 100644 --- a/src/components/modals/UpdateFirmware/index.js +++ b/src/components/modals/UpdateFirmware/index.js @@ -14,18 +14,20 @@ import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' import type { LedgerScriptParams } from 'helpers/common' import StepOSUInstaller from './steps/01-step-osu-installer' +import StepFullFirmwareInstall from './steps/01-step-install-full-firmware' import StepFlashMcu from './steps/02-step-flash-mcu' import StepConfirmation, { StepConfirmFooter } from './steps/03-step-confirmation' export type StepProps = DefaultStepProps & { firmware: ?LedgerScriptParams, onCloseModal: () => void, + installOsuFirmware: () => void, + installFinalFirmware: () => void, + flashMcu: () => void, } type StepId = 'idCheck' | 'updateMCU' | 'finish' -// FIXME: Debugging for now to move between steps -// Remove when plugged to firmware update function DebugFooter({ transitionTo, where, @@ -36,45 +38,74 @@ function DebugFooter({ return } -const createSteps = ({ t }: { t: T }) => [ - { - id: 'idCheck', - label: t('app:manager.modal.steps.idCheck'), - component: StepOSUInstaller, - footer: ({ firmware, ...props }: StepProps) => ( - - ), - onBack: null, - hideFooter: false, - }, - { - id: 'updateMCU', - label: t('app:manager.modal.steps.updateMCU'), - component: StepFlashMcu, - footer: ({ firmware, ...props }: StepProps) => ( - - - - - ), - onBack: null, - hideFooter: false, - }, - { +const createSteps = ({ + t, + firmware, +}: { + t: T, + firmware: LedgerScriptParams & { shouldUpdateMcu: boolean }, +}): Array<*> => { + const finalStep = { id: 'finish', label: t('app:addAccounts.breadcrumb.finish'), component: StepConfirmation, footer: StepConfirmFooter, onBack: null, hideFooter: false, - }, -] + } + + if (firmware.shouldUpdateMcu) { + return [ + { + id: 'idCheck', + label: t('app:manager.modal.steps.idCheck'), + component: StepOSUInstaller, + footer: ({ firmware, ...props }: StepProps) => ( + + ), + onBack: null, + hideFooter: false, + }, + { + id: 'updateMCU', + label: t('app:manager.modal.steps.updateMCU'), + component: StepFlashMcu, + footer: ({ firmware, ...props }: StepProps) => ( + + + + + ), + onBack: null, + hideFooter: false, + }, + finalStep, + ] + } + + return [ + { + id: 'idCheck', + label: t('app:manager.modal.steps.idCheck'), + component: StepFullFirmwareInstall, + footer: ({ firmware, ...props }: StepProps) => ( + + ), + onBack: null, + hideFooter: false, + }, + finalStep, + ] +} type Props = { t: T, status: ModalStatus, onClose: () => void, - firmware: ?LedgerScriptParams, + firmware: LedgerScriptParams & { shouldUpdateMcu: boolean }, + installOsuFirmware: () => void, + installFinalFirmware: () => void, + flashMcu: () => void, } type State = { @@ -86,17 +117,18 @@ class UpdateModal extends PureComponent { stepId: 'idCheck', } - STEPS = createSteps({ t: this.props.t }) + STEPS = createSteps({ t: this.props.t, firmware: this.props.firmware }) handleStepChange = (step: Step) => this.setState({ stepId: step.id }) render(): React$Node { - const { status, t, firmware, onClose } = this.props + const { status, t, firmware, onClose, ...props } = this.props const { stepId } = this.state const additionalProps = { firmware, onCloseModal: onClose, + ...props, } return ( @@ -104,6 +136,7 @@ class UpdateModal extends PureComponent { onClose={onClose} isOpened={status === 'install'} refocusWhenChange={stepId} + preventBackdropClick={false} render={() => ( - {t('app:manager.modal.confirmIdentifier')} - - {t('app:manager.modal.confirmIdentifierText')} - - - - {t('app:manager.modal.identifier')} +class StepFullFirmwareInstall extends PureComponent { + componentDidMount() { + this.install() + } + + install = async () => { + const { installOsuFirmware, installFinalFirmware, transitionTo } = this.props + const success = await installOsuFirmware() + if (success) { + const finalSuccess = await installFinalFirmware() + if (finalSuccess) { + transitionTo('finish') + } + } + } + + renderBody = () => { + const { installing } = this.state + const { firmware, t } = this.props + + if (installing) { + return ( + + + + ) + } + + return ( + + + + {t('app:manager.modal.identifier')} + +
+ {firmware && firmware.hash} +
+
+ + + +
+ ) + } + + render() { + const { t } = this.props + return ( + + {t('app:manager.modal.confirmIdentifier')} + + {t('app:manager.modal.confirmIdentifierText')} -
- {firmware && firmware.hash} -
-
- - - - - ) + {this.renderBody()} + + ) + } } export default StepFullFirmwareInstall diff --git a/src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js b/src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js index 5aa12aa2..e42cb954 100644 --- a/src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js +++ b/src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js @@ -1,11 +1,10 @@ // @flow -import React from 'react' +import React, { PureComponent } from 'react' import styled from 'styled-components' import Box from 'components/base/Box' import Text from 'components/base/Text' -import Button from 'components/base/Button' import DeviceConfirm from 'components/DeviceConfirm' import type { StepProps } from '../' @@ -46,27 +45,41 @@ const Ellipsis = styled.span` width: 100%; ` -// TODO: Change to class component and add osu firmware install -function StepOSUInstaller({ t, firmware }: StepProps) { - return ( - - {t('app:manager.modal.confirmIdentifier')} - - {t('app:manager.modal.confirmIdentifierText')} - - - - {t('app:manager.modal.identifier')} +class StepOSUInstaller extends PureComponent { + componentDidMount() { + this.install() + } + + install = async () => { + const { installOsuFirmware, transitionTo } = this.props + const success = await installOsuFirmware() + if (success) { + transitionTo('updateMCU') + } + } + + render() { + const { t, firmware } = this.props + return ( + + {t('app:manager.modal.confirmIdentifier')} + + {t('app:manager.modal.confirmIdentifierText')} -
- {firmware && firmware.hash} -
-
- - - -
- ) + + + {t('app:manager.modal.identifier')} + +
+ {firmware && firmware.hash} +
+
+ + + + + ) + } } export default StepOSUInstaller diff --git a/src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js b/src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js index 3991634c..b4781936 100644 --- a/src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js +++ b/src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js @@ -1,12 +1,21 @@ // @flow -import React from 'react' +import React, { PureComponent, Fragment } from 'react' import styled from 'styled-components' +import { connect } from 'react-redux' +import { timeout } from 'rxjs/operators/timeout' +import { DEVICE_INFOS_TIMEOUT } from 'config/constants' import { i } from 'helpers/staticPath' +import { getCurrentDevice } from 'reducers/devices' +import { createCancelablePolling } from 'helpers/promise' +import getDeviceInfo from 'commands/getDeviceInfo' import Box from 'components/base/Box' import Text from 'components/base/Text' +import Progress from 'components/base/Progress' + +import type { Device } from 'types/common' import type { StepProps } from '../' @@ -35,36 +44,112 @@ const Separator = styled(Box).attrs({ background-color: currentColor; ` -// TODO: Change to class component and add flash mcu and final -function StepFlashMcu({ t }: StepProps) { - return ( - - {t('app:manager.modal.mcuTitle')} - - - {'1.'} - {t('app:manager.modal.mcuFirst')} - - {t('app:manager.modal.mcuFirst')} - - - - - {'2.'} - {t('app:manager.modal.mcuSecond')} - - {t('app:manager.modal.mcuFirst')} - - - ) +const mapStateToProps = state => ({ + device: getCurrentDevice(state), +}) + +type Props = StepProps & { device?: Device } + +type State = { + installing: boolean, +} + +class StepFlashMcu extends PureComponent { + state = { + installing: false, + } + + componentDidMount() { + this.install() + } + + componentWillUnmount() { + this._unsubConnect() + } + + waitForDeviceInBootloader = () => { + const { unsubscribe, promise } = createCancelablePolling(async () => { + const { device } = this.props + if (!device) { + throw new Error('No device') + } + const deviceInfo = await getDeviceInfo + .send({ devicePath: device.path }) + .pipe(timeout(DEVICE_INFOS_TIMEOUT)) + .toPromise() + if (!deviceInfo.isBootloader) { + throw new Error('Device is not in bootloader') + } + return { device, deviceInfo } + }) + this._unsubConnect = unsubscribe + return promise + } + + install = async () => { + await this.waitForDeviceInBootloader() + const { flashMcu, installFinalFirmware, transitionTo } = this.props + this.setState({ installing: true }) + await flashMcu() + const finalSuccess = await installFinalFirmware() + + if (finalSuccess) { + transitionTo('finish') + } + } + + renderBody = () => { + const { installing } = this.state + const { t } = this.props + + if (installing) { + return ( + + + + ) + } + + return ( + + + + {'1.'} + {t('app:manager.modal.mcuFirst')} + + {t('app:manager.modal.mcuFirst')} + + + + + {'2.'} + {t('app:manager.modal.mcuSecond')} + + {t('app:manager.modal.mcuFirst')} + + + ) + } + + _unsubConnect: * + + render() { + const { t } = this.props + return ( + + {t('app:manager.modal.mcuTitle')} + {this.renderBody()} + + ) + } } -export default StepFlashMcu +export default connect(mapStateToProps)(StepFlashMcu) diff --git a/src/helpers/devices/getLatestFirmwareForDevice.js b/src/helpers/devices/getLatestFirmwareForDevice.js index 6a0a2624..3406b8af 100644 --- a/src/helpers/devices/getLatestFirmwareForDevice.js +++ b/src/helpers/devices/getLatestFirmwareForDevice.js @@ -3,6 +3,9 @@ import network from 'api/network' import { GET_LATEST_FIRMWARE } from 'helpers/urls' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' +import getFinalFirmwareById from 'helpers/firmware/getFinalFirmwareById' +import getMcus from 'helpers/firmware/getMcus' + import getCurrentFirmware from './getCurrentFirmware' import getDeviceVersion from './getDeviceVersion' @@ -34,7 +37,23 @@ export default async (deviceInfo: DeviceInfo) => { } const { se_firmware_osu_version } = data - return se_firmware_osu_version + const { next_se_firmware_final_version } = se_firmware_osu_version + const seFirmwareFinalVersion = await getFinalFirmwareById(next_se_firmware_final_version) + + const mcus = await getMcus() + + const currentMcuVersionId = mcus + .filter(mcu => mcu.name === deviceInfo.mcuVersion) + .map(mcu => mcu.id) + + if (!seFirmwareFinalVersion.mcu_versions.includes(...currentMcuVersionId)) { + return { + ...se_firmware_osu_version, + shouldUpdateMcu: true, + } + } + + return { ...se_firmware_osu_version, shouldUpdateMcu: false } } catch (err) { const error = Error(err.message) error.stack = err.stack diff --git a/src/helpers/firmware/getMcus.js b/src/helpers/firmware/getMcus.js new file mode 100644 index 00000000..3f0b0399 --- /dev/null +++ b/src/helpers/firmware/getMcus.js @@ -0,0 +1,19 @@ +// @flow +import network from 'api/network' + +import { GET_MCUS } from 'helpers/urls' + +export default async (): Promise<*> => { + try { + const { data } = await network({ + method: 'GET', + url: GET_MCUS, + }) + + return data + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + throw err + } +} diff --git a/src/helpers/devices/getNextMCU.js b/src/helpers/firmware/getNextMCU.js similarity index 100% rename from src/helpers/devices/getNextMCU.js rename to src/helpers/firmware/getNextMCU.js diff --git a/src/helpers/firmware/installFinalFirmware.js b/src/helpers/firmware/installFinalFirmware.js index e4d5269b..6e47a9fc 100644 --- a/src/helpers/firmware/installFinalFirmware.js +++ b/src/helpers/firmware/installFinalFirmware.js @@ -6,12 +6,14 @@ import { WS_INSTALL } from 'helpers/urls' import { createDeviceSocket } from 'helpers/socket' import getDeviceVersion from 'helpers/devices/getDeviceVersion' import getOsuFirmware from 'helpers/devices/getOsuFirmware' +import getDeviceInfo from 'helpers/devices/getDeviceInfo' import getFinalFirmwareById from './getFinalFirmwareById' -type Result = * +type Result = Promise<{ success: boolean, error?: string }> -export default async (transport: Transport<*>, deviceInfo: DeviceInfo): Result => { +export default async (transport: Transport<*>): Result => { try { + const deviceInfo: DeviceInfo = await getDeviceInfo(transport) const device = await getDeviceVersion(deviceInfo.targetId, deviceInfo.providerId) const firmware = await getOsuFirmware({ deviceId: device.id, version: deviceInfo.fullVersion }) const { next_se_firmware_final_version } = firmware diff --git a/src/helpers/firmware/installMcu.js b/src/helpers/firmware/installMcu.js index 44ae9c37..b07faea3 100644 --- a/src/helpers/firmware/installMcu.js +++ b/src/helpers/firmware/installMcu.js @@ -3,18 +3,16 @@ import type Transport from '@ledgerhq/hw-transport' import { WS_MCU } from 'helpers/urls' import { createDeviceSocket } from 'helpers/socket' -import getNextMCU from 'helpers/devices/getNextMCU' +import getNextMCU from 'helpers/firmware/getNextMCU' +import getDeviceInfo from 'helpers/devices/getDeviceInfo' type Result = Promise<*> -export default async ( - transport: Transport<*>, - args: { targetId: string | number, version: string }, -): Result => { - const { version } = args +export default async (transport: Transport<*>): Result => { + const { seVersion: version, targetId } = await getDeviceInfo(transport) const nextVersion = await getNextMCU(version) const params = { - targetId: args.targetId, + targetId, version: nextVersion.name, } const url = WS_MCU(params) diff --git a/src/helpers/firmware/installOsuFirmware.js b/src/helpers/firmware/installOsuFirmware.js index 5995b7ae..2795f951 100644 --- a/src/helpers/firmware/installOsuFirmware.js +++ b/src/helpers/firmware/installOsuFirmware.js @@ -6,12 +6,31 @@ import { createDeviceSocket } from 'helpers/socket' import type { LedgerScriptParams } from 'helpers/common' +import { createCustomErrorClass } from '../errors' + +const ManagerUnexpectedError = createCustomErrorClass('ManagerUnexpected') +const ManagerNotEnoughSpaceError = createCustomErrorClass('ManagerNotEnoughSpace') +const ManagerDeviceLockedError = createCustomErrorClass('ManagerDeviceLocked') + +function remapError(promise) { + return promise.catch((e: Error) => { + switch (true) { + case e.message.endsWith('6982'): + throw new ManagerDeviceLockedError() + case e.message.endsWith('6a84') || e.message.endsWith('6a85'): + throw new ManagerNotEnoughSpaceError() + default: + throw new ManagerUnexpectedError(e.message, { msg: e.message }) + } + }) +} + type Result = Promise<{ success: boolean, error?: any }> export default async ( transport: Transport<*>, targetId: string | number, - firmware: LedgerScriptParams, + firmware: LedgerScriptParams & { shouldUpdateMcu: boolean }, ): Result => { try { const params = { @@ -19,8 +38,9 @@ export default async ( ...firmware, firmwareKey: firmware.firmware_key, } + delete params.shouldUpdateMcu const url = WS_INSTALL(params) - await createDeviceSocket(transport, url).toPromise() + await remapError(createDeviceSocket(transport, url).toPromise()) return { success: true } } catch (error) { const result = { success: false, error } diff --git a/src/helpers/urls.js b/src/helpers/urls.js index 6ff24a70..11bed849 100644 --- a/src/helpers/urls.js +++ b/src/helpers/urls.js @@ -21,6 +21,7 @@ export const GET_CURRENT_FIRMWARE: string = managerUrlbuilder('get_firmware_vers export const GET_CURRENT_OSU: string = managerUrlbuilder('get_osu_version') export const GET_LATEST_FIRMWARE: string = managerUrlbuilder('get_latest_firmware') export const GET_NEXT_MCU: string = managerUrlbuilder('mcu_versions_bootloader') +export const GET_MCUS: string = managerUrlbuilder('mcu_versions') export const GET_CATEGORIES: string = managerUrlbuilder('categories') export const GET_APPLICATIONS: string = managerUrlbuilder('applications') diff --git a/static/images/logos/bootloaderMode.png b/static/images/logos/bootloaderMode.png new file mode 100755 index 0000000000000000000000000000000000000000..b6983a79b35f76e373c356af5a0f0e7cdf53b5b6 GIT binary patch literal 14499 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 HL*V}bGwlOG literal 0 HcmV?d00001 diff --git a/static/images/logos/unplugDevice.png b/static/images/logos/unplugDevice.png index 41aac779a55c8605ba5ecca57c4870d496a28826..38158a4a211d97bc6a9d1ecf55fcecb833bbce3e 100755 GIT binary patch literal 11201 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