Browse Source

Read memory infos from device and display it on manager page

master
meriadec 7 years ago
parent
commit
1e7c6a2cb2
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 151
      src/components/ManagerPage/AppsList.js
  2. 75
      src/components/ManagerPage/DeviceInfos.js
  3. 41
      src/components/ManagerPage/MemInfos.js
  4. 170
      src/components/ManagerPage/index.js
  5. 19
      src/components/ManagerPage/stories/MemInfos.stories.js
  6. 2
      src/components/base/Box/index.js
  7. 3
      src/internals/usb/manager/constants.js
  8. 36
      src/internals/usb/manager/helpers.js
  9. 8
      src/internals/usb/manager/index.js
  10. 10
      src/types/common.js
  11. 3
      static/i18n/en/manager.yml
  12. 3
      yarn.lock

151
src/components/ManagerPage/AppsList.js

@ -0,0 +1,151 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { runJob } from 'renderer/events'
import Box from 'components/base/Box'
import Modal, { ModalBody } from 'components/base/Modal'
import type { Device } from 'types/common'
import ManagerApp from './ManagerApp'
const List = styled(Box).attrs({
horizontal: true,
m: -2,
})`
flex-wrap: wrap;
`
let CACHED_APPS = null
const ICONS_FALLBACK = {
bitcoin_testnet: 'bitcoin',
}
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type jobHandlerOptions = {
job: string,
successResponse: string,
errorResponse: string,
}
type LedgerApp = {
name: string,
icon: string,
app: Object,
}
type Props = {
device: Device,
}
type State = {
status: Status,
error: string | null,
appsList: LedgerApp[],
}
class AppsList extends PureComponent<Props, State> {
state = {
status: 'loading',
error: null,
appsList: [],
}
componentDidMount() {
this.fetchList()
}
componentWillUnmount() {
this._unmounted = true
}
_unmounted = false
async fetchList() {
const appsList =
CACHED_APPS ||
(await runJob({
channel: 'usb',
job: 'manager.listApps',
successResponse: 'manager.listAppsSuccess',
errorResponse: 'manager.listAppsError',
}))
CACHED_APPS = appsList
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
}
}
createDeviceJobHandler = (options: jobHandlerOptions) => (args: { app: any }) => async () => {
const appParams = args.app
this.setState({ status: 'busy' })
try {
const { job, successResponse, errorResponse } = options
const { device: { path: devicePath } } = this.props
const data = { appParams, devicePath }
await runJob({ channel: 'usb', job, successResponse, errorResponse, data })
this.setState({ status: 'success' })
} catch (err) {
this.setState({ status: 'error', error: err.message })
}
}
handleInstall = this.createDeviceJobHandler({
job: 'manager.installApp',
successResponse: 'device.appInstalled',
errorResponse: 'device.appInstallError',
})
handleUninstall = this.createDeviceJobHandler({
job: 'manager.uninstallApp',
successResponse: 'device.appUninstalled',
errorResponse: 'device.appUninstallError',
})
handleCloseModal = () => this.setState({ status: 'idle' })
render() {
const { status, error } = this.state
return (
<List>
{this.state.appsList.map(c => (
<ManagerApp
key={c.name}
name={c.name}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstall(c)}
onUninstall={this.handleUninstall(c)}
/>
))}
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody p={6} align="center" justify="center" style={{ height: 300 }}>
{status === 'busy' ? (
<Box>{'Loading...'}</Box>
) : status === 'error' ? (
<Box>
<div>{'error happened'}</div>
{error}
<button onClick={this.handleCloseModal}>close</button>
</Box>
) : status === 'success' ? (
<Box>
{'success'}
<button onClick={this.handleCloseModal}>close</button>
</Box>
) : null}
</ModalBody>
)}
/>
</List>
)
}
}
export default AppsList

75
src/components/ManagerPage/DeviceInfos.js

