diff --git a/src/actions/RouterActions.js b/src/actions/RouterActions.js index 8bab4d55..e1101652 100644 --- a/src/actions/RouterActions.js +++ b/src/actions/RouterActions.js @@ -2,6 +2,7 @@ import { push, LOCATION_CHANGE } from 'react-router-redux'; import { routes } from 'support/routes'; +import * as deviceUtils from 'utils/device'; import type { RouterLocationState, @@ -59,6 +60,11 @@ export const paramsValidation = (params: RouterLocationState): PayloadAction => let url: ?string; if (!device.features) { url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`; - } else if (device.features.bootloader_mode) { + } else if (device.mode === 'bootloader') { // device in bootloader doesn't have device_id url = `/device/${device.path}/bootloader`; - } else if (!device.features.initialized) { + } else if (device.mode === 'initialize') { url = `/device/${device.features.device_id}/initialize`; + } else if (device.firmware === 'required') { + url = `/device/${device.features.device_id}/firmware-update`; } else if (typeof device.instance === 'number') { url = `/device/${device.features.device_id}:${device.instance}`; } else { @@ -303,6 +311,24 @@ export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispat } }; +/* +* Go to UpdateBridge page +*/ +export const gotoBridgeUpdate = (): ThunkAction => (dispatch: Dispatch): void => { + dispatch(goto('/bridge')); +}; + +/* +* Go to UpdateFirmware page +* Called from App notification +*/ +export const gotoFirmwareUpdate = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + const { selectedDevice } = getState().wallet; + if (!selectedDevice || !selectedDevice.features) return; + const devUrl: string = `${selectedDevice.features.device_id}${selectedDevice.instance ? `:${selectedDevice.instance}` : ''}`; + dispatch(goto(`/device/${devUrl}/firmware-update`)); +}; + /* * Try to redirect to initial url */ diff --git a/src/actions/TrezorConnectActions.js b/src/actions/TrezorConnectActions.js index 7553edd4..b54a97a0 100644 --- a/src/actions/TrezorConnectActions.js +++ b/src/actions/TrezorConnectActions.js @@ -154,50 +154,50 @@ export const postInit = (): ThunkAction => (dispatch: Dispatch): void => { export const getSelectedDeviceState = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { const selected = getState().wallet.selectedDevice; - if (selected - && selected.connected - && (selected.features && !selected.features.bootloader_mode && selected.features.initialized) - && !selected.state) { - const response = await TrezorConnect.getDeviceState({ - device: { - path: selected.path, - instance: selected.instance, - state: selected.state, - }, - useEmptyPassphrase: !selected.instance, - }); + if (!selected) return; + const isDeviceReady = selected.connected && selected.features && !selected.state && selected.mode === 'normal' && selected.firmware !== 'required'; + if (!isDeviceReady) return; - if (response && response.success) { - dispatch({ - type: CONNECT.AUTH_DEVICE, - device: selected, - state: response.payload.state, - }); - } else { - dispatch({ - type: NOTIFICATION.ADD, - payload: { - devicePath: selected.path, - type: 'error', - title: 'Authentication error', - message: response.payload.error, - cancelable: false, - actions: [ - { - label: 'Try again', - callback: () => { - dispatch({ - type: NOTIFICATION.CLOSE, - payload: { devicePath: selected.path }, - }); - dispatch(getSelectedDeviceState()); - }, + const response = await TrezorConnect.getDeviceState({ + device: { + path: selected.path, + instance: selected.instance, + state: selected.state, + }, + useEmptyPassphrase: !selected.instance, + }); + + if (response && response.success) { + dispatch({ + type: CONNECT.AUTH_DEVICE, + device: selected, + state: response.payload.state, + }); + } else { + dispatch({ + type: NOTIFICATION.ADD, + payload: { + devicePath: selected.path, + type: 'error', + title: 'Authentication error', + message: response.payload.error, + cancelable: false, + actions: [ + { + label: 'Try again', + callback: () => { + dispatch({ + type: NOTIFICATION.CLOSE, + payload: { devicePath: selected.path }, + }); + dispatch(getSelectedDeviceState()); }, - ], - }, - }); - } + }, + ], + }, + }); } + }; export const deviceDisconnect = (device: Device): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 9d68a29d..a7325d25 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -5,6 +5,7 @@ import { DEVICE } from 'trezor-connect'; import * as CONNECT from 'actions/constants/TrezorConnect'; import * as WALLET from 'actions/constants/wallet'; import * as reducerUtils from 'reducers/utils'; +import * as deviceUtils from 'utils/device'; import type { Device, @@ -104,7 +105,7 @@ export const observe = (prevState: State, action: Action): PayloadAction { @@ -123,7 +123,7 @@ const DeviceHeader = ({ {getStatusName(status)} - {icon && !disabled && !isBootloader && icon} + {icon && !disabled && isAccessible && icon} @@ -131,7 +131,7 @@ const DeviceHeader = ({ }; DeviceHeader.propTypes = { - isBootloader: PropTypes.bool, + isAccessible: PropTypes.bool, device: PropTypes.object, icon: PropTypes.element, isHoverable: PropTypes.bool, diff --git a/src/components/notifications/App/Container.js b/src/components/notifications/App/Container.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/notifications/App/components/OnlineStatus/index.js b/src/components/notifications/App/components/OnlineStatus/index.js new file mode 100644 index 00000000..b7712253 --- /dev/null +++ b/src/components/notifications/App/components/OnlineStatus/index.js @@ -0,0 +1,11 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + const { online } = props.wallet; + if (online) return null; + return (); +}; \ No newline at end of file diff --git a/src/components/notifications/App/components/UpdateBridge/index.js b/src/components/notifications/App/components/UpdateBridge/index.js new file mode 100644 index 00000000..e418bb37 --- /dev/null +++ b/src/components/notifications/App/components/UpdateBridge/index.js @@ -0,0 +1,23 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + if (props.connect.transport && props.connect.transport.outdated) { + return ( + + ); + } + return null; +}; \ No newline at end of file diff --git a/src/components/notifications/App/components/UpdateFirmware/index.js b/src/components/notifications/App/components/UpdateFirmware/index.js new file mode 100644 index 00000000..c14c9e29 --- /dev/null +++ b/src/components/notifications/App/components/UpdateFirmware/index.js @@ -0,0 +1,23 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + const { selectedDevice } = props.wallet; + const outdated = selectedDevice && selectedDevice.features && selectedDevice.firmware === 'outdated'; + if (!outdated) return null; + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/notifications/App/index.js b/src/components/notifications/App/index.js index e69de29b..6167cfcf 100644 --- a/src/components/notifications/App/index.js +++ b/src/components/notifications/App/index.js @@ -0,0 +1,49 @@ +/* @flow */ +import * as React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; +import type { State, Dispatch } from 'flowtype'; + +import * as NotificationActions from 'actions/NotificationActions'; +import * as RouterActions from 'actions/RouterActions'; + +import OnlineStatus from './components/OnlineStatus'; +import UpdateBridge from './components/UpdateBridge'; +import UpdateFirmware from './components/UpdateFirmware'; + +export type StateProps = { + connect: $ElementType; + wallet: $ElementType; + children?: React.Node; +} + +export type DispatchProps = { + close: typeof NotificationActions.close; + routerActions: typeof RouterActions; +} + +export type Props = StateProps & DispatchProps; + +type OwnProps = {}; + +const Notifications = (props: Props) => ( + + + + + +); + +const mapStateToProps: MapStateToProps = (state: State): StateProps => ({ + connect: state.connect, + wallet: state.wallet, +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + close: bindActionCreators(NotificationActions.close, dispatch), + routerActions: bindActionCreators(RouterActions, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Notifications); \ No newline at end of file diff --git a/src/constants/bridge.js b/src/constants/bridge.js deleted file mode 100644 index 7c911ebc..00000000 --- a/src/constants/bridge.js +++ /dev/null @@ -1,8 +0,0 @@ -export default [ - { id: 'Windows', value: 'trezor-bridge-2.0.11-win32-install.exe', label: 'Windows' }, - { id: 'macOS', value: 'trezor-bridge-2.0.11.pkg', label: 'macOS' }, - { id: 'Linux', value: 'trezor-bridge_2.0.11_amd64.deb', label: 'Linux 64-bit (deb)' }, - { id: 'Linux-rpm', value: 'trezor-bridge_2.0.11_amd64.rpm', label: 'Linux 64-bit (rpm)' }, - { id: '01', value: 'trezor-bridge_2.0.11_amd32.deb', label: 'Linux 32-bit (deb)' }, - { id: '02', value: 'trezor-bridge_2.0.11_amd32.rpm', label: 'Linux 32-bit (rpm)' }, -]; diff --git a/src/flowtype/index.js b/src/flowtype/index.js index 6c138e58..203033d1 100644 --- a/src/flowtype/index.js +++ b/src/flowtype/index.js @@ -38,6 +38,7 @@ import type { Features, DeviceStatus, DeviceFirmwareStatus, + DeviceMode, DeviceMessageType, TransportMessageType, BlockchainMessageType, @@ -53,6 +54,7 @@ export type AcquiredDevice = $Exact<{ +features: Features, +firmware: DeviceFirmwareStatus, status: DeviceStatus, + +mode: DeviceMode, state: ?string, remember: boolean; // device should be remembered diff --git a/src/reducers/TrezorConnectReducer.js b/src/reducers/TrezorConnectReducer.js index f8adf4dd..6d032e14 100644 --- a/src/reducers/TrezorConnectReducer.js +++ b/src/reducers/TrezorConnectReducer.js @@ -9,12 +9,24 @@ export type SelectedDevice = { instance: ?number; } +export type LatestBridge = { + version: Array; + directory: string; + packages: Array<{ name: string; url: string; signature?: string; preferred: boolean; }>; + changelog: Array; +} + export type State = { initialized: boolean; error: ?string; - transport: ?{ + transport: { type: string; version: string; + outdated: boolean; + bridge: LatestBridge; + } | { + type: null, + bridge: LatestBridge; }; // browserState: { // name: string; @@ -30,7 +42,15 @@ export type State = { const initialState: State = { initialized: false, error: null, - transport: null, + transport: { + type: null, + bridge: { + version: [], + directory: '', + packages: [], + changelog: [], + }, + }, browserState: {}, acquiringDevice: false, }; @@ -63,9 +83,12 @@ export default function connect(state: State = initialState, action: Action): St case TRANSPORT.ERROR: return { ...state, - // error: action.payload, // message is wrapped in "device" field. It's dispatched from TrezorConnect.on(DEVICE_EVENT...) in TrezorConnectService + // error: action.payload.error, // message is wrapped in "device" field. It's dispatched from TrezorConnect.on(DEVICE_EVENT...) in TrezorConnectService error: 'Transport is missing', - transport: null, + transport: { + type: null, + bridge: action.payload.bridge, + }, }; case CONNECT.START_ACQUIRING: diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index 3d6a8de4..5de5aece 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -21,7 +21,7 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => { return state.devices.find((d) => { if (!d.features && d.path === locationState.device) { return true; - } if (d.features && d.features.bootloader_mode && d.path === locationState.device) { + } if (d.mode === 'bootloader' && d.path === locationState.device) { return true; } if (d.features && d.features.device_id === locationState.device && d.instance === instance) { return true; @@ -30,9 +30,6 @@ export const getSelectedDevice = (state: State): ?TrezorDevice => { }); }; -// -export const isSelectedDevice = (current: ?TrezorDevice, device: ?TrezorDevice): boolean => !!((current && device && (current.path === device.path && current.instance === device.instance))); - // find device by id and state export const findDevice = (devices: Array, deviceId: string, deviceState: string /*, instance: ?number*/): ?TrezorDevice => devices.find((d) => { // TODO: && (instance && d.instance === instance) diff --git a/src/utils/device.js b/src/utils/device.js index a8b03564..94924325 100644 --- a/src/utils/device.js +++ b/src/utils/device.js @@ -1,64 +1,99 @@ +/* @flow */ + import colors from 'config/colors'; -const getStatus = (device) => { - let status = 'connected'; - if (device.features && device.features.bootloader_mode) { - status = 'connected-bootloader'; - } else if (!device.connected) { - status = 'disconnected'; - } else if (!device.available) { - status = 'unavailable'; - } else if (device.type === 'acquired') { - if (device.status === 'occupied') { - status = 'used-in-other-window'; +import type { + TrezorDevice, + State, +} from 'flowtype'; + +type Transport = $ElementType<$ElementType, 'transport'>; + +export const getStatus = (device: TrezorDevice): string => { + if (!device.connected) { + return 'disconnected'; + } + if (device.type === 'acquired') { + if (device.mode === 'bootloader') { + return 'bootloader'; } - } else if (device.type === 'unacquired') { - status = 'unacquired'; + if (device.mode === 'initialize') { + return 'initialize'; + } + if (device.firmware === 'required') { + return 'firmware-required'; + } + if (device.status === 'occupied') { + return 'used-in-other-window'; + } + if (device.status === 'used') { + return 'used-in-other-window'; + } + if (device.firmware === 'outdated') { + return 'firmware-recommended'; + } + return 'connected'; } - - return status; + if (!device.available) { // deprecated + return 'unavailable'; + } + if (device.type === 'unacquired') { + return 'unacquired'; + } + if (device.type === 'unreadable') { + return 'unreadable'; + } + return 'unknown'; }; -const getStatusName = (deviceStatus) => { - let statusName; +export const getStatusName = (deviceStatus: string): string => { switch (deviceStatus) { - case 'used-in-other-window': - statusName = 'Used in other window'; - break; case 'connected': - statusName = 'Connected'; - break; - case 'connected-bootloader': - statusName = 'Connected (bootloader mode)'; - break; + return 'Connected'; case 'disconnected': - statusName = 'Disconnected'; - break; + return 'Disconnected'; + case 'bootloader': + return 'Connected (bootloader mode)'; + case 'initialize': + return 'Connected (not initialized)'; + case 'firmware-required': + return 'Connected (update required)'; + case 'firmware-recommended': + return 'Connected (update recommended)'; + case 'used-in-other-window': + return 'Used in other window'; case 'unacquired': - statusName = 'Used in other window'; - break; + return 'Used in other window'; case 'unavailable': - statusName = 'Unavailable'; - break; + return 'Unavailable'; + case 'unreadable': + return 'Unreadable'; default: - statusName = 'Status unknown'; + return 'Status unknown'; } - return statusName; }; -const isWebUSB = transport => !!((transport && transport.version.indexOf('webusb') >= 0)); +export const isWebUSB = (transport: Transport) => !!((transport.type && transport.version.indexOf('webusb') >= 0)); -const isDisabled = (selectedDevice, devices, transport) => { +export const isDisabled = (selectedDevice: TrezorDevice, devices: Array, transport: Transport) => { if (isWebUSB(transport)) return false; // always enabled if webusb if (devices.length < 1) return true; // no devices if (devices.length === 1) { if (!selectedDevice.features) return true; // unacquired, unreadable - if (selectedDevice.features.bootloader_mode || !selectedDevice.features.initialized) return true; // bootlader, not initialized + if (selectedDevice.mode !== 'normal') return true; // bootloader, not initialized + if (selectedDevice.firmware === 'required') return true; // bootloader, not initialized } return false; // default }; -const getVersion = (device) => { +export const isDeviceAccessible = (device: ?TrezorDevice): boolean => { + if (!device || !device.features) return false; + return device.mode === 'normal' && device.firmware !== 'required'; +}; + +export const isSelectedDevice = (current: ?TrezorDevice, device: ?TrezorDevice): boolean => !!((current && device && (current.path === device.path && current.instance === device.instance))); + +export const getVersion = (device: TrezorDevice): string => { let version; if (device.features && device.features.major_version > 1) { version = 'T'; @@ -68,38 +103,23 @@ const getVersion = (device) => { return version; }; -const getStatusColor = (deviceStatus) => { - let color; +export const getStatusColor = (deviceStatus: string): string => { switch (deviceStatus) { - case 'used-in-other-window': - color = colors.WARNING_PRIMARY; - break; case 'connected': - color = colors.GREEN_PRIMARY; - break; - case 'connected-bootloader': - color = colors.WARNING_PRIMARY; - break; - case 'unacquired': - color = colors.WARNING_PRIMARY; - break; + return colors.GREEN_PRIMARY; case 'disconnected': - color = colors.ERROR_PRIMARY; - break; + return colors.ERROR_PRIMARY; + case 'bootloader': + case 'initialize': + case 'firmware-recommended': + case 'used-in-other-window': + case 'unacquired': + return colors.WARNING_PRIMARY; + case 'firmware-required': case 'unavailable': - color = colors.ERROR_PRIMARY; - break; + case 'unreadable': + return colors.ERROR_PRIMARY; default: - color = colors.TEXT_PRIMARY; + return colors.TEXT_PRIMARY; } - return color; -}; - -export { - isWebUSB, - getStatus, - isDisabled, - getStatusName, - getVersion, - getStatusColor, }; \ No newline at end of file diff --git a/src/views/Landing/Container.js b/src/views/Landing/Container.js index 6c891761..2a58e643 100644 --- a/src/views/Landing/Container.js +++ b/src/views/Landing/Container.js @@ -1,10 +1,13 @@ /* @flow */ import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import * as RouterActions from 'actions/RouterActions'; import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; import type { State, Dispatch } from 'flowtype'; import LandingPage from './index'; + export type StateProps = { localStorage: $ElementType, modal: $ElementType, @@ -34,8 +37,8 @@ const mapStateToProps: MapStateToProps = (state: St devices: state.devices, }); -const mapDispatchToProps: MapDispatchToProps = (/* dispatch: Dispatch */): DispatchProps => ({ - +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectFirstAvailableDevice: bindActionCreators(RouterActions.selectFirstAvailableDevice, dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(LandingPage); \ No newline at end of file diff --git a/src/views/Landing/components/InstallBridge/index.js b/src/views/Landing/components/InstallBridge/index.js index 67177f79..c0743e3d 100644 --- a/src/views/Landing/components/InstallBridge/index.js +++ b/src/views/Landing/components/InstallBridge/index.js @@ -4,7 +4,6 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import colors from 'config/colors'; import { FONT_SIZE, FONT_WEIGHT } from 'config/variables'; -import installers from 'constants/bridge'; import { Select } from 'components/Select'; import Link from 'components/Link'; import Button from 'components/Button'; @@ -13,24 +12,25 @@ import P from 'components/Paragraph'; import Icon from 'components/Icon'; import ICONS from 'config/icons'; +import type { State as TrezorConnectState } from 'reducers/TrezorConnectReducer'; + type InstallTarget = { - id: string; value: string; label: string; + signature: ?string; + preferred: boolean; } type State = { - version: string; - target: ?InstallTarget; - url: string; + currentVersion: string; + latestVersion: string; + installers: Array; + target: InstallTarget; + uri: string; } -// import type { Props } from './index'; - type Props = { - browserState: { - osname: string, - }; + transport: $ElementType; } const InstallBridgeWrapper = styled.div` @@ -85,21 +85,21 @@ export default class InstallBridge extends Component { constructor(props: Props) { super(props); - const currentTarget: ?InstallTarget = installers.find(i => i.id === props.browserState.osname); - this.state = { - version: '2.0.12', - url: 'https://wallet.trezor.io/data/bridge/2.0.12/', - target: currentTarget, - }; - } + const installers = props.transport.bridge.packages.map(p => ({ + label: p.name, + value: p.url, + signature: p.signature, + preferred: p.preferred, + })); - componentWillUpdate() { - if (this.props.browserState.osname && !this.state.target) { - const currentTarget: ?InstallTarget = installers.find(i => i.id === this.props.browserState.osname); - this.setState({ - target: currentTarget, - }); - } + const currentTarget: ?InstallTarget = installers.find(i => i.preferred === true); + this.state = { + currentVersion: props.transport.type ? `Your version ${props.transport.version}` : 'Not installed', + latestVersion: props.transport.bridge.version.join('.'), + installers, + target: currentTarget || installers[0], + uri: 'https://wallet.trezor.io/data/', + }; } onChange(value: InstallTarget) { @@ -109,23 +109,26 @@ export default class InstallBridge extends Component { } render() { - if (!this.state.target) { + const { target } = this.state; + if (!target) { return ; } - const { label } = this.state.target; - const url = `${this.state.url}${this.state.target.value}`; + const changelog = this.props.transport.bridge.changelog.map(entry => ( +
  • {entry}
  • + )); + const url = `${this.state.uri}${target.value}`; return ( - TREZOR Bridge.Version {this.state.version} + TREZOR Bridge.{this.state.currentVersion}

    New communication tool to facilitate the connection between your TREZOR and your internet browser.

    this.onChange(val)} - options={installers} + value={target} + onChange={v => this.onChange(v)} + options={this.state.installers} /> @@ -134,12 +137,24 @@ export default class InstallBridge extends Component { color={colors.WHITE} size={30} /> - Download for {label} + Download latest Bridge {this.state.latestVersion} + {target.signature && ( +

    + Check PGP signature + +

    + )}

    - Learn more about latest version in + { changelog } + Learn more about latest versions in { >Changelog

    + {this.props.transport.type && ( + +

    + No, i dont want to upgrade Bridge now, +

    +

    + Take me back to the wallet +

    +
    + )}
    ); } diff --git a/src/views/Landing/index.js b/src/views/Landing/index.js index 13ec9331..00b5facd 100644 --- a/src/views/Landing/index.js +++ b/src/views/Landing/index.js @@ -103,7 +103,7 @@ export default (props: Props) => { {shouldShowUnsupportedBrowser && } - {shouldShowInstallBridge && } + {shouldShowInstallBridge && } {(shouldShowConnectDevice || shouldShowDisconnectDevice) && (
    diff --git a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/DeviceList/index.js b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/DeviceList/index.js index 723e177b..e6c8baa9 100644 --- a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/DeviceList/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/DeviceList/index.js @@ -1,24 +1,33 @@ +/* @flow */ + import React, { Component } from 'react'; import styled from 'styled-components'; import Icon from 'components/Icon'; import DeviceHeader from 'components/DeviceHeader'; +import * as deviceUtils from 'utils/device'; import icons from 'config/icons'; import colors from 'config/colors'; -import { withRouter } from 'react-router-dom'; + +import type { TrezorDevice } from 'flowtype'; +import type { Props as CommonProps } from '../../../common'; + const Wrapper = styled.div``; const IconClick = styled.div``; -class DeviceList extends Component { - sortByInstance(a, b) { +type Props = { + devices: $ElementType; + selectedDevice: $ElementType<$ElementType, 'selectedDevice'>; + onSelectDevice: $ElementType; + forgetDevice: $ElementType; +}; + +class DeviceList extends Component { + sortByInstance(a: TrezorDevice, b: TrezorDevice) { if (!a.instance || !b.instance) return -1; return a.instance > b.instance ? 1 : -1; } - redirectToBootloader(selectedDevice) { - this.props.history.push(`/device/${selectedDevice.features.device_id}/bootloader`); - } - render() { const { devices, selectedDevice, onSelectDevice, forgetDevice, @@ -28,16 +37,11 @@ class DeviceList extends Component { {devices .sort(this.sortByInstance) .map(device => ( - device !== selectedDevice && ( + !deviceUtils.isSelectedDevice(selectedDevice, device) && ( { - if (device.features) { - if (device.features.bootloader_mode) { - this.redirectToBootloader(selectedDevice); - } - } onSelectDevice(device); }} onClickIcon={() => forgetDevice(device)} @@ -69,4 +73,4 @@ class DeviceList extends Component { } } -export default withRouter(DeviceList); \ No newline at end of file +export default DeviceList; \ No newline at end of file diff --git a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js index 7ca2c305..a52bb769 100644 --- a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/components/MenuItems/index.js @@ -42,8 +42,8 @@ class MenuItems extends Component { } showDeviceMenu() { - const device = this.props.device; - return device && device.features && !device.features.bootloader_mode && device.features.initialized; + const { device } = this.props; + return device && device.mode === 'normal'; } showClone() { diff --git a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/index.js b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/index.js index dc34884e..af12c149 100644 --- a/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/index.js +++ b/src/views/Wallet/components/LeftNavigation/components/DeviceMenu/index.js @@ -4,7 +4,7 @@ import styled from 'styled-components'; import TrezorConnect from 'trezor-connect'; import type { TrezorDevice } from 'flowtype'; import Button from 'components/Button'; -import { isWebUSB } from 'utils/device'; +import * as deviceUtils from 'utils/device'; import MenuItems from './components/MenuItems'; import DeviceList from './components/DeviceList'; @@ -28,10 +28,6 @@ type DeviceMenuItem = { } class DeviceMenu extends Component { - mouseDownHandler: (event: MouseEvent) => void; - - blurHandler: (event: FocusEvent) => void; - constructor(props: Props) { super(props); this.mouseDownHandler = this.mouseDownHandler.bind(this); @@ -42,12 +38,33 @@ class DeviceMenu extends Component { window.addEventListener('mousedown', this.mouseDownHandler, false); // window.addEventListener('blur', this.blurHandler, false); const { transport } = this.props.connect; - if (transport && transport.version.indexOf('webusb') >= 0) TrezorConnect.renderWebUSBButton(); + if (transport.type && transport.version.indexOf('webusb') >= 0) TrezorConnect.renderWebUSBButton(); } componentDidUpdate() { const { transport } = this.props.connect; - if (isWebUSB(transport)) TrezorConnect.renderWebUSBButton(); + if (deviceUtils.isWebUSB(transport)) TrezorConnect.renderWebUSBButton(); + } + + componentWillUnmount(): void { + window.removeEventListener('mousedown', this.mouseDownHandler, false); + } + + onDeviceMenuClick(item: DeviceMenuItem, device: TrezorDevice): void { + if (item.type === 'reload') { + this.props.acquireDevice(); + } else if (item.type === 'forget') { + this.props.forgetDevice(device); + } else if (item.type === 'clone') { + this.props.duplicateDevice(device); + } else if (item.type === 'settings') { + this.props.toggleDeviceDropdown(false); + this.props.gotoDeviceSettings(device); + } + } + + blurHandler(): void { + this.props.toggleDeviceDropdown(false); } mouseDownHandler(event: MouseEvent): void { @@ -67,34 +84,16 @@ class DeviceMenu extends Component { } } - blurHandler(): void { - this.props.toggleDeviceDropdown(false); - } + mouseDownHandler: (event: MouseEvent) => void; - onDeviceMenuClick(item: DeviceMenuItem, device: TrezorDevice): void { - if (item.type === 'reload') { - this.props.acquireDevice(); - } else if (item.type === 'forget') { - this.props.forgetDevice(device); - } else if (item.type === 'clone') { - this.props.duplicateDevice(device); - } else if (item.type === 'settings') { - this.props.toggleDeviceDropdown(false); - this.props.gotoDeviceSettings(device); - } - } - - componentWillUnmount(): void { - window.removeEventListener('mousedown', this.mouseDownHandler, false); - } + blurHandler: (event: FocusEvent) => void; showDivider() { return this.props.devices.length > 1; } showMenuItems() { - const { selectedDevice } = this.props.wallet; - return selectedDevice && selectedDevice.features; + return deviceUtils.isDeviceAccessible(this.props.wallet.selectedDevice); } render() { @@ -113,7 +112,7 @@ class DeviceMenu extends Component { forgetDevice={forgetDevice} /> - {isWebUSB(transport) && ( + {deviceUtils.isWebUSB(transport) && ( Check for devices )} diff --git a/src/views/Wallet/components/LeftNavigation/index.js b/src/views/Wallet/components/LeftNavigation/index.js index 2f2c87f5..db79af1f 100644 --- a/src/views/Wallet/components/LeftNavigation/index.js +++ b/src/views/Wallet/components/LeftNavigation/index.js @@ -1,3 +1,5 @@ +/* @flow */ + import * as React from 'react'; import PropTypes from 'prop-types'; import colors from 'config/colors'; @@ -6,10 +8,13 @@ import icons from 'config/icons'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; import styled from 'styled-components'; import DeviceHeader from 'components/DeviceHeader'; +import * as deviceUtils from 'utils/device'; + import AccountMenu from './components/AccountMenu'; import CoinMenu from './components/CoinMenu'; import DeviceMenu from './components/DeviceMenu'; import StickyContainer from './components/StickyContainer'; + import type { Props } from './components/common'; const Header = styled(DeviceHeader)` @@ -105,6 +110,7 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element { @@ -119,11 +125,11 @@ class LeftNavigation extends React.PureComponent { }; } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { dropdownOpened, selectedDevice } = nextProps.wallet; - const hasNetwork = nextProps.location.state && nextProps.location.state.network; - const hasFeatures = selectedDevice && selectedDevice.features; - const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized; + const { location } = nextProps.router; + const hasNetwork = location && location.state.network; + const deviceReady = selectedDevice && selectedDevice.features && selectedDevice.mode === 'normal'; if (dropdownOpened) { this.setState({ shouldRenderDeviceSelection: true }); } else if (hasNetwork) { @@ -176,22 +182,23 @@ class LeftNavigation extends React.PureComponent { ); } - const isDeviceInBootloader = this.props.wallet.selectedDevice.features && this.props.wallet.selectedDevice.features.bootloader_mode; + const { selectedDevice } = props.wallet; + const isDeviceAccessible = deviceUtils.isDeviceAccessible(selectedDevice); return (
    { - if (!isDeviceInBootloader || this.props.devices.length > 1) { + if (isDeviceAccessible || this.props.devices.length > 1) { this.handleOpen(); } }} device={this.props.wallet.selectedDevice} - disabled={isDeviceInBootloader && this.props.devices.length === 1} + disabled={!isDeviceAccessible && this.props.devices.length === 1} isOpen={this.props.wallet.dropdownOpened} icon={( @@ -211,7 +218,7 @@ class LeftNavigation extends React.PureComponent { /> {this.state.shouldRenderDeviceSelection && } - {!isDeviceInBootloader && menu} + {isDeviceAccessible && menu}