1
0
mirror of https://github.com/trezor/trezor-wallet synced 2025-01-09 15:40:55 +00:00

merge master

This commit is contained in:
slowbackspace 2019-01-22 15:06:19 +01:00
commit 2be71ddea2
25 changed files with 400 additions and 132 deletions

View File

@ -19,6 +19,9 @@ export type BlockchainAction = {
type: typeof BLOCKCHAIN.UPDATE_FEE, type: typeof BLOCKCHAIN.UPDATE_FEE,
shortcut: string, shortcut: string,
feeLevels: Array<BlockchainFeeLevel>, feeLevels: Array<BlockchainFeeLevel>,
} | {
type: typeof BLOCKCHAIN.START_SUBSCRIBE,
shortcut: string,
} }
// Conditionally subscribe to blockchain backend // Conditionally subscribe to blockchain backend
@ -52,6 +55,11 @@ export const subscribe = (networkName: string): PromiseAction<void> => async (di
const network = config.networks.find(c => c.shortcut === networkName); const network = config.networks.find(c => c.shortcut === networkName);
if (!network) return; if (!network) return;
dispatch({
type: BLOCKCHAIN.START_SUBSCRIBE,
shortcut: network.shortcut,
});
switch (network.type) { switch (network.type) {
case 'ethereum': case 'ethereum':
await dispatch(EthereumBlockchainActions.subscribe(networkName)); await dispatch(EthereumBlockchainActions.subscribe(networkName));

View File

@ -18,11 +18,11 @@ import * as EthereumSendFormActions from './ethereum/SendFormActions';
import * as RippleSendFormActions from './ripple/SendFormActions'; import * as RippleSendFormActions from './ripple/SendFormActions';
export type SendFormAction = { export type SendFormAction = {
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
networkType: 'ethereum', networkType: 'ethereum',
state: EthereumState, state: EthereumState,
} | { } | {
type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE, type: typeof SEND.INIT | typeof SEND.VALIDATION | typeof SEND.CHANGE | typeof SEND.CLEAR,
networkType: 'ripple', networkType: 'ripple',
state: RippleState, state: RippleState,
} | { } | {

View File

@ -1,4 +1,5 @@
/* @flow */ /* @flow */
export const START_SUBSCRIBE: 'blockchain__start_subscribe' = 'blockchain__start_subscribe';
export const READY: 'blockchain__ready' = 'blockchain__ready'; export const READY: 'blockchain__ready' = 'blockchain__ready';
export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee'; export const UPDATE_FEE: 'blockchain__update_fee' = 'blockchain__update_fee';

View File

@ -7,3 +7,4 @@ export const TX_SENDING: 'send__tx_sending' = 'send__tx_sending';
export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete'; export const TX_COMPLETE: 'send__tx_complete' = 'send__tx_complete';
export const TX_ERROR: 'send__tx_error' = 'send__tx_error'; export const TX_ERROR: 'send__tx_error' = 'send__tx_error';
export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced'; export const TOGGLE_ADVANCED: 'send__toggle_advanced' = 'send__toggle_advanced';
export const CLEAR: 'send__clear' = 'send__clear';

View File

@ -152,6 +152,41 @@ export const toggleAdvanced = (): Action => ({
networkType: 'ethereum', networkType: 'ethereum',
}); });
/*
* Called from UI from "clear" button
*/
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormEthereum;
if (!network) return;
// clear transaction draft from session storage
dispatch(SessionStorageActions.clear());
const gasPrice: BigNumber = await dispatch(BlockchainActions.getGasPrice(network.shortcut, network.defaultGasPrice));
const gasLimit = network.defaultGasLimit.toString();
const feeLevels = ValidationActions.getFeeLevels(network.symbol, gasPrice, gasLimit);
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
dispatch({
type: SEND.CLEAR,
networkType: 'ethereum',
state: {
...initialState,
networkName: network.shortcut,
networkSymbol: network.symbol,
currency: network.symbol,
feeLevels,
selectedFeeLevel,
recommendedGasPrice: gasPrice.toString(),
gasLimit,
gasPrice: gasPrice.toString(),
advanced,
},
});
};
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
@ -613,4 +648,5 @@ export default {
onNonceChange, onNonceChange,
onDataChange, onDataChange,
onSend, onSend,
onClear,
}; };

View File

@ -119,6 +119,38 @@ export const toggleAdvanced = (): Action => ({
networkType: 'ripple', networkType: 'ripple',
}); });
/*
* Called from UI from "clear" button
*/
export const onClear = (): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
const { network } = getState().selectedAccount;
const { advanced } = getState().sendFormRipple;
if (!network) return;
// clear transaction draft from session storage
dispatch(SessionStorageActions.clear());
const blockchainFeeLevels = dispatch(BlockchainActions.getFeeLevels(network));
const feeLevels = dispatch(ValidationActions.getFeeLevels(blockchainFeeLevels));
const selectedFeeLevel = ValidationActions.getSelectedFeeLevel(feeLevels, initialState.selectedFeeLevel);
dispatch({
type: SEND.CLEAR,
networkType: 'ripple',
state: {
...initialState,
networkName: network.shortcut,
networkSymbol: network.symbol,
feeLevels,
selectedFeeLevel,
fee: network.fee.defaultFee,
sequence: '1',
advanced,
},
});
};
/* /*
* Called from UI on "address" field change * Called from UI on "address" field change
*/ */
@ -368,4 +400,5 @@ export default {
onFeeChange, onFeeChange,
onDestinationTagChange, onDestinationTagChange,
onSend, onSend,
onClear,
}; };

