diff --git a/package.json b/package.json index bdbcee25..10113d6c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-0": "^6.24.1", "electron-builder": "^19.49.0", - "electron-rebuild": "^1.6.0", + "electron-rebuild": "^1.6.1", "electron-webpack": "1.11.0", "eslint": "^4.13.1", "eslint-config-airbnb": "^16.1.0", @@ -69,7 +69,7 @@ "eslint-plugin-react": "^7.5.1", "flow-bin": "^0.63.1", "flow-typed": "^2.2.3", - "prettier": "^1.9.2", + "prettier": "^1.10.2", "react-hot-loader": "^4.0.0-beta.12" } } diff --git a/src/actions/devices.js b/src/actions/devices.js index d11081ec..c5af4ad5 100644 --- a/src/actions/devices.js +++ b/src/actions/devices.js @@ -2,11 +2,11 @@ // eslint-disable import/prefer-default-export -import type { Device } from 'types/common' +import type { Device, Devices } from 'types/common' -type devicesUpdateType = (Array) => { type: string, payload: Array } -export const devicesUpdate: devicesUpdateType = payload => ({ - type: 'DEVICES_UPDATE', +export type deviceChooseType = (Device | null) => { type: string, payload: Device | null } +export const deviceChoose: deviceChooseType = payload => ({ + type: 'DEVICE_CHOOSE', payload, }) @@ -21,3 +21,9 @@ export const deviceRemove: devicesRemoveType = payload => ({ type: 'DEVICE_REMOVE', payload, }) + +type devicesUpdateType = Devices => { type: string, payload: Devices } +export const devicesUpdate: devicesUpdateType = payload => ({ + type: 'DEVICES_UPDATE', + payload, +}) diff --git a/src/components/Home.js b/src/components/Home.js index b408f4ae..0d37bf87 100644 --- a/src/components/Home.js +++ b/src/components/Home.js @@ -1,20 +1,32 @@ // @flow import React, { PureComponent } from 'react' -import { compose } from 'redux' import { connect } from 'react-redux' -import { translate } from 'react-i18next' + +import type { MapStateToProps } from 'react-redux' +import type { Device } from 'types/common' + +import { getCurrentDevice } from 'reducers/devices' + +import Box from 'components/base/Box' + +const mapStateToProps: MapStateToProps<*, *, *> = state => ({ + currentDevice: getCurrentDevice(state), +}) type Props = { - devices: Array, - t: (string, ?Object) => string, + currentDevice: Device | null, } class Home extends PureComponent { render() { - const { devices, t } = this.props - return
{t('common.connectedDevices', { count: devices.length })}
+ const { currentDevice } = this.props + return currentDevice !== null ? ( + + Your current device: {currentDevice.path} + + ) : null } } -export default compose(connect(({ devices }: Props): Object => ({ devices })), translate())(Home) +export default connect(mapStateToProps)(Home) diff --git a/src/components/TopBar.js b/src/components/TopBar.js index 06ec7f34..80ad0f9b 100644 --- a/src/components/TopBar.js +++ b/src/components/TopBar.js @@ -1,17 +1,124 @@ // @flow -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' +import { connect } from 'react-redux' + +import type { MapStateToProps, MapDispatchToProps } from 'react-redux' +import type { Device, Devices } from 'types/common' +import type { deviceChooseType } from 'actions/devices' + +import { getDevices, getCurrentDevice } from 'reducers/devices' + +import { deviceChoose } from 'actions/devices' import Box from 'components/base/Box' +import Overlay from 'components/base/Overlay' + +const mapStateToProps: MapStateToProps<*, *, *> = state => ({ + devices: getDevices(state), + currentDevice: getCurrentDevice(state), +}) + +const mapDispatchToProps: MapDispatchToProps<*, *, *> = { + deviceChoose, +} + +type Props = { + devices: Devices, + currentDevice: Device, + deviceChoose: deviceChooseType, +} + +type State = { + changeDevice: boolean, +} + +const hasDevices = props => props.currentDevice === null && props.devices.length > 0 + +class TopBar extends PureComponent { + state = { + changeDevice: hasDevices(this.props), + } + + componentWillReceiveProps(nextProps) { + if (hasDevices(nextProps) && this.props.currentDevice !== null) { + this.setState({ + changeDevice: true, + }) + } + } + + handleChangeDevice = () => { + const { devices } = this.props + + if (devices.length > 0) { + this.setState({ + changeDevice: true, + }) + } + } + + handleSelectDevice = device => () => { + const { deviceChoose } = this.props + + deviceChoose(device) + + this.setState({ + changeDevice: false, + }) + } -class TopBar extends PureComponent<{}> { render() { + const { devices } = this.props + const { changeDevice } = this.state + return ( - - {''} - + + {changeDevice && ( + + {devices.map(device => ( + + {device.path} + + ))} + + )} + + + + ) } } -export default TopBar +const CountDevices = ({ count, onChangeDevice } = { count: Number, onChangeDevice: Function }) => ( + + + + + {count} + +) + +const DeviceIcon = props => ( + + + +) + +export default connect(mapStateToProps, mapDispatchToProps)(TopBar) diff --git a/src/components/Wrapper.js b/src/components/Wrapper.js index 92025554..902fc036 100644 --- a/src/components/Wrapper.js +++ b/src/components/Wrapper.js @@ -1,34 +1,23 @@ // @flow -import React, { Fragment } from 'react' -import { compose } from 'redux' -import { connect } from 'react-redux' +import React from 'react' import { Route } from 'react-router' import { translate } from 'react-i18next' import Box from 'components/base/Box' -import Overlay from 'components/base/Overlay' import Home from 'components/Home' import SideBar from 'components/SideBar' import TopBar from 'components/TopBar' -const Wrapper = ({ devices, t }: { devices: Array, t: string => string }) => ( - - {devices.length === 0 ? ( - - {t('common.connectDevice')} - - ) : ( - - - - - - - - )} - +const Wrapper = () => ( + + + + + + + ) -export default compose(connect(({ devices }): Object => ({ devices })), translate())(Wrapper) +export default translate()(Wrapper) diff --git a/src/components/base/Overlay.js b/src/components/base/Overlay.js index d0e01fed..93be10ad 100644 --- a/src/components/base/Overlay.js +++ b/src/components/base/Overlay.js @@ -3,10 +3,12 @@ import React from 'react' import styled from 'styled-components' +import { rgba } from 'styles/helpers' + import Box from 'components/base/Box' const Overlay = styled(({ sticky, ...props }) => )` - background-color: ${p => p.theme.colors.night}; + background-color: ${p => rgba(p.theme.colors.night, 0.4)}; position: fixed; ` diff --git a/src/i18n/en/translation.yml b/src/i18n/en/translation.yml index 60aebb64..7c818448 100644 --- a/src/i18n/en/translation.yml +++ b/src/i18n/en/translation.yml @@ -1,6 +1,6 @@ common: ok: Okay cancel: Cancel - connectDevice: Please connect your device connectedDevices: You have {{count}} device connected + connectedDevices_0: You don't have device connected connectedDevices_plural: You have {{count}} devices connected diff --git a/src/main/bridge.js b/src/main/bridge.js new file mode 100644 index 00000000..d2be2236 --- /dev/null +++ b/src/main/bridge.js @@ -0,0 +1,29 @@ +// @flow + +import { fork } from 'child_process' +import { ipcMain } from 'electron' +import { resolve } from 'path' + +ipcMain.on('msg', (event: any, payload) => { + const { type, data } = payload + + const compute = fork('./usb', { + cwd: resolve(__dirname, './'), + }) + + const send = (msgType, data) => { + event.sender.send('msg', { + type: msgType, + data, + }) + } + + compute.send({ type, data }) + compute.on('message', payload => { + const { type, data, options = {} } = payload + send(type, data) + if (options.kill) { + compute.kill() + } + }) +}) diff --git a/src/main/index.js b/src/main/index.js index ac1236a2..9ad87886 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,5 +1,5 @@ // @flow require('../globals') -require('./ledger') +require('./bridge') require('./app') diff --git a/src/main/ledger.js b/src/main/ledger.js deleted file mode 100644 index 7d403af1..00000000 --- a/src/main/ledger.js +++ /dev/null @@ -1,70 +0,0 @@ -// @flow - -import { ipcMain } from 'electron' -import { isLedgerDevice } from 'ledgerco/lib/utils' -import ledgerco, { comm_node } from 'ledgerco' -import objectPath from 'object-path' - -import HID from 'ledger-node-js-hid' - -async function getWalletInfos(path: string, wallet: string) { - if (wallet === 'btc') { - const comm = new comm_node(new HID.HID(path), true, 0, false) - const btc = new ledgerco.btc(comm) - const walletInfos = await btc.getWalletPublicKey_async("44'/0'/0'/0") - return walletInfos - } - throw new Error('invalid wallet') -} - -let isListenDevices = false - -const handlers = { - devices: { - listen: send => { - if (isListenDevices) { - return - } - - isListenDevices = true - - HID.listenDevices.start() - - HID.listenDevices.events.on( - 'add', - device => isLedgerDevice(device) && send('device.add', device), - ) - HID.listenDevices.events.on( - 'remove', - device => isLedgerDevice(device) && send('device.remove', device), - ) - }, - all: send => send('devices.update', HID.devices().filter(isLedgerDevice)), - }, - requestWalletInfos: async (send, { path, wallet }) => { - try { - const publicKey = await getWalletInfos(path, wallet) - send('receiveWalletInfos', { path, publicKey }) - } catch (err) { - send('failWalletInfos', { path, err: err.stack }) - } - }, -} - -ipcMain.on('msg', (event: *, payload) => { - const { type, data } = payload - - const handler = objectPath.get(handlers, type) - if (!handler) { - return - } - - const send = (msgType: string, data: *) => { - event.sender.send('msg', { - type: msgType, - data, - }) - } - - handler(send, data) -}) diff --git a/src/main/usb.js b/src/main/usb.js new file mode 100644 index 00000000..194b61fb --- /dev/null +++ b/src/main/usb.js @@ -0,0 +1,66 @@ +process.title = 'ledger-wallet-desktop-usb' + +const HID = require('ledger-node-js-hid') +const objectPath = require('object-path') +const { isLedgerDevice } = require('ledgerco/lib/utils') +const ledgerco = require('ledgerco') + +function send(type, data, options = { kill: true }) { + process.send({ type, data, options }) +} + +async function getWalletInfos(path, wallet) { + if (wallet === 'btc') { + const comm = new ledgerco.comm_node(new HID.HID(path), true, 0, false) + const btc = new ledgerco.btc(comm) + const walletInfos = await btc.getWalletPublicKey_async("44'/0'/0'/0") + return walletInfos + } + throw new Error('invalid wallet') +} + +let isListenDevices = false + +const handlers = { + devices: { + listen: () => { + if (isListenDevices) { + return + } + + isListenDevices = true + + const handleChangeDevice = eventName => device => + isLedgerDevice(device) && send(eventName, device, { kill: false }) + + HID.listenDevices.start() + + HID.listenDevices.events.on('add', handleChangeDevice('device.add')) + HID.listenDevices.events.on('remove', handleChangeDevice('device.remove')) + }, + all: () => send('devices.update', HID.devices().filter(isLedgerDevice)), + }, + wallet: { + infos: { + request: async ({ path, wallet }) => { + try { + const publicKey = await getWalletInfos(path, wallet) + send('wallet.infos.success', { path, publicKey }) + } catch (err) { + send('wallet.infos.fail', { path, err: err.stack || err }) + } + }, + }, + }, +} + +process.on('message', payload => { + const { type, data } = payload + + const handler = objectPath.get(handlers, type) + if (!handler) { + return + } + + handler(data) +}) diff --git a/src/reducers/devices.js b/src/reducers/devices.js index 817a21f3..d74c0c17 100644 --- a/src/reducers/devices.js +++ b/src/reducers/devices.js @@ -2,13 +2,57 @@ import { handleActions } from 'redux-actions' -const state = [] +import type { Device, Devices } from 'types/common' -const handlers = { - DEVICES_UPDATE: (state, { payload: devices }) => devices, - DEVICE_ADD: (state, { payload: device }) => - [...state, device].filter((v, i, s) => s.findIndex(t => t.path === v.path) === i), - DEVICE_REMOVE: (state, { payload: device }) => state.filter(d => d.path !== device.path), +type stateType = { + currentDevice: Device | null, + devices: Devices, +} +const state = { + currentDevice: null, + devices: [], +} + +function setCurrentDevice(state) { + return { + ...state, + currentDevice: state.devices.length === 1 ? state.devices[0] : state.currentDevice, + } +} + +const handlers: Object = { + DEVICES_UPDATE: (state: stateType, { payload: devices }: { payload: Devices }) => + setCurrentDevice({ + ...state, + devices, + }), + DEVICE_ADD: (state: stateType, { payload: device }: { payload: Device }) => + setCurrentDevice({ + ...state, + devices: [...state.devices, device].filter( + (v, i, s) => s.findIndex(t => t.path === v.path) === i, + ), + }), + DEVICE_REMOVE: (state: stateType, { payload: device }: { payload: Device }) => ({ + ...state, + currentDevice: + state.currentDevice !== null && state.currentDevice.path === device.path + ? null + : state.currentDevice, + devices: state.devices.filter(d => d.path !== device.path), + }), + DEVICE_CHOOSE: (state: stateType, { payload: currentDevice }: { payload: Device }) => ({ + ...state, + currentDevice, + }), +} + +export function getCurrentDevice(state: Object) { + return state.devices.currentDevice +} + +export function getDevices(state: Object) { + return state.devices.devices } export default handleActions(handlers, state) diff --git a/src/renderer/i18n.js b/src/renderer/i18n.js index 82f635e3..39129a61 100644 --- a/src/renderer/i18n.js +++ b/src/renderer/i18n.js @@ -16,4 +16,9 @@ i18n.use(Backend).init({ }, }) +i18n.services.pluralResolver.addRule('en', { + numbers: [0, 1, 'plural'], + plurals: n => Number(n >= 2 ? 2 : n), +}) + export default i18n diff --git a/src/renderer/initEvents.js b/src/renderer/initEvents.js index ad8995ad..129388bb 100644 --- a/src/renderer/initEvents.js +++ b/src/renderer/initEvents.js @@ -23,7 +23,7 @@ export default (store: Object) => { update: devices => { store.dispatch(devicesUpdate(devices)) if (devices.length) { - send('requestWalletInfos', { + send('wallet.infos.request', { path: devices[0].path, wallet: 'btc', }) @@ -34,11 +34,15 @@ export default (store: Object) => { add: device => store.dispatch(deviceAdd(device)), remove: device => store.dispatch(deviceRemove(device)), }, - receiveWalletInfos: ({ path, publicKey }) => { - console.log({ path, publicKey }) - }, - failWalletInfos: ({ path, err }) => { - console.log({ path, err }) + wallet: { + infos: { + success: ({ path, publicKey }) => { + console.log({ path, publicKey }) + }, + fail: ({ path, err }) => { + console.log({ path, err }) + }, + }, }, } diff --git a/src/styles/global.js b/src/styles/global.js index d22f3d22..7c4bb783 100644 --- a/src/styles/global.js +++ b/src/styles/global.js @@ -11,7 +11,6 @@ injectGlobal` font: inherit; color: inherit; user-select: none; - cursor: default; min-width: 0; } diff --git a/src/types/common.js b/src/types/common.js index 24f905de..80c1c1bb 100644 --- a/src/types/common.js +++ b/src/types/common.js @@ -3,4 +3,9 @@ export type Device = { vendorId: string, productId: string, + path: string, } + +export type Devices = Array + +export type T = (string, ?Object) => string diff --git a/yarn.lock b/yarn.lock index 9a2f9d12..e4b8cdc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2263,6 +2263,10 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + detect-node@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" @@ -2518,12 +2522,13 @@ electron-publish@19.52.0: fs-extra-p "^4.5.0" mime "^2.1.0" -electron-rebuild@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.0.tgz#e8d26f4d8e9fe5388df35864b3658e5cfd4dcb7e" +electron-rebuild@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.1.tgz#3c7ab64db31e5e78ef76fedd7a53aec087b723c5" dependencies: colors "^1.1.2" debug "^2.6.3" + detect-libc "^1.0.3" fs-extra "^3.0.1" node-abi "^2.0.0" node-gyp "^3.6.0" @@ -5487,9 +5492,9 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.2.tgz#96bc2132f7a32338e6078aeb29727178c6335827" +prettier@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" pretty-bytes@^1.0.2: version "1.0.4"