Improve Gas Price UX (Part 2) (#850)

* Remove gas dropdown & Add gas sliders

* Update styles

* Revert changes made to requestpayment.tsx

* Update style & add custom labels to GasLimitField

* Update styles

* Update confirm transaction modal

* Revert "Update confirm transaction modal"

This reverts commit 743c9a505fe070feb55f7af550ad918a3d8899d1.

* Add transaction fee to tx confirmation modal

* Update styles

* Remove old gasPriceDropdown files & use network units in tx fee

* Add option to lock gaslimit data

* fix tslint errors

* Rename lockData to readOnly

* Add option to check if validAmount before generating transaction

* Add nonce field if gas slider is readonly

* Automatically set nonce in  <Send/>

* Update snapshot

* Move getNonceRequested to GasSlider component

* Add optional to check value for isValidAmount selector

* Add selector for transaction fee

* Update GasSlider component & Rename to Gas

* update snapshots

* Fix subtabs className

* Update styles

* Remove dataField on contract interact

* rename <Gas/> to <TXMetaDataPanel/>
This commit is contained in:
James Prado 2018-01-24 22:43:27 -05:00 committed by Daniel Ternyak
parent 22c107fe4c
commit c631f45ab7
38 changed files with 459 additions and 320 deletions

View File

@ -1,4 +1,4 @@
import { GasPrice } from './components'; import { TransactionFee } from './components';
import { Amount } from '../../Amount'; import { Amount } from '../../Amount';
import React from 'react'; import React from 'react';
@ -9,9 +9,9 @@ export const AmountAndGasPrice: React.SFC<{}> = () => (
<strong> <strong>
<Amount /> <Amount />
</strong>{' '} </strong>{' '}
with a gas price of{' '} with a transaction fee of{' '}
<strong> <strong>
<GasPrice /> <TransactionFee />
</strong> </strong>
</p> </p>
</li> </li>

View File

@ -0,0 +1,57 @@
import React from 'react';
import { getTransactionFee, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import { UnitDisplay } from 'components/ui';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { getNetworkConfig } from 'selectors/config';
import BN from 'bn.js';
interface Props {
rates: AppState['rates']['rates'];
network: AppState['config']['network'];
isOffline: AppState['config']['offline'];
}
class TransactionFeeClass extends React.Component<Props> {
public render() {
const { rates, network, isOffline } = this.props;
return (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const fee = getTransactionFee(transactionInstance);
const usdFee = network.isTestnet ? new BN(0) : fee.muln(rates[network.unit].USD);
return (
<React.Fragment>
<UnitDisplay unit={'ether'} value={fee} symbol={network.unit} checkOffline={false} />{' '}
{!isOffline &&
rates[network.unit] && (
<span>
($
<UnitDisplay
value={usdFee}
unit="ether"
displayShortBalance={2}
displayTrailingZeroes={true}
checkOffline={true}
/>)
</span>
)}
</React.Fragment>
);
}}
/>
);
}
}
function mapStateToProps(state: AppState) {
return {
rates: state.rates.rates,
network: getNetworkConfig(state),
isOffline: state.config.offline
};
}
export const TransactionFee = connect(mapStateToProps)(TransactionFeeClass);

View File

@ -1 +1,2 @@
export * from './GasPrice'; export * from './GasPrice';
export * from './TransactionFee';

View File

@ -8,27 +8,46 @@ import { gasLimitValidator } from 'libs/validators';
interface Props { interface Props {
includeLabel: boolean; includeLabel: boolean;
onlyIncludeLoader: boolean; onlyIncludeLoader: boolean;
customLabel?: string;
disabled?: boolean;
} }
export const GaslimitLoading: React.SFC<{ gasEstimationPending: boolean }> = ({ export const GaslimitLoading: React.SFC<{
gasEstimationPending gasEstimationPending: boolean;
}) => ( onlyIncludeLoader?: boolean;
}> = ({ gasEstimationPending, onlyIncludeLoader }) => (
<CSSTransition in={gasEstimationPending} timeout={300} classNames="fade"> <CSSTransition in={gasEstimationPending} timeout={300} classNames="fade">
<div className={`SimpleGas-estimating small ${gasEstimationPending ? 'active' : ''}`}> <div className={`Calculating-limit small ${gasEstimationPending ? 'active' : ''}`}>
Calculating gas limit {!!onlyIncludeLoader ? 'Calculating gas limit' : 'Calculating'}
<Spinner /> <Spinner />
</div> </div>
</CSSTransition> </CSSTransition>
); );
export const GasLimitField: React.SFC<Props> = ({ includeLabel, onlyIncludeLoader }) => ( export const GasLimitField: React.SFC<Props> = ({
includeLabel,
onlyIncludeLoader,
customLabel,
disabled
}) => (
<React.Fragment> <React.Fragment>
{includeLabel ? <label>{translate('TRANS_gas')} </label> : null}
<GasLimitFieldFactory <GasLimitFieldFactory
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => ( withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
<> <React.Fragment>
<GaslimitLoading gasEstimationPending={gasEstimationPending} /> <div className="flex-wrapper">
{includeLabel ? (
customLabel ? (
<label>{customLabel} </label>
) : (
<label>{translate('TRANS_gas')} </label>
)
) : null}
<div className="flex-spacer" />
<GaslimitLoading
gasEstimationPending={gasEstimationPending}
onlyIncludeLoader={false}
/>
</div>
{onlyIncludeLoader ? null : ( {onlyIncludeLoader ? null : (
<input <input
className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`} className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`}
@ -37,9 +56,10 @@ export const GasLimitField: React.SFC<Props> = ({ includeLabel, onlyIncludeLoade
readOnly={!!readOnly} readOnly={!!readOnly}
value={raw} value={raw}
onChange={onChange} onChange={onChange}
disabled={disabled}
/> />
)} )}
</> </React.Fragment>
)} )}
/> />
</React.Fragment> </React.Fragment>

View File

@ -1,10 +0,0 @@
@import 'common/sass/variables';
.GasSlider {
&-toggle {
display: inline-block;
position: relative;
margin-top: $space-sm;
left: -8px;
}
}

View File

@ -1,37 +0,0 @@
.AdvancedGas {
margin-top: 0;
margin-bottom: 0;
.checkbox {
display: flex;
align-items: center;
width: fit-content;
input[type='checkbox'] {
position: initial;
margin: 0;
margin-right: 8px;
}
span {
font-size: 1rem;
font-weight: 400;
}
}
&-gasLimit {
display: flex;
flex-wrap: wrap;
align-items: baseline;
.flex-spacer {
flex-grow: 2;
}
input {
width: 100%;
margin-top: 0;
}
}
&-nonce {
input {
margin-top: 0;
}
}
}

View File

@ -1,101 +0,0 @@
import React from 'react';
import classnames from 'classnames';
import translate from 'translations';
import FeeSummary from './FeeSummary';
import './AdvancedGas.scss';
import { TToggleAutoGasLimit, toggleAutoGasLimit } from 'actions/config';
import { AppState } from 'reducers';
import { TInputGasPrice } from 'actions/transaction';
import { NonceField, GasLimitField, DataField } from 'components';
import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
interface OwnProps {
inputGasPrice: TInputGasPrice;
gasPrice: AppState['transaction']['fields']['gasPrice'];
}
interface StateProps {
autoGasLimitEnabled: AppState['config']['autoGasLimit'];
validGasPrice: boolean;
}
interface DispatchProps {
toggleAutoGasLimit: TToggleAutoGasLimit;
}
type Props = OwnProps & StateProps & DispatchProps;
class AdvancedGas extends React.Component<Props> {
public render() {
const { autoGasLimitEnabled, gasPrice, validGasPrice } = this.props;
return (
<div className="AdvancedGas row form-group">
<div className="col-md-12">
<label className="checkbox">
<input
type="checkbox"
defaultChecked={autoGasLimitEnabled}
onChange={this.handleToggleAutoGasLimit}
/>
<span>Automatically Calculate Gas Limit</span>
</label>
</div>
<div className="col-md-4 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input
className={classnames('form-control', { 'is-invalid': !validGasPrice })}
type="number"
placeholder="e.g. 40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
</div>
<div className="col-md-4 col-sm-6 col-xs-12 AdvancedGas-gasLimit">
<label>{translate('OFFLINE_Step2_Label_4')}</label>
<div className="SimpleGas-flex-spacer" />
<GasLimitField includeLabel={false} onlyIncludeLoader={false} />
</div>
<div className="col-md-4 col-sm-12 col-xs-12 AdvancedGas-nonce">
<NonceField alwaysDisplay={true} />
</div>
<div className="col-md-12 col-xs-12">
<DataField />
</div>
<div className="col-sm-12 col-xs-12">
<FeeSummary
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}
</span>
)}
/>
</div>
</div>
);
}
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputGasPrice(sanitizeNumericalInput(value));
};
private handleToggleAutoGasLimit = (_: React.FormEvent<HTMLInputElement>) => {
this.props.toggleAutoGasLimit();
};
}
export default connect(
(state: AppState) => ({
autoGasLimitEnabled: getAutoGasLimitEnabled(state),
validGasPrice: isValidGasPrice(state)
}),
{ toggleAutoGasLimit }
)(AdvancedGas);