View File

@ -37,6 +37,11 @@ const Wrapper = styled.button`
background: ${colors.GREEN_TERTIARY}; background: ${colors.GREEN_TERTIARY};
} }
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
${props => props.isDisabled && css` ${props => props.isDisabled && css`
pointer-events: none; pointer-events: none;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
@ -48,6 +53,10 @@ const Wrapper = styled.button`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
border: 1px solid ${colors.DIVIDER}; border: 1px solid ${colors.DIVIDER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
}
&:hover { &:hover {
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};
background: ${colors.DIVIDER}; background: ${colors.DIVIDER};
@ -64,6 +73,11 @@ const Wrapper = styled.button`
border: 0px; border: 0px;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
&:focus {
color: ${colors.TEXT_PRIMARY};
box-shadow: none;
}
&:hover, &:hover,
&:active { &:active {
color: ${colors.TEXT_PRIMARY}; color: ${colors.TEXT_PRIMARY};

View File

@ -15,6 +15,7 @@ const Wrapper = styled.div`
position: relative; position: relative;
height: 70px; height: 70px;
width: 320px; width: 320px;
z-index: 10;
display: flex; display: flex;
align-items: center; align-items: center;
background: ${props => (props.disabled ? colors.GRAY_LIGHT : 'transparent')}; background: ${props => (props.disabled ? colors.GRAY_LIGHT : 'transparent')};

View File

@ -27,14 +27,20 @@ type Props = {
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
position: relative; position: relative;
padding: 24px 48px 9px 24px; display: flex;
justify-content: center;
color: ${props => getPrimaryColor(props.type)};
background: ${props => getSecondaryColor(props.type)};
`;
const Content = styled.div`
width: 100%;
max-width: 1170px;
padding: 24px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
text-align: left; text-align: left;
justify-content: center;
align-items: center; align-items: center;
color: ${props => getPrimaryColor(props.type)};
background: ${props => getSecondaryColor(props.type)};
`; `;
const Body = styled.div` const Body = styled.div`
@ -42,7 +48,6 @@ const Body = styled.div`
`; `;
const Message = styled.div` const Message = styled.div`
padding-bottom: 13px;
font-size: ${FONT_SIZE.SMALL}; font-size: ${FONT_SIZE.SMALL};
`; `;
@ -56,7 +61,9 @@ const CloseClick = styled.div`
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
padding: 20px 10px 0 0; padding-right: inherit;
padding-top: inherit;
cursor: pointer;
`; `;
const StyledIcon = styled(Icon)` const StyledIcon = styled(Icon)`
@ -86,7 +93,6 @@ const ActionContent = styled.div`
display: flex; display: flex;
justify-content: right; justify-content: right;
align-items: flex-end; align-items: flex-end;
padding-bottom: 14px;
`; `;
const Notification = (props: Props): React$Element<string> => { const Notification = (props: Props): React$Element<string> => {
@ -94,43 +100,45 @@ const Notification = (props: Props): React$Element<string> => {
return ( return (
<Wrapper className={props.className} type={props.type}> <Wrapper className={props.className} type={props.type}>
{props.loading && <Loader size={50} /> } <Content>
{props.cancelable && ( {props.loading && <Loader size={50} /> }
<CloseClick onClick={() => close()}> {props.cancelable && (
<Icon <CloseClick onClick={() => close()}>
color={getPrimaryColor(props.type)} <Icon
icon={icons.CLOSE} color={getPrimaryColor(props.type)}
size={20} icon={icons.CLOSE}
/> size={20}
</CloseClick> />
)} </CloseClick>
<Body>
<IconWrapper>
<StyledIcon
color={getPrimaryColor(props.type)}
icon={getIcon(props.type)}
/>
</IconWrapper>
<Texts>
<Title>{ props.title }</Title>
{ props.message ? <Message>{props.message}</Message> : '' }
</Texts>
</Body>
<AdditionalContent>
{props.actions && props.actions.length > 0 && (
<ActionContent>
{props.actions.map(action => (
<NotificationButton
key={action.label}
type={props.type}
isLoading={props.isActionInProgress}
onClick={() => { close(); action.callback(); }}
>{action.label}
</NotificationButton>
))}
</ActionContent>
)} )}
</AdditionalContent> <Body>
<IconWrapper>
<StyledIcon
color={getPrimaryColor(props.type)}
icon={getIcon(props.type)}
/>
</IconWrapper>
<Texts>
<Title>{ props.title }</Title>
{ props.message ? <Message>{props.message}</Message> : '' }
</Texts>
</Body>
<AdditionalContent>
{props.actions && props.actions.length > 0 && (
<ActionContent>
{props.actions.map(action => (
<NotificationButton
key={action.label}
type={props.type}
isLoading={props.isActionInProgress}
onClick={() => { close(); action.callback(); }}
>{action.label}
</NotificationButton>
))}
</ActionContent>
)}
</AdditionalContent>
</Content>
</Wrapper> </Wrapper>
); );
}; };

View File

@ -11,17 +11,16 @@ const styles = isSearchable => ({
width: '100%', width: '100%',
color: colors.TEXT_SECONDARY, color: colors.TEXT_SECONDARY,
}), }),
control: (base, { isDisabled }) => ({ control: (base, { isDisabled, isFocused }) => ({
...base, ...base,
minHeight: 'initial', minHeight: 'initial',
height: '40px', height: '40px',
borderRadius: '2px', borderRadius: '2px',
borderColor: colors.DIVIDER, borderColor: isFocused ? colors.INPUT_FOCUSED_BORDER : colors.DIVIDER,
boxShadow: 'none', boxShadow: isFocused ? `0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW}` : 'none',
background: isDisabled ? colors.LANDING : colors.WHITE, background: isDisabled ? colors.LANDING : colors.WHITE,
'&:hover': { '&:hover': {
cursor: isSearchable ? 'text' : 'pointer', cursor: isSearchable ? 'text' : 'pointer',
borderColor: colors.DIVIDER,
}, },
}), }),
indicatorSeparator: () => ({ indicatorSeparator: () => ({

View File

@ -67,6 +67,11 @@ const StyledTextarea = styled(Textarea)`
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
} }
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};

