1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-02-28 00:12:21 +00:00

Merge pull request #70 from trezor/fix-left-navigation-bootloader

fix: LeftNavigation+device in bootloader mode
This commit is contained in:
Szymon Lesisz 2018-10-03 19:14:13 +02:00 committed by GitHub
commit 49f0d089ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 159 additions and 122 deletions

View File

@ -1,12 +1,9 @@
import React, { Component } from 'react'; import React from 'react';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'components/Icon';
import icons from 'config/icons';
import { import {
getStatusColor, getStatusColor,
getStatusName, getStatusName,
isDisabled,
getStatus, getStatus,
getVersion, getVersion,
} from 'utils/device'; } from 'utils/device';
@ -19,15 +16,17 @@ const Wrapper = styled.div`
width: 320px; width: 320px;
display: flex; display: flex;
align-items: center; align-items: center;
background: ${colors.WHITE}; background: ${props => (props.disabled ? colors.GRAY_LIGHT : 'transparent')};
border-radius: 4px 0 0 0; background: ${props => (props.isSelected ? colors.WHITE : 'transparent')};
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
${props => props.isOpen && css` border-radius: 4px 0 0 0;
box-shadow: ${props => (props.disabled ? 'none' : '0 3px 8px rgba(0, 0, 0, 0.04)')};
${props => (props.isOpen || !props.isSelected) && css`
box-shadow: none; box-shadow: none;
`} `}
${props => props.isHoverable && css` ${props => props.isHoverable && !props.disabled && css`
&:hover { &:hover {
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};
} }
@ -43,7 +42,7 @@ const ClickWrapper = styled.div`
cursor: pointer; cursor: pointer;
${props => props.disabled && css` ${props => props.disabled && css`
cursor: initial; cursor: default;
`} `}
`; `;
@ -71,18 +70,6 @@ const Status = styled.div`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
`; `;
const Counter = styled.div`
border: 1px solid ${colors.DIVIDER};
border-radius: 50%;
color: ${colors.TEXT_SECONDARY};
width: 24px;
height: 24px;
line-height: 22px;
text-align: center;
font-size: 11px;
margin-right: 8px;
`;
const IconWrapper = styled.div` const IconWrapper = styled.div`
padding-right: 25px; padding-right: 25px;
display: flex; display: flex;
@ -104,36 +91,29 @@ const Dot = styled.div`
height: 10px; height: 10px;
`; `;
class DeviceHeader extends Component {
constructor(props) {
super(props);
this.state = {
clicked: false,
};
}
isDisabled(device, devices, transport) { const DeviceHeader = ({
return isDisabled(device, devices, transport); isOpen,
} icon,
device,
handleClickWrapper() { isHoverable = true,
this.setState({ clicked: true }); onClickWrapper,
if (!this.props.disabled) { isBootloader = false,
this.props.onClickWrapper(); disabled = false,
} isSelected = false,
} }) => {
render() {
const {
isOpen, icon, device, devices, transport, isHoverable,
} = this.props;
const status = getStatus(device); const status = getStatus(device);
const disabled = isDisabled(device, devices, transport);
const deviceCount = devices.length;
return ( return (
<Wrapper isOpen={isOpen} isHoverable={isHoverable}> <Wrapper
<ClickWrapper disabled={disabled} onClick={() => this.handleClickWrapper()}> isSelected={isSelected}
isOpen={isOpen}
isHoverable={isHoverable}
disabled={disabled}
>
<ClickWrapper
disabled={disabled}
onClick={onClickWrapper}
>
<ImageWrapper> <ImageWrapper>
<Dot color={getStatusColor(status)} /> <Dot color={getStatusColor(status)} />
<TrezorImage model={getVersion(device)} /> <TrezorImage model={getVersion(device)} />
@ -143,33 +123,21 @@ class DeviceHeader extends Component {
<Status>{getStatusName(status)}</Status> <Status>{getStatusName(status)}</Status>
</LabelWrapper> </LabelWrapper>
<IconWrapper> <IconWrapper>
{icon && icon} {icon && !disabled && !isBootloader && icon}
{!icon && deviceCount > 1 && <Counter>{deviceCount}</Counter>}
{!icon && !disabled && (
<Icon
canAnimate={this.state.clicked === true}
isActive={isOpen}
size={25}
color={colors.TEXT_SECONDARY}
icon={icons.ARROW_DOWN}
/>
)
}
</IconWrapper> </IconWrapper>
</ClickWrapper> </ClickWrapper>
</Wrapper> </Wrapper>
); );
} };
}
DeviceHeader.propTypes = { DeviceHeader.propTypes = {
isBootloader: PropTypes.bool,
device: PropTypes.object, device: PropTypes.object,
devices: PropTypes.array,
transport: PropTypes.object,
icon: PropTypes.element, icon: PropTypes.element,
isHoverable: PropTypes.bool, isHoverable: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
isSelected: PropTypes.bool,
onClickWrapper: PropTypes.func.isRequired, onClickWrapper: PropTypes.func.isRequired,
}; };

View File

@ -2,7 +2,9 @@ import colors from 'config/colors';
const getStatus = (device) => { const getStatus = (device) => {
let status = 'connected'; let status = 'connected';
if (!device.connected) { if (device.features && device.features.bootloader_mode) {
status = 'connected-bootloader';
} else if (!device.connected) {
status = 'disconnected'; status = 'disconnected';
} else if (!device.available) { } else if (!device.available) {
status = 'unavailable'; status = 'unavailable';
@ -26,6 +28,9 @@ const getStatusName = (deviceStatus) => {
case 'connected': case 'connected':
statusName = 'Connected'; statusName = 'Connected';
break; break;
case 'connected-bootloader':
statusName = 'Connected (bootloader mode)';
break;
case 'disconnected': case 'disconnected':
statusName = 'Disconnected'; statusName = 'Disconnected';
break; break;
@ -43,7 +48,15 @@ const getStatusName = (deviceStatus) => {
const isWebUSB = transport => !!((transport && transport.version.indexOf('webusb') >= 0)); const isWebUSB = transport => !!((transport && transport.version.indexOf('webusb') >= 0));
const isDisabled = (selectedDevice, devices, transport) => (devices.length < 1 && !isWebUSB(transport)) || (devices.length === 1 && !selectedDevice.features && !isWebUSB(transport)); const isDisabled = (selectedDevice, devices, 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
}
return false; // default
};
const getVersion = (device) => { const getVersion = (device) => {
let version; let version;
@ -64,6 +77,9 @@ const getStatusColor = (deviceStatus) => {
case 'connected': case 'connected':
color = colors.GREEN_PRIMARY; color = colors.GREEN_PRIMARY;
break; break;
case 'connected-bootloader':
color = colors.WARNING_PRIMARY;
break;
case 'unacquired': case 'unacquired':
color = colors.WARNING_PRIMARY; color = colors.WARNING_PRIMARY;
break; break;

View File

@ -21,7 +21,6 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
connect: state.connect, connect: state.connect,
accounts: state.accounts, accounts: state.accounts,
router: state.router, router: state.router,
deviceDropdownOpened: state.wallet.dropdownOpened,
fiat: state.fiat, fiat: state.fiat,
localStorage: state.localStorage, localStorage: state.localStorage,
discovery: state.discovery, discovery: state.discovery,

View File

@ -4,6 +4,7 @@ import Icon from 'components/Icon';
import DeviceHeader from 'components/DeviceHeader'; import DeviceHeader from 'components/DeviceHeader';
import icons from 'config/icons'; import icons from 'config/icons';
import colors from 'config/colors'; import colors from 'config/colors';
import { withRouter } from 'react-router-dom';
const Wrapper = styled.div``; const Wrapper = styled.div``;
const IconClick = styled.div``; const IconClick = styled.div``;
@ -14,8 +15,14 @@ class DeviceList extends Component {
return a.instance > b.instance ? 1 : -1; return a.instance > b.instance ? 1 : -1;
} }
redirectToBootloader(selectedDevice) {
this.props.history.push(`/device/${selectedDevice.features.device_id}/bootloader`);
}
render() { render() {
const { devices, selectedDevice, onSelectDevice } = this.props; const {
devices, selectedDevice, onSelectDevice, forgetDevice,
} = this.props;
return ( return (
<Wrapper> <Wrapper>
{devices {devices
@ -23,14 +30,23 @@ class DeviceList extends Component {
.map(device => ( .map(device => (
device !== selectedDevice && ( device !== selectedDevice && (
<DeviceHeader <DeviceHeader
key={`${device.instanceLabel}`} key={device.state || device.path}
onClickWrapper={() => onSelectDevice(device)} isBootloader={device.features.bootloader_mode}
onClickIcon={() => this.onDeviceMenuClick({ type: 'forget', label: '' }, device)} onClickWrapper={() => {
if (device.features) {
if (device.features.bootloader_mode) {
this.redirectToBootloader(selectedDevice);
}
onSelectDevice(device);
}
}}
onClickIcon={() => forgetDevice(device)}
icon={( icon={(
<React.Fragment>
<IconClick onClick={(event) => { <IconClick onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
this.onDeviceMenuClick({ type: 'forget', label: '' }, device); forgetDevice(device);
}} }}
> >
<Icon <Icon
@ -39,6 +55,7 @@ class DeviceList extends Component {
color={colors.TEXT_SECONDARY} color={colors.TEXT_SECONDARY}
/> />
</IconClick> </IconClick>
</React.Fragment>
)} )}
device={device} device={device}
devices={devices} devices={devices}
@ -52,4 +69,4 @@ class DeviceList extends Component {
} }
} }
export default DeviceList; export default withRouter(DeviceList);

View File

@ -41,6 +41,11 @@ class MenuItems extends Component {
} }
} }
showDeviceMenu() {
const device = this.props.device;
return device && device.features && !device.features.bootloader_mode && device.features.initialized;
}
showClone() { showClone() {
return this.props.device && this.props.device.features.passphrase_protection && this.props.device.connected && this.props.device.available; return this.props.device && this.props.device.features.passphrase_protection && this.props.device.connected && this.props.device.available;
} }
@ -50,6 +55,7 @@ class MenuItems extends Component {
} }
render() { render() {
if (!this.showDeviceMenu()) return null;
return ( return (
<Wrapper> <Wrapper>
<Item onClick={() => this.onDeviceMenuClick('settings', this.props.device)}> <Item onClick={() => this.onDeviceMenuClick('settings', this.props.device)}>

View File

@ -98,7 +98,7 @@ class DeviceMenu extends Component<Props> {
} }
render() { render() {
const { devices, onSelectDevice } = this.props; const { devices, onSelectDevice, forgetDevice } = this.props;
const { transport } = this.props.connect; const { transport } = this.props.connect;
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
@ -110,6 +110,7 @@ class DeviceMenu extends Component<Props> {
devices={devices} devices={devices}
selectedDevice={selectedDevice} selectedDevice={selectedDevice}
onSelectDevice={onSelectDevice} onSelectDevice={onSelectDevice}
forgetDevice={forgetDevice}
/> />
<ButtonWrapper> <ButtonWrapper>
{isWebUSB(transport) && ( {isWebUSB(transport) && (

View File

@ -8,7 +8,6 @@ export type StateProps = {
connect: $ElementType<State, 'connect'>, connect: $ElementType<State, 'connect'>,
accounts: $ElementType<State, 'accounts'>, accounts: $ElementType<State, 'accounts'>,
router: $ElementType<State, 'router'>, router: $ElementType<State, 'router'>,
deviceDropdownOpened: boolean,
fiat: $ElementType<State, 'fiat'>, fiat: $ElementType<State, 'fiat'>,
localStorage: $ElementType<State, 'localStorage'>, localStorage: $ElementType<State, 'localStorage'>,
discovery: $ElementType<State, 'discovery'>, discovery: $ElementType<State, 'discovery'>,

View File

@ -1,5 +1,3 @@
/* @flow */
import * as React from 'react'; import * as React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colors from 'config/colors'; import colors from 'config/colors';
@ -16,6 +14,18 @@ import type { Props } from './components/common';
const Header = styled(DeviceHeader)``; const Header = styled(DeviceHeader)``;
const Counter = styled.div`
border: 1px solid ${colors.DIVIDER};
border-radius: 50%;
color: ${colors.TEXT_SECONDARY};
width: 24px;
height: 24px;
line-height: 22px;
text-align: center;
font-size: 11px;
margin-right: 8px;
`;
const TransitionGroupWrapper = styled(TransitionGroup)` const TransitionGroupWrapper = styled(TransitionGroup)`
width: 640px; width: 640px;
`; `;
@ -103,26 +113,26 @@ class LeftNavigation extends React.PureComponent<Props, State> {
this.state = { this.state = {
animationType: hasNetwork ? 'slide-left' : null, animationType: hasNetwork ? 'slide-left' : null,
shouldRenderDeviceSelection: false, shouldRenderDeviceSelection: false,
clicked: false,
}; };
} }
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps) {
const { deviceDropdownOpened } = nextProps; const { dropdownOpened, selectedDevice } = nextProps.wallet;
const { selectedDevice } = nextProps.wallet; const hasNetwork = nextProps.location.state && nextProps.location.state.network;
const hasNetwork = nextProps.router.location.state && nextProps.router.location.state.network; const hasFeatures = selectedDevice && selectedDevice.features;
const deviceReady = selectedDevice && selectedDevice.features && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized; const deviceReady = hasFeatures && !selectedDevice.features.bootloader_mode && selectedDevice.features.initialized;
if (dropdownOpened) {
if (deviceDropdownOpened) {
this.setState({ shouldRenderDeviceSelection: true }); this.setState({ shouldRenderDeviceSelection: true });
} else if (hasNetwork) { } else if (hasNetwork) {
this.setState({ this.setState({
shouldRenderDeviceSelection: false, shouldRenderDeviceSelection: false,
animationType: 'slide-left', animationType: 'slide-left',
}); });
} else if (deviceReady) { } else {
this.setState({ this.setState({
shouldRenderDeviceSelection: false, shouldRenderDeviceSelection: false,
animationType: 'slide-right', animationType: deviceReady ? 'slide-right' : null,
}); });
} }
} }
@ -139,7 +149,8 @@ class LeftNavigation extends React.PureComponent<Props, State> {
} }
handleOpen() { handleOpen() {
this.props.toggleDeviceDropdown(!this.props.deviceDropdownOpened); this.setState({ clicked: true });
this.props.toggleDeviceDropdown(!this.props.wallet.dropdownOpened);
} }
shouldRenderCoins() { shouldRenderCoins() {
@ -163,22 +174,42 @@ class LeftNavigation extends React.PureComponent<Props, State> {
); );
} }
const isDeviceInBootloader = this.props.wallet.selectedDevice.features && this.props.wallet.selectedDevice.features.bootloader_mode;
return ( return (
<StickyContainer <StickyContainer
location={this.props.router.location.pathname} location={this.props.location.pathname}
deviceSelection={this.props.deviceDropdownOpened} deviceSelection={this.props.wallet.dropdownOpened}
> >
<Header <Header
onClickWrapper={() => this.handleOpen()} isSelected
isHoverable={false}
onClickWrapper={() => {
if (!isDeviceInBootloader || this.props.devices.length > 1) {
this.handleOpen();
}
}}
device={this.props.wallet.selectedDevice} device={this.props.wallet.selectedDevice}
transport={this.props.connect.transport} disabled={isDeviceInBootloader && this.props.devices.length === 1}
devices={this.props.devices} isOpen={this.props.wallet.dropdownOpened}
isOpen={this.props.deviceDropdownOpened} icon={(
<React.Fragment>
{this.props.devices.length > 1 && (
<Counter>{this.props.devices.length}</Counter>
)}
<Icon
canAnimate={this.state.clicked === true}
isActive={this.props.wallet.dropdownOpened}
size={25}
color={colors.TEXT_SECONDARY}
icon={icons.ARROW_DOWN}
/>
</React.Fragment>
)}
{...this.props} {...this.props}
/> />
<Body> <Body>
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />}
{menu} {!isDeviceInBootloader && menu}
</Body> </Body>
<Footer key="sticky-footer"> <Footer key="sticky-footer">
<Help> <Help>
@ -200,7 +231,6 @@ LeftNavigation.propTypes = {
connect: PropTypes.object, connect: PropTypes.object,
accounts: PropTypes.array, accounts: PropTypes.array,
router: PropTypes.object, router: PropTypes.object,
deviceDropdownOpened: PropTypes.bool,
fiat: PropTypes.array, fiat: PropTypes.array,
localStorage: PropTypes.object, localStorage: PropTypes.object,
discovery: PropTypes.array, discovery: PropTypes.array,
@ -209,6 +239,7 @@ LeftNavigation.propTypes = {
pending: PropTypes.array, pending: PropTypes.array,
toggleDeviceDropdown: PropTypes.func, toggleDeviceDropdown: PropTypes.func,
selectedDevice: PropTypes.object,
}; };