Improved Gas UX (Pt. 1 - Gas Slider on Send) (#728)

* Initial crack at simple only gas slider component.

* Work on advanced component. Refactor redux and components to specify gas limit vs price.

* Convert fee summary to a render cbesque thing.

* Rework responsive columns.

* Remove force offline button.

* Tweak styles.

* Fix tscheck issues, remove unneeded prop.

* Fix references to GasField

* Gas slider in lite send.

* Make gas slider network-aware for symbol and price calculation.
This commit is contained in:
William O'Beirne 2018-01-07 11:43:06 -05:00 committed by Daniel Ternyak
parent 98afc22537
commit edda9f71ea
35 changed files with 481 additions and 110 deletions

View File

@ -5,6 +5,7 @@ import {
SetNonceFieldAction, SetNonceFieldAction,
SetValueFieldAction, SetValueFieldAction,
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
ResetAction, ResetAction,
@ -18,6 +19,12 @@ const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({
payload payload
}); });
type TInputGasPrice = typeof inputGasPrice;
const inputGasPrice = (payload: InputGasPriceAction['payload']) => ({
type: TypeKeys.GAS_PRICE_INPUT,
payload
});
type TInputNonce = typeof inputNonce; type TInputNonce = typeof inputNonce;
const inputNonce = (payload: InputNonceAction['payload']) => ({ const inputNonce = (payload: InputNonceAction['payload']) => ({
type: TypeKeys.NONCE_INPUT, type: TypeKeys.NONCE_INPUT,
@ -71,6 +78,7 @@ const reset = (): ResetAction => ({ type: TypeKeys.RESET });
export { export {
TInputGasLimit, TInputGasLimit,
TInputGasPrice,
TInputNonce, TInputNonce,
TInputData, TInputData,
TSetGasLimitField, TSetGasLimitField,
@ -81,6 +89,7 @@ export {
TSetGasPriceField, TSetGasPriceField,
TReset, TReset,
inputGasLimit, inputGasLimit,
inputGasPrice,
inputNonce, inputNonce,
inputData, inputData,
setGasLimitField, setGasLimitField,

View File

@ -6,6 +6,10 @@ interface InputGasLimitAction {
type: TypeKeys.GAS_LIMIT_INPUT; type: TypeKeys.GAS_LIMIT_INPUT;
payload: string; payload: string;
} }
interface InputGasPriceAction {
type: TypeKeys.GAS_PRICE_INPUT;
payload: string;
}
interface InputDataAction { interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT; type: TypeKeys.DATA_FIELD_INPUT;
payload: string; payload: string;
@ -79,6 +83,7 @@ type FieldAction =
export { export {
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
SetGasLimitFieldAction, SetGasLimitFieldAction,

View File

@ -28,6 +28,7 @@ export enum TypeKeys {
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT', DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT', GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
NONCE_INPUT = 'NONCE_INPUT', NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET', DATA_FIELD_SET = 'DATA_FIELD_SET',

View File

@ -1,52 +0,0 @@
import React from 'react';
import { forceOfflineConfig as dForceOfflineConfig, TForceOfflineConfig } from 'actions/config';
import OfflineSymbol from 'components/ui/OfflineSymbol';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
type sizeType = 'small' | 'medium' | 'large';
interface OfflineToggleProps {
offline: boolean;
forceOffline: boolean;
forceOfflineConfig: TForceOfflineConfig;
size?: sizeType;
}
class OfflineToggle extends React.Component<OfflineToggleProps, {}> {
public render() {
const { forceOfflineConfig, offline, forceOffline, size } = this.props;
return (
<div>
{!offline ? (
<div className="row text-center">
<div className="col-xs-3">
<OfflineSymbol offline={offline || forceOffline} size={size} />
</div>
<div className="col-xs-6">
<button className="btn-xs btn-info" onClick={forceOfflineConfig}>
{forceOffline ? 'Go Online' : 'Go Offline'}
</button>
</div>
</div>
) : (
<div className="text-center">
<h5>You are currently offline.</h5>
</div>
)}
</div>
);
}
}
function mapStateToProps(state: AppState) {
return {
offline: state.config.offline,
forceOffline: state.config.forceOffline
};
}
export default connect(mapStateToProps, {
forceOfflineConfig: dForceOfflineConfig
})(OfflineToggle);

View File

@ -10,7 +10,6 @@ import AccountInfo from './AccountInfo';
import EquivalentValues from './EquivalentValues'; import EquivalentValues from './EquivalentValues';
import Promos from './Promos'; import Promos from './Promos';
import TokenBalances from './TokenBalances'; import TokenBalances from './TokenBalances';
import OfflineToggle from './OfflineToggle';
interface Props { interface Props {
wallet: IWallet; wallet: IWallet;
@ -37,10 +36,6 @@ export class BalanceSidebar extends React.Component<Props, {}> {
} }
const blocks: Block[] = [ const blocks: Block[] = [
{
name: 'Go Offline',
content: <OfflineToggle />
},
{ {
name: 'Account Info', name: 'Account Info',
content: <AccountInfo wallet={wallet} balance={balance} network={network} /> content: <AccountInfo wallet={wallet} balance={balance} network={network} />

View File

@ -1 +0,0 @@
export * from './GasFieldFactory';

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { GasFieldFactory } from './GasFieldFactory'; import { GasLimitFieldFactory } from './GasLimitFieldFactory';
import translate from 'translations'; import translate from 'translations';
import { Aux } from 'components/ui'; import { Aux } from 'components/ui';
export const GasField: React.SFC<{}> = () => ( export const GasLimitField: React.SFC<{}> = () => (
<Aux> <Aux>
<label>{translate('TRANS_gas')} </label> <label>{translate('TRANS_gas')} </label>
<GasFieldFactory <GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => ( withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input <input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`} className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { GasQuery } from 'components/renderCbs'; import { GasQuery } from 'components/renderCbs';
import { GasInput } from './GasInputFactory'; import { GasLimitInput } from './GasLimitInputFactory';
import { inputGasLimit, TInputGasLimit } from 'actions/transaction'; import { inputGasLimit, TInputGasLimit } from 'actions/transaction';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
@ -34,7 +34,7 @@ class GasLimitFieldClass extends Component<Props, {}> {
} }
public render() { public render() {
return <GasInput onChange={this.setGas} withProps={this.props.withProps} />; return <GasLimitInput onChange={this.setGas} withProps={this.props.withProps} />;
} }
private setGas = (ev: React.FormEvent<HTMLInputElement>) => { private setGas = (ev: React.FormEvent<HTMLInputElement>) => {
@ -45,13 +45,13 @@ class GasLimitFieldClass extends Component<Props, {}> {
const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass); const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass);
interface DefaultGasFieldProps { interface DefaultGasLimitFieldProps {
withProps(props: CallBackProps): React.ReactElement<any> | null; withProps(props: CallBackProps): React.ReactElement<any> | null;
} }
const DefaultGasField: React.SFC<DefaultGasFieldProps> = ({ withProps }) => ( const DefaultGasLimitField: React.SFC<DefaultGasLimitFieldProps> = ({ withProps }) => (
<GasQuery <GasQuery
withQuery={({ gasLimit }) => <GasLimitField gasLimit={gasLimit} withProps={withProps} />} withQuery={({ gasLimit }) => <GasLimitField gasLimit={gasLimit} withProps={withProps} />}
/> />
); );
export { DefaultGasField as GasFieldFactory }; export { DefaultGasLimitField as GasLimitFieldFactory };

View File

@ -3,7 +3,7 @@ import { Query } from 'components/renderCbs';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getGasLimit } from 'selectors/transaction'; import { getGasLimit } from 'selectors/transaction';
import { CallBackProps } from 'components/GasFieldFactory'; import { CallBackProps } from 'components/GasLimitFieldFactory';
interface StateProps { interface StateProps {
gasLimit: AppState['transaction']['fields']['gasLimit']; gasLimit: AppState['transaction']['fields']['gasLimit'];
@ -15,7 +15,7 @@ interface OwnProps {
} }
type Props = StateProps & OwnProps; type Props = StateProps & OwnProps;
class GasInputClass extends Component<Props> { class GasLimitInputClass extends Component<Props> {
public render() { public render() {
const { gasLimit, onChange } = this.props; const { gasLimit, onChange } = this.props;
return ( return (
@ -29,6 +29,6 @@ class GasInputClass extends Component<Props> {
} }
} }
export const GasInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))( export const GasLimitInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
GasInputClass GasLimitInputClass
); );

View File

@ -0,0 +1 @@
export * from './GasLimitFieldFactory';

View File

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

View File

@ -0,0 +1,99 @@
import React from 'react';
import { translateRaw } from 'translations';
import { connect } from 'react-redux';
import {
inputGasPrice,
TInputGasPrice,
inputGasLimit,
TInputGasLimit,
inputNonce,
TInputNonce
} from 'actions/transaction';
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig } from 'selectors/config';
import { AppState } from 'reducers';
import SimpleGas from './components/SimpleGas';
import AdvancedGas from './components/AdvancedGas';
import './GasSlider.scss';
interface Props {
// Component configuration
disableAdvanced?: boolean;
// Data
gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit'];
offline: AppState['config']['offline'];
network: AppState['config']['network'];
// Actions
inputGasPrice: TInputGasPrice;
inputGasLimit: TInputGasLimit;
inputNonce: TInputNonce;
fetchCCRates: TFetchCCRates;
}
interface State {
showAdvanced: boolean;
}
class GasSlider extends React.Component<Props, State> {
public state: State = {
showAdvanced: false
};
public componentDidMount() {
this.props.fetchCCRates([this.props.network.unit]);
}
public render() {
const { gasPrice, gasLimit, offline, disableAdvanced } = this.props;
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
return (
<div className="GasSlider">
{showAdvanced ? (
<AdvancedGas
gasPrice={gasPrice.raw}
gasLimit={gasLimit.raw}
changeGasPrice={this.props.inputGasPrice}
changeGasLimit={this.props.inputGasLimit}
/>
) : (
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
)}
{!offline &&
!disableAdvanced && (
<div className="help-block">
<a className="GasSlider-toggle" onClick={this.toggleAdvanced}>
<strong>
{showAdvanced
? `- ${translateRaw('Back to simple')}`
: `+ ${translateRaw('Advanced: Data, Gas Price, Gas Limit')}`}
</strong>
</a>
</div>
)}
</div>
);
}
private toggleAdvanced = () => {
this.setState({ showAdvanced: !this.state.showAdvanced });
};
}
function mapStateToProps(state: AppState) {
return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit,
offline: state.config.offline,
network: getNetworkConfig(state)
};
}
export default connect(mapStateToProps, {
inputGasPrice,
inputGasLimit,
inputNonce,
fetchCCRates
})(GasSlider);

View File

@ -0,0 +1,4 @@
.AdvancedGas {
margin-top: 0;
margin-bottom: 0;
}

View File

@ -0,0 +1,70 @@
import React from 'react';
import translate from 'translations';
import { DataFieldFactory } from 'components/DataFieldFactory';
import FeeSummary from './FeeSummary';
import './AdvancedGas.scss';
interface Props {
gasPrice: string;
gasLimit: string;
changeGasPrice(gwei: string): void;
changeGasLimit(wei: string): void;
}
export default class AdvancedGas extends React.Component<Props> {
public render() {
return (
<div className="AdvancedGas row form-group">
<div className="col-md-3 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
<input
className="form-control"
value={this.props.gasPrice}
onChange={this.handleGasPriceChange}
/>
</div>
<div className="col-md-3 col-sm-6 col-xs-12">
<label>{translate('OFFLINE_Step2_Label_4')}</label>
<input
className="form-control"
value={this.props.gasLimit}
onChange={this.handleGasLimitChange}
/>
</div>
<div className="col-md-6 col-sm-12">
<label>{translate('OFFLINE_Step2_Label_6')}</label>
<DataFieldFactory
withProps={({ data, onChange }) => (
<input
className="form-control"
value={data.raw}
onChange={onChange}
placeholder="0x7cB57B5A..."
/>
)}
/>
</div>
<div className="col-sm-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>) => {
this.props.changeGasPrice(ev.currentTarget.value);
};
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.changeGasLimit(ev.currentTarget.value);
};
}

View File

@ -0,0 +1,11 @@
@import 'common/sass/variables';
.FeeSummary {
background: $gray-lighter;
height: 42px;
line-height: 42px;
padding: 0 12px;
font-family: $font-family-monospace;
text-align: center;
font-size: 14px;
}

View File

@ -0,0 +1,78 @@
import React from 'react';
import BN from 'bn.js';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import { UnitDisplay } from 'components/ui';
import './FeeSummary.scss';
interface RenderData {
gasPriceWei: string;
gasPriceGwei: string;
gasLimit: string;
fee: React.ReactElement<string>;
usd: React.ReactElement<string> | null;
}
interface Props {
// Redux props
gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates'];
network: AppState['config']['network'];
// Component props
render(data: RenderData): React.ReactElement<string> | string;
}
class FeeSummary extends React.Component<Props> {
public render() {
const { gasPrice, gasLimit, rates, network } = this.props;
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
const fee = (
<UnitDisplay
value={feeBig}
unit="ether"
symbol={network.unit}
displayShortBalance={6}
checkOffline={false}
/>
);
const usdBig = network.isTestnet
? new BN(0)
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
const usd = (
<UnitDisplay
value={usdBig}
unit="ether"
displayShortBalance={2}
displayTrailingZeroes={true}
checkOffline={true}
/>
);
return (
<div className="FeeSummary">
{this.props.render({
gasPriceWei: gasPrice.value.toString(),
gasPriceGwei: gasPrice.raw,
fee,
usd,
gasLimit: gasLimit.raw
})}
</div>
);
}
}
function mapStateToProps(state: AppState) {
return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit,
rates: state.rates.rates,
network: getNetworkConfig(state)
};
}
export default connect(mapStateToProps)(FeeSummary);

View File

@ -0,0 +1,36 @@
@import 'common/sass/variables';
.SimpleGas {
margin-top: 0;
margin-bottom: 0;
&-label {
display: block;
}
&-slider {
padding-top: 8px;
margin-bottom: $space-xs;
&-labels {
margin-top: 4px;
display: flex;
> span {
flex: 1;
padding: 0 $space-xs;
text-align: center;
color: $gray-light;
font-size: $font-size-xs;
&:first-child {
text-align: left;
}
&:last-child {
text-align: right;
}
}
}
}
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import Slider from 'rc-slider';
import translate from 'translations';
import { gasPriceDefaults } from 'config/data';
import FeeSummary from './FeeSummary';
import './SimpleGas.scss';
interface Props {
gasPrice: string;
changeGasPrice(gwei: string): void;
}
export default class SimpleGas extends React.Component<Props> {
public render() {
const { gasPrice } = this.props;
return (
<div className="SimpleGas row form-group">
<div className="col-md-12">
<label className="SimpleGas-label">{translate('Transaction Fee')}</label>
</div>
<div className="col-md-8 col-sm-12">
<div className="SimpleGas-slider">
<Slider
onChange={this.handleSlider}
min={gasPriceDefaults.gasPriceMinGwei}
max={gasPriceDefaults.gasPriceMaxGwei}
value={gasPrice}
/>
<div className="SimpleGas-slider-labels">
<span>{translate('Cheap')}</span>
<span>{translate('Balanced')}</span>
<span>{translate('Fast')}</span>
</div>
</div>
</div>
<div className="col-md-4 col-sm-12">
<FeeSummary
render={({ fee, usd }) => (
<span>
{fee} {usd && <span>/ ${usd}</span>}
</span>
)}
/>
</div>
</div>
);
}
private handleSlider = (gasGwei: number) => {
this.props.changeGasPrice(gasGwei.toString());
};
}

View File

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

View File

@ -27,8 +27,8 @@ class NonceInputClass extends Component<Props> {
const { nonce: { raw, value }, onChange, shouldDisplay } = this.props; const { nonce: { raw, value }, onChange, shouldDisplay } = this.props;
const content = ( const content = (
<Aux> <Aux>
{nonceHelp}
<label>Nonce</label> <label>Nonce</label>
{nonceHelp}
<Query <Query
params={['readOnly']} params={['readOnly']}

View File

@ -1,6 +1,6 @@
export * from './AddressField'; export * from './AddressField';
export * from './DataField'; export * from './DataField';
export * from './GasField'; export * from './GasLimitField';
export * from './NonceField'; export * from './NonceField';
export * from './AmountField'; export * from './AmountField';
export * from './SendEverything'; export * from './SendEverything';
@ -15,4 +15,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 WalletDecrypt } from './WalletDecrypt'; export { default as WalletDecrypt } from './WalletDecrypt';

View File

@ -25,7 +25,8 @@ interface Props {
* @memberof Props * @memberof Props
*/ */
displayShortBalance?: boolean | number; displayShortBalance?: boolean | number;
checkOffline: boolean; displayTrailingZeroes?: boolean;
checkOffline?: boolean;
} }
interface EthProps extends Props { interface EthProps extends Props {
@ -39,7 +40,7 @@ const isEthereumUnit = (param: EthProps | TokenProps): param is EthProps =>
!!(param as EthProps).unit; !!(param as EthProps).unit;
const UnitDisplay: React.SFC<EthProps | TokenProps> = params => { const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
const { value, symbol, displayShortBalance, checkOffline } = params; const { value, symbol, displayShortBalance, displayTrailingZeroes, checkOffline } = params;
let element; let element;
if (!value) { if (!value) {
@ -58,6 +59,9 @@ const UnitDisplay: React.SFC<EthProps | TokenProps> = params => {
if (parseFloat(formattedValue) === 0 && parseFloat(convertedValue) !== 0) { if (parseFloat(formattedValue) === 0 && parseFloat(convertedValue) !== 0) {
const padding = digits !== 0 ? `.${'0'.repeat(digits - 1)}1` : ''; const padding = digits !== 0 ? `.${'0'.repeat(digits - 1)}1` : '';
formattedValue = `< 0${padding}`; formattedValue = `< 0${padding}`;
} else if (displayTrailingZeroes) {
const [whole, deci] = formattedValue.split('.');
formattedValue = `${whole}.${(deci || '').padEnd(digits, '0')}`;
} }
} else { } else {
formattedValue = convertedValue; formattedValue = convertedValue;

View File

@ -83,6 +83,7 @@ export interface NetworkConfig {
chainId: number; chainId: number;
tokens: Token[]; tokens: Token[];
contracts: NetworkContract[] | null; contracts: NetworkContract[] | null;
isTestnet?: boolean;
} }
export interface CustomNetworkConfig { export interface CustomNetworkConfig {
@ -150,7 +151,8 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
color: '#adc101', color: '#adc101',
blockExplorer: makeExplorer('https://ropsten.etherscan.io'), blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
tokens: require('./tokens/ropsten.json'), tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json') contracts: require('./contracts/ropsten.json'),
isTestnet: true
}, },
Kovan: { Kovan: {
name: 'Kovan', name: 'Kovan',
@ -159,7 +161,8 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
color: '#adc101', color: '#adc101',
blockExplorer: makeExplorer('https://kovan.etherscan.io'), blockExplorer: makeExplorer('https://kovan.etherscan.io'),
tokens: require('./tokens/ropsten.json'), tokens: require('./tokens/ropsten.json'),
contracts: require('./contracts/ropsten.json') contracts: require('./contracts/ropsten.json'),
isTestnet: true
}, },
Rinkeby: { Rinkeby: {
name: 'Rinkeby', name: 'Rinkeby',
@ -168,7 +171,8 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
color: '#adc101', color: '#adc101',
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'), blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
tokens: require('./tokens/rinkeby.json'), tokens: require('./tokens/rinkeby.json'),
contracts: require('./contracts/rinkeby.json') contracts: require('./contracts/rinkeby.json'),
isTestnet: true
}, },
RSK: { RSK: {
name: 'RSK', name: 'RSK',

View File

@ -1,7 +1,7 @@
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 { GasFieldFactory } from 'components/GasFieldFactory'; 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 { NonceField } from 'components/NonceField';
@ -48,7 +48,7 @@ class DeployClass extends Component<DispatchProps> {
<label className="Deploy-field form-group"> <label className="Deploy-field form-group">
<h4 className="Deploy-field-label">Gas Limit</h4> <h4 className="Deploy-field-label">Gas Limit</h4>
<GasFieldFactory <GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => ( withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input <input
name="gasLimit" name="gasLimit"

View File

@ -1,4 +1,4 @@
import { GasField } from './GasField'; 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 { NonceField, SendButton, SigningStatus } from 'components';
@ -13,7 +13,7 @@ export class Fields extends Component<OwnProps> {
public render() { public render() {
const makeContent = () => ( const makeContent = () => (
<Aux> <Aux>
<GasField /> <GasLimitField />
<AmountField /> <AmountField />
<NonceField /> <NonceField />
{this.props.button} {this.props.button}

View File

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

View File

@ -5,8 +5,7 @@ import {
NonceField, NonceField,
AddressField, AddressField,
AmountField, AmountField,
DataField, GasSlider,
GasField,
SendEverything, SendEverything,
CurrentCustomMessage, CurrentCustomMessage,
GenerateTransaction, GenerateTransaction,
@ -23,30 +22,26 @@ const content = (
<div className="Tab-content-pane"> <div className="Tab-content-pane">
<AddressField /> <AddressField />
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-12">
<AmountField hasUnitDropdown={true} /> <AmountField hasUnitDropdown={true} />
<SendEverything /> <SendEverything />
</div> </div>
<div className="col-xs-1" />
</div> </div>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-12">
<GasField /> <GasSlider />
</div> </div>
</div> </div>
<div className="row form-group"> <div className="row form-group">
<div className="col-xs-11"> <div className="col-xs-12">
<NonceField /> <NonceField />
</div> </div>
</div> </div>
<div className="row form-group">
<div className="col-xs-11">
<DataField />
</div>
</div>
<CurrentCustomMessage /> <CurrentCustomMessage />
<NonStandardTransaction /> <NonStandardTransaction />
<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

@ -15,7 +15,7 @@ import BN from 'bn.js';
import { NetworkConfig } from 'config/data'; import { NetworkConfig } from 'config/data';
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, GasField } from 'components'; import { AddressField, AmountField, GasLimitField } 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,7 @@ 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">
<GasField /> <GasLimitField />
</div> </div>
</div> </div>

View File

@ -1,12 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { AmountFieldFactory } from 'components/AmountFieldFactory'; import { AmountFieldFactory } from 'components/AmountFieldFactory';
import { GasFieldFactory } from 'components/GasFieldFactory';
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 { Aux } from 'components/ui'; import { Aux } from 'components/ui';
import { GenerateTransaction, SendButton, SigningStatus } from 'components'; import { GenerateTransaction, SendButton, SigningStatus, GasSlider } 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';
@ -69,13 +68,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">
<label>{translate('TRANS_gas')} </label> <GasSlider disableAdvanced={true} />
<GasFieldFactory
withProps={({ gasLimit }) => (
<input className="form-control" type="text" value={gasLimit.raw} readOnly={true} />
)}
/>
</div> </div>
</div> </div>
<SigningStatus /> <SigningStatus />

View File

@ -1,3 +1,4 @@
import BN from 'bn.js';
import { import {
FieldAction, FieldAction,
TypeKeys as TK, TypeKeys as TK,
@ -16,7 +17,7 @@ const INITIAL_STATE: State = {
data: { raw: '', value: null }, data: { raw: '', value: null },
nonce: { raw: '', value: null }, nonce: { raw: '', value: null },
value: { raw: '', value: null }, value: { raw: '', value: null },
gasLimit: { raw: '', value: null }, gasLimit: { raw: '21000', value: new BN(21000) },
gasPrice: { raw: '21', value: gasPricetoBase(21) } gasPrice: { raw: '21', value: gasPricetoBase(21) }
}; };

View File

@ -1,14 +1,21 @@
import BN from 'bn.js';
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga'; import { SagaIterator } from 'redux-saga';
import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators'; import {
setDataField,
setGasLimitField,
setGasPriceField,
setNonceField
} from 'actions/transaction/actionCreators';
import { import {
InputDataAction, InputDataAction,
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction,
InputNonceAction, InputNonceAction,
TypeKeys TypeKeys
} from 'actions/transaction'; } from 'actions/transaction';
import { isValidHex, isValidNonce, validNumber } from 'libs/validators'; import { isValidHex, isValidNonce, validNumber } from 'libs/validators';
import { Data, Wei, Nonce } from 'libs/units'; import { Data, Wei, Nonce, gasPricetoBase } from 'libs/units';
export function* handleDataInput({ payload }: InputDataAction): SagaIterator { export function* handleDataInput({ payload }: InputDataAction): SagaIterator {
const validData: boolean = yield call(isValidHex, payload); const validData: boolean = yield call(isValidHex, payload);
@ -21,6 +28,17 @@ export function* handleGasLimitInput({ payload }: InputGasLimitAction): SagaIter
yield put(setGasLimitField({ raw: payload, value: validGasLimit ? Wei(payload) : null })); yield put(setGasLimitField({ raw: payload, value: validGasLimit ? Wei(payload) : null }));
} }
export function* handleGasPriceInput({ payload }: InputGasPriceAction): SagaIterator {
const priceFloat = parseFloat(payload);
const validGasPrice = validNumber(priceFloat) && isFinite(priceFloat) && priceFloat > 0;
yield put(
setGasPriceField({
raw: payload,
value: validGasPrice ? gasPricetoBase(priceFloat) : new BN(0)
})
);
}
export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator { export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator {
const validNonce: boolean = yield call(isValidNonce, payload); const validNonce: boolean = yield call(isValidNonce, payload);
yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null })); yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null }));
@ -29,5 +47,6 @@ export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator {
export const fields = [ export const fields = [
takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput), takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput),
takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput), takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput),
takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput),
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput) takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput)
]; ];

View File

@ -21,6 +21,9 @@
@import "~bootstrap-sass/assets/stylesheets/bootstrap/utilities"; @import "~bootstrap-sass/assets/stylesheets/bootstrap/utilities";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities"; @import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";
// --- RC SLIDER ---
@import "~rc-slider/assets/index.css";
// --- CUSTOM --- // --- CUSTOM ---
@import "./styles/badbrowser"; @import "./styles/badbrowser";
@import "./styles/noscript"; @import "./styles/noscript";

View File

@ -8,3 +8,6 @@
@import "./overrides/grid"; @import "./overrides/grid";
@import "./overrides/input-groups"; @import "./overrides/input-groups";
@import "./overrides/type"; @import "./overrides/type";
// And an override for rc-slider
@import "./overrides/rc-slider";

View File

@ -0,0 +1,25 @@
@import 'common/sass/variables';
$rail-height: 4px;
$handle-size: 22px;
$speed: 70ms;
.rc-slider {
&-rail {
background: $gray-lighter;
}
&-track {
background: $brand-primary;
}
&-handle {
top: 50%;
width: $handle-size;
height: $handle-size;
background: $brand-primary;
margin: 0;
border: none;
transform: translate(-50%, -50%);
}
}

View File

@ -29,6 +29,7 @@
"qrcode": "1.2.0", "qrcode": "1.2.0",
"qrcode.react": "0.7.2", "qrcode.react": "0.7.2",
"query-string": "5.0.1", "query-string": "5.0.1",
"rc-slider": "^8.5.0",
"react": "16.2.0", "react": "16.2.0",
"react-dom": "16.2.0", "react-dom": "16.2.0",
"react-markdown": "2.5.1", "react-markdown": "2.5.1",