View File

@ -59,6 +59,11 @@ const StyledInput = styled.input`
background-color: ${colors.WHITE}; background-color: ${colors.WHITE};
transition: ${TRANSITION.HOVER}; transition: ${TRANSITION.HOVER};
&:focus {
border-color: ${colors.INPUT_FOCUSED_BORDER};
box-shadow: 0 0px 6px 0 ${colors.INPUT_FOCUSED_SHADOW};
}
&:disabled { &:disabled {
pointer-events: none; pointer-events: none;
background: ${colors.GRAY_LIGHT}; background: ${colors.GRAY_LIGHT};

View File

@ -8,6 +8,7 @@ import type { Props } from '../../index';
export default (props: Props) => { export default (props: Props) => {
const { network, notification } = props.selectedAccount; const { network, notification } = props.selectedAccount;
if (!network || !notification) return null; if (!network || !notification) return null;
const blockchain = props.blockchain.find(b => b.shortcut === network.shortcut);
if (notification.type === 'backend') { if (notification.type === 'backend') {
// special case: backend is down // special case: backend is down
@ -17,6 +18,7 @@ export default (props: Props) => {
type="error" type="error"
title={notification.title} title={notification.title}
message={notification.message} message={notification.message}
isActionInProgress={blockchain && blockchain.connecting}
actions={ actions={
[{ [{
label: 'Connect', label: 'Connect',

View File

@ -71,4 +71,13 @@ export const FADE_IN = keyframes`
100% { 100% {
opacity: 1; opacity: 1;
} }
`;
export const SLIDE_DOWN = keyframes`
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0%);
}
`; `;

View File