@ -0,0 +1,75 @@
// @flow
import React, { PureComponent } from 'react'
import { runJob } from 'renderer/events'
import Text from 'components/base/Text'
import Box, { Card } from 'components/base/Box'
import Button from 'components/base/Button'
import type { Device, MemoryInfos } from 'types/common'
import MemInfos from './MemInfos'
type Props = {
device: Device,
}
type State = {
memoryInfos: ?MemoryInfos,
isLoading: boolean,
}
class DeviceInfos extends PureComponent<Props, State> {
state = {
isLoading: false,
memoryInfos: null,
}
handleGetMemInfos = async () => {
try {
this.setState({ isLoading: true })
const { device: { path: devicePath } } = this.props
const memoryInfos = await runJob({
channel: 'usb',
job: 'manager.getMemInfos',
successResponse: 'device.getMemInfosSuccess',
errorResponse: 'device.getMemInfosError',
data: { devicePath },
})
this.setState({ memoryInfos, isLoading: false })
} catch (err) {
this.setState({ isLoading: false })
}
}
render() {
const { device } = this.props
const { memoryInfos, isLoading } = this.state
const title = (
<Text>
{device.manufacturer}
<Text ff="Museo Sans|Bold">{` ${device.product}`}</Text>
</Text>
)
return (
<Card title={title} p={6}>
{memoryInfos ? (
<MemInfos memoryInfos={memoryInfos} />
) : (
<Box flow={2}>
<Box horizontal>
<Button primary onClick={this.handleGetMemInfos} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Read device memory'}
</Button>
</Box>
{isLoading && <Box>{'If asked, confirm operation on device'}</Box>}
</Box>
)}
</Card>
)
}
}
export default DeviceInfos

41
src/components/ManagerPage/MemInfos.js

@ -0,0 +1,41 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import Box from 'components/base/Box'
import type { MemoryInfos } from 'types/common'
const Container = styled(Box).attrs({
bg: 'lightgrey',
horizontal: true,
})`
border-radius: ${p => p.theme.radii[1]}px;
overflow: hidden;
height: 24px;
`
const Step = styled(Box).attrs({
bg: p => p.theme.colors[p.c || 'grey'],
px: 1,
color: 'white',
})`
width: ${p => (p.last ? '' : `${p.percent}%`)};
flex-grow: ${p => (p.last ? '1' : '')};
text-align: ${p => (p.last ? 'right' : '')};
`
export default function MemInfos(props: { memoryInfos: MemoryInfos }) {
const { memoryInfos: infos } = props
const totalSize = infos.applicationsSize + infos.systemSize
const appPercent = infos.applicationsSize * 100 / totalSize
return (
<Container>
<Step c="wallet" percent={appPercent}>{`${Math.round(
infos.applicationsSize / 1000,
)}kb`}</Step>
<Step last>{`${Math.round(infos.freeSize / 1000)}kb`}</Step>
</Container>
)
}

170
src/components/ManagerPage/index.js

