Browse Source

Merge pull request #1 from loeck/feature/choose-device

Feature/choose device
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
4c660d0d07
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      src/actions/devices.js
  2. 26
      src/components/Home.js
  3. 107
      src/components/TopBar.js
  4. 31
      src/components/Wrapper.js
  5. 4
      src/components/base/Overlay.js
  6. 2
      src/i18n/en/translation.yml
  7. 2
      src/main/app.js
  8. 18
      src/main/ledger.js
  9. 43
      src/reducers/devices.js
  10. 5
      src/renderer/i18n.js
  11. 16
      src/renderer/initEvents.js
  12. 5
      src/types/common.js

65
src/actions/devices.js

@ -2,22 +2,59 @@
// eslint-disable import/prefer-default-export
import type { Device } from 'types/common'
import type { Dispatch } from 'redux'
type devicesUpdateType = (Array<Device>) => { type: string, payload: Array<Device> }
export const devicesUpdate: devicesUpdateType = payload => ({
type: 'DEVICES_UPDATE',
payload,
})
import { getDevices, getCurrentDevice } from 'reducers/devices'
type devicesAddType = Device => { type: string, payload: Device }
export const deviceAdd: devicesAddType = payload => ({
type: 'DEVICE_ADD',
payload,
})
import type { Device, Devices } from 'types/common'
type devicesRemoveType = Device => { type: string, payload: Device }
export const deviceRemove: devicesRemoveType = payload => ({
type: 'DEVICE_REMOVE',
export type deviceChooseType = (?Device) => { type: string, payload: ?Device }
export const deviceChoose: deviceChooseType = payload => ({
type: 'DEVICE_CHOOSE',
payload,
})
type deviceChooseFirstType = () => (Dispatch<any>, () => { devices: Devices }) => void
export const deviceChooseFirst: deviceChooseFirstType = () => (dispatch, getState) => {
const devices = getDevices(getState())
// If we detect only 1 device, we choose it
if (devices.length === 1) {
dispatch(deviceChoose(devices[0]))
}
}
type devicesAddType = Device => (Dispatch<any>) => void
export const deviceAdd: devicesAddType = payload => dispatch => {
dispatch({
type: 'DEVICE_ADD',
payload,
})
dispatch(deviceChooseFirst())
}
type devicesRemoveType = Device => (Dispatch<any>, () => { devices: Devices }) => void
export const deviceRemove: devicesRemoveType = payload => (dispatch, getState) => {
dispatch({
type: 'DEVICE_REMOVE',
payload,
})
const currentDevice = getCurrentDevice(getState())
// If we disconnect the currentDevice we reset it
if (currentDevice.path === payload.path) {
dispatch(deviceChoose(null))
}
}
type devicesUpdateType = Devices => (Dispatch<any>) => void
export const devicesUpdate: devicesUpdateType = payload => dispatch => {
dispatch({
type: 'DEVICES_UPDATE',
payload,
})
dispatch(deviceChooseFirst())
}

26
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<Object>,
t: (string, ?Object) => string,
currentDevice: Device | null,
}
class Home extends PureComponent<Props> {
render() {
const { devices, t } = this.props
return <div>{t('common.connectedDevices', { count: devices.length })}</div>
const { currentDevice } = this.props
return currentDevice !== null ? (
<Box style={{ wordBreak: 'break-word' }} p={20}>
Your current device: {currentDevice.path}
</Box>
) : null
}
}
export default compose(connect(({ devices }: Props): Object => ({ devices })), translate())(Home)
export default connect(mapStateToProps)(Home)

107
src/components/TopBar.js

@ -1,17 +1,112 @@
// @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 | null,
deviceChoose: deviceChooseType,
}
type State = {
changeDevice: boolean,
}
const hasDevices = props => props.currentDevice === null && props.devices.length > 0
class TopBar extends PureComponent<Props, State> {
state = {
changeDevice: hasDevices(this.props),
}
componentWillReceiveProps(nextProps) {
if (hasDevices(nextProps) && this.props.currentDevice !== null) {
this.setState({
changeDevice: true,
})
}
}
handleChangeDevice = () =>
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 (
<Box bg="white" noShrink style={{ height: 60 }}>
{''}
</Box>
<Fragment>
{changeDevice && (
<Overlay p={20}>
{devices.map(device => (
<Box
key={device.path}
color="white"
bg="night"
onClick={this.handleSelectDevice(device)}
>
{device.path}
</Box>
))}
</Overlay>
)}
<Box bg="white" noShrink style={{ height: 60 }} justify="center" align="flex-end">
<CountDevices count={devices.length} onChangeDevice={this.handleChangeDevice} />
</Box>
</Fragment>
)
}
}
export default TopBar
const CountDevices = ({ count, onChangeDevice } = { count: Number, onChangeDevice: Function }) => (
<Box color="night" mr={20} horizontal flow={10} onClick={onChangeDevice}>
<Box>
<DeviceIcon height={20} width={20} />
</Box>
<Box>{count}</Box>
</Box>
)
const DeviceIcon = props => (
<svg {...props} viewBox="0 0 19.781 19.781">
<path
d="M14.507 0L9.8 4.706a2.92 2.92 0 0 0-1.991.854l-6.89 6.889a2.93 2.93 0 0 0 0 4.143l2.33 2.33a2.925 2.925 0 0 0 4.141 0l6.89-6.891c.613-.612.895-1.43.851-2.232l4.589-4.588L14.507 0zm.386 8.792a2.927 2.927 0 0 0-.611-.902l-2.33-2.331a2.945 2.945 0 0 0-1.08-.682l3.637-3.636 3.968 3.969-3.584 3.582zm.693-5.381l-.949.949-1.26-1.26.949-.949 1.26 1.26zm1.881 1.882l-.949.949-1.26-1.26.948-.949 1.261 1.26z"
fill="currentColor"
/>
</svg>
)
export default connect(mapStateToProps, mapDispatchToProps)(TopBar)

