1
0
mirror of https://github.com/trezor/trezor-wallet synced 2024-12-29 02:18:06 +00:00

Merge branch 'styled-components-refactor' of https://github.com/satoshilabs/trezor-wallet into styled-components-refactor

This commit is contained in:
Vasek Mlejnsky 2018-08-16 14:57:37 +02:00
commit 65d76162cb
21 changed files with 1393 additions and 12 deletions

View File

@ -10,6 +10,7 @@
},
"rules": {
"react/require-default-props": 0,
"react/forbid-prop-types": 0,
"react/destructuring-assignment": 0,
"react/jsx-one-expression-per-line": 0,
"react/jsx-indent": [2, 4],

View File

@ -13,15 +13,12 @@ const Wrapper = styled.button`
background: ${colors.GREEN_PRIMARY};
color: ${colors.WHITE};
border: 0;
&:hover {
background: ${colors.GREEN_SECONDARY};
}
&:active {
background: ${colors.GREEN_TERTIARY};
}
${props => props.disabled && css`
pointer-events: none;
color: ${colors.TEXT_SECONDARY};
@ -56,4 +53,4 @@ Button.defaultProps = {
white: false,
};
export default Button;
export default Button;

View File

@ -103,6 +103,8 @@ class Modal extends Component<Props> {
case CONNECT.TRY_TO_DUPLICATE:
component = (<DuplicateDevice {...this.props} />);
break;
default: null;
}
let ch = null;

View File

@ -1,4 +1,4 @@
export default {
REDIRECT: 'M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z',
REDIRECT: 'M16 8c-4.418 0-8 3.583-8 8 0 4.419 3.582 8 8 8s8-3.581 8-8c0-4.417-3.582-8-8-8zM16.533 19.733v-2.133c-3.2-1.067-4.267 0-5.333 2.133 0-5.333 3.2-6.4 5.333-6.4v-2.133l4.267 4.267-4.267 4.266z',
WARNING: 'M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z',
};

View File

@ -0,0 +1,45 @@
/* @flow */
import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { default as ReceiveActions } from 'actions/ReceiveActions';
import * as TokenActions from 'actions/TokenActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import Receive from './Receive';
import type {
StateProps as BaseStateProps,
DispatchProps as BaseDispatchProps,
} from '../SelectedAccount';
type OwnProps = { }
type StateProps = BaseStateProps & {
receive: $ElementType<State, 'receive'>,
modal: $ElementType<State, 'modal'>,
}
type DispatchProps = BaseDispatchProps & {
showAddress: typeof ReceiveActions.showAddress
};
export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State, own: OwnProps): StateProps => ({
className: 'receive',
selectedAccount: state.selectedAccount,
wallet: state.wallet,
receive: state.receive,
modal: state.modal,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
showAddress: bindActionCreators(ReceiveActions.showAddress, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Receive);

View File

@ -0,0 +1,101 @@
/* @flow */
import React, { Component } from 'react';
import styled from 'styled-components';
import { H2 } from 'components/common/Heading';
import Tooltip from 'rc-tooltip';
import { QRCode } from 'react-qr-svg';
import { Notification } from 'components/common/Notification';
import SelectedAccount from '../SelectedAccount';
import type { Props } from './index';
const Wrapper = styled.div``;
const StyledH2 = styled(H2)`
padding: 20px 48px;
`;
const Receive = (props: Props) => {
const device = props.wallet.selectedDevice;
const {
account,
network,
discovery,
} = props.selectedAccount;
if (!device || !account || !discovery) return null;
const {
addressVerified,
addressUnverified,
} = props.receive;
let qrCode = null;
let address = `${account.address.substring(0, 20)}...`;
let className = 'address hidden';
let button = (
<button disabled={device.connected && !discovery.completed} onClick={event => props.showAddress(account.addressPath)}>
<span>Show full address</span>
</button>
);
if (addressVerified || addressUnverified) {
qrCode = (
<QRCode
className="qr"
bgColor="#FFFFFF"
fgColor="#000000"
level="Q"
style={{ width: 256 }}
value={account.address}
/>
);
address = account.address;
className = addressUnverified ? 'address unverified' : 'address';
const tooltip = addressUnverified
? (<div>Unverified address.<br />{ device.connected && device.available ? 'Show on TREZOR' : 'Connect your TREZOR to verify it.' }</div>)
: (<div>{ device.connected ? 'Show on TREZOR' : 'Connect your TREZOR to verify address.' }</div>);
button = (
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />}
overlay={tooltip}
placement="bottomRight"
>
<button className="white" onClick={event => props.showAddress(account.addressPath)}>
<span />
</button>
</Tooltip>
);
}
let ver = null;
if (props.modal.opened && props.modal.windowType === 'ButtonRequest_Address') {
className = 'address verifying';
address = account.address;
ver = (<div className="confirm">Confirm address on TREZOR</div>);
button = (<div className="confirm">{ account.network } account #{ (account.index + 1) }</div>);
}
return (
<Wrapper>
<StyledH2>Receive Ethereum or tokens</StyledH2>
<div className={className}>
{ ver }
<div className="value">
{ address }
</div>
{ button }
</div>
{ qrCode }
</Wrapper>
);
};
export default (props: Props) => (
<SelectedAccount {...props}>
<Receive {...props} />
</SelectedAccount>
);

View File

@ -0,0 +1,114 @@
/* @flow */
import * as React from 'react';
import { Notification } from 'components/common/Notification';
import type {
State, TrezorDevice, Action, ThunkAction,
} from 'flowtype';
import type { Account } from 'reducers/AccountsReducer';
import type { Discovery } from 'reducers/DiscoveryReducer';
export type StateProps = {
className: string;
selectedAccount: $ElementType<State, 'selectedAccount'>,
wallet: $ElementType<State, 'wallet'>,
children?: React.Node
}
export type DispatchProps = {
}
export type Props = StateProps & DispatchProps;
const SelectedAccount = (props: Props) => {
const device = props.wallet.selectedDevice;
if (!device || !device.state) {
return (<section><Notification className="info" title="Loading device..." /></section>);
}
const accountState = props.selectedAccount;
const {
account,
discovery,
} = accountState;
// account not found (yet). checking why...
if (!account) {
if (!discovery || discovery.waitingForDevice) {
if (device.connected) {
// case 1: device is connected but discovery not started yet (probably waiting for auth)
if (device.available) {
return (
<section>
<Notification className="info" title="Loading accounts..." />
</section>
);
}
// case 2: device is unavailable (created with different passphrase settings) account cannot be accessed
return (
<section>
<Notification
className="info"
title={`Device ${device.instanceLabel} is unavailable`}
message="Change passphrase settings to use this device"
/>
</section>
);
}
// case 3: device is disconnected
return (
<section>
<Notification
className="info"
title={`Device ${device.instanceLabel} is disconnected`}
message="Connect device to load accounts"
/>
</section>
);
} if (discovery.waitingForBackend) {
// case 4: backend is not working
return (
<section>
<Notification className="warning" title="Backend not working" />
</section>
);
} if (discovery.completed) {
// case 5: account not found and discovery is completed
return (
<section>
<Notification className="warning" title="Account does not exist" />
</section>
);
}
// case 6: discovery is not completed yet
return (
<section>
<Notification className="info" title="Loading accounts..." />
</section>
);
}
let notification: ?React$Element<typeof Notification> = null;
if (!device.connected) {
notification = <Notification className="info" title={`Device ${device.instanceLabel} is disconnected`} />;
} else if (!device.available) {
notification = <Notification className="info" title={`Device ${device.instanceLabel} is unavailable`} message="Change passphrase settings to use this device" />;
}
if (discovery && !discovery.completed && !notification) {
notification = <Notification className="info" title="Loading accounts..." />;
}
return (
<section className={props.className}>
{ notification }
{ props.children }
</section>
);
};
export default SelectedAccount;

View File

@ -0,0 +1,47 @@
/* @flow */
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { default as SendFormActions } from 'actions/SendFormActions';
import * as SessionStorageActions from 'actions/SessionStorageActions';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import type { State, Dispatch } from 'flowtype';
import SendForm from './SendForm';
import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from '../SelectedAccount';
type OwnProps = { }
export type StateProps = BaseStateProps & {
sendForm: $ElementType<State, 'sendForm'>,
fiat: $ElementType<State, 'fiat'>,
localStorage: $ElementType<State, 'localStorage'>,
children?: React.Node;
}
export type DispatchProps = BaseDispatchProps & {
sendFormActions: typeof SendFormActions,
saveSessionStorage: typeof SessionStorageActions.save
}
export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State, own: OwnProps): StateProps => ({
className: 'send-from',
selectedAccount: state.selectedAccount,
wallet: state.wallet,
sendForm: state.sendForm,
fiat: state.fiat,
localStorage: state.localStorage,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
sendFormActions: bindActionCreators(SendFormActions, dispatch),
saveSessionStorage: bindActionCreators(SessionStorageActions.save, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(SendForm);

View File

@ -0,0 +1,191 @@
/* @flow */
import React from 'react';
import Tooltip from 'rc-tooltip';
import type { Props as BaseProps } from './index';
type Props = {
selectedAccount: $ElementType<BaseProps, 'selectedAccount'>,
sendForm: $ElementType<BaseProps, 'sendForm'>,
sendFormActions: $ElementType<BaseProps, 'sendFormActions'>,
children?: $ElementType<BaseProps, 'children'>,
};
const AdvancedForm = (props: Props) => {
const {
account,
network,
} = props.selectedAccount;
const {
networkSymbol,
currency,
gasPrice,
gasLimit,
recommendedGasPrice,
calculatingGasLimit,
nonce,
data,
errors,
warnings,
infos,
advanced,
} = props.sendForm;
const {
toggleAdvanced,
onGasPriceChange,
onGasLimitChange,
onNonceChange,
onDataChange,
} = props.sendFormActions;
if (!advanced) {
return (
<div className="advanced-container">
<a className="advanced" onClick={toggleAdvanced}>Advanced settings</a>
{ props.children }
</div>
);
}
const nonceTooltip = (
<div className="tooltip-wrapper">
TODO<br />
</div>
);
let gasLimitTooltipCurrency: string;
let gasLimitTooltipValue: string;
if (networkSymbol !== currency) {
gasLimitTooltipCurrency = 'tokens';
gasLimitTooltipValue = network.defaultGasLimitTokens;
} else {
gasLimitTooltipCurrency = networkSymbol;
gasLimitTooltipValue = network.defaultGasLimit;
}
const gasLimitTooltip = (
<div className="tooltip-wrapper">
Gas limit is the amount of gas to send with your transaction.<br />
<span>TX fee = gas price * gas limit</span> &amp; is paid to miners for including your TX in a block.<br />
Increasing this number will not get your TX mined faster.<br />
Default value for sending { gasLimitTooltipCurrency } is <span>{ gasLimitTooltipValue }</span>
</div>
);
const gasPriceTooltip = (
<div className="tooltip-wrapper">
Gas Price is the amount you pay per unit of gas.<br />
<span>TX fee = gas price * gas limit</span> &amp; is paid to miners for including your TX in a block.<br />
Higher the gas price = faster transaction, but more expensive. Recommended is <span>{ recommendedGasPrice } GWEI.</span><br />
<a className="green" href="https://myetherwallet.github.io/knowledge-base/gas/what-is-gas-ethereum.html" target="_blank" rel="noreferrer noopener">Read more</a>
</div>
);
const dataTooltip = (
<div className="tooltip-wrapper">
Data is usually used when you send transactions to contracts.
</div>
);
return (
<div className="advanced-container opened">
<a className="advanced" onClick={toggleAdvanced}>Advanced settings</a>
<div className="row gas-row">
{/* <div className="column nonce">
<label>
Nonce
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner"></div>}
overlay={ nonceTooltip }
placement="top">
<span className="what-is-it"></span>
</Tooltip>
</label>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={ nonce }
onChange={ event => onNonceChange(event.target.value) } />
{ errors.nonce ? (<span className="error">{ errors.nonce }</span>) : null }
{ warnings.nonce ? (<span className="warning">{ warnings.nonce }</span>) : null }
</div> */}
<div className="column">
<label>
Gas limit
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />}
overlay={gasLimitTooltip}
placement="top"
>
<span className="what-is-it" />
</Tooltip>
</label>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={gasLimit}
disabled={networkSymbol === currency && data.length > 0}
onChange={event => onGasLimitChange(event.target.value)}
/>
{ errors.gasLimit ? (<span className="error">{ errors.gasLimit }</span>) : null }
{ warnings.gasLimit ? (<span className="warning">{ warnings.gasLimit }</span>) : null }
{ calculatingGasLimit ? (<span className="info">Calculating...</span>) : null }
</div>
<div className="column">
<label>
Gas price
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />}
overlay={gasPriceTooltip}
placement="top"
>
<span className="what-is-it" />
</Tooltip>
</label>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={gasPrice}
onChange={event => onGasPriceChange(event.target.value)}
/>
{ errors.gasPrice ? (<span className="error">{ errors.gasPrice }</span>) : null }
</div>
</div>
<div className="row">
<label>
Data
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />}
overlay={dataTooltip}
placement="top"
>
<span className="what-is-it" />
</Tooltip>
</label>
<textarea disabled={networkSymbol !== currency} value={networkSymbol !== currency ? '' : data} onChange={event => onDataChange(event.target.value)} />
{ errors.data ? (<span className="error">{ errors.data }</span>) : null }
</div>
<div className="row">
{ props.children }
</div>
</div>
);
};
export default AdvancedForm;

View File

@ -0,0 +1,51 @@
/* @flow */
import * as React from 'react';
type Props = {
children: React.Node,
className: string,
isDisabled: boolean,
isFocused: boolean,
isSelected: boolean,
onFocus: Function,
onSelect: Function,
option: any,
}
export default class CoinSelectOption extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
handleMouseDown(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event: MouseEvent) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event: MouseEvent) {
if (this.props.isFocused) return;
this.props.onFocus(this.props.option, event);
}
render() {
const css = `${this.props.className} ${this.props.option.value}`;
return (
<div
className={css}
onMouseDown={this.handleMouseDown.bind(this)}
onMouseEnter={this.handleMouseEnter.bind(this)}
onMouseMove={this.handleMouseMove.bind(this)}
title={this.props.option.label}
>
<span>{ this.props.children }</span>
</div>
);
}
}