View File

@ -1,2 +0,0 @@
import GasSlider from './GasSlider';
export default GasSlider;

View File

@ -1,38 +0,0 @@
@import "common/sass/variables";
.GasPrice {
&-dropdown-menu {
padding: 0.5rem !important;
min-width: 300px !important;
@media screen and (max-width: $screen-xs) {
left: 0;
right: auto;
}
}
&-header {
max-width: 26rem;
color: $text-color;
p {
font-weight: 400;
margin: $space-sm 0 0;
}
a, a:hover, a:focus, a:visited {
color: $brand-primary !important;
}
}
&-padding-reset {
padding-left: 0 !important;
padding-right: 0 !important;
}
&-description {
white-space: normal;
font-weight: 300 !important;
margin: 2rem 0 0;
}
}

View File

@ -20,7 +20,6 @@ import {
CustomNodeConfig, CustomNodeConfig,
CustomNetworkConfig CustomNetworkConfig
} from 'config'; } from 'config';
import GasPriceDropdown from './components/GasPriceDropdown';
import Navigation from './components/Navigation'; import Navigation from './components/Navigation';
import CustomNodeModal from './components/CustomNodeModal'; import CustomNodeModal from './components/CustomNodeModal';
import OnlineStatus from './components/OnlineStatus'; import OnlineStatus from './components/OnlineStatus';
@ -134,10 +133,6 @@ export default class Header extends Component<Props, State> {
<OnlineStatus isOffline={isOffline} /> <OnlineStatus isOffline={isOffline} />
</div> </div>
<div className="Header-branding-right-dropdown">
<GasPriceDropdown onChange={this.props.setGasPriceField} />
</div>
<div className="Header-branding-right-dropdown"> <div className="Header-branding-right-dropdown">
<LanguageDropDown <LanguageDropDown
ariaLabel={`change language. current language ${languages[selectedLanguage]}`} ariaLabel={`change language. current language ${languages[selectedLanguage]}`}

View File

@ -17,7 +17,7 @@ export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
<NonceFieldFactory <NonceFieldFactory
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => { withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
const content = ( const content = (
<> <div>
<label>Nonce</label> <label>Nonce</label>
{nonceHelp} {nonceHelp}
@ -29,7 +29,7 @@ export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
readOnly={readOnly} readOnly={readOnly}
onChange={onChange} onChange={onChange}
/> />
</> </div>
); );
return alwaysDisplay || shouldDisplay ? content : null; return alwaysDisplay || shouldDisplay ? content : null;

View File

@ -21,7 +21,7 @@ export default class SubTabs extends React.Component<Props> {
return ( return (
<div className="SubTabs row"> <div className="SubTabs row">
<div className="SubTabs-tabs col-sm-8"> <div className="SubTabs-tabs col-sm-12">
{tabs.map((t, i) => ( {tabs.map((t, i) => (
// Same as normal Link, but knows when it's active, and applies activeClassName // Same as normal Link, but knows when it's active, and applies activeClassName
<NavLink <NavLink

View File

@ -0,0 +1,26 @@
@import 'common/sass/variables';
.Gas {
&-toggle {
display: inline-block;
position: relative;
margin-top: $space-sm;
left: -8px;
}
}
.Calculating-limit {
color: rgba(51, 51, 51, 0.7);
display: flex;
align-items: baseline;
font-weight: 400;
opacity: 0;
pointer-events: none;
&.active {
opacity: 1;
}
.Spinner {
margin-left: 8px;
}
}

View File

@ -1,15 +1,24 @@
import React from 'react'; import React from 'react';
import { translateRaw } from 'translations'; import { translateRaw } from 'translations';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { inputGasPrice, TInputGasPrice } from 'actions/transaction'; import {
inputGasPrice,
TInputGasPrice,
getNonceRequested,
TGetNonceRequested,
reset,
TReset
} from 'actions/transaction';
import { fetchCCRates, TFetchCCRates } from 'actions/rates'; import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig, getOffline } from 'selectors/config'; import { getNetworkConfig, getOffline } from 'selectors/config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import SimpleGas from './components/SimpleGas'; import SimpleGas from './components/SimpleGas';
import AdvancedGas from './components/AdvancedGas'; import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas';
import './GasSlider.scss'; import './TXMetaDataPanel.scss';
import { getGasPrice } from 'selectors/transaction'; import { getGasPrice } from 'selectors/transaction';
type SliderStates = 'simple' | 'advanced';
interface StateProps { interface StateProps {
gasPrice: AppState['transaction']['fields']['gasPrice']; gasPrice: AppState['transaction']['fields']['gasPrice'];
offline: AppState['config']['offline']; offline: AppState['config']['offline'];
@ -19,26 +28,42 @@ interface StateProps {
interface DispatchProps { interface DispatchProps {
inputGasPrice: TInputGasPrice; inputGasPrice: TInputGasPrice;
fetchCCRates: TFetchCCRates; fetchCCRates: TFetchCCRates;
getNonceRequested: TGetNonceRequested;
reset: TReset;
}
// Set default props for props that can't be truthy or falsy
interface DefaultProps {
initialState: SliderStates;
} }
interface OwnProps { interface OwnProps {
disableAdvanced?: boolean; initialState?: SliderStates;
disableToggle?: boolean;
advancedGasOptions?: AdvancedOptions;
className?: string;
} }
type Props = DispatchProps & OwnProps & StateProps; type Props = DispatchProps & OwnProps & StateProps;
interface State { interface State {
showAdvanced: boolean; sliderState: SliderStates;
} }
class GasSlider extends React.Component<Props, State> { class TXMetaDataPanel extends React.Component<Props, State> {
public static defaultProps: DefaultProps = {
initialState: 'simple'
};
public state: State = { public state: State = {
showAdvanced: false sliderState: (this.props as DefaultProps).initialState
}; };
public componentDidMount() { public componentDidMount() {
if (!this.props.offline) { if (!this.props.offline) {
this.props.reset();
this.props.fetchCCRates([this.props.network.unit]); this.props.fetchCCRates([this.props.network.unit]);
this.props.getNonceRequested();
} }
} }
@ -49,21 +74,24 @@ class GasSlider extends React.Component<Props, State> {
} }
public render() { public render() {
const { offline, disableAdvanced, gasPrice } = this.props; const { offline, disableToggle, gasPrice, advancedGasOptions, className = '' } = this.props;
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced; const showAdvanced = this.state.sliderState === 'advanced' || offline;
return ( return (
<div className="GasSlider"> <div className={`Gas col-md-12 ${className}`}>
{showAdvanced ? ( {showAdvanced ? (
<AdvancedGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} /> <AdvancedGas
gasPrice={gasPrice}
inputGasPrice={this.props.inputGasPrice}
options={advancedGasOptions}
/>
) : ( ) : (
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} /> <SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
)} )}
{!offline && {!offline &&
!disableAdvanced && ( !disableToggle && (
<div className="help-block"> <div className="help-block">
<a className="GasSlider-toggle" onClick={this.toggleAdvanced}> <a className="Gas-toggle" onClick={this.toggleAdvanced}>
<strong> <strong>
{showAdvanced {showAdvanced
? `- ${translateRaw('Back to simple')}` ? `- ${translateRaw('Back to simple')}`
@ -77,7 +105,7 @@ class GasSlider extends React.Component<Props, State> {
} }
private toggleAdvanced = () => { private toggleAdvanced = () => {
this.setState({ showAdvanced: !this.state.showAdvanced }); this.setState({ sliderState: this.state.sliderState === 'advanced' ? 'simple' : 'advanced' });
}; };
} }
@ -91,5 +119,7 @@ function mapStateToProps(state: AppState): StateProps {
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
inputGasPrice, inputGasPrice,
fetchCCRates fetchCCRates,
})(GasSlider); getNonceRequested,
reset
})(TXMetaDataPanel);

View File

@ -0,0 +1,49 @@
@import 'common/sass/variables';
.AdvancedGas {
margin-top: 0;
margin-bottom: 0;
&-calculate-limit {
.checkbox {
display: flex;
align-items: center;
width: fit-content;
input[type='checkbox'] {
position: initial;
margin: 0;
margin-right: 8px;
}
span {
font-size: 1rem;
font-weight: 400;
}
}
}
&-flex-wrapper {
margin: 0px -8px;
}
&-gas-price,
&-gas-limit,
&-nonce {
width: initial;
flex-grow: 1;
margin: 0px 8px;
}
@media screen and (max-width: $screen-lg) {
&-flex-wrapper {
flex-wrap: wrap;
}
&-nonce {
width: 100%;
}
}
&-data {
}
&-fee-summary {
}
}

View File

@ -0,0 +1,139 @@
import React from 'react';
import classnames from 'classnames';
import translate, { translateRaw } from 'translations';
import FeeSummary from './FeeSummary';
import './AdvancedGas.scss';
import { TToggleAutoGasLimit, toggleAutoGasLimit } from 'actions/config';
import { AppState } from 'reducers';
import { TInputGasPrice } from 'actions/transaction';
import { NonceField, GasLimitField, DataField } from 'components';
import { connect } from 'react-redux';
import { getAutoGasLimitEnabled } from 'selectors/config';
import { isValidGasPrice } from 'selectors/transaction';
import { sanitizeNumericalInput } from 'libs/values';
export interface AdvancedOptions {
gasPriceField?: boolean;
gasLimitField?: boolean;
nonceField?: boolean;
dataField?: boolean;
feeSummary?: boolean;
}
interface OwnProps {
inputGasPrice: TInputGasPrice;
gasPrice: AppState['transaction']['fields']['gasPrice'];
options?: AdvancedOptions;
}
interface StateProps {
autoGasLimitEnabled: AppState['config']['autoGasLimit'];
validGasPrice: boolean;
}
interface DispatchProps {
toggleAutoGasLimit: TToggleAutoGasLimit;
}
interface State {
options: AdvancedOptions;
}
type Props = OwnProps & StateProps & DispatchProps;
class AdvancedGas extends React.Component<Props, State> {
public state = {
options: {
gasPriceField: true,
gasLimitField: true,
nonceField: true,
dataField: true,
feeSummary: true,
...this.props.options
}
};
public render() {
const { autoGasLimitEnabled, gasPrice, validGasPrice } = this.props;
const { gasPriceField, gasLimitField, nonceField, dataField, feeSummary } = this.state.options;
return (
<div className="AdvancedGas row form-group">
<div className="AdvancedGas-calculate-limit">
<label className="checkbox">
<input
type="checkbox"
defaultChecked={autoGasLimitEnabled}
onChange={this.handleToggleAutoGasLimit}
/>
<span>Automatically Calculate Gas Limit</span>
</label>
</div>
<div className="AdvancedGas-flex-wrapper flex-wrapper">
{gasPriceField && (
<div className="AdvancedGas-gas-price">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input
className={classnames('form-control', { 'is-invalid': !validGasPrice })}
type="number"
placeholder="40"
value={gasPrice.raw}
onChange={this.handleGasPriceChange}
/>
</div>
)}
{gasLimitField && (
<div className="AdvancedGas-gas-limit">
<GasLimitField
includeLabel={true}
customLabel={translateRaw('OFFLINE_Step2_Label_4')}
onlyIncludeLoader={false}
/>
</div>
)}
{nonceField && (
<div className="AdvancedGas-nonce">
<NonceField alwaysDisplay={true} />
</div>
)}
</div>
{dataField && (
<div className="AdvancedGas-data">
<DataField />
</div>
)}
{feeSummary && (
<div className="AdvancedGas-fee-summary">
<FeeSummary
render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}
</span>
)}
/>
</div>
)}
</div>
);
}
private handleGasPriceChange = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputGasPrice(sanitizeNumericalInput(value));
};
private handleToggleAutoGasLimit = (_: React.FormEvent<HTMLInputElement>) => {
this.props.toggleAutoGasLimit();
};
}
export default connect(
(state: AppState) => ({
autoGasLimitEnabled: getAutoGasLimitEnabled(state),
validGasPrice: isValidGasPrice(state)
}),
{ toggleAutoGasLimit }
)(AdvancedGas);

View File

@ -4,23 +4,26 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
&-flex-spacer { &-input-group {
flex-grow: 2;
}
&-title {
display: flex; display: flex;
} > .SimpleGas-slider {
&-estimating { flex-grow: 1;
color: rgba(51, 51, 51, 0.7); margin-right: $input-padding-x;
display: flex;
align-items: baseline;
font-weight: 400;
opacity: 0;
&.active {
opacity: 1;
} }
.Spinner { > .FeeSummary {
margin-left: 8px; margin-left: $input-padding-x;
min-width: 224px;
}
@media screen and (max-width: $screen-md) {
flex-wrap: wrap;
> .SimpleGas-slider {
width: 100%;
margin-right: 0;
}
> .FeeSummary {
width: 100%;
margin-left: 0;
}
} }
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import Slider from 'rc-slider'; import Slider from 'rc-slider';
import translate from 'translations'; import translate, { translateRaw } from 'translations';
import { gasPriceDefaults } from 'config'; import { gasPriceDefaults } from 'config';
import FeeSummary from './FeeSummary'; import FeeSummary from './FeeSummary';
import { TInputGasPrice } from 'actions/transaction'; import { TInputGasPrice } from 'actions/transaction';
@ -29,14 +29,16 @@ class SimpleGas extends React.Component<Props> {
return ( return (
<div className="SimpleGas row form-group"> <div className="SimpleGas row form-group">
<div className="col-md-12 SimpleGas-title"> <div className="SimpleGas-title">
<label className="SimpleGas-label">{translate('Transaction Fee')}</label> <GasLimitField
<div className="SimpleGas-flex-spacer" /> includeLabel={true}
<GasLimitField includeLabel={false} onlyIncludeLoader={true} /> customLabel={translateRaw('Transaction Fee')}
onlyIncludeLoader={true}
/>
</div> </div>
{gasLimitEstimationTimedOut && ( {gasLimitEstimationTimedOut && (
<div className="col-md-12 prompt-toggle-gas-limit"> <div className="prompt-toggle-gas-limit">
<p className="small"> <p className="small">
{isWeb3Node {isWeb3Node
? "Couldn't calculate gas limit, if you know what your doing, try setting manually in Advanced settings" ? "Couldn't calculate gas limit, if you know what your doing, try setting manually in Advanced settings"
@ -45,7 +47,7 @@ class SimpleGas extends React.Component<Props> {
</div> </div>
)} )}
<div className="col-md-8 col-sm-12"> <div className="SimpleGas-input-group">
<div className="SimpleGas-slider"> <div className="SimpleGas-slider">
<Slider <Slider
onChange={this.handleSlider} onChange={this.handleSlider}
@ -59,8 +61,6 @@ class SimpleGas extends React.Component<Props> {
<span>{translate('Fast')}</span> <span>{translate('Fast')}</span>
</div> </div>
</div> </div>
</div>
<div className="col-md-4 col-sm-12">
<FeeSummary <FeeSummary
render={({ fee, usd }) => ( render={({ fee, usd }) => (
<span> <span>

View File

@ -0,0 +1,2 @@
import TXMetaDataPanel from './TXMetaDataPanel';
export default TXMetaDataPanel;

View File

@ -3,12 +3,12 @@
.DWModal { .DWModal {
&-path { &-path {
display: flex;
margin-bottom: 20px; margin-bottom: 20px;
&-label { &-label {
font-size: $font-size-medium; font-size: $font-size-medium;
margin-right: 16px; margin-right: 16px;
line-height: $input-height-base;
} }
.form-control { .form-control {
@ -25,7 +25,7 @@
&-addresses { &-addresses {
overflow-y: scroll; overflow-y: scroll;
&-table { &-table {
width: 695px; width: 732px;
text-align: center; text-align: center;
margin: auto; margin: auto;
margin-bottom: 10px; margin-bottom: 10px;

View File

@ -122,8 +122,10 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
handleClose={onCancel} handleClose={onCancel}
> >
<div className="DWModal"> <div className="DWModal">
{/* TODO: replace styles for flexbox with flexbox classes in https://github.com/MyEtherWallet/MyEtherWallet/pull/850/files#diff-2150778b9391533fec7b8afd060c7672 */} <form
<form className="DWModal-path form-group-sm" onSubmit={this.handleSubmitCustomPath}> className="DWModal-path form-group-sm flex-wrapper"
onSubmit={this.handleSubmitCustomPath}
>
<span className="DWModal-path-label">Addresses </span> <span className="DWModal-path-label">Addresses </span>
<Select <Select
name="fieldDPath" name="fieldDPath"

View File

@ -14,5 +14,5 @@ export { default as Footer } from './Footer';
export { default as BalanceSidebar } from './BalanceSidebar'; export { default as BalanceSidebar } from './BalanceSidebar';
export { default as PaperWallet } from './PaperWallet'; export { default as PaperWallet } from './PaperWallet';
export { default as AlphaAgreement } from './AlphaAgreement'; export { default as AlphaAgreement } from './AlphaAgreement';
export { default as GasSlider } from './GasSlider'; export { default as TXMetaDataPanel } from './TXMetaDataPanel';
export { default as WalletDecrypt } from './WalletDecrypt'; export { default as WalletDecrypt } from './WalletDecrypt';

View File

@ -7,10 +7,12 @@
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 0.4rem 1rem; padding: 0.4rem 1rem;
border-radius: 2px; border-radius: 2px;
height: 2.5rem;
&:focus { &:focus {
outline: none; outline: none;
} }
&:active, &:hover { &:active,
&:hover {
opacity: 0.8; opacity: 0.8;
} }
> li { > li {
@ -46,10 +48,10 @@
text-align: left; text-align: left;
z-index: 500; z-index: 500;
background: white; background: white;
box-shadow: 2px 1px 60px rgba(0,0,0,.175); box-shadow: 2px 1px 60px rgba(0, 0, 0, 0.175);
&::before { &::before {
content: ""; content: '';
position: absolute; position: absolute;
top: -20px; top: -20px;
left: 50%; left: 50%;
@ -59,7 +61,7 @@
border-top: 10px solid transparent; border-top: 10px solid transparent;
border-bottom: 10px solid #fff; border-bottom: 10px solid #fff;
} }
&.open { &.open {
display: block; display: block;
} }
@ -74,7 +76,7 @@
padding: 5px 20px; padding: 5px 20px;
color: #163151; color: #163151;
&:hover { &:hover {
opacity: .8; opacity: 0.8;
background-color: #163151; background-color: #163151;
color: #fff; color: #fff;
} }
@ -84,7 +86,7 @@
color: grey; color: grey;
&:hover { &:hover {
background-color: #fff; background-color: #fff;
color:#163151; color: #163151;
cursor: not-allowed; cursor: not-allowed;
} }
} }
@ -109,4 +111,4 @@
img { img {
padding-right: 1px; padding-right: 1px;
} }
} }

View File

@ -1,10 +1,8 @@
import translate from 'translations'; import translate from 'translations';
import classnames from 'classnames'; import classnames from 'classnames';
import { DataFieldFactory } from 'components/DataFieldFactory'; import { DataFieldFactory } from 'components/DataFieldFactory';
import { GasLimitFieldFactory } from 'components/GasLimitFieldFactory';
import { SendButtonFactory } from 'components/SendButtonFactory'; import { SendButtonFactory } from 'components/SendButtonFactory';
import { SigningStatus } from 'components/SigningStatus'; import { SigningStatus } from 'components/SigningStatus';
import { NonceField } from 'components/NonceField';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt'; import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import { GenerateTransaction } from 'components/GenerateTransaction'; import { GenerateTransaction } from 'components/GenerateTransaction';
import React, { Component } from 'react'; import React, { Component } from 'react';
@ -12,6 +10,7 @@ import { setToField, TSetToField } from 'actions/transaction';
import { resetWallet, TResetWallet } from 'actions/wallet'; import { resetWallet, TResetWallet } from 'actions/wallet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { FullWalletOnly } from 'components/renderCbs'; import { FullWalletOnly } from 'components/renderCbs';
import { NonceField, TXMetaDataPanel } from 'components';
import './Deploy.scss'; import './Deploy.scss';
interface DispatchProps { interface DispatchProps {
@ -46,27 +45,22 @@ class DeployClass extends Component<DispatchProps> {
/> />
</div> </div>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">Gas Limit</h4>
<GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input
name="gasLimit"
value={raw}
disabled={readOnly}
onChange={onChange}
className={classnames('Deploy-field-input', 'form-control', {
'is-invalid': !value
})}
/>
)}
/>
</label>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-12 clearfix">
<NonceField alwaysDisplay={false} /> <NonceField alwaysDisplay={false} />
</div> </div>
</div> </div>
<div className="row form-group">
<div className="col-xs-12 clearfix">
<TXMetaDataPanel
initialState="advanced"
disableToggle={true}
advancedGasOptions={{ dataField: false }}
/>
</div>
</div>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-12 clearfix"> <div className="col-xs-12 clearfix">
<GenerateTransaction /> <GenerateTransaction />

View File

@ -1,7 +1,6 @@
import { GasLimitField } from './GasLimitField';
import { AmountField } from './AmountField'; import { AmountField } from './AmountField';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { NonceField, SendButton, SigningStatus } from 'components'; import { SendButton, SigningStatus, TXMetaDataPanel } from 'components';
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt'; import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import { FullWalletOnly } from 'components/renderCbs'; import { FullWalletOnly } from 'components/renderCbs';
@ -12,9 +11,13 @@ export class Fields extends Component<OwnProps> {
public render() { public render() {
const makeContent = () => ( const makeContent = () => (
<React.Fragment> <React.Fragment>
<GasLimitField />
<AmountField /> <AmountField />
<NonceField alwaysDisplay={false} /> <TXMetaDataPanel
className="form-group"
initialState="advanced"
disableToggle={true}
advancedGasOptions={{ dataField: false }}
/>
{this.props.button} {this.props.button}
<SigningStatus /> <SigningStatus />
<SendButton /> <SendButton />

View File

@ -1,22 +0,0 @@
import React from 'react';
import { GasLimitFieldFactory } from 'components/GasLimitFieldFactory';
import classnames from 'classnames';
export const GasLimitField: React.SFC<{}> = () => (
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Gas Limit</h4>
<GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input
name="gasLimit"
value={raw}
disabled={readOnly}
onChange={onChange}
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !value
})}
/>
)}
/>
</label>
);

View File

@ -4,7 +4,7 @@ import { isAnyOfflineWithWeb3 } from 'selectors/derived';
import { import {
AddressField, AddressField,
AmountField, AmountField,
GasSlider, TXMetaDataPanel,
SendEverything, SendEverything,
CurrentCustomMessage, CurrentCustomMessage,
GenerateTransaction, GenerateTransaction,
@ -29,7 +29,7 @@ const content = (
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-12"> <div className="col-xs-12">
<GasSlider /> <TXMetaDataPanel />
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ import BN from 'bn.js';
import { NetworkConfig } from 'config'; import { NetworkConfig } from 'config';
import { validNumber, validDecimal } from 'libs/validators'; import { validNumber, validDecimal } from 'libs/validators';
import { getGasLimit } from 'selectors/transaction'; import { getGasLimit } from 'selectors/transaction';
import { AddressField, AmountField, GasLimitField } from 'components'; import { AddressField, AmountField, TXMetaDataPanel } from 'components';
import { SetGasLimitFieldAction } from 'actions/transaction/actionTypes/fields'; import { SetGasLimitFieldAction } from 'actions/transaction/actionTypes/fields';
import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values'; import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values';
import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config'; import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config';
@ -106,7 +106,16 @@ class RequestPayment extends React.Component<Props, {}> {
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-11">
<GasLimitField includeLabel={true} onlyIncludeLoader={false} /> <TXMetaDataPanel
initialState="advanced"
disableToggle={true}
advancedGasOptions={{
gasPriceField: false,
nonceField: false,
dataField: false,
feeSummary: false
}}
/>
</div> </div>
</div> </div>

View File

@ -34,13 +34,14 @@
} }
&-dropdown { &-dropdown {
display: inline-block; display: inline-block;
margin: 0.5rem 0; margin: 0 0;
} }
&-input { &-input {
width: 100%; width: 100%;
max-width: 10rem; max-width: 10rem;
margin-right: $space-sm; margin-right: $space-sm;
margin-bottom: 0;
} }
&-divider { &-divider {
@ -54,6 +55,3 @@
margin-top: $space * 2.5; margin-top: $space * 2.5;
} }
} }

View File

@ -329,7 +329,7 @@ export default class CurrencySwap extends Component<Props, State> {
<article className="CurrencySwap"> <article className="CurrencySwap">
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1> <h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
{loaded || timeoutLoaded ? ( {loaded || timeoutLoaded ? (
<div className="form-inline CurrencySwap-inner-wrap"> <div className="CurrencySwap-inner-wrap">
<div className="CurrencySwap-input-group"> <div className="CurrencySwap-input-group">
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>} {originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
<input <input

View File

@ -3,7 +3,7 @@ import { AmountFieldFactory } from 'components/AmountFieldFactory';
import { AddressFieldFactory } from 'components/AddressFieldFactory'; import { AddressFieldFactory } from 'components/AddressFieldFactory';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { GenerateTransaction, SendButton, SigningStatus, GasSlider } from 'components'; import { GenerateTransaction, SendButton, SigningStatus, TXMetaDataPanel } from 'components';
import { resetWallet, TResetWallet } from 'actions/wallet'; import { resetWallet, TResetWallet } from 'actions/wallet';
import translate from 'translations'; import translate from 'translations';
import { getUnit } from 'selectors/transaction'; import { getUnit } from 'selectors/transaction';
@ -76,7 +76,7 @@ class FieldsClass extends Component<Props> {
</div> </div>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-12"> <div className="col-xs-12">
<GasSlider disableAdvanced={true} /> <TXMetaDataPanel initialState={'simple'} disableToggle={true} />
</div> </div>
</div> </div>
<SigningStatus /> <SigningStatus />

View File

@ -30,6 +30,11 @@ const getTransactionFields = (t: Tx): IHexStrTransaction => {
}; };
}; };
const getTransactionFee = (t: Tx) => {
const { gasPrice, gasLimit } = getTransactionFields(t);
return Wei(gasPrice).mul(Wei(gasLimit));
};
/** /**
* @description Return the minimum amount of ether needed * @description Return the minimum amount of ether needed
* @param t * @param t
@ -101,5 +106,6 @@ export {
validateTx, validateTx,
makeTransaction, makeTransaction,
getTransactionFields, getTransactionFields,
getTransactionFee,
computeIndexingHash computeIndexingHash
}; };

View File

@ -20,6 +20,7 @@ export {
validGasLimit, validGasLimit,
makeTransaction, makeTransaction,
getTransactionFields, getTransactionFields,
getTransactionFee,
computeIndexingHash computeIndexingHash
} from './ether'; } from './ether';
export * from './token'; export * from './token';

View File

@ -33,4 +33,5 @@
@import './styles/overrides'; @import './styles/overrides';
@import './styles/scaffolding'; @import './styles/scaffolding';
@import './styles/tab'; @import './styles/tab';
@import './styles/flexbox';
@import './fonts'; @import './fonts';

View File

@ -0,0 +1,12 @@
.flex-wrapper {
display: flex;
&-wrap {
flex-wrap: wrap;
}
&-nowrap {
flex-wrap: nowrap;
}
.flex-spacer {
flex-grow: 2;
}
}

View File

@ -30,7 +30,6 @@ input[readonly] {
} }
.form-control { .form-control {
margin-top: $space-sm;
margin-bottom: $space-sm; margin-bottom: $space-sm;
transition: $transition; transition: $transition;
padding: $input-padding; padding: $input-padding;