From c7745bc91e6f40add9abdab0c7f910a2ad8af919 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 27 Jun 2018 19:13:01 +0200 Subject: [PATCH] modal ui for firmware update --- .../ManagerPage/FirmwareFinalUpdate.js | 11 +- src/components/ManagerPage/FirmwareUpdate.js | 70 +++------- .../ManagerPage/UpdateFirmwareButton.js | 4 +- src/components/ManagerPage/index.js | 1 - .../modals/UpdateFirmware/Disclaimer.js | 80 ++++++++++++ src/components/modals/UpdateFirmware/index.js | 122 ++++++++++++++++++ .../steps/01-step-install-full-firmware.js | 72 +++++++++++ .../steps/01-step-osu-installer.js | 72 +++++++++++ .../UpdateFirmware/steps/02-step-flash-mcu.js | 70 ++++++++++ .../steps/03-step-confirmation.js | 52 ++++++++ src/helpers/common.js | 1 + src/helpers/socket.js | 1 + static/i18n/en/app.yml | 14 +- static/images/logos/unplugDevice.png | Bin 0 -> 10289 bytes 14 files changed, 511 insertions(+), 59 deletions(-) create mode 100644 src/components/modals/UpdateFirmware/Disclaimer.js create mode 100644 src/components/modals/UpdateFirmware/index.js create mode 100644 src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js create mode 100644 src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js create mode 100644 src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js create mode 100644 src/components/modals/UpdateFirmware/steps/03-step-confirmation.js create mode 100755 static/images/logos/unplugDevice.png diff --git a/src/components/ManagerPage/FirmwareFinalUpdate.js b/src/components/ManagerPage/FirmwareFinalUpdate.js index f1498929..19e9b76f 100644 --- a/src/components/ManagerPage/FirmwareFinalUpdate.js +++ b/src/components/ManagerPage/FirmwareFinalUpdate.js @@ -10,7 +10,7 @@ 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' +import Button from 'components/base/Button' type Props = { t: T, @@ -33,7 +33,10 @@ class FirmwareFinalUpdate extends PureComponent { try { const { device, deviceInfo } = this.props const { success } = await installFinalFirmware - .send({ devicePath: device.path, deviceInfo }) + .send({ + devicePath: device.path, + deviceInfo, + }) .toPromise() if (success) { this.setState() @@ -52,7 +55,9 @@ class FirmwareFinalUpdate extends PureComponent { {t('app:manager.firmware.update')} - + + + ) diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index d00fdaa6..60afcc15 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -1,8 +1,8 @@ // @flow /* eslint-disable react/jsx-no-literals */ // FIXME -import React, { PureComponent, Fragment } from 'react' -import { translate, Trans } from 'react-i18next' +import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' import isEqual from 'lodash/isEqual' import isEmpty from 'lodash/isEmpty' import invariant from 'invariant' @@ -14,14 +14,13 @@ import type { LedgerScriptParams } from 'helpers/common' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import installOsuFirmware from 'commands/installOsuFirmware' +import DisclaimerModal from 'components/modals/UpdateFirmware/Disclaimer' +import UpdateModal from 'components/modals/UpdateFirmware' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' import Tooltip from 'components/base/Tooltip' import Box, { Card } from 'components/base/Box' import Text from 'components/base/Text' -import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' -import Button from 'components/base/Button' -// import Progress from 'components/base/Progress' import NanoS from 'icons/device/NanoS' import CheckFull from 'icons/CheckFull' @@ -29,12 +28,10 @@ import CheckFull from 'icons/CheckFull' import { PreventDeviceChangeRecheck } from 'components/EnsureDevice' import UpdateFirmwareButton from './UpdateFirmwareButton' -let CACHED_LATEST_FIRMWARE = null - export const getCleanVersion = (input: string): string => input.endsWith('-osu') ? input.replace('-osu', '') : input -type ModalStatus = 'closed' | 'disclaimer' | 'installing' | 'error' | 'success' +export type ModalStatus = 'closed' | 'disclaimer' | 'install' | 'error' | 'success' type Props = { t: T, @@ -58,7 +55,7 @@ class FirmwareUpdate extends PureComponent { } componentDidUpdate() { - if (!CACHED_LATEST_FIRMWARE || isEmpty(this.state.latestFirmware)) { + if (isEmpty(this.state.latestFirmware)) { this.fetchLatestFirmware() } } @@ -71,14 +68,12 @@ class FirmwareUpdate extends PureComponent { fetchLatestFirmware = async () => { const { deviceInfo } = this.props - const latestFirmware = - CACHED_LATEST_FIRMWARE || (await getLatestFirmwareForDevice.send(deviceInfo).toPromise()) + const latestFirmware = await getLatestFirmwareForDevice.send(deviceInfo).toPromise() if ( !isEmpty(latestFirmware) && !isEqual(this.state.latestFirmware, latestFirmware) && !this._unmounting ) { - CACHED_LATEST_FIRMWARE = latestFirmware this.setState({ latestFirmware }) } } @@ -91,7 +86,7 @@ class FirmwareUpdate extends PureComponent { const { device: { path: devicePath }, } = this.props - this.setState({ modal: 'installing' }) + this.setState({ modal: 'install' }) const { success } = await installOsuFirmware .send({ devicePath, firmware: latestFirmware, targetId: deviceInfo.targetId }) .toPromise() @@ -105,43 +100,8 @@ class FirmwareUpdate extends PureComponent { handleCloseModal = () => this.setState({ modal: 'closed' }) - handleInstallModal = () => this.setState({ modal: 'disclaimer' }) - - renderModal = () => { - const { t } = this.props - const { modal, latestFirmware } = this.state - return ( - ( - - - {t('app:manager.firmware.update')} - - - - You are about to install the latest - - {`firmware ${latestFirmware ? getCleanVersion(latestFirmware.name) : ''}`} - - - - - {t('app:manager.firmware.disclaimerAppDelete')} - {t('app:manager.firmware.disclaimerAppReinstall')} - - - - - - - - )} - /> - ) - } + handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' }) + handleInstallModal = () => this.setState({ modal: 'install' }) render() { const { deviceInfo, t } = this.props @@ -172,11 +132,17 @@ class FirmwareUpdate extends PureComponent { {modal !== 'closed' ? : null} - {this.renderModal()} + + ) } diff --git a/src/components/ManagerPage/UpdateFirmwareButton.js b/src/components/ManagerPage/UpdateFirmwareButton.js index ebeb884c..cdfead05 100644 --- a/src/components/ManagerPage/UpdateFirmwareButton.js +++ b/src/components/ManagerPage/UpdateFirmwareButton.js @@ -4,7 +4,7 @@ import { translate } from 'react-i18next' import type { T } from 'types/common' -import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants' +// import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants' import Button from 'components/base/Button' import Text from 'components/base/Text' @@ -27,7 +27,7 @@ const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) => {t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })} - diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index 56035d87..e0b3f76e 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -7,7 +7,6 @@ import type { Device } from 'types/common' import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' import Dashboard from './Dashboard' -// import FlashMcu from './FlashMcu' import ManagerGenuineCheck from './ManagerGenuineCheck' diff --git a/src/components/modals/UpdateFirmware/Disclaimer.js b/src/components/modals/UpdateFirmware/Disclaimer.js new file mode 100644 index 00000000..379c8928 --- /dev/null +++ b/src/components/modals/UpdateFirmware/Disclaimer.js @@ -0,0 +1,80 @@ +// @flow +/* eslint react/jsx-no-literals: 0 */ + +import React, { PureComponent, Fragment } from 'react' +import { translate, Trans } from 'react-i18next' + +import type { T } from 'types/common' + +import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal' +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 type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' + +type FirmwareInfos = { + name: string, + notes: string, +} + +type Props = { + t: T, + status: ModalStatus, + firmware: FirmwareInfos, + goToNextStep: () => void, + onClose: () => void, +} + +type State = * + +class DisclaimerModal extends PureComponent { + render(): React$Node { + const { status, firmware, onClose, t, goToNextStep } = this.props + return ( + ( + + + {t('app:manager.firmware.update')} + + + + You are about to install the latest + + {`firmware ${firmware ? getCleanVersion(firmware.name) : ''}`} + + + + + {t('app:manager.firmware.disclaimerAppDelete')} + {t('app:manager.firmware.disclaimerAppReinstall')} + + + + + + {firmware.notes} + + + + + + + + + + )} + /> + ) + } +} + +export default translate()(DisclaimerModal) diff --git a/src/components/modals/UpdateFirmware/index.js b/src/components/modals/UpdateFirmware/index.js new file mode 100644 index 00000000..abb71c8b --- /dev/null +++ b/src/components/modals/UpdateFirmware/index.js @@ -0,0 +1,122 @@ +// @flow +import React, { PureComponent, Fragment } from 'react' +import { translate } from 'react-i18next' + +import type { T } from 'types/common' + +import Modal from 'components/base/Modal' +import Stepper from 'components/base/Stepper' +import Button from 'components/base/Button' +import SyncSkipUnderPriority from 'components/SyncSkipUnderPriority' + +import type { StepProps as DefaultStepProps, Step } from 'components/base/Stepper' +import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate' +import type { LedgerScriptParams } from 'helpers/common' + +import StepOSUInstaller from './steps/01-step-osu-installer' +import StepFlashMcu from './steps/02-step-flash-mcu' +import StepConfirmation, { StepConfirmFooter } from './steps/03-step-confirmation' + +export type StepProps = DefaultStepProps & { + firmware: ?LedgerScriptParams, + onCloseModal: () => void, +} + +type StepId = 'idCheck' | 'updateMCU' | 'finish' + +// FIXME: Debugging for now to move between steps +// Remove when plugged to firmware update +function DebugFooter({ + transitionTo, + where, +}: { + where: string, + transitionTo: (where: string) => void, +}) { + 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, + }, + { + id: 'finish', + label: t('app:addAccounts.breadcrumb.finish'), + component: StepConfirmation, + footer: StepConfirmFooter, + onBack: null, + hideFooter: false, + }, +] + +type Props = { + t: T, + status: ModalStatus, + onClose: () => void, + firmware: ?LedgerScriptParams, +} + +type State = { + stepId: StepId | string, +} + +class UpdateModal extends PureComponent { + state = { + stepId: 'idCheck', + } + + STEPS = createSteps({ t: this.props.t }) + + handleStepChange = (step: Step) => this.setState({ stepId: step.id }) + + render(): React$Node { + const { status, t, firmware, onClose } = this.props + const { stepId } = this.state + + const additionalProps = { + firmware, + onCloseModal: onClose, + } + + return ( + ( + + + + )} + /> + ) + } +} + +export default translate()(UpdateModal) diff --git a/src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js b/src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js new file mode 100644 index 00000000..333ac961 --- /dev/null +++ b/src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js @@ -0,0 +1,72 @@ +// @flow + +import React 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 '../' + +const Container = styled(Box).attrs({ + alignItems: 'center', + fontSize: 4, + color: 'dark', + px: 7, +})`` + +const Title = styled(Box).attrs({ + ff: 'Museo Sans|Regular', + fontSize: 5, + mb: 3, +})`` + +const Address = styled(Box).attrs({ + bg: p => (p.notValid ? 'transparent' : p.withQRCode ? 'white' : 'lightGrey'), + borderRadius: 1, + color: 'dark', + ff: 'Open Sans|SemiBold', + fontSize: 4, + mt: 2, + px: p => (p.notValid ? 0 : 4), + py: p => (p.notValid ? 0 : 3), +})` + border: ${p => (p.notValid ? 'none' : `1px dashed ${p.theme.colors.fog}`)}; + cursor: text; + user-select: text; + width: 325px; +` + +const Ellipsis = styled.span` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +` + +// TODO: Change to class component and add osu firmware install +function StepFullFirmwareInstall({ t, firmware }: StepProps) { + return ( + + {t('app:manager.modal.confirmIdentifier')} + + {t('app:manager.modal.confirmIdentifierText')} + + + + {t('app:manager.modal.identifier')} + +
+ {firmware && firmware.hash} +
+
+ + + +
+ ) +} + +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 new file mode 100644 index 00000000..5aa12aa2 --- /dev/null +++ b/src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js @@ -0,0 +1,72 @@ +// @flow + +import React 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 '../' + +const Container = styled(Box).attrs({ + alignItems: 'center', + fontSize: 4, + color: 'dark', + px: 7, +})`` + +const Title = styled(Box).attrs({ + ff: 'Museo Sans|Regular', + fontSize: 5, + mb: 3, +})`` + +const Address = styled(Box).attrs({ + bg: p => (p.notValid ? 'transparent' : p.withQRCode ? 'white' : 'lightGrey'), + borderRadius: 1, + color: 'dark', + ff: 'Open Sans|SemiBold', + fontSize: 4, + mt: 2, + px: p => (p.notValid ? 0 : 4), + py: p => (p.notValid ? 0 : 3), +})` + border: ${p => (p.notValid ? 'none' : `1px dashed ${p.theme.colors.fog}`)}; + cursor: text; + user-select: text; + width: 325px; +` + +const Ellipsis = styled.span` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + 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')} + +
+ {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 new file mode 100644 index 00000000..3991634c --- /dev/null +++ b/src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js @@ -0,0 +1,70 @@ +// @flow + +import React from 'react' +import styled from 'styled-components' + +import { i } from 'helpers/staticPath' + +import Box from 'components/base/Box' +import Text from 'components/base/Text' + +import type { StepProps } from '../' + +const Container = styled(Box).attrs({ + alignItems: 'center', + fontSize: 4, + color: 'dark', +})`` + +const Title = styled(Box).attrs({ + ff: 'Museo Sans|Regular', + fontSize: 5, + mb: 3, +})`` + +const Bullet = styled.span` + font-weight: 600; + color: #142533; +` + +const Separator = styled(Box).attrs({ + color: 'fog', +})` + height: 1px; + width: 100%; + 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')} + + + ) +} + +export default StepFlashMcu diff --git a/src/components/modals/UpdateFirmware/steps/03-step-confirmation.js b/src/components/modals/UpdateFirmware/steps/03-step-confirmation.js new file mode 100644 index 00000000..553c0c5a --- /dev/null +++ b/src/components/modals/UpdateFirmware/steps/03-step-confirmation.js @@ -0,0 +1,52 @@ +// @flow + +import React 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 CheckCircle from 'icons/CheckCircle' + +import type { StepProps } from '../' + +const Container = styled(Box).attrs({ + alignItems: 'center', + fontSize: 4, + color: 'dark', +})`` + +const Title = styled(Box).attrs({ + fontFamily: 'Museo Sans', + fontSize: 6, + color: 'dark', +})` + font-weight: 500; +` + +function StepConfirmation({ t }: StepProps) { + return ( + + + + + {t('app:manager.modal.successTitle')} + + + {t('app:manager.modal.successText')} + + + + + ) +} + +export function StepConfirmFooter({ t, onCloseModal }: StepProps) { + return ( + + ) +} + +export default StepConfirmation diff --git a/src/helpers/common.js b/src/helpers/common.js index 0015c91d..e04c37d8 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -20,6 +20,7 @@ export type LedgerScriptParams = { version: string, icon: string, app?: number, + hash?: string, } /** diff --git a/src/helpers/socket.js b/src/helpers/socket.js index 9c35ecba..18d98795 100644 --- a/src/helpers/socket.js +++ b/src/helpers/socket.js @@ -74,6 +74,7 @@ export const createDeviceSocket = (transport: Transport<*>, url: string) => for (const apdu of data) { const r: Buffer = await transport.exchange(Buffer.from(apdu, 'hex')) lastStatus = r.slice(r.length - 2) + if (lastStatus.toString('hex') !== '9000') break } diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index 4158726b..e85caab8 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -204,12 +204,24 @@ manager: firmware: installed: 'Firmware version {{version}}' update: Update firmware - updateTitle: Firmware update continue: Continue update latest: 'Firmware version {{version}} is available' disclaimerTitle: 'You are about to install the latest <1><0>firmware {{version}}' disclaimerAppDelete: Please note that all the apps installed on your device will be deleted. disclaimerAppReinstall: You will be able to re-install your apps after the firmware update + modal: + steps: + idCheck: Identifier check + updateMCU: Update MCU + confirm: Confirmation + confirmIdentifier: Confirm identifier + confirmIdentifierText: Please confirm identifier on your Device. Be sure the identifier is the same as below + identifier: Identifier + mcuTitle: Updating MCU + mcuFirst: Unplug your device from your computer + mcuSecond: Press and hold left button and plug your device until the processing screen appears + successTitle: Firmware has been updated with success + successText: You can now re-install your applications on your device title: Manager subtitle: Install or uninstall apps on your device device: diff --git a/static/images/logos/unplugDevice.png b/static/images/logos/unplugDevice.png new file mode 100755 index 0000000000000000000000000000000000000000..41aac779a55c8605ba5ecca57c4870d496a28826 GIT binary patch literal 10289 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 literal 0 HcmV?d00001