Browse Source

Merge pull request #731 from valpinkman/feat/firmware-update-steps

modal ui for firmware update
master
Valentin D. Pinkman 7 years ago
committed by GitHub
parent
commit
dae2d5a757
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/commands/installFinalFirmware.js
  2. 8
      src/commands/installMcu.js
  3. 2
      src/commands/installOsuFirmware.js
  4. 62
      src/components/ManagerPage/FirmwareFinalUpdate.js
  5. 114
      src/components/ManagerPage/FirmwareUpdate.js
  6. 50
      src/components/ManagerPage/FlashMcu.js
  7. 8
      src/components/ManagerPage/UpdateFirmwareButton.js
  8. 5
      src/components/ManagerPage/index.js
  9. 2
      src/components/modals/ReleaseNotes.js
  10. 82
      src/components/modals/UpdateFirmware/Disclaimer.js
  11. 155
      src/components/modals/UpdateFirmware/index.js
  12. 108
      src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js
  13. 85
      src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js
  14. 155
      src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js
  15. 52
      src/components/modals/UpdateFirmware/steps/03-step-confirmation.js
  16. 1
      src/helpers/common.js
  17. 21
      src/helpers/devices/getLatestFirmwareForDevice.js
  18. 19
      src/helpers/firmware/getMcus.js
  19. 0
      src/helpers/firmware/getNextMCU.js
  20. 6
      src/helpers/firmware/installFinalFirmware.js
  21. 12
      src/helpers/firmware/installMcu.js
  22. 24
      src/helpers/firmware/installOsuFirmware.js
  23. 1
      src/helpers/socket.js
  24. 1
      src/helpers/urls.js
  25. 14
      static/i18n/en/app.yml
  26. BIN
      static/images/logos/bootloaderMode.png
  27. BIN
      static/images/logos/unplugDevice.png

8
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<Input, Result> = createCommand(
'installFinalFirmware',
({ devicePath, deviceInfo }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, deviceInfo))),
const cmd: Command<Input, Result> = createCommand('installFinalFirmware', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport))),
)
export default cmd

8
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<Input, Result> = createCommand(
'installMcu',
({ devicePath, targetId, version }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport, { targetId, version }))),
const cmd: Command<Input, Result> = createCommand('installMcu', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport))),
)
export default cmd

2
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 = *

62
src/components/ManagerPage/FirmwareFinalUpdate.js

@ -1,62 +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<Props, State> {
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 (
<Box flow={4} {...props}>
<Box color="dark" ff="Museo Sans" fontSize={6}>
{t('app:manager.firmware.update')}
</Box>
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2} />
</Card>
</Box>
)
}
}
export default translate()(FirmwareFinalUpdate)

114
src/components/ManagerPage/FirmwareUpdate.js