View File

@ -0,0 +1,61 @@
/* @flow */
import * as React from 'react';
import PropTypes from 'prop-types';
export const FeeSelectValue = (props: any): any => (
<div>
<div className="Select-value fee-option">
<span className="fee-value">{ props.value.value }</span>
<span className="fee-label">{ props.value.label }</span>
</div>
</div>
);
type Props = {
children: React.Node,
className: string,
isDisabled: boolean,
isFocused: boolean,
isSelected: boolean,
onFocus: Function,
onSelect: Function,
option: any,
}
export class FeeSelectOption extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
handleMouseDown(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event: MouseEvent) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event: MouseEvent) {
if (this.props.isFocused) return;
this.props.onFocus(this.props.option, event);
}
render() {
return (
<div
className={this.props.className}
onMouseDown={this.handleMouseDown.bind(this)}
onMouseEnter={this.handleMouseEnter.bind(this)}
onMouseMove={this.handleMouseMove.bind(this)}
>
<span className="fee-value">{ this.props.option.value }</span>
<span className="fee-label">{ this.props.option.label }</span>
</div>
);
}
}

View File

@ -0,0 +1,95 @@
/* @flow */
import React from 'react';
import ColorHash from 'color-hash';
import { H2 } from 'components/common/Heading';
import ScaleText from 'react-scale-text';
import { findAccountTokens } from 'reducers/TokensReducer';
import type { Coin } from 'reducers/LocalStorageReducer';
import type { Token } from 'reducers/TokensReducer';
import type { Props as BaseProps } from './index';
type Props = {
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
tokens: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'tokens'>,
network: Coin
}
type Style = {
+color: string,
+background: string,
+borderColor: string
}
const PendingTransactions = (props: Props) => {
const pending = props.pending.filter(tx => !tx.rejected);
if (pending.length < 1) return null;
const tokens: Array<Token> = props.tokens;
const bgColorFactory = new ColorHash({ lightness: 0.7 });
const textColorFactory = new ColorHash();
const pendingTxs: React$Element<string> = pending.map((tx, i) => {
let iconColor: Style;
let symbol: string;
let name: string;
const isSmartContractTx: boolean = tx.currency !== props.network.symbol;
if (isSmartContractTx) {
const token: ?Token = tokens.find(t => t.symbol === tx.currency);
if (!token) {
iconColor = {
color: '#ffffff',
background: '#000000',
borderColor: '#000000',
};
symbol = 'Unknown';
name = 'Unknown';
} else {
const bgColor: string = bgColorFactory.hex(token.name);
iconColor = {
color: textColorFactory.hex(token.name),
background: bgColor,
borderColor: bgColor,
};
symbol = token.symbol.toUpperCase();
name = token.name;
}
} else {
iconColor = {
color: textColorFactory.hex(tx.network),
background: bgColorFactory.hex(tx.network),
borderColor: bgColorFactory.hex(tx.network),
};
symbol = props.network.symbol;
name = props.network.name;
}
return (
<div key={i} className="tx">
<div className="icon" style={iconColor}>
<div className="icon-inner">
<ScaleText widthOnly><p>{ symbol }</p></ScaleText>
</div>
</div>
<div className="name">
<a href={`${props.network.explorer.tx}${tx.id}`} target="_blank" rel="noreferrer noopener">{ name }</a>
</div>
<div className="amount">{ isSmartContractTx ? tx.amount : tx.total } { symbol }</div>
</div>
);
});
return (
<div className="pending-transactions">
<H2>Pending transactions</H2>
{ pendingTxs }
</div>
);
};
export default PendingTransactions;