31
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<Object>, t: string => string }) => (
<Fragment>
{devices.length === 0 ? (
<Overlay align="center" justify="center">
<Box color="white">{t('common.connectDevice')}</Box>
</Overlay>
) : (
<Box grow horizontal>
<SideBar />
<Box grow bg="cream">
<TopBar />
<Route path="/" component={Home} />
</Box>
</Box>
)}
</Fragment>
const Wrapper = () => (
<Box grow horizontal>
<SideBar />
<Box grow bg="cream">
<TopBar />
<Route path="/" component={Home} />
</Box>
</Box>
)
export default compose(connect(({ devices }): Object => ({ devices })), translate())(Wrapper)
export default translate()(Wrapper)

4
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 }) => <Box sticky {...props} />)`
background-color: ${p => p.theme.colors.night};
background-color: ${p => rgba(p.theme.colors.night, 0.4)};
position: fixed;
`

2
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

2
src/main/app.js

@ -35,7 +35,7 @@ function createMainWindow() {
return window
}
// dsq
// Quit application when all windows are closed
app.on('window-all-closed', () => {
// On macOS it is common for applications to stay open

18
src/main/ledger.js

@ -41,13 +41,17 @@ const handlers = {
},
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 })
}
wallet: {
infos: {
request: async (send, { 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 })
}
},
},
},
}

43
src/reducers/devices.js

@ -2,13 +2,44 @@
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,
devices: Devices,
}
const state = {
currentDevice: null,
devices: [],
}
const handlers: Object = {
DEVICES_UPDATE: (state: stateType, { payload: devices }: { payload: Devices }) => ({
...state,
devices,
}),
DEVICE_ADD: (state: stateType, { payload: device }: { payload: Device }) => ({
...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,
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)

5
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

16
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 })
},
},
},
}

5
src/types/common.js

@ -3,4 +3,9 @@
export type Device = {
vendorId: string,
productId: string,
path: string,
}
export type Devices = Array<Device>
export type T = (string, ?Object) => string

Loading…
Cancel
Save