From cd75857b8eabc74413cdd64ec02d5fc807cfb1e4 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Fri, 8 Jun 2018 14:23:39 +0200 Subject: [PATCH] ui manager for apps and search --- src/commands/index.js | 2 + src/commands/isDashboardOpen.js | 19 ++ src/commands/listApps.js | 2 +- src/components/EnsureDeviceApp/index.js | 9 +- src/components/ManagerPage/AppSearchBar.js | 106 ++++++++ src/components/ManagerPage/AppsList.js | 37 +-- src/components/ManagerPage/EnsureDashboard.js | 36 +-- src/components/ManagerPage/EnsureDevice.js | 37 +-- src/components/ManagerPage/EnsureGenuine.js | 41 ++- src/components/ManagerPage/FirmwareUpdate.js | 8 +- src/components/ManagerPage/ManagerApp.js | 22 +- src/components/ManagerPage/Workflow.js | 249 ++++++++++++++++++ src/components/ManagerPage/index.js | 132 ++++++---- src/components/base/Button/index.js | 18 +- src/helpers/devices/getIsGenuine.js | 3 +- src/helpers/devices/isDashboardOpen.js | 22 ++ src/helpers/firmware/installFinalFirmware.js | 9 +- src/helpers/firmware/installOsuFirmware.js | 9 +- src/icons/Trash.js | 18 ++ static/i18n/en/manager.yml | 14 +- static/images/logos/connectDevice.png | Bin 0 -> 14871 bytes 21 files changed, 610 insertions(+), 183 deletions(-) create mode 100644 src/commands/isDashboardOpen.js create mode 100644 src/components/ManagerPage/AppSearchBar.js create mode 100644 src/components/ManagerPage/Workflow.js create mode 100644 src/helpers/devices/isDashboardOpen.js create mode 100644 src/icons/Trash.js create mode 100755 static/images/logos/connectDevice.png diff --git a/src/commands/index.js b/src/commands/index.js index 86ae1ad5..6d7cb04b 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -13,6 +13,7 @@ import installFinalFirmware from 'commands/installFinalFirmware' import installMcu from 'commands/installMcu' import installOsuFirmware from 'commands/installOsuFirmware' import isCurrencyAppOpened from 'commands/isCurrencyAppOpened' +import isDashboardOpen from 'commands/isDashboardOpen' import libcoreGetVersion from 'commands/libcoreGetVersion' import libcoreScanAccounts from 'commands/libcoreScanAccounts' import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast' @@ -37,6 +38,7 @@ const all: Array> = [ installMcu, installOsuFirmware, isCurrencyAppOpened, + isDashboardOpen, libcoreGetVersion, libcoreScanAccounts, libcoreSignAndBroadcast, diff --git a/src/commands/isDashboardOpen.js b/src/commands/isDashboardOpen.js new file mode 100644 index 00000000..2a33ff22 --- /dev/null +++ b/src/commands/isDashboardOpen.js @@ -0,0 +1,19 @@ +// @flow + +import { createCommand, Command } from 'helpers/ipc' +import { fromPromise } from 'rxjs/observable/fromPromise' +import { withDevice } from 'helpers/deviceAccess' + +import isDashboardOpen from '../helpers/devices/isDashboardOpen' + +type Input = { + devicePath: string, +} + +type Result = boolean + +const cmd: Command = createCommand('isDashboardOpen', ({ devicePath }) => + fromPromise(withDevice(devicePath)(transport => isDashboardOpen(transport))), +) + +export default cmd diff --git a/src/commands/listApps.js b/src/commands/listApps.js index d9b542b5..c497111c 100644 --- a/src/commands/listApps.js +++ b/src/commands/listApps.js @@ -13,7 +13,7 @@ type Input = * type Result = * const cmd: Command = createCommand('listApps', () => - /* { targetId } */ fromPromise(listApps()), + /* { targetId } */ fromPromise(listApps(/* targetId */)), ) export default cmd diff --git a/src/components/EnsureDeviceApp/index.js b/src/components/EnsureDeviceApp/index.js index 6b237512..e21fdd3e 100644 --- a/src/components/EnsureDeviceApp/index.js +++ b/src/components/EnsureDeviceApp/index.js @@ -10,6 +10,7 @@ import { getDevices } from 'reducers/devices' import type { State as StoreState } from 'reducers/index' import getAddress from 'commands/getAddress' import isCurrencyAppOpened from 'commands/isCurrencyAppOpened' +import isDashboardOpen from 'commands/isDashboardOpen' import { CHECK_APP_INTERVAL_WHEN_VALID, CHECK_APP_INTERVAL_WHEN_INVALID } from 'config/constants' @@ -139,11 +140,11 @@ class EnsureDeviceApp extends PureComponent { throw new Error(`${currency.name} app is not opened on the device`) } } else { - // TODO: real check if user is on the device dashboard - if (!deviceSelected) { - throw new Error('No device') + const isDashboard = isDashboardOpen.send({ devicePath: deviceSelected.path }).toPromise() + + if (!isDashboard) { + throw new Error(`dashboard is not opened`) } - await sleep(1) // WTF } this.handleStatusChange(this.state.deviceStatus, 'success') diff --git a/src/components/ManagerPage/AppSearchBar.js b/src/components/ManagerPage/AppSearchBar.js new file mode 100644 index 00000000..8d38b4f6 --- /dev/null +++ b/src/components/ManagerPage/AppSearchBar.js @@ -0,0 +1,106 @@ +// @flow +import React, { PureComponent } from 'react' +import styled from 'styled-components' + +import Box from 'components/base/Box' +import Search from 'components/base/Search' + +import SearchIcon from 'icons/Search' +import CrossIcon from 'icons/Cross' + +type LedgerApp = { + name: string, + version: string, + icon: string, + app: Object, + bolos_version: { + min: number, + max: number, + }, +} + +type Props = { + list: Array, + children: (list: Array) => React$Node, +} + +type State = { + query: string, + focused: boolean, +} + +const SearchBarWrapper = styled(Box).attrs({ + horizontal: true, + borderRadius: 4, +})` + height: 42px; + width: 100%; + margin: 0 0 20px 0; + background-color: white; + padding: 0 13px; +` + +const Input = styled.input` + width: 100%; + border: 0; + margin: 0 13px; + flex: 1; + outline: none; + background: transparent; + color: black; + font-family: 'Open Sans'; + font-weight: 600; +` + +class AppSearchBar extends PureComponent { + state = { + query: '', + focused: false, + } + + handleChange = (e: any) => this.setState({ query: e.target.value }) + + handleFocus = (bool: boolean) => () => this.setState({ focused: bool }) + + reset = () => { + const { input } = this + this.setState(state => ({ ...state, query: '' }), () => input && input.focus()) + } + + input = null + + render() { + const { children, list } = this.props + const { query, focused } = this.state + + const color = focused ? 'black' : 'grey' + + return ( + + + + (this.input = c)} + type="text" + value={query} + onChange={this.handleChange} + onFocus={this.handleFocus(true)} + onBlur={this.handleFocus(false)} + /> + {!!query && } + + children(items)} + /> + + ) + } +} + +export default AppSearchBar diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 74c3f85c..5930b5e8 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -14,6 +14,7 @@ import Modal, { ModalBody } from 'components/base/Modal' import type { Device, T } from 'types/common' import ManagerApp from './ManagerApp' +import AppSearchBar from './AppSearchBar' const List = styled(Box).attrs({ horizontal: true, @@ -72,7 +73,7 @@ class AppsList extends PureComponent { async fetchAppList() { try { - // const { targetId } = this.props + // const { targetId } = this.props // TODO: REUSE THIS WHEN SERVER IS UP const appsList = CACHED_APPS || (await listApps.send().toPromise()) CACHED_APPS = appsList if (!this._unmounted) { @@ -116,19 +117,25 @@ class AppsList extends PureComponent { handleCloseModal = () => this.setState({ status: 'idle' }) renderList() { - const { status, error } = this.state + const { status, error, appsList } = this.state return ( - - {this.state.appsList.map(c => ( - - ))} + + + {items => ( + + {items.map(c => ( + + ))} + + )} + ( @@ -150,7 +157,7 @@ class AppsList extends PureComponent { )} /> - + ) } @@ -159,7 +166,7 @@ class AppsList extends PureComponent { return ( - + {t('manager:allApps')} {this.renderList()} diff --git a/src/components/ManagerPage/EnsureDashboard.js b/src/components/ManagerPage/EnsureDashboard.js index 51e6ff86..96c9e4ca 100644 --- a/src/components/ManagerPage/EnsureDashboard.js +++ b/src/components/ManagerPage/EnsureDashboard.js @@ -1,9 +1,9 @@ // @flow -import React, { PureComponent, Fragment } from 'react' -import { translate } from 'react-i18next' +import { PureComponent } from 'react' import isEqual from 'lodash/isEqual' -import type { Device, T } from 'types/common' +import type { Node } from 'react' +import type { Device } from 'types/common' import getDeviceInfo from 'commands/getDeviceInfo' @@ -14,18 +14,19 @@ type DeviceInfo = { mcu: boolean, } +type Error = { + message: string, + stack: string, +} + type Props = { - t: T, - device: Device, - children: Function, + device: ?Device, + children: (deviceInfo: ?DeviceInfo, error: ?Error) => Node, } type State = { deviceInfo: ?DeviceInfo, - error: ?{ - message: string, - stack: string, - }, + error: ?Error, } class EnsureDashboard extends PureComponent { @@ -74,19 +75,10 @@ class EnsureDashboard extends PureComponent { render() { const { deviceInfo, error } = this.state - const { children, t } = this.props - - if (deviceInfo) { - return children(deviceInfo) - } + const { children } = this.props - return error ? ( - - {error.message} - {t('manager:erros:noDashboard')} - - ) : null + return children(deviceInfo, error) } } -export default translate()(EnsureDashboard) +export default EnsureDashboard diff --git a/src/components/ManagerPage/EnsureDevice.js b/src/components/ManagerPage/EnsureDevice.js index 9dcee989..3327c781 100644 --- a/src/components/ManagerPage/EnsureDevice.js +++ b/src/components/ManagerPage/EnsureDevice.js @@ -1,39 +1,28 @@ // @flow -import React, { PureComponent } from 'react' +import { PureComponent } from 'react' import { connect } from 'react-redux' -import { translate } from 'react-i18next' -import { compose } from 'redux' -import type { Device, T } from 'types/common' +import type { Node } from 'react' +import type { Device } from 'types/common' -import { getCurrentDevice, getDevices } from 'reducers/devices' - -const mapStateToProps = state => ({ - device: getCurrentDevice(state), - nbDevices: getDevices(state).length, -}) +import { getCurrentDevice } from 'reducers/devices' type Props = { - t: T, - device: ?Device, - nbDevices: number, - children: Function, + device: Device, + children: (device: Device) => Node, } type State = {} class EnsureDevice extends PureComponent { - static defaultProps = { - device: null, - } - render() { - const { device, nbDevices, children, t } = this.props - return device ? children(device, nbDevices) : {t('manager:errors.noDevice')} + const { device, children } = this.props + return children(device) } } -export default compose( - translate(), - connect(mapStateToProps), -)(EnsureDevice) +const mapStateToProps = state => ({ + device: getCurrentDevice(state), +}) + +export default connect(mapStateToProps)(EnsureDevice) diff --git a/src/components/ManagerPage/EnsureGenuine.js b/src/components/ManagerPage/EnsureGenuine.js index a08c6876..2de56aeb 100644 --- a/src/components/ManagerPage/EnsureGenuine.js +++ b/src/components/ManagerPage/EnsureGenuine.js @@ -1,36 +1,36 @@ // @flow -import React, { PureComponent, Fragment } from 'react' -import { translate } from 'react-i18next' +import { PureComponent } from 'react' import isEqual from 'lodash/isEqual' import type { Node } from 'react' -import type { Device, T } from 'types/common' +import type { Device } from 'types/common' import getIsGenuine from 'commands/getIsGenuine' +type Error = { + message: string, + stack: string, +} + type Props = { - t: T, - device: Device, - children: Node, + device: ?Device, + children: (isGenuine: ?boolean, error: ?Error) => Node, } type State = { - genuine: boolean, - error: ?{ - message: string, - stack: string, - }, + genuine: ?boolean, + error: ?Error, } class EnsureGenuine extends PureComponent { static defaultProps = { - children: null, + children: () => null, firmwareInfo: null, } state = { error: null, - genuine: false, + genuine: null, } componentDidMount() { @@ -68,19 +68,10 @@ class EnsureGenuine extends PureComponent { render() { const { error, genuine } = this.state - const { children, t } = this.props - - if (genuine) { - return children - } + const { children } = this.props - return error ? ( - - {error.message} - {t('manager:errors.noGenuine')} - - ) : null + return children(genuine, error) } } -export default translate()(EnsureGenuine) +export default EnsureGenuine diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index 772dfc4c..de7d4046 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -12,6 +12,7 @@ import installOsuFirmware from 'commands/installOsuFirmware' import Box, { Card } from 'components/base/Box' import Button from 'components/base/Button' +import Text from 'components/base/Text' let CACHED_LATEST_FIRMWARE = null @@ -97,12 +98,11 @@ class FirmwareUpdate extends PureComponent { return ( - - {t('manager:firmwareUpdate')} - - {`${t('manager:latestFirmware')}: ${latestFirmware.name}`} + {`${t('manager:latestFirmware')}: ${ + latestFirmware.name + }`} diff --git a/src/components/ManagerPage/ManagerApp.js b/src/components/ManagerPage/ManagerApp.js index d4680a5c..36988230 100644 --- a/src/components/ManagerPage/ManagerApp.js +++ b/src/components/ManagerPage/ManagerApp.js @@ -6,27 +6,29 @@ import { translate } from 'react-i18next' import type { T } from 'types/common' +import Trash from 'icons/Trash' + import Box from 'components/base/Box' import Text from 'components/base/Text' import Button from 'components/base/Button' const Container = styled(Box).attrs({ - align: 'center', + horizontal: true, m: 3, p: 4, boxShadow: 0, + borderRadius: 4, flow: 3, })` - width: 156px; - height: 186px; + width: 342px; background: white; line-height: normal; ` const AppIcon = styled.img` display: block; - width: 50px; - height: 50px; + width: 36px; + height: 36px; ` const AppName = styled(Box).attrs({ @@ -36,7 +38,6 @@ const AppName = styled(Box).attrs({ })` display: block; width: 115px; - text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -48,16 +49,16 @@ type Props = { version: string, icon: string, onInstall: Function, - // onUninstall: Function, + onUninstall: Function, } function ManagerApp(props: Props) { - const { name, version, icon, onInstall, t } = props + const { name, version, icon, onInstall, onUninstall, t } = props const iconUrl = `https://api.ledgerwallet.com/update/assets/icons/${icon}` return ( - + {name} {version} @@ -66,6 +67,9 @@ function ManagerApp(props: Props) { + ) } diff --git a/src/components/ManagerPage/Workflow.js b/src/components/ManagerPage/Workflow.js new file mode 100644 index 00000000..971970fa --- /dev/null +++ b/src/components/ManagerPage/Workflow.js @@ -0,0 +1,249 @@ +// @flow + +import React, { PureComponent } from 'react' +import styled from 'styled-components' +import { Trans, translate } from 'react-i18next' +import isNull from 'lodash/isNull' + +import type { Node } from 'react' +import type { Device, T } from 'types/common' + +import { i } from 'helpers/staticPath' +import Box from 'components/base/Box' +import Space from 'components/base/Space' +import Spinner from 'components/base/Spinner' +import Text from 'components/base/Text' + +import IconCheck from 'icons/Check' +import IconExclamationCircle from 'icons/ExclamationCircle' +import IconUsb from 'icons/Usb' +import IconHome from 'icons/Home' + +import EnsureDevice from './EnsureDevice' +import EnsureDashboard from './EnsureDashboard' +import EnsureGenuine from './EnsureGenuine' + +type DeviceInfo = { + targetId: number | string, + version: string, + final: boolean, + mcu: boolean, +} + +type Error = { + message: string, + stack: string, +} + +type Props = { + t: T, + renderMcuUpdate: (deviceInfo: DeviceInfo) => Node, + renderFinalUpdate: (deviceInfo: DeviceInfo) => Node, + renderDashboard: (device: Device, deviceInfo: DeviceInfo) => Node, + renderError: (dashboardError: ?Error, genuineError: ?Error) => Node, +} +type State = {} + +const Step = styled(Box).attrs({ + borderRadius: 1, + justifyContent: 'center', + fontSize: 4, +})` + border: 1px solid + ${p => + p.validated + ? p.theme.colors.wallet + : p.hasErrors + ? p.theme.colors.alertRed + : p.theme.colors.fog}; +` + +const StepIcon = styled(Box).attrs({ + alignItems: 'center', + justifyContent: 'center', +})` + width: 64px; +` + +const StepContent = styled(Box).attrs({ + color: 'dark', + horizontal: true, + alignItems: 'center', +})` + height: 60px; + line-height: 1.2; + + strong { + font-weight: 600; + } +` + +const StepCheck = ({ checked, hasErrors }: { checked: ?boolean, hasErrors?: boolean }) => ( + + {checked ? ( + + + + ) : hasErrors ? ( + + + + ) : ( + + )} + +) + +StepCheck.defaultProps = { + hasErrors: false, +} + +const WrapperIconCurrency = styled(Box).attrs({ + alignItems: 'center', + justifyContent: 'center', +})` + border: 1px solid ${p => p.theme.colors[p.color]}; + border-radius: 8px; + height: 24px; + width: 24px; +` + +class Workflow extends PureComponent { + render() { + const { renderDashboard, renderFinalUpdate, renderMcuUpdate, renderError, t } = this.props + return ( + + {(device: Device) => ( + + {(deviceInfo: ?DeviceInfo, dashboardError: ?Error) => ( + + {(isGenuine: ?boolean, genuineError: ?Error) => { + if (dashboardError || genuineError) { + return renderError ? ( + renderError(dashboardError, genuineError) + ) : ( +
+ {dashboardError && {dashboardError.message}} + {genuineError && {genuineError.message}} +
+ ) + } + + if (deviceInfo && deviceInfo.mcu) { + return renderMcuUpdate(deviceInfo) + } + + if (deviceInfo && deviceInfo.final) { + return renderFinalUpdate(deviceInfo) + } + + if (isGenuine && deviceInfo && device && !dashboardError && !genuineError) { + return renderDashboard(device, deviceInfo) + } + + return ( + + + + connect your device + + {t('manager:plugYourDevice:title')} + + + {t('manager:plugYourDevice:desc')} + + + + {/* DEVICE CHECK */} + + + + + + + + {'Connect your '} + Ledger device + {' to your computer and enter your '} + PIN code + {' on your device'} + + + + + + + {/* DASHBOARD CHECK */} + + + + + + + + + + {'Go to the '} + {'dashboard'} + {' on your device'} + + + + + + + {/* GENUINE CHECK */} + + + + + + + + + + {'Confirm '} + {'authentication'} + {' on your device'} + + + + + + + + ) + }} +
+ )} +
+ )} +
+ ) + } +} + +export default translate()(Workflow) diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index f09557d0..7c819be5 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -1,62 +1,82 @@ // @flow -// import React, { Fragment } from 'react' -import React from 'react' -// import { translate } from 'react-i18next' +import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' import type { Node } from 'react' -// import type { T, Device } from 'types/common' -import type { Device } from 'types/common' +import type { T, Device } from 'types/common' + +import Box from 'components/base/Box' +import Text from 'components/base/Text' import AppsList from './AppsList' -// import DeviceInfos from './DeviceInfos' -// import FirmwareUpdate from './FirmwareUpdate' -import EnsureDevice from './EnsureDevice' -// import EnsureDashboard from './EnsureDashboard' -// import EnsureGenuine from './EnsureGenuine' - -// type DeviceInfo = { -// targetId: number | string, -// version: string, -// final: boolean, -// mcu: boolean, -// } - -// type Props = { -// t: T, -// } - -// const ManagerPage = ({ t }: Props): Node => ( -// -// {(device: Device) => ( -// -// {(deviceInfo: DeviceInfo) => ( -// -// {deviceInfo.mcu && bootloader mode } -// {deviceInfo.final && osu mode } -// {!deviceInfo.mcu && -// !deviceInfo.final && ( -// -// -// -// -// )} -// -// )} -// -// )} -// -// ) - -const ManagerPage = (): Node => ( - {(device: Device) => } -) - -export default ManagerPage +import FirmwareUpdate from './FirmwareUpdate' +import Workflow from './Workflow' + +type DeviceInfo = { + targetId: number | string, + version: string, + final: boolean, + mcu: boolean, +} + +type Error = { + message: string, + stack: string, +} + +type Props = { + t: T, +} + +type State = { + modalOpen: boolean, +} + +class ManagerPage extends PureComponent { + render(): Node { + const { t } = this.props + return ( + { + if (dashboardError) return Dashboard Error: {dashboardError.message} + if (genuineError) return Genuine Error: {genuineError.message} + return Error + }} + renderFinalUpdate={(deviceInfo: DeviceInfo) => ( +

UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}

+ )} + renderMcuUpdate={(deviceInfo: DeviceInfo) => ( +

FLASH MCU (TEMPLATE + ACTION WIP) {deviceInfo.mcu}

+ )} + renderDashboard={(device: Device, deviceInfo: DeviceInfo) => ( + + + + {t('manager:title')} + + + {t('manager:subtitle')} + + + + + + + + + + )} + /> + ) + } +} + +export default translate()(ManagerPage) diff --git a/src/components/base/Button/index.js b/src/components/base/Button/index.js index a27c7f22..0f673f4c 100644 --- a/src/components/base/Button/index.js +++ b/src/components/base/Button/index.js @@ -37,12 +37,22 @@ const buttonStyles = { outline: { default: p => ` background: transparent; - border: 1px solid ${p.theme.colors.wallet}; - color: ${p.theme.colors.wallet}; + border: 1px solid ${ + p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet + }; + color: ${ + p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet + }; `, active: p => ` - color: ${darken(p.theme.colors.wallet, 0.1)}; - border-color: ${darken(p.theme.colors.wallet, 0.1)}; + color: ${darken( + p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet, + 0.1, + )}; + border-color: ${darken( + p.outlineColor ? p.theme.colors[p.outlineColor] || p.outlineColor : p.theme.colors.wallet, + 0.1, + )}; `, }, outlineGrey: { diff --git a/src/helpers/devices/getIsGenuine.js b/src/helpers/devices/getIsGenuine.js index a0a8ad3a..0c546e26 100644 --- a/src/helpers/devices/getIsGenuine.js +++ b/src/helpers/devices/getIsGenuine.js @@ -2,4 +2,5 @@ // import type Transport from '@ledgerhq/hw-transport' -export default async (/* transport: Transport<*> */) => new Promise(resolve => resolve(true)) +export default async (/* transport: Transport<*> */) => + new Promise(resolve => setTimeout(() => resolve(true), 1000)) diff --git a/src/helpers/devices/isDashboardOpen.js b/src/helpers/devices/isDashboardOpen.js new file mode 100644 index 00000000..5f5cc619 --- /dev/null +++ b/src/helpers/devices/isDashboardOpen.js @@ -0,0 +1,22 @@ +// @flow + +import type Transport from '@ledgerhq/hw-transport' + +import { getFirmwareInfo } from 'helpers/common' + +type Result = boolean + +export default async (transport: Transport<*>): Promise => { + try { + const { targetId, version } = await getFirmwareInfo(transport) + if (targetId && version) { + return true + } + + return false + } catch (err) { + const error = Error(err.message) + error.stack = err.stack + throw error + } +} diff --git a/src/helpers/firmware/installFinalFirmware.js b/src/helpers/firmware/installFinalFirmware.js index af3410ac..3c46e27d 100644 --- a/src/helpers/firmware/installFinalFirmware.js +++ b/src/helpers/firmware/installFinalFirmware.js @@ -4,17 +4,14 @@ import type Transport from '@ledgerhq/hw-transport' import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' -type Input = { - firmware: Object, -} - +type Input = Object type Result = * const buildOsuParams = buildParamsFromFirmware('final') -export default async (transport: Transport<*>, data: Input): Result => { +export default async (transport: Transport<*>, firmware: Input): Result => { try { - const osuData = buildOsuParams(data.firmware) + const osuData = buildOsuParams(firmware) await createSocketDialog(transport, '/update/install', osuData) return { success: true } } catch (err) { diff --git a/src/helpers/firmware/installOsuFirmware.js b/src/helpers/firmware/installOsuFirmware.js index fec012c5..5a53fdc0 100644 --- a/src/helpers/firmware/installOsuFirmware.js +++ b/src/helpers/firmware/installOsuFirmware.js @@ -4,18 +4,15 @@ import type Transport from '@ledgerhq/hw-transport' import { createSocketDialog, buildParamsFromFirmware } from 'helpers/common' -type Input = { - devicePath: string, - firmware: Object, -} +type Input = Object type Result = Promise<{ success: boolean, error?: any }> const buildOsuParams = buildParamsFromFirmware('osu') -export default async (transport: Transport<*>, data: Input): Result => { +export default async (transport: Transport<*>, firmware: Input): Result => { try { - const osuData = buildOsuParams(data.firmware) + const osuData = buildOsuParams(firmware) await createSocketDialog(transport, '/update/install', osuData) return { success: true } } catch (err) { diff --git a/src/icons/Trash.js b/src/icons/Trash.js new file mode 100644 index 00000000..a6630967 --- /dev/null +++ b/src/icons/Trash.js @@ -0,0 +1,18 @@ +// @flow + +import React from 'react' + +const path = ( + + + +) + +export default ({ size, ...p }: { size: number }) => ( + + {path} + +) diff --git a/static/i18n/en/manager.yml b/static/i18n/en/manager.yml index f882d5fb..ccbc4011 100644 --- a/static/i18n/en/manager.yml +++ b/static/i18n/en/manager.yml @@ -2,14 +2,16 @@ tabs: apps: Apps device: My device install: Install -allApps: All apps +allApps: Apps +title: Manager +subtitle: Get all your apps here firmwareUpdate: Firmware update -latestFirmware: Latest firmware +latestFirmware: Firmware plugYourDevice: title: Plug your device - desc: Lorem Ipsum is simply dummy text of the printing and typesetting industry’s standard dummy text + desc: Please connect your Ledger device and follow the steps below to access the manager cta: Plug my device errors: - noDevice: Please make sur your device is connected - noDashboard: Please make sure your device is on the dashboard screen - noGenuine: You did not approve request on your device or your device is not genuine \ No newline at end of file + noDevice: Please make sur your device is connected (TEMPLATE NEEDED) + noDashboard: Please make sure your device is on the dashboard screen (TEMPLATED NEEDED) + noGenuine: You did not approve request on your device or your device is not genuine (TEMPLATE NEEDED) \ No newline at end of file diff --git a/static/images/logos/connectDevice.png b/static/images/logos/connectDevice.png new file mode 100755 index 0000000000000000000000000000000000000000..ee3e4e2e22bdfaf3f0f482ad70126c3207385319 GIT binary patch literal 14871 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 literal 0 HcmV?d00001