View File

@ -0,0 +1,205 @@
/* @flow */
import React, { Component } from 'react';
import styled from 'styled-components';
import Select from 'react-select';
import { H2 } from 'components/common/Heading';
import { calculate, validation } from 'actions/SendFormActions';
import type { Token } from 'flowtype';
import AdvancedForm from './components/AdvancedForm';
import PendingTransactions from './components/PendingTransactions';
import { FeeSelectValue, FeeSelectOption } from './components/FeeSelect';
import SelectedAccount from '../SelectedAccount';
import type { Props } from './index';
export default class SendContainer extends Component<Props> {
componentWillReceiveProps(newProps: Props) {
calculate(this.props, newProps);
validation(newProps);
this.props.saveSessionStorage();
}
render() {
return (
<SelectedAccount {...this.props}>
<Send {...this.props} />
</SelectedAccount>
);
}
}
const StyledH2 = styled(H2)`
padding: 20px 48px;
`;
const Send = (props: Props) => {
const device = props.wallet.selectedDevice;
const {
account,
network,
discovery,
tokens,
} = props.selectedAccount;
if (!device || !account || !discovery || !network) return null;
const {
address,
amount,
setMax,
networkSymbol,
currency,
feeLevels,
selectedFeeLevel,
gasPriceNeedsUpdate,
total,
errors,
warnings,
infos,
advanced,
data,
sending,
} = props.sendForm;
const {
onAddressChange,
onAmountChange,
onSetMax,
onCurrencyChange,
onFeeLevelChange,
updateFeeLevels,
onSend,
} = props.sendFormActions;
const fiatRate = props.fiat.find(f => f.network === network);
const tokensSelectData = tokens.map(t => ({ value: t.symbol, label: t.symbol }));
tokensSelectData.unshift({ value: network.symbol, label: network.symbol });
const setMaxClassName: string = setMax ? 'set-max enabled' : 'set-max';
let updateFeeLevelsButton = null;
if (gasPriceNeedsUpdate) {
updateFeeLevelsButton = (
<span className="update-fee-levels">Recommended fees updated. <a onClick={updateFeeLevels}>Click here to use them</a></span>
);
}
let addressClassName: ?string;
if (errors.address) {
addressClassName = 'not-valid';
} else if (warnings.address) {
addressClassName = 'warning';
} else if (address.length > 0) {
addressClassName = 'valid';
}
let buttonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
let buttonLabel: string = 'Send';
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
buttonLabel += ` ${amount} ${currency.toUpperCase()}`;
} else if (networkSymbol === currency && total !== '0') {
buttonLabel += ` ${total} ${network.symbol}`;
}
if (!device.connected) {
buttonLabel = 'Device is not connected';
buttonDisabled = true;
} else if (!device.available) {
buttonLabel = 'Device is unavailable';
buttonDisabled = true;
} else if (!discovery.completed) {
buttonLabel = 'Loading accounts';
buttonDisabled = true;
}
return (
<section className="send-form">
<StyledH2>Send Ethereum or tokens</StyledH2>
<div className="row address-input">
<label>Address</label>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={address}
className={addressClassName}
onChange={event => onAddressChange(event.target.value)}
/>
<span className="input-icon" />
{ errors.address ? (<span className="error">{ errors.address }</span>) : null }
{ warnings.address ? (<span className="warning">{ warnings.address }</span>) : null }
{ infos.address ? (<span className="info">{ infos.address }</span>) : null }
</div>
<div className="row">
<label>Amount</label>
<div className="amount-input">
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={amount}
className={errors.amount ? 'not-valid' : null}
onChange={event => onAmountChange(event.target.value)}
/>
<a className={setMaxClassName} onClick={onSetMax}>Set max</a>
<Select
name="currency"
className="currency"
searchable={false}
clearable={false}
multi={false}
value={currency}
disabled={tokensSelectData.length < 2}
onChange={onCurrencyChange}
options={tokensSelectData}
/>
</div>
{ errors.amount ? (<span className="error">{ errors.amount }</span>) : null }
{ warnings.amount ? (<span className="warning">{ warnings.amount }</span>) : null }
</div>
<div className="row">
<label>Fee{ updateFeeLevelsButton }</label>
<Select
name="fee"
className="fee"
searchable={false}
clearable={false}
value={selectedFeeLevel}
onChange={onFeeLevelChange}
valueComponent={FeeSelectValue}
optionComponent={FeeSelectOption}
disabled={networkSymbol === currency && data.length > 0}
optionClassName="fee-option"
options={feeLevels}
/>
</div>
<AdvancedForm
selectedAccount={props.selectedAccount}
sendForm={props.sendForm}
sendFormActions={props.sendFormActions}
>
<button disabled={buttonDisabled} onClick={event => onSend()}>{ buttonLabel }</button>
</AdvancedForm>
<PendingTransactions
pending={props.selectedAccount.pending}
tokens={props.selectedAccount.tokens}
network={network}
/>
</section>
);
};