@ -1,171 +1,67 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import { compose } from 'redux'
import type { Device } from 'types/common'
import type { Device, T } from 'types/common'
import { runJob } from 'renderer/events'
import { getCurrentDevice } from 'reducers/devices'
import { getCurrentDevice, getDevices } from 'reducers/devices'
import DeviceMonit from 'components/DeviceMonit'
import Box from 'components/base/Box'
import Modal, { ModalBody } from 'components/base/Modal'
import Pills from 'components/base/Pills'
import ManagerApp from './ManagerApp'
const ICONS_FALLBACK = {
bitcoin_testnet: 'bitcoin',
}
const List = styled(Box).attrs({
horizontal: true,
m: -2,
})`
flex-wrap: wrap;
`
import AppsList from './AppsList'
import DeviceInfos from './DeviceInfos'
const mapStateToProps = state => ({
device: getCurrentDevice(state),
nbDevices: getDevices(state).length,
})
const TABS = [{ key: 'apps', value: 'apps' }, { key: 'device', value: 'device' }]
type Props = {
t: T,
device: Device,
}
type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error'
type LedgerApp = {
name: string,
icon: string,
app: Object,
nbDevices: number,
}
type State = {
status: Status,
error: string | null,
appsList: LedgerApp[],
currentTab: 'apps' | 'device',
}
class ManagerPage extends PureComponent<Props, State> {
state = {
status: 'loading',
error: null,
appsList: [],
}
componentDidMount() {
this.fetchList()
}
componentWillUnmount() {
this._unmounted = true
}
_unmounted = false
async fetchList() {
const appsList = await runJob({
channel: 'usb',
job: 'manager.listApps',
successResponse: 'manager.listAppsSuccess',
errorResponse: 'manager.listAppsError',
})
if (!this._unmounted) {
this.setState({ appsList, status: 'idle' })
}
}
createDeviceJobHandler = options => ({ app: appParams }) => async () => {
this.setState({ status: 'busy' })
try {
const { job, successResponse, errorResponse } = options
const {
device: { path: devicePath },
} = this.props
const data = { appParams, devicePath }
await runJob({ channel: 'usb', job, successResponse, errorResponse, data })
this.setState({ status: 'success' })
} catch (err) {
this.setState({ status: 'error', error: err.message })
}
currentTab: 'device',
}
handleInstall = this.createDeviceJobHandler({
job: 'manager.installApp',
successResponse: 'device.appInstalled',
errorResponse: 'device.appInstallError',
})
handleUninstall = this.createDeviceJobHandler({
job: 'manager.uninstallApp',
successResponse: 'device.appUninstalled',
errorResponse: 'device.appUninstallError',
})
handleCloseModal = () => this.setState({ status: 'idle' })
renderList = () => (
<List>
{this.state.appsList.map(c => (
<ManagerApp
key={c.name}
name={c.name}
icon={ICONS_FALLBACK[c.icon] || c.icon}
onInstall={this.handleInstall(c)}
onUninstall={this.handleUninstall(c)}
/>
))}
</List>
)
handleTabChange = t => this.setState({ currentTab: t.value })
render() {
const { status, error } = this.state
const { device, t, nbDevices } = this.props
const { currentTab } = this.state
if (!device) {
return 'eu... connecte ton device?'
}
const tabs = TABS.map(i => {
let label = t(`manager:tabs.${i.key}`)
if (i.key === 'device') {
label += ` (${nbDevices})`
}
return { ...i, label }
})
return (
<DeviceMonit
render={deviceStatus => (
<Box>
{deviceStatus === 'unconnected' && (
<Box style={{ height: 500 }} align="center" justify="center">
<Box fontSize={8}>{'Connect your device'}</Box>
</Box>
)}
{deviceStatus === 'connected' && (
<Box>
{status === 'loading' ? (
<Box ff="Museo Sans|Bold">{'Loading app list...'}</Box>
) : (
this.renderList()
)}
</Box>
)}
<Modal
isOpened={status !== 'idle' && status !== 'loading'}
render={() => (
<ModalBody p={6} align="center" justify="center" style={{ height: 300 }}>
{status === 'busy' ? (
<Box>{'Loading...'}</Box>
) : status === 'error' ? (
<Box>
<div>{'error happened'}</div>
{error}
<button onClick={this.handleCloseModal}>close</button>
</Box>
) : status === 'success' ? (
<Box>
{'success'}
<button onClick={this.handleCloseModal}>close</button>
</Box>
) : null}
</ModalBody>
)}
/>
<Fragment>
<Pills items={tabs} activeKey={currentTab} onChange={this.handleTabChange} mb={6} />
{currentTab === 'apps' && <AppsList device={device} />}
{currentTab === 'device' && (
<Box flow={4}>
<DeviceInfos device={device} />
</Box>
)}
/>
</Fragment>
)
}
}

19
src/components/ManagerPage/stories/MemInfos.stories.js

@ -0,0 +1,19 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import MemInfos from '../MemInfos'
const memoryInfos = {
applicationsSize: 36862,
freeSize: 118784,
systemSize: 171776,
totalAppSlots: 30,
usedAppSlots: 2,
}
const stories = storiesOf('Components', module)
stories.add('MemInfos', () => <MemInfos memoryInfos={memoryInfos} />)

2
src/components/base/Box/index.js

@ -53,7 +53,7 @@ const Box = styled.div`
const RawCard = styled(Box).attrs({ bg: 'white', p: 3, boxShadow: 0, borderRadius: 1 })``
export const Card = ({ title, ...props }: { title?: string }) => {
export const Card = ({ title, ...props }: { title?: any }) => {
if (title) {
return (
<Box flow={4} grow>

3
src/internals/usb/manager/constants.js

@ -1,5 +1,8 @@
// Socket endpoint
export const BASE_SOCKET_URL = 'ws://api.ledgerwallet.com/update/install'
// If you want to test locally with https://github.com/LedgerHQ/ledger-update-python-api
// export const BASE_SOCKET_URL = 'ws://localhost:3001/update'
// List of APDUS
export const APDUS = {

36
src/internals/usb/manager/helpers.js

@ -4,7 +4,6 @@ import CommNodeHid from '@ledgerhq/hw-transport-node-hid'
import chalk from 'chalk'
import Websocket from 'ws'
import qs from 'qs'
import noop from 'lodash/noop'
import type Transport from '@ledgerhq/hw-transport'
import type { IPCSend } from 'types/electron'
@ -23,10 +22,10 @@ type Message = {
}
type LedgerAppParams = {
firmware: string,
firmwareKey: string,
delete: string,
deleteKey: string,
firmware?: string,
firmwareKey?: string,
delete?: string,
deleteKey?: string,
}
/**
@ -54,8 +53,8 @@ export function createTransportHandler(
try {
const transport: Transport<*> = await CommNodeHid.open(devicePath)
// $FlowFixMe
await action(transport, params)
send(successResponse)
const data = await action(transport, params)
send(successResponse, data)
} catch (err) {
if (!err) {
send(errorResponse, { message: 'Unknown error...' })
@ -72,7 +71,7 @@ export async function installApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerAppParams },
): Promise<void> {
return createSocketDialog(transport, appParams)
return createSocketDialog(transport, '/install', appParams)
}
/**
@ -82,13 +81,19 @@ export async function uninstallApp(
transport: Transport<*>,
{ appParams }: { appParams: LedgerAppParams },
): Promise<void> {
return createSocketDialog(transport, {
return createSocketDialog(transport, '/install', {
...appParams,
firmware: appParams.delete,
firmwareKey: appParams.deleteKey,
})
}
export async function getMemInfos(transport: Transport<*>): Promise<Object> {
const { targetId } = await getFirmwareInfo(transport)
// Dont ask me about this `perso_11`: I don't know. But we need it.
return createSocketDialog(transport, '/get-mem-infos', { targetId, perso: 'perso_11' })
}
/**
* Send data through ws
*/
@ -142,10 +147,11 @@ async function bulk(ws: WebsocketType, transport: Transport<*>, msg: Message) {
* Open socket connection with firmware api, and init a dialog
* with the device
*/
function createSocketDialog(transport: Transport<*>, appParams: LedgerAppParams) {
function createSocketDialog(transport: Transport<*>, endpoint: string, appParams: LedgerAppParams) {
return new Promise(async (resolve, reject) => {
try {
const url = `${BASE_SOCKET_URL}?${qs.stringify(appParams)}`
let lastData
const url = `${BASE_SOCKET_URL}${endpoint}?${qs.stringify(appParams)}`
log('WS CONNECTING', url)
const ws: WebsocketType = new Websocket(url)
@ -154,14 +160,18 @@ function createSocketDialog(transport: Transport<*>, appParams: LedgerAppParams)
ws.on('close', () => {
log('WS CLOSED')
resolve()
resolve(lastData)
})
ws.on('message', async rawMsg => {
const handlers = {
exchange: msg => exchange(ws, transport, msg),
bulk: msg => bulk(ws, transport, msg),
success: noop,
success: msg => {
if (msg.data) {
lastData = msg.data
}
},
error: msg => {
log('WS ERROR', ':(')
throw new Error(msg.data)

8
src/internals/usb/manager/index.js

@ -18,7 +18,7 @@
import type { IPCSend } from 'types/electron'
import axios from 'axios'
import { createTransportHandler, installApp, uninstallApp } from './helpers'
import { createTransportHandler, installApp, uninstallApp, getMemInfos } from './helpers'
export default (send: IPCSend) => ({
installApp: createTransportHandler(send, {
@ -33,6 +33,12 @@ export default (send: IPCSend) => ({
errorResponse: 'device.appUninstallError',
}),
getMemInfos: createTransportHandler(send, {
action: getMemInfos,
successResponse: 'device.getMemInfosSuccess',
errorResponse: 'device.getMemInfosError',
}),
listApps: async () => {
try {
const { data } = await axios.get('https://api.ledgerwallet.com/update/applications')

10
src/types/common.js

@ -30,3 +30,13 @@ export type SettingsMoney = {
export type Settings = SettingsProfile & SettingsDisplay & SettingsMoney
export type T = (?string, ?Object) => string
// -------------------- Manager
export type MemoryInfos = {
applicationsSize: number,
freeSize: number,
systemSize: number,
totalAppSlots: number,
usedAppSlots: number,
}

3
static/i18n/en/manager.yml

@ -0,0 +1,3 @@
tabs:
apps: Apps
device: My device

3
yarn.lock

@ -7984,9 +7984,6 @@ ledger-test-library@KhalilBellakrid/ledger-test-library-nodejs#7d37482:
dependencies:
axios "^0.17.1"
bindings "^1.3.0"
electron "^1.8.2"
electron-builder "^20.0.4"
electron-rebuild "^1.7.3"
nan "^2.6.2"
prebuild-install "^2.2.2"

Loading…
Cancel
Save