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:
parent
22c107fe4c
commit
c631f45ab7
|
@ -1,4 +1,4 @@
|
|||
import { GasPrice } from './components';
|
||||
import { TransactionFee } from './components';
|
||||
import { Amount } from '../../Amount';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -9,9 +9,9 @@ export const AmountAndGasPrice: React.SFC<{}> = () => (
|
|||
<strong>
|
||||
<Amount />
|
||||
</strong>{' '}
|
||||
with a gas price of{' '}
|
||||
with a transaction fee of{' '}
|
||||
<strong>
|
||||
<GasPrice />
|
||||
<TransactionFee />
|
||||
</strong>
|
||||
</p>
|
||||
</li>
|
||||
|
|
|
@ -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);
|
|
@ -1 +1,2 @@
|
|||
export * from './GasPrice';
|
||||
export * from './TransactionFee';
|
||||
|
|
|
@ -8,27 +8,46 @@ import { gasLimitValidator } from 'libs/validators';
|
|||
interface Props {
|
||||
includeLabel: boolean;
|
||||
onlyIncludeLoader: boolean;
|
||||
customLabel?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const GaslimitLoading: React.SFC<{ gasEstimationPending: boolean }> = ({
|
||||
gasEstimationPending
|
||||
}) => (
|
||||
export const GaslimitLoading: React.SFC<{
|
||||
gasEstimationPending: boolean;
|
||||
onlyIncludeLoader?: boolean;
|
||||
}> = ({ gasEstimationPending, onlyIncludeLoader }) => (
|
||||
<CSSTransition in={gasEstimationPending} timeout={300} classNames="fade">
|
||||
<div className={`SimpleGas-estimating small ${gasEstimationPending ? 'active' : ''}`}>
|
||||
Calculating gas limit
|
||||
<div className={`Calculating-limit small ${gasEstimationPending ? 'active' : ''}`}>
|
||||
{!!onlyIncludeLoader ? 'Calculating gas limit' : 'Calculating'}
|
||||
<Spinner />
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
|
||||
export const GasLimitField: React.SFC<Props> = ({ includeLabel, onlyIncludeLoader }) => (
|
||||
export const GasLimitField: React.SFC<Props> = ({
|
||||
includeLabel,
|
||||
onlyIncludeLoader,
|
||||
customLabel,
|
||||
disabled
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
{includeLabel ? <label>{translate('TRANS_gas')} </label> : null}
|
||||
|
||||
<GasLimitFieldFactory
|
||||
withProps={({ gasLimit: { raw }, onChange, readOnly, gasEstimationPending }) => (
|
||||
<>
|
||||
<GaslimitLoading gasEstimationPending={gasEstimationPending} />
|
||||
<React.Fragment>
|
||||
<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 : (
|
||||
<input
|
||||
className={`form-control ${gasLimitValidator(raw) ? 'is-valid' : 'is-invalid'}`}
|
||||
|
@ -37,9 +56,10 @@ export const GasLimitField: React.SFC<Props> = ({ includeLabel, onlyIncludeLoade
|
|||
readOnly={!!readOnly}
|
||||
value={raw}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.GasSlider {
|
||||
&-toggle {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-top: $space-sm;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -1,2 +0,0 @@
|
|||
import GasSlider from './GasSlider';
|
||||
export default GasSlider;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import {
|
|||
CustomNodeConfig,
|
||||
CustomNetworkConfig
|
||||
} from 'config';
|
||||
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||
import Navigation from './components/Navigation';
|
||||
import CustomNodeModal from './components/CustomNodeModal';
|
||||
import OnlineStatus from './components/OnlineStatus';
|
||||
|
@ -134,10 +133,6 @@ export default class Header extends Component<Props, State> {
|
|||
<OnlineStatus isOffline={isOffline} />
|
||||
</div>
|
||||
|
||||
<div className="Header-branding-right-dropdown">
|
||||
<GasPriceDropdown onChange={this.props.setGasPriceField} />
|
||||
</div>
|
||||
|
||||
<div className="Header-branding-right-dropdown">
|
||||
<LanguageDropDown
|
||||
ariaLabel={`change language. current language ${languages[selectedLanguage]}`}
|
||||
|
|
|
@ -17,7 +17,7 @@ export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
|
|||
<NonceFieldFactory
|
||||
withProps={({ nonce: { raw, value }, onChange, readOnly, shouldDisplay }) => {
|
||||
const content = (
|
||||
<>
|
||||
<div>
|
||||
<label>Nonce</label>
|
||||
{nonceHelp}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export const NonceField: React.SFC<Props> = ({ alwaysDisplay }) => (
|
|||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
return alwaysDisplay || shouldDisplay ? content : null;
|
||||
|
|
|
@ -21,7 +21,7 @@ export default class SubTabs extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div className="SubTabs row">
|
||||
<div className="SubTabs-tabs col-sm-8">
|
||||
<div className="SubTabs-tabs col-sm-12">
|
||||
{tabs.map((t, i) => (
|
||||
// Same as normal Link, but knows when it's active, and applies activeClassName
|
||||
<NavLink
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,24 @@
|
|||
import React from 'react';
|
||||
import { translateRaw } from 'translations';
|
||||
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 { getNetworkConfig, getOffline } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import SimpleGas from './components/SimpleGas';
|
||||
import AdvancedGas from './components/AdvancedGas';
|
||||
import './GasSlider.scss';
|
||||
import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas';
|
||||
import './TXMetaDataPanel.scss';
|
||||
import { getGasPrice } from 'selectors/transaction';
|
||||
|
||||
type SliderStates = 'simple' | 'advanced';
|
||||
|
||||
interface StateProps {
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
offline: AppState['config']['offline'];
|
||||
|
@ -19,26 +28,42 @@ interface StateProps {
|
|||
interface DispatchProps {
|
||||
inputGasPrice: TInputGasPrice;
|
||||
fetchCCRates: TFetchCCRates;
|
||||
getNonceRequested: TGetNonceRequested;
|
||||
reset: TReset;
|
||||
}
|
||||
|
||||
// Set default props for props that can't be truthy or falsy
|
||||
interface DefaultProps {
|
||||
initialState: SliderStates;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
disableAdvanced?: boolean;
|
||||
initialState?: SliderStates;
|
||||
disableToggle?: boolean;
|
||||
advancedGasOptions?: AdvancedOptions;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type Props = DispatchProps & OwnProps & StateProps;
|
||||
|
||||
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 = {
|
||||
showAdvanced: false
|
||||
sliderState: (this.props as DefaultProps).initialState
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this.props.offline) {
|
||||
this.props.reset();
|
||||
this.props.fetchCCRates([this.props.network.unit]);
|
||||
this.props.getNonceRequested();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,21 +74,24 @@ class GasSlider extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { offline, disableAdvanced, gasPrice } = this.props;
|
||||
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
|
||||
|
||||
const { offline, disableToggle, gasPrice, advancedGasOptions, className = '' } = this.props;
|
||||
const showAdvanced = this.state.sliderState === 'advanced' || offline;
|
||||
return (
|
||||
<div className="GasSlider">
|
||||
<div className={`Gas col-md-12 ${className}`}>
|
||||
{showAdvanced ? (
|
||||
<AdvancedGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
|
||||
<AdvancedGas
|
||||
gasPrice={gasPrice}
|
||||
inputGasPrice={this.props.inputGasPrice}
|
||||
options={advancedGasOptions}
|
||||
/>
|
||||
) : (
|
||||
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} />
|
||||
)}
|
||||
|
||||
{!offline &&
|
||||
!disableAdvanced && (
|
||||
!disableToggle && (
|
||||
<div className="help-block">
|
||||
<a className="GasSlider-toggle" onClick={this.toggleAdvanced}>
|
||||
<a className="Gas-toggle" onClick={this.toggleAdvanced}>
|
||||
<strong>
|
||||
{showAdvanced
|
||||
? `- ${translateRaw('Back to simple')}`
|
||||
|
@ -77,7 +105,7 @@ class GasSlider extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
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, {
|
||||
inputGasPrice,
|
||||
fetchCCRates
|
||||
})(GasSlider);
|
||||
fetchCCRates,
|
||||
getNonceRequested,
|
||||
reset
|
||||
})(TXMetaDataPanel);
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -4,23 +4,26 @@
|
|||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&-flex-spacer {
|
||||
flex-grow: 2;
|
||||
}
|
||||
&-title {
|
||||
&-input-group {
|
||||
display: flex;
|
||||
}
|
||||
&-estimating {
|
||||
color: rgba(51, 51, 51, 0.7);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-weight: 400;
|
||||
opacity: 0;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
> .SimpleGas-slider {
|
||||
flex-grow: 1;
|
||||
margin-right: $input-padding-x;
|
||||
}
|
||||
.Spinner {
|
||||
margin-left: 8px;
|
||||
> .FeeSummary {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import Slider from 'rc-slider';
|
||||
import translate from 'translations';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { gasPriceDefaults } from 'config';
|
||||
import FeeSummary from './FeeSummary';
|
||||
import { TInputGasPrice } from 'actions/transaction';
|
||||
|
@ -29,14 +29,16 @@ class SimpleGas extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<div className="SimpleGas row form-group">
|
||||
<div className="col-md-12 SimpleGas-title">
|
||||
<label className="SimpleGas-label">{translate('Transaction Fee')}</label>
|
||||
<div className="SimpleGas-flex-spacer" />
|
||||
<GasLimitField includeLabel={false} onlyIncludeLoader={true} />
|
||||
<div className="SimpleGas-title">
|
||||
<GasLimitField
|
||||
includeLabel={true}
|
||||
customLabel={translateRaw('Transaction Fee')}
|
||||
onlyIncludeLoader={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{gasLimitEstimationTimedOut && (
|
||||
<div className="col-md-12 prompt-toggle-gas-limit">
|
||||
<div className="prompt-toggle-gas-limit">
|
||||
<p className="small">
|
||||
{isWeb3Node
|
||||
? "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 className="col-md-8 col-sm-12">
|
||||
<div className="SimpleGas-input-group">
|
||||
<div className="SimpleGas-slider">
|
||||
<Slider
|
||||
onChange={this.handleSlider}
|
||||
|
@ -59,8 +61,6 @@ class SimpleGas extends React.Component<Props> {
|
|||
<span>{translate('Fast')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4 col-sm-12">
|
||||
<FeeSummary
|
||||
render={({ fee, usd }) => (
|
||||
<span>
|
|
@ -0,0 +1,2 @@
|
|||
import TXMetaDataPanel from './TXMetaDataPanel';
|
||||
export default TXMetaDataPanel;
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
.DWModal {
|
||||
&-path {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&-label {
|
||||
font-size: $font-size-medium;
|
||||
margin-right: 16px;
|
||||
line-height: $input-height-base;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
|
@ -25,7 +25,7 @@
|
|||
&-addresses {
|
||||
overflow-y: scroll;
|
||||
&-table {
|
||||
width: 695px;
|
||||
width: 732px;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
|
|
|
@ -122,8 +122,10 @@ class DeterministicWalletsModalClass extends React.Component<Props, State> {
|
|||
handleClose={onCancel}
|
||||
>
|
||||
<div className="DWModal">
|
||||
{/* TODO: replace styles for flexbox with flexbox classes in https://github.com/MyEtherWallet/MyEtherWallet/pull/850/files#diff-2150778b9391533fec7b8afd060c7672 */}
|
||||
<form className="DWModal-path form-group-sm" onSubmit={this.handleSubmitCustomPath}>
|
||||
<form
|
||||
className="DWModal-path form-group-sm flex-wrapper"
|
||||
onSubmit={this.handleSubmitCustomPath}
|
||||
>
|
||||
<span className="DWModal-path-label">Addresses </span>
|
||||
<Select
|
||||
name="fieldDPath"
|
||||
|
|
|
@ -14,5 +14,5 @@ export { default as Footer } from './Footer';
|
|||
export { default as BalanceSidebar } from './BalanceSidebar';
|
||||
export { default as PaperWallet } from './PaperWallet';
|
||||
export { default as AlphaAgreement } from './AlphaAgreement';
|
||||
export { default as GasSlider } from './GasSlider';
|
||||
export { default as TXMetaDataPanel } from './TXMetaDataPanel';
|
||||
export { default as WalletDecrypt } from './WalletDecrypt';
|
||||
|
|
|
@ -7,10 +7,12 @@
|
|||
border: 1px solid #ccc;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 2px;
|
||||
height: 2.5rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:active, &:hover {
|
||||
&:active,
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
> li {
|
||||
|
@ -46,10 +48,10 @@
|
|||
text-align: left;
|
||||
z-index: 500;
|
||||
background: white;
|
||||
box-shadow: 2px 1px 60px rgba(0,0,0,.175);
|
||||
box-shadow: 2px 1px 60px rgba(0, 0, 0, 0.175);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
|
@ -59,7 +61,7 @@
|
|||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid #fff;
|
||||
}
|
||||
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
}
|
||||
|
@ -74,7 +76,7 @@
|
|||
padding: 5px 20px;
|
||||
color: #163151;
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
background-color: #163151;
|
||||
color: #fff;
|
||||
}
|
||||
|
@ -84,7 +86,7 @@
|
|||
color: grey;
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color:#163151;
|
||||
color: #163151;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
@ -109,4 +111,4 @@
|
|||
img {
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import translate from 'translations';
|
||||
import classnames from 'classnames';
|
||||
import { DataFieldFactory } from 'components/DataFieldFactory';
|
||||
import { GasLimitFieldFactory } from 'components/GasLimitFieldFactory';
|
||||
import { SendButtonFactory } from 'components/SendButtonFactory';
|
||||
import { SigningStatus } from 'components/SigningStatus';
|
||||
import { NonceField } from 'components/NonceField';
|
||||
import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
|
||||
import { GenerateTransaction } from 'components/GenerateTransaction';
|
||||
import React, { Component } from 'react';
|
||||
|
@ -12,6 +10,7 @@ import { setToField, TSetToField } from 'actions/transaction';
|
|||
import { resetWallet, TResetWallet } from 'actions/wallet';
|
||||
import { connect } from 'react-redux';
|
||||
import { FullWalletOnly } from 'components/renderCbs';
|
||||
import { NonceField, TXMetaDataPanel } from 'components';
|
||||
import './Deploy.scss';
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -46,27 +45,22 @@ class DeployClass extends Component<DispatchProps> {
|
|||
/>
|
||||
</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="col-xs-11">
|
||||
<div className="col-xs-12 clearfix">
|
||||
<NonceField alwaysDisplay={false} />
|
||||
</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="col-xs-12 clearfix">
|
||||
<GenerateTransaction />
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { GasLimitField } from './GasLimitField';
|
||||
import { AmountField } from './AmountField';
|
||||
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 { FullWalletOnly } from 'components/renderCbs';
|
||||
|
||||
|
@ -12,9 +11,13 @@ export class Fields extends Component<OwnProps> {
|
|||
public render() {
|
||||
const makeContent = () => (
|
||||
<React.Fragment>
|
||||
<GasLimitField />
|
||||
<AmountField />
|
||||
<NonceField alwaysDisplay={false} />
|
||||
<TXMetaDataPanel
|
||||
className="form-group"
|
||||
initialState="advanced"
|
||||
disableToggle={true}
|
||||
advancedGasOptions={{ dataField: false }}
|
||||
/>
|
||||
{this.props.button}
|
||||
<SigningStatus />
|
||||
<SendButton />
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -4,7 +4,7 @@ import { isAnyOfflineWithWeb3 } from 'selectors/derived';
|
|||
import {
|
||||
AddressField,
|
||||
AmountField,
|
||||
GasSlider,
|
||||
TXMetaDataPanel,
|
||||
SendEverything,
|
||||
CurrentCustomMessage,
|
||||
GenerateTransaction,
|
||||
|
@ -29,7 +29,7 @@ const content = (
|
|||
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<GasSlider />
|
||||
<TXMetaDataPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import BN from 'bn.js';
|
|||
import { NetworkConfig } from 'config';
|
||||
import { validNumber, validDecimal } from 'libs/validators';
|
||||
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 { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values';
|
||||
import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config';
|
||||
|
@ -106,7 +106,16 @@ class RequestPayment extends React.Component<Props, {}> {
|
|||
|
||||
<div className="row form-group">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -34,13 +34,14 @@
|
|||
}
|
||||
&-dropdown {
|
||||
display: inline-block;
|
||||
margin: 0.5rem 0;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
max-width: 10rem;
|
||||
margin-right: $space-sm;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
|
@ -54,6 +55,3 @@
|
|||
margin-top: $space * 2.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -329,7 +329,7 @@ export default class CurrencySwap extends Component<Props, State> {
|
|||
<article className="CurrencySwap">
|
||||
<h1 className="CurrencySwap-title">{translate('SWAP_init_1')}</h1>
|
||||
{loaded || timeoutLoaded ? (
|
||||
<div className="form-inline CurrencySwap-inner-wrap">
|
||||
<div className="CurrencySwap-inner-wrap">
|
||||
<div className="CurrencySwap-input-group">
|
||||
{originErr && <span className="CurrencySwap-error-message">{originErr}</span>}
|
||||
<input
|
||||
|
|
|
@ -3,7 +3,7 @@ import { AmountFieldFactory } from 'components/AmountFieldFactory';
|
|||
import { AddressFieldFactory } from 'components/AddressFieldFactory';
|
||||
import { connect } from 'react-redux';
|
||||
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 translate from 'translations';
|
||||
import { getUnit } from 'selectors/transaction';
|
||||
|
@ -76,7 +76,7 @@ class FieldsClass extends Component<Props> {
|
|||
</div>
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<GasSlider disableAdvanced={true} />
|
||||
<TXMetaDataPanel initialState={'simple'} disableToggle={true} />
|
||||
</div>
|
||||
</div>
|
||||
<SigningStatus />
|
||||
|
|
|
@ -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
|
||||
* @param t
|
||||
|
@ -101,5 +106,6 @@ export {
|
|||
validateTx,
|
||||
makeTransaction,
|
||||
getTransactionFields,
|
||||
getTransactionFee,
|
||||
computeIndexingHash
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export {
|
|||
validGasLimit,
|
||||
makeTransaction,
|
||||
getTransactionFields,
|
||||
getTransactionFee,
|
||||
computeIndexingHash
|
||||
} from './ether';
|
||||
export * from './token';
|
||||
|
|
|
@ -33,4 +33,5 @@
|
|||
@import './styles/overrides';
|
||||
@import './styles/scaffolding';
|
||||
@import './styles/tab';
|
||||
@import './styles/flexbox';
|
||||
@import './fonts';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.flex-wrapper {
|
||||
display: flex;
|
||||
&-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&-nowrap {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.flex-spacer {
|
||||
flex-grow: 2;
|
||||
}
|
||||
}
|
|
@ -30,7 +30,6 @@ input[readonly] {
|
|||
}
|
||||
|
||||
.form-control {
|
||||
margin-top: $space-sm;
|
||||
margin-bottom: $space-sm;
|
||||
transition: $transition;
|
||||
padding: $input-padding;
|
||||
|
|
Loading…
Reference in New Issue