View File

@ -0,0 +1,66 @@
import React from 'react';
import styled from 'styled-components';
import { H2 } from 'components/common/Heading';
import colors from 'config/colors';
const Wrapper = styled.div`
display: flex;
flex: 1;
flex-direction: row;
background: ${colors.WHITE};
padding: 50px;
`;
const StyledH2 = styled(H2)`
padding-bottom: 10px;
`;
const Column = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`;
const Sign = styled(Column)``;
const Verify = styled(Column)`
padding-left: 20px;
`;
const Label = styled.div`
color: ${colors.LABEL};
padding: 5px 0px;
`;
const Textarea = styled.textarea`
resize: vertical;
width: 100%;
`;
const Input = styled.input``;
const SignVerify = () => (
<Wrapper>
<Sign>
<StyledH2>Sign message</StyledH2>
<Label>Message</Label>
<Textarea rows="4" maxLength="255" />
<Label>Address</Label>
<Input type="text" />
<Label>Signature</Label>
<Textarea rows="4" maxLength="255" readOnly="readonly" />
</Sign>
<Verify>
<StyledH2>Verify message</StyledH2>
<Label>Message</Label>
<Textarea rows="4" maxLength="255" />
<Label>Address</Label>
<Input type="text" />
<Label>Signature</Label>
<Textarea rows="4" maxLength="255" />
</Verify>
</Wrapper>
);
export default SignVerify;

View File

@ -0,0 +1,52 @@
/* @flow */
import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
import * as SummaryActions from 'actions/SummaryActions';
import * as TokenActions from 'actions/TokenActions';
import type { State, Dispatch } from 'flowtype';
import Summary from './index';
import type { StateProps as BaseStateProps, DispatchProps as BaseDispatchProps } from '../SelectedAccount';
type OwnProps = { }
type StateProps = BaseStateProps & {
tokens: $ElementType<State, 'tokens'>,
summary: $ElementType<State, 'summary'>,
fiat: $ElementType<State, 'fiat'>,
localStorage: $ElementType<State, 'localStorage'>,
};
type DispatchProps = BaseDispatchProps & {
onDetailsToggle: typeof SummaryActions.onDetailsToggle,
addToken: typeof TokenActions.add,
loadTokens: typeof TokenActions.load,
removeToken: typeof TokenActions.remove,
}
export type Props = StateProps & BaseStateProps & DispatchProps & BaseDispatchProps;
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State, own: OwnProps): StateProps => ({
className: 'summary',
selectedAccount: state.selectedAccount,
wallet: state.wallet,
tokens: state.tokens,
summary: state.summary,
fiat: state.fiat,
localStorage: state.localStorage,
});
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
onDetailsToggle: bindActionCreators(SummaryActions.onDetailsToggle, dispatch),
addToken: bindActionCreators(TokenActions.add, dispatch),
loadTokens: bindActionCreators(TokenActions.load, dispatch),
removeToken: bindActionCreators(TokenActions.remove, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Summary);

View File

@ -0,0 +1,75 @@
/* @flow */
import React from 'react';
import BigNumber from 'bignumber.js';
import type { Coin } from 'reducers/LocalStorageReducer';
import type { Props as BaseProps } from './index';
type Props = {
// coin: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'coin'>,
coin: Coin,
summary: $ElementType<BaseProps, 'summary'>,
balance: string,
network: string,
fiat: $ElementType<BaseProps, 'fiat'>,
onToggle: $ElementType<BaseProps, 'onDetailsToggle'>
}
const SummaryDetails = (props: Props): ?React$Element<string> => {
if (!props.summary.details) {
return (
<div className="summary-details">
<div className="toggle" onClick={props.onToggle} />
</div>
);
}
const selectedCoin = props.coin;
const fiatRate = props.fiat.find(f => f.network === selectedCoin.network);
let balanceColumn = null;
let rateColumn = null;
if (fiatRate) {
const accountBalance = new BigNumber(props.balance);
const fiatValue = new BigNumber(fiatRate.value);
const fiat = accountBalance.times(fiatValue).toFixed(2);
balanceColumn = (
<div className="column">
<div className="label">Balance</div>
<div className="fiat-value">${ fiat }</div>
<div className="value">{ props.balance } { selectedCoin.symbol }</div>
</div>
);
rateColumn = (
<div className="column">
<div className="label">Rate</div>
<div className="fiat-value">${ fiatValue.toFixed(2) }</div>
<div className="value">1.00 { selectedCoin.symbol }</div>
</div>
);
} else {
balanceColumn = (
<div className="column">
<div className="label">Balance</div>
<div className="fiat-value">{ props.balance } { selectedCoin.symbol }</div>
</div>
);
}
return (
<div className="summary-details opened">
<div className="toggle" onClick={props.onToggle} />
<div className="content">
{ balanceColumn }
{ rateColumn }
</div>
</div>
);
};
export default SummaryDetails;

View File

@ -0,0 +1,54 @@
/* @flow */
import React from 'react';
import ColorHash from 'color-hash';
import ScaleText from 'react-scale-text';
import * as stateUtils from 'reducers/utils';
import BigNumber from 'bignumber.js';
import type { Props as BaseProps } from './index';
type Props = {
pending: $PropertyType<$ElementType<BaseProps, 'selectedAccount'>, 'pending'>,
tokens: $ElementType<BaseProps, 'tokens'>,
removeToken: $ElementType<BaseProps, 'removeToken'>
}
const SummaryTokens = (props: Props) => {
if (!props.tokens || props.tokens.length < 1) return null;
const bgColor = new ColorHash({ lightness: 0.7 });
const textColor = new ColorHash();
const tokens = props.tokens.map((token, index) => {
const iconColor = {
color: textColor.hex(token.name),
background: bgColor.hex(token.name),
borderColor: bgColor.hex(token.name),
};
const pendingAmount: BigNumber = stateUtils.getPendingAmount(props.pending, token.symbol, true);
const balance: string = new BigNumber(token.balance).minus(pendingAmount).toString(10);
return (
<div key={index} className="token">
<div className="icon" style={iconColor}>
<div className="icon-inner">
<ScaleText widthOnly><p>{ token.symbol }</p></ScaleText>
</div>
</div>
<div className="name">{ token.name }</div>
<div className="balance">{ balance } { token.symbol }</div>
<button className="transparent" onClick={event => props.removeToken(token)} />
</div>
);
});
return (
<div>
{ tokens }
</div>
);
};
export default SummaryTokens;

View File

@ -0,0 +1,120 @@
/* @flow */
import styled from 'styled-components';
import React, { Component } from 'react';
import { H2 } from 'components/common/Heading';
import BigNumber from 'bignumber.js';
import { Async as AsyncSelect } from 'react-select';
import Tooltip from 'rc-tooltip';
import { resolveAfter } from 'utils/promiseUtils';
import { Notification } from 'components/common/Notification';
import * as stateUtils from 'reducers/utils';
import type { NetworkToken } from 'reducers/LocalStorageReducer';
import SelectedAccount from '../SelectedAccount';
import SummaryDetails from './components/Details';
import SummaryTokens from './components/Tokens';
import type { Props } from './index';
const StyledH2 = styled(H2)`
padding: 20px 48px;
`;
const Summary = (props: Props) => {
const device = props.wallet.selectedDevice;
const {
account,
network,
tokens,
pending,
} = props.selectedAccount;
// flow
if (!device || !account || !network) return null;
const tokensTooltip = (
<div className="tooltip-wrapper">
Insert token name, symbol or address to be able to send it.
</div>
);
const explorerLink: string = `${network.explorer.address}${account.address}`;
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
return (
<div>
<StyledH2 className={`summary-header ${account.network}`}>
Account #{ parseInt(account.index) + 1 }
<a href={explorerLink} className="gray" target="_blank" rel="noreferrer noopener">See full transaction history</a>
</StyledH2>
<SummaryDetails
coin={network}
summary={props.summary}
balance={balance}
network={network.network}
fiat={props.fiat}
localStorage={props.localStorage}
onToggle={props.onDetailsToggle}
/>
<StyledH2>
Tokens
<Tooltip
arrowContent={<div className="rc-tooltip-arrow-inner" />}
overlay={tokensTooltip}
placement="top"
>
<span className="what-is-it" />
</Tooltip>
</StyledH2>
{/* 0x58cda554935e4a1f2acbe15f8757400af275e084 Lahod */}
{/* 0x58cda554935e4a1f2acbe15f8757400af275e084 T01 */}
<div className="filter">
<AsyncSelect
className="token-select"
multi={false}
autoload={false}
ignoreCase
backspaceRemoves
value={null}
onChange={token => props.addToken(token, account)}
loadOptions={input => props.loadTokens(input, account.network)}
filterOptions={
(options: Array<NetworkToken>, search: string, values: Array<NetworkToken>) => options.map((o) => {
const added = tokens.find(t => t.symbol === o.symbol);
if (added) {
return {
...o,
name: `${o.name} (Already added)`,
disabled: true,
};
}
return o;
})
}
valueKey="symbol"
labelKey="name"
placeholder="Search for token"
searchPromptText="Type token name or address"
noResultsText="Token not found"
/>
</div>
<SummaryTokens
pending={pending}
tokens={tokens}
removeToken={props.removeToken}
/>
</div>
);
};
export default (props: Props) => (
<SelectedAccount {...props}>
<Summary {...props} />
</SelectedAccount>
);

View File

@ -0,0 +1,105 @@
/* @flow */
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
type Props = {
pathname: string;
}
type State = {
style: {
width: number,
left: number
};
}
class Indicator extends Component<Props, State> {
reposition: () => void;
state: State;
constructor(props: Props) {
super(props);
this.state = {
style: {
width: 0,
left: 0,
},
};
this.reposition = this.reposition.bind(this);
}
handleResize() {
this.reposition();
}
componentDidMount() {
this.reposition();
window.addEventListener('resize', this.reposition, false);
}
componentWillUnmount() {
window.removeEventListener('resize', this.reposition, false);
}
componentDidUpdate(newProps: Props) {
this.reposition();
}
reposition() {
const tabs = document.querySelector('.account-tabs');
if (!tabs) return;
const active = tabs.querySelector('.active');
if (!active) return;
const bounds = active.getBoundingClientRect();
const left = bounds.left - tabs.getBoundingClientRect().left;
if (this.state.style.left !== left) {
this.setState({
style: {
width: bounds.width,
left,
},
});
}
}
render() {
return (
<div className="indicator" style={this.state.style}>{ this.props.pathname }</div>
);
}
}
const AccountTabs = (props: any) => {
const urlParams = props.match.params;
const basePath = `/device/${urlParams.device}/network/${urlParams.network}/account/${urlParams.account}`;
return (
<div className="account-tabs">
{/* <NavLink to={ `${basePath}` }>
History
</NavLink> */}
<NavLink exact to={`${basePath}`}>
Summary
</NavLink>
<NavLink to={`${basePath}/send`}>
Send
</NavLink>
<NavLink to={`${basePath}/receive`}>
Receive
</NavLink>
{/* <NavLink to={ `${basePath}/signverify` }>
Sign &amp; Verify
</NavLink> */}
<Indicator pathname={props.match.pathname} />
</div>
);
};
export default AccountTabs;

View File

@ -36,7 +36,6 @@ const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: St
wallet: state.wallet,
connect: state.connect,
router: state.router,
wallet: state.wallet,
devices: state.devices,
});

View File

@ -37,12 +37,6 @@ export default class InstallBridge extends Component<Props, State> {
};
}
onChange(value: InstallTarget) {
this.setState({
target: value,
});
}
componentWillUpdate() {
if (this.props.browserState.osname && !this.state.target) {
const currentTarget: ?InstallTarget = installers.find(i => i.id === this.props.browserState.osname);
@ -52,6 +46,12 @@ export default class InstallBridge extends Component<Props, State> {
}
}
onChange(value: InstallTarget) {
this.setState({
target: value,
});
}
render() {
if (!this.state.target) {
return <Preloader />;