@ -34,4 +34,7 @@ export default {
LABEL_COLOR: '#A9A9A9', LABEL_COLOR: '#A9A9A9',
TOOLTIP_BACKGROUND: '#333333', TOOLTIP_BACKGROUND: '#333333',
INPUT_FOCUSED_BORDER: '#A9A9A9',
INPUT_FOCUSED_SHADOW: '#d6d7d7',
}; };

View File

@ -16,6 +16,7 @@ export type BlockchainNetwork = {
feeTimestamp: number, feeTimestamp: number,
feeLevels: Array<BlockchainFeeLevel>, feeLevels: Array<BlockchainFeeLevel>,
connected: boolean, connected: boolean,
connecting: boolean,
block: number, block: number,
}; };
@ -23,6 +24,26 @@ export type State = Array<BlockchainNetwork>;
export const initialState: State = []; export const initialState: State = [];
const onStartSubscribe = (state: State, shortcut: string): State => {
const network = state.find(b => b.shortcut === shortcut);
if (network) {
const others = state.filter(b => b !== network);
return others.concat([{
...network,
connecting: true,
}]);
}
return state.concat([{
shortcut,
connected: false,
connecting: true,
block: 0,
feeTimestamp: 0,
feeLevels: [],
}]);
};
const onConnect = (state: State, action: BlockchainConnect): State => { const onConnect = (state: State, action: BlockchainConnect): State => {
const shortcut = action.payload.coin.shortcut.toLowerCase(); const shortcut = action.payload.coin.shortcut.toLowerCase();
const network = state.find(b => b.shortcut === shortcut); const network = state.find(b => b.shortcut === shortcut);
@ -31,13 +52,16 @@ const onConnect = (state: State, action: BlockchainConnect): State => {
const others = state.filter(b => b !== network); const others = state.filter(b => b !== network);
return others.concat([{ return others.concat([{
...network, ...network,
block: info.block,
connected: true, connected: true,
connecting: false,
}]); }]);
} }
return state.concat([{ return state.concat([{
shortcut, shortcut,
connected: true, connected: true,
connecting: false,
block: info.block, block: info.block,
feeTimestamp: 0, feeTimestamp: 0,
feeLevels: [], feeLevels: [],
@ -52,12 +76,14 @@ const onError = (state: State, action: BlockchainError): State => {
return others.concat([{ return others.concat([{
...network, ...network,
connected: false, connected: false,
connecting: false,
}]); }]);
} }
return state.concat([{ return state.concat([{
shortcut, shortcut,
connected: false, connected: false,
connecting: false,
block: 0, block: 0,
feeTimestamp: 0, feeTimestamp: 0,
feeLevels: [], feeLevels: [],
@ -93,6 +119,8 @@ const updateFee = (state: State, shortcut: string, feeLevels: Array<BlockchainFe
export default (state: State = initialState, action: Action): State => { export default (state: State = initialState, action: Action): State => {
switch (action.type) { switch (action.type) {
case BLOCKCHAIN_ACTION.START_SUBSCRIBE:
return onStartSubscribe(state, action.shortcut);
case BLOCKCHAIN_EVENT.CONNECT: case BLOCKCHAIN_EVENT.CONNECT:
return onConnect(state, action); return onConnect(state, action);
case BLOCKCHAIN_EVENT.ERROR: case BLOCKCHAIN_EVENT.ERROR:

View File

@ -82,6 +82,7 @@ export default (state: State = initialState, action: Action): State => {
case SEND.INIT: case SEND.INIT:
case SEND.CHANGE: case SEND.CHANGE:
case SEND.VALIDATION: case SEND.VALIDATION:
case SEND.CLEAR:
return action.state; return action.state;
case SEND.TOGGLE_ADVANCED: case SEND.TOGGLE_ADVANCED:

View File

@ -77,6 +77,7 @@ export default (state: State = initialState, action: Action): State => {
case SEND.INIT: case SEND.INIT:
case SEND.CHANGE: case SEND.CHANGE:
case SEND.VALIDATION: case SEND.VALIDATION:
case SEND.CLEAR:
return action.state; return action.state;
case SEND.TOGGLE_ADVANCED: case SEND.TOGGLE_ADVANCED:

View File

@ -71,24 +71,6 @@ const AddAccountIconWrapper = styled.div`
margin-right: 12px; margin-right: 12px;
`; `;
const DiscoveryStatusWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
font-size: ${FONT_SIZE.BASE};
padding: ${LEFT_NAVIGATION_ROW.PADDING};
white-space: nowrap;
border-top: 1px solid ${colors.DIVIDER};
`;
const DiscoveryStatusText = styled.div`
display: block;
font-size: ${FONT_SIZE.SMALL};
color: ${colors.TEXT_SECONDARY};
overflow: hidden;
text-overflow: ellipsis;
`;
const DiscoveryLoadingWrapper = styled.div` const DiscoveryLoadingWrapper = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@ -162,7 +144,28 @@ const AccountMenu = (props: Props) => {
if (discovery && discovery.completed) { if (discovery && discovery.completed) {
const lastAccount = deviceAccounts[deviceAccounts.length - 1]; const lastAccount = deviceAccounts[deviceAccounts.length - 1];
if (lastAccount && !lastAccount.empty) { if (!selected.connected) {
discoveryStatus = (
<Tooltip
maxWidth={200}
content={`To add a new account, connect ${selected.instanceLabel} device.`}
placement="bottom"
>
<Row>
<RowAddAccountWrapper disabled>
<AddAccountIconWrapper>
<Icon
icon={ICONS.PLUS}
size={24}
color={colors.TEXT_SECONDARY}
/>
</AddAccountIconWrapper>
Add account
</RowAddAccountWrapper>
</Row>
</Tooltip>
);
} else if (lastAccount && !lastAccount.empty) {
discoveryStatus = ( discoveryStatus = (
<Row onClick={props.addAccount}> <Row onClick={props.addAccount}>
<RowAddAccountWrapper> <RowAddAccountWrapper>
@ -199,17 +202,6 @@ const AccountMenu = (props: Props) => {
</Tooltip> </Tooltip>
); );
} }
} else if (!selected.connected) {
discoveryStatus = (
<Row>
<DiscoveryStatusWrapper>
Accounts could not be loaded
<DiscoveryStatusText>
{`Connect ${selected.instanceLabel} device`}
</DiscoveryStatusText>
</DiscoveryStatusWrapper>
</Row>
);
} else { } else {
discoveryStatus = ( discoveryStatus = (
<Row> <Row>

View File

@ -8,18 +8,20 @@ import colors from 'config/colors';
import { FONT_SIZE } from 'config/variables'; import { FONT_SIZE } from 'config/variables';
const Wrapper = styled.div` const Wrapper = styled.div`
padding: 0px 24px 8px 19px;
border-bottom: 1px solid ${colors.DIVIDER};
background: ${colors.WHITE}; background: ${colors.WHITE};
`; `;
const Item = styled.div` const Item = styled.div`
padding: 4px 2px; padding: 6px 24px;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: ${FONT_SIZE.BASE}; font-size: ${FONT_SIZE.BASE};
cursor: pointer; cursor: pointer;
color: ${colors.TEXT_SECONDARY}; color: ${colors.TEXT_SECONDARY};
&:hover {
background: ${colors.GRAY_LIGHT};
}
`; `;
const Label = styled.div` const Label = styled.div`

View File

@ -2,6 +2,11 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import TrezorConnect from 'trezor-connect'; import TrezorConnect from 'trezor-connect';
import COLORS from 'config/colors';
import { FONT_SIZE, FONT_WEIGHT } from 'config/variables';
import { SLIDE_DOWN } from 'config/animations';
import Button from 'components/Button'; import Button from 'components/Button';
import * as deviceUtils from 'utils/device'; import * as deviceUtils from 'utils/device';
import MenuItems from './components/MenuItems'; import MenuItems from './components/MenuItems';
@ -9,11 +14,20 @@ import DeviceList from './components/DeviceList';
import type { Props } from '../common'; import type { Props } from '../common';
import AsideDivider from '../Divider'; import Divider from '../Divider';
const Wrapper = styled.div`
position: absolute;
width: 100%;
padding-bottom: 8px;
border-bottom: 1px solid #E3E3E3;
background: white;
box-shadow: 0 3px 8px rgba(0,0,0,0.06);
animation: ${SLIDE_DOWN} 0.25s cubic-bezier(0.17, 0.04, 0.03, 0.94) forwards;
`;
const Wrapper = styled.div``;
const ButtonWrapper = styled.div` const ButtonWrapper = styled.div`
margin-top: 10px; margin: 10px 0;
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
`; `;
@ -21,11 +35,20 @@ const StyledButton = styled(Button)`
flex: 1; flex: 1;
`; `;
const StyledDivider = styled(Divider)`
background: #fff;
color: ${COLORS.TEXT_PRIMARY};
font-weight: ${FONT_WEIGHT.MEDIUM};
font-size: ${FONT_SIZE.BASE};
border: none;
`;
class DeviceMenu extends PureComponent<Props> { class DeviceMenu extends PureComponent<Props> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.mouseDownHandler = this.mouseDownHandler.bind(this); this.mouseDownHandler = this.mouseDownHandler.bind(this);
this.blurHandler = this.blurHandler.bind(this); this.blurHandler = this.blurHandler.bind(this);
this.myRef = React.createRef();
} }
componentDidMount(): void { componentDidMount(): void {
@ -44,6 +67,10 @@ class DeviceMenu extends PureComponent<Props> {
window.removeEventListener('mousedown', this.mouseDownHandler, false); window.removeEventListener('mousedown', this.mouseDownHandler, false);
} }
getMenuHeight(): number {
return this.myRef.current ? this.myRef.current.getBoundingClientRect().height : 0;
}
blurHandler(): void { blurHandler(): void {
this.props.toggleDeviceDropdown(false); this.props.toggleDeviceDropdown(false);
} }
@ -77,26 +104,28 @@ class DeviceMenu extends PureComponent<Props> {
return deviceUtils.isDeviceAccessible(this.props.wallet.selectedDevice); return deviceUtils.isDeviceAccessible(this.props.wallet.selectedDevice);
} }
myRef: { current: ?HTMLDivElement }
render() { render() {
const { devices, onSelectDevice, forgetDevice } = 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;
return ( return (
<Wrapper> <Wrapper ref={this.myRef}>
{this.showMenuItems() && <MenuItems device={selectedDevice} {...this.props} />} {this.showMenuItems() && <MenuItems device={selectedDevice} {...this.props} />}
{this.showDivider() && <AsideDivider textLeft="Other devices" />} {this.showDivider() && <StyledDivider hasBorder textLeft="Other devices" />}
<DeviceList <DeviceList
devices={devices} devices={devices}
selectedDevice={selectedDevice} selectedDevice={selectedDevice}
onSelectDevice={onSelectDevice} onSelectDevice={onSelectDevice}
forgetDevice={forgetDevice} forgetDevice={forgetDevice}
/> />
<ButtonWrapper> {deviceUtils.isWebUSB(transport) && (
{deviceUtils.isWebUSB(transport) && ( <ButtonWrapper>
<StyledButton isWebUsb>Check for devices</StyledButton> <StyledButton isWebUsb>Check for devices</StyledButton>
)} </ButtonWrapper>
</ButtonWrapper> )}
</Wrapper> </Wrapper>
); );
} }

View File

@ -19,10 +19,11 @@ const Wrapper = styled.div`
`; `;
const Divider = ({ const Divider = ({
textLeft, textRight, hasBorder = false, textLeft, textRight, hasBorder = false, className,
}) => ( }) => (
<Wrapper <Wrapper
hasBorder={hasBorder} hasBorder={hasBorder}
className={className}
> >
<p>{textLeft}</p> <p>{textLeft}</p>
<p>{textRight}</p> <p>{textRight}</p>
@ -30,6 +31,7 @@ const Divider = ({
); );
Divider.propTypes = { Divider.propTypes = {
className: PropTypes.string,
textLeft: PropTypes.string, textLeft: PropTypes.string,
textRight: PropTypes.string, textRight: PropTypes.string,
hasBorder: PropTypes.bool, hasBorder: PropTypes.bool,

View File

@ -60,6 +60,7 @@ const Footer = styled.div.attrs(props => ({
const Body = styled.div` const Body = styled.div`
width: 320px; width: 320px;
min-height: ${props => (props.minHeight ? `${props.minHeight}px` : '0px')};
`; `;
const Help = styled.div` const Help = styled.div`
@ -113,42 +114,48 @@ const TransitionMenu = (props: TransitionMenuProps): React$Element<TransitionGro
type State = { type State = {
animationType: ?string; animationType: ?string;
shouldRenderDeviceSelection: boolean;
clicked: boolean; clicked: boolean;
bodyMinHeight: number;
} }
class LeftNavigation extends React.PureComponent<Props, State> { class LeftNavigation extends React.PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.deviceMenuRef = React.createRef();
const { location } = this.props.router; const { location } = this.props.router;
const hasNetwork = location && location.state && location.state.network; const hasNetwork = location && location.state && location.state.network;
this.state = { this.state = {
animationType: hasNetwork ? 'slide-left' : null, animationType: hasNetwork ? 'slide-left' : null,
shouldRenderDeviceSelection: false,
clicked: false, clicked: false,
bodyMinHeight: 0,
}; };
} }
componentDidMount() {
this.recalculateBodyMinHeight();
}
componentWillReceiveProps(nextProps: Props) { componentWillReceiveProps(nextProps: Props) {
const { dropdownOpened, selectedDevice } = nextProps.wallet; const { selectedDevice } = nextProps.wallet;
const { location } = nextProps.router; const { location } = nextProps.router;
const hasNetwork = location && location.state.network; const hasNetwork = location && location.state.network;
const deviceReady = selectedDevice && selectedDevice.features && selectedDevice.mode === 'normal'; const deviceReady = selectedDevice && selectedDevice.features && selectedDevice.mode === 'normal';
if (dropdownOpened) {
this.setState({ shouldRenderDeviceSelection: true }); if (hasNetwork) {
} else if (hasNetwork) {
this.setState({ this.setState({
shouldRenderDeviceSelection: false,
animationType: 'slide-left', animationType: 'slide-left',
}); });
} else { } else {
this.setState({ this.setState({
shouldRenderDeviceSelection: false,
animationType: deviceReady ? 'slide-right' : null, animationType: deviceReady ? 'slide-right' : null,
}); });
} }
} }
componentDidUpdate() {
this.recalculateBodyMinHeight();
}
shouldRenderAccounts() { shouldRenderAccounts() {
const { selectedDevice } = this.props.wallet; const { selectedDevice } = this.props.wallet;
const { location } = this.props.router; const { location } = this.props.router;
@ -156,7 +163,6 @@ class LeftNavigation extends React.PureComponent<Props, State> {
&& location && location
&& location.state && location.state
&& location.state.network && location.state.network
&& !this.state.shouldRenderDeviceSelection
&& this.state.animationType === 'slide-left'; && this.state.animationType === 'slide-left';
} }
@ -166,9 +172,19 @@ class LeftNavigation extends React.PureComponent<Props, State> {
} }
shouldRenderCoins() { shouldRenderCoins() {
return !this.state.shouldRenderDeviceSelection && this.state.animationType !== 'slide-left'; return this.state.animationType !== 'slide-left';
} }
recalculateBodyMinHeight() {
if (this.deviceMenuRef.current) {
this.setState({
bodyMinHeight: this.deviceMenuRef.current.getMenuHeight(),
});
}
}
deviceMenuRef: { current: any };
render() { render() {
const { props } = this; const { props } = this;
let menu; let menu;
@ -186,7 +202,7 @@ class LeftNavigation extends React.PureComponent<Props, State> {
); );
} }
const { selectedDevice } = props.wallet; const { selectedDevice, dropdownOpened } = props.wallet;
const isDeviceAccessible = deviceUtils.isDeviceAccessible(selectedDevice); const isDeviceAccessible = deviceUtils.isDeviceAccessible(selectedDevice);
const walletType = selectedDevice && !selectedDevice.useEmptyPassphrase ? 'hidden' : 'standard'; const walletType = selectedDevice && !selectedDevice.useEmptyPassphrase ? 'hidden' : 'standard';
const showWalletType = selectedDevice && selectedDevice.features && selectedDevice.features.passphrase_protection; const showWalletType = selectedDevice && selectedDevice.features && selectedDevice.features.passphrase_protection;
@ -263,8 +279,8 @@ class LeftNavigation extends React.PureComponent<Props, State> {
)} )}
{...this.props} {...this.props}
/> />
<Body> <Body minHeight={this.state.bodyMinHeight}>
{this.state.shouldRenderDeviceSelection && <DeviceMenu {...this.props} />} {dropdownOpened && <DeviceMenu ref={this.deviceMenuRef} {...this.props} />}
{isDeviceAccessible && menu} {isDeviceAccessible && menu}
</Body> </Body>
<Footer key="sticky-footer"> <Footer key="sticky-footer">

View File

@ -142,17 +142,33 @@ const ToggleAdvancedSettingsButton = styled(Button)`
min-height: 40px; min-height: 40px;
padding: 0; padding: 0;
display: flex; display: flex;
flex: 1 1 0;
align-items: center; align-items: center;
font-weight: ${FONT_WEIGHT.SEMIBOLD}; font-weight: ${FONT_WEIGHT.SEMIBOLD};
`; `;
const SendButton = styled(Button)` const FormButtons = styled.div`
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')}; display: flex;
word-break: break-all; flex: 1 1;
@media screen and (max-width: ${SmallScreenWidth}) { @media screen and (max-width: ${SmallScreenWidth}) {
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)}; margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
} }
Button + Button {
margin-left: 5px;
}
`;
const SendButton = styled(Button)`
word-break: break-all;
flex: 1;
`;
const ClearButton = styled(Button)`
`; `;
const AdvancedSettingsIcon = styled(Icon)` const AdvancedSettingsIcon = styled(Icon)`
@ -228,6 +244,7 @@ const AccountSend = (props: Props) => {
onFeeLevelChange, onFeeLevelChange,
updateFeeLevels, updateFeeLevels,
onSend, onSend,
onClear,
} = props.sendFormActions; } = props.sendFormActions;
if (!device || !account || !discovery || !network || !shouldRender) { if (!device || !account || !discovery || !network || !shouldRender) {
@ -387,24 +404,43 @@ const AccountSend = (props: Props) => {
</ToggleAdvancedSettingsButton> </ToggleAdvancedSettingsButton>
{isAdvancedSettingsHidden && ( {isAdvancedSettingsHidden && (
<SendButton <FormButtons
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden} isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
> >
{sendButtonText} <ClearButton
</SendButton> isWhite
onClick={() => onClear()}
>
Clear
</ClearButton>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</FormButtons>
)} )}
</ToggleAdvancedSettingsWrapper> </ToggleAdvancedSettingsWrapper>
{advanced && ( {advanced && (
<AdvancedForm {...props}> <AdvancedForm {...props}>
<SendButton <FormButtons
isDisabled={isSendButtonDisabled} isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
> >
{sendButtonText} <ClearButton
</SendButton> isWhite
onClick={() => onClear()}
>
Clear
</ClearButton>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</FormButtons>
</AdvancedForm> </AdvancedForm>
)} )}

View File

@ -139,17 +139,33 @@ const ToggleAdvancedSettingsButton = styled(Button)`
min-height: 40px; min-height: 40px;
padding: 0; padding: 0;
display: flex; display: flex;
flex: 1 1 0;
align-items: center; align-items: center;
font-weight: ${FONT_WEIGHT.SEMIBOLD}; font-weight: ${FONT_WEIGHT.SEMIBOLD};
`; `;
const SendButton = styled(Button)` const FormButtons = styled.div`
min-width: ${props => (props.isAdvancedSettingsHidden ? '50%' : '100%')}; display: flex;
word-break: break-all; flex: 1 1;
@media screen and (max-width: ${SmallScreenWidth}) { @media screen and (max-width: ${SmallScreenWidth}) {
margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)}; margin-top: ${props => (props.isAdvancedSettingsHidden ? '10px' : 0)};
} }
Button + Button {
margin-left: 5px;
}
`;
const SendButton = styled(Button)`
word-break: break-all;
flex: 1;
`;
const ClearButton = styled(Button)`
`; `;
const AdvancedSettingsIcon = styled(Icon)` const AdvancedSettingsIcon = styled(Icon)`
@ -214,6 +230,7 @@ const AccountSend = (props: Props) => {
onFeeLevelChange, onFeeLevelChange,
updateFeeLevels, updateFeeLevels,
onSend, onSend,
onClear,
} = props.sendFormActions; } = props.sendFormActions;
if (!device || !account || !discovery || !network || !shouldRender) { if (!device || !account || !discovery || !network || !shouldRender) {
@ -359,24 +376,43 @@ const AccountSend = (props: Props) => {
</ToggleAdvancedSettingsButton> </ToggleAdvancedSettingsButton>
{isAdvancedSettingsHidden && ( {isAdvancedSettingsHidden && (
<SendButton <FormButtons
isDisabled={isSendButtonDisabled}
isAdvancedSettingsHidden={isAdvancedSettingsHidden} isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
> >
{sendButtonText} <ClearButton
</SendButton> isWhite
onClick={() => onClear()}
>
Clear
</ClearButton>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</FormButtons>
)} )}
</ToggleAdvancedSettingsWrapper> </ToggleAdvancedSettingsWrapper>
{advanced && ( {advanced && (
<AdvancedForm {...props}> <AdvancedForm {...props}>
<SendButton <FormButtons
isDisabled={isSendButtonDisabled} isAdvancedSettingsHidden={isAdvancedSettingsHidden}
onClick={() => onSend()}
> >
{sendButtonText} <ClearButton
</SendButton> isWhite
onClick={() => onClear()}
>
Clear
</ClearButton>
<SendButton
isDisabled={isSendButtonDisabled}
onClick={() => onSend()}
>
{sendButtonText}
</SendButton>
</FormButtons>
</AdvancedForm> </AdvancedForm>
)} )}