@ -2,7 +2,7 @@
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant'
@ -14,14 +14,15 @@ 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'
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 +30,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,
@ -43,7 +42,7 @@ type Props = {
}
type State = {
latestFirmware: ?LedgerScriptParams,
latestFirmware: ?LedgerScriptParams & ?{ shouldUpdateMcu: boolean },
modal: ModalStatus,
}
@ -58,7 +57,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
}
componentDidUpdate() {
if (!CACHED_LATEST_FIRMWARE || isEmpty(this.state.latestFirmware)) {
if (isEmpty(this.state.latestFirmware)) {
this.fetchLatestFirmware()
}
}
@ -71,78 +70,57 @@ class FirmwareUpdate extends PureComponent<Props, State> {
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 })
}
}
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
this.setState({ modal: 'installing' })
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
}
}
handleCloseModal = () => this.setState({ modal: 'closed' })
handleInstallModal = () => this.setState({ modal: 'disclaimer' })
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
}
}
renderModal = () => {
const { t } = this.props
const { modal, latestFirmware } = this.state
return (
<Modal
isOpened={modal !== 'closed'}
render={() => (
<ModalBody grow align="center" justify="center" mt={3}>
<Fragment>
<ModalTitle>{t('app:manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="app:manager.firmware.disclaimerTitle">
You are about to install the latest
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware ${latestFirmware ? getCleanVersion(latestFirmware.name) : ''}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('app:manager.firmware.disclaimerAppDelete')}
{t('app:manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.installFirmware}>
{t('app:manager.firmware.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
)
flashMCU = async () => {
const { device } = this.props
await installMcu.send({ devicePath: device.path }).toPromise()
}
handleCloseModal = () => this.setState({ modal: 'closed' })
handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' })
handleInstallModal = () => this.setState({ modal: 'install' })
render() {
const { deviceInfo, t } = this.props
const { latestFirmware, modal } = this.state
@ -170,13 +148,27 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})}
</Text>
</Box>
<UpdateFirmwareButton
firmware={latestFirmware}
installFirmware={this.handleInstallModal}
/>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} />
</Box>
{modal !== 'closed' ? <PreventDeviceChangeRecheck /> : null}
{this.renderModal()}
{latestFirmware && (
<Fragment>
<DisclaimerModal
firmware={latestFirmware}
status={modal}
goToNextStep={this.handleInstallModal}
onClose={this.handleCloseModal}
/>
<UpdateModal
status={modal}
onClose={this.handleCloseModal}
firmware={latestFirmware}
installOsuFirmware={this.installOsuFirmware}
installFinalFirmware={this.installFinalFirmware}
flashMCU={this.flashMCU}
/>
</Fragment>
)}
</Card>
)
}

50
src/components/ManagerPage/FlashMcu.js

@ -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<Props, State> {
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 (
<div>
<h1>{'Flashing MCU'}</h1>
<button onClick={this.flashMCU}>{'flash'}</button>
</div>
)
}
}
export default FlashMcu

8
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'
@ -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 ? (
<Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
</Text>
<Button primary onClick={installFirmware} disabled={!EXPERIMENTAL_FIRMWARE_UPDATE}>
<Button primary onClick={onClick}>
{t('app:manager.firmware.update')}
</Button>
</Fragment>

5
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'
@ -41,10 +40,6 @@ class ManagerPage extends PureComponent<Props, State> {
invariant(device, 'Inexistant device considered genuine')
invariant(deviceInfo, 'Inexistant device infos for genuine device')
// TODO
// renderFinalUpdate
// renderMcuUpdate
return <Dashboard device={device} deviceInfo={deviceInfo} />
}
}

2
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',

82
src/components/modals/UpdateFirmware/Disclaimer.js

@ -0,0 +1,82 @@
// @flow
/* eslint react/jsx-no-literals: 0 */
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
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 { Notes } from 'components/modals/ReleaseNotes'
import type { ModalStatus } from 'components/ManagerPage/FirmwareUpdate'
import { getCleanVersion } 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<Props, State> {
render(): React$Node {
const { status, firmware, onClose, t, goToNextStep } = this.props
return (
<Modal
isOpened={status === 'disclaimer'}
onClose={onClose}
render={({ onClose }) => (
<ModalBody onClose={onClose} grow align="center" justify="center" mt={3}>
<Fragment>
<ModalTitle>{t('app:manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="app:manager.firmware.disclaimerTitle">
You are about to install the latest
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware ${firmware ? getCleanVersion(firmware.name) : ''}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('app:manager.firmware.disclaimerAppDelete')}
{t('app:manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalContent style={{ height: 250, width: '100%' }}>
<GrowScroll>
<Notes>
<ReactMarkdown>{firmware.notes}</ReactMarkdown>
</Notes>
</GrowScroll>
<GradientBox />
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={goToNextStep}>
{t('app:manager.firmware.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
)
}
}
export default translate()(DisclaimerModal)

155
src/components/modals/UpdateFirmware/index.js

@ -0,0 +1,155 @@
// @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 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'
function DebugFooter({
transitionTo,
where,
}: {
where: string,
transitionTo: (where: string) => void,
}) {
return <Button onClick={() => transitionTo(where)}>{where}</Button>
}
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) => (
<DebugFooter firmware={firmware} {...props} where="updateMCU" />
),
onBack: null,
hideFooter: false,
},
{
id: 'updateMCU',
label: t('app:manager.modal.steps.updateMCU'),
component: StepFlashMcu,
footer: ({ firmware, ...props }: StepProps) => (
<Fragment>
<DebugFooter firmware={firmware} {...props} where="idCheck" />
<DebugFooter firmware={firmware} {...props} where="finish" />
</Fragment>
),
onBack: null,
hideFooter: false,
},
finalStep,
]
}
return [
{
id: 'idCheck',
label: t('app:manager.modal.steps.idCheck'),
component: StepFullFirmwareInstall,
footer: ({ firmware, ...props }: StepProps) => (
<DebugFooter firmware={firmware} {...props} where="finish" />
),
onBack: null,
hideFooter: false,
},
finalStep,
]
}
type Props = {
t: T,
status: ModalStatus,
onClose: () => void,
firmware: LedgerScriptParams & { shouldUpdateMcu: boolean },
installOsuFirmware: () => void,
installFinalFirmware: () => void,
flashMcu: () => void,
}
type State = {
stepId: StepId | string,
}
class UpdateModal extends PureComponent<Props, State> {
state = {
stepId: 'idCheck',
}
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, ...props } = this.props
const { stepId } = this.state
const additionalProps = {
firmware,
onCloseModal: onClose,
...props,
}
return (
<Modal
onClose={onClose}
isOpened={status === 'install'}
refocusWhenChange={stepId}
preventBackdropClick={false}
render={() => (
<Stepper
title={t('app:manager.firmware.update')}
initialStepId="idCheck"
steps={this.STEPS}
{...additionalProps}
>
<SyncSkipUnderPriority priority={100} />
</Stepper>
)}
/>
)
}
}
export default translate()(UpdateModal)

108
src/components/modals/UpdateFirmware/steps/01-step-install-full-firmware.js

@ -0,0 +1,108 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import Progress from 'components/base/Progress'
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%;
`
class StepFullFirmwareInstall extends PureComponent<StepProps, *> {
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 (
<Box mx={7}>
<Progress infinite style={{ width: '100%' }} />
</Box>
)
}
return (
<Fragment>
<Box mx={7} mt={5}>
<Text ff="Open Sans|SemiBold" align="center" color="smoke">
{t('app:manager.modal.identifier')}
</Text>
<Address>
<Ellipsis>{firmware && firmware.hash}</Ellipsis>
</Address>
</Box>
<Box mt={5}>
<DeviceConfirm />
</Box>
</Fragment>
)
}
render() {
const { t } = this.props
return (
<Container>
<Title>{t('app:manager.modal.confirmIdentifier')}</Title>
<Text ff="Open Sans|Regular" align="center" color="smoke">
{t('app:manager.modal.confirmIdentifierText')}
</Text>
{this.renderBody()}
</Container>
)
}
}
export default StepFullFirmwareInstall

85
src/components/modals/UpdateFirmware/steps/01-step-osu-installer.js

@ -0,0 +1,85 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
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%;
`
class StepOSUInstaller extends PureComponent<StepProps, *> {
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 (
<Container>
<Title>{t('app:manager.modal.confirmIdentifier')}</Title>
<Text ff="Open Sans|Regular" align="center" color="smoke">
{t('app:manager.modal.confirmIdentifierText')}
</Text>
<Box mx={7} mt={5}>
<Text ff="Open Sans|SemiBold" align="center" color="smoke">
{t('app:manager.modal.identifier')}
</Text>
<Address>
<Ellipsis>{firmware && firmware.hash}</Ellipsis>
</Address>
</Box>
<Box mt={5}>
<DeviceConfirm />
</Box>
</Container>
)
}
}
export default StepOSUInstaller

155
src/components/modals/UpdateFirmware/steps/02-step-flash-mcu.js

@ -0,0 +1,155 @@
// @flow
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 '../'
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;
`
const mapStateToProps = state => ({
device: getCurrentDevice(state),
})
type Props = StepProps & { device?: Device }
type State = {
installing: boolean,
}
class StepFlashMcu extends PureComponent<Props, State> {
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 (
<Box mx={7}>
<Progress infinite style={{ width: '100%' }} />
</Box>
)
}
return (
<Fragment>
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'1.'}</Bullet>
{t('app:manager.modal.mcuFirst')}
</Text>
<img
src={i('logos/unplugDevice.png')}
style={{ width: '100%', maxWidth: 368, marginTop: 30 }}
alt={t('app:manager.modal.mcuFirst')}
/>
</Box>
<Separator my={6} />
<Box mx={7}>
<Text ff="Open Sans|Regular" align="center" color="smoke">
<Bullet>{'2.'}</Bullet>
{t('app:manager.modal.mcuSecond')}
</Text>
<img
src={i('logos/bootloaderMode.png')}
style={{ width: '100%', maxWidth: 368, marginTop: 30 }}
alt={t('app:manager.modal.mcuFirst')}
/>
</Box>
</Fragment>
)
}
_unsubConnect: *
render() {
const { t } = this.props
return (
<Container>
<Title>{t('app:manager.modal.mcuTitle')}</Title>
{this.renderBody()}
</Container>
)
}
}
export default connect(mapStateToProps)(StepFlashMcu)

52
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 (
<Container>
<Box mx={7} color="positiveGreen" my={4}>
<CheckCircle size={44} />
</Box>
<Title>{t('app:manager.modal.successTitle')}</Title>
<Box mt={2} mb={8}>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite">
{t('app:manager.modal.successText')}
</Text>
</Box>
<Box mx={7} />
</Container>
)
}
export function StepConfirmFooter({ t, onCloseModal }: StepProps) {
return (
<Button primary onClick={onCloseModal}>
{t('app:common.close')}
</Button>
)
}
export default StepConfirmation

1
src/helpers/common.js

@ -20,6 +20,7 @@ export type LedgerScriptParams = {
version: string,
icon: string,
app?: number,
hash?: string,
}
/**

21
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

19
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
}
}

0
src/helpers/devices/getNextMCU.js → src/helpers/firmware/getNextMCU.js

6
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

12
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)

24
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 }

1
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
}

1
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')

14
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}}</0></1>'
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:

BIN
static/images/logos/bootloaderMode.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
static/images/logos/unplugDevice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Loading…
Cancel
Save