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

View File

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

View File

@ -28,6 +28,7 @@ export enum TypeKeys {
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
NONCE_INPUT = 'NONCE_INPUT',
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 Promos from './Promos';
import TokenBalances from './TokenBalances';
import OfflineToggle from './OfflineToggle';
interface Props {
wallet: IWallet;
@ -37,10 +36,6 @@ export class BalanceSidebar extends React.Component<Props, {}> {
}
const blocks: Block[] = [
{
name: 'Go Offline',
content: <OfflineToggle />
},
{
name: 'Account Info',
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 { GasFieldFactory } from './GasFieldFactory';
import { GasLimitFieldFactory } from './GasLimitFieldFactory';
import translate from 'translations';
import { Aux } from 'components/ui';
export const GasField: React.SFC<{}> = () => (
export const GasLimitField: React.SFC<{}> = () => (
<Aux>
<label>{translate('TRANS_gas')} </label>
<GasFieldFactory
<GasLimitFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { GasQuery } from 'components/renderCbs';
import { GasInput } from './GasInputFactory';
import { GasLimitInput } from './GasLimitInputFactory';
import { inputGasLimit, TInputGasLimit } from 'actions/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
@ -34,7 +34,7 @@ class GasLimitFieldClass extends Component<Props, {}> {
}
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>) => {
@ -45,13 +45,13 @@ class GasLimitFieldClass extends Component<Props, {}> {
const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass);
interface DefaultGasFieldProps {
interface DefaultGasLimitFieldProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
}
const DefaultGasField: React.SFC<DefaultGasFieldProps> = ({ withProps }) => (
const DefaultGasLimitField: React.SFC<DefaultGasLimitFieldProps> = ({ withProps }) => (
<GasQuery
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 { AppState } from 'reducers';
import { getGasLimit } from 'selectors/transaction';
import { CallBackProps } from 'components/GasFieldFactory';
import { CallBackProps } from 'components/GasLimitFieldFactory';
interface StateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
@ -15,7 +15,7 @@ interface OwnProps {
}
type Props = StateProps & OwnProps;
class GasInputClass extends Component<Props> {
class GasLimitInputClass extends Component<Props> {
public render() {
const { gasLimit, onChange } = this.props;
return (
@ -29,6 +29,6 @@ class GasInputClass extends Component<Props> {
}
}
export const GasInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
GasInputClass
export const GasLimitInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
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 content = (
<Aux>
{nonceHelp}
<label>Nonce</label>
{nonceHelp}
<Query
params={['readOnly']}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import BN from 'bn.js';
import { NetworkConfig } from 'config/data';
import { validNumber, validDecimal } from 'libs/validators';
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 { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values';
import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config';
@ -106,7 +106,7 @@ class RequestPayment extends React.Component<Props, {}> {
<div className="row form-group">
<div className="col-xs-11">
<GasField />
<GasLimitField />
</div>
</div>

View File

@ -1,12 +1,11 @@
import React, { Component } from 'react';
import { AmountFieldFactory } from 'components/AmountFieldFactory';
import { GasFieldFactory } from 'components/GasFieldFactory';
import { AddressFieldFactory } from 'components/AddressFieldFactory';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
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 translate from 'translations';
import { getUnit } from 'selectors/transaction';
@ -69,13 +68,7 @@ class FieldsClass extends Component<Props> {
</div>
<div className="row form-group">
<div className="col-xs-12">
<label>{translate('TRANS_gas')} </label>
<GasFieldFactory
withProps={({ gasLimit }) => (
<input className="form-control" type="text" value={gasLimit.raw} readOnly={true} />
)}
/>
<GasSlider disableAdvanced={true} />
</div>
</div>
<SigningStatus />

View File

@ -1,3 +1,4 @@
import BN from 'bn.js';
import {
FieldAction,
TypeKeys as TK,
@ -16,7 +17,7 @@ const INITIAL_STATE: State = {
data: { raw: '', value: null },
nonce: { raw: '', value: null },
value: { raw: '', value: null },
gasLimit: { raw: '', value: null },
gasLimit: { raw: '21000', value: new BN(21000) },
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 { SagaIterator } from 'redux-saga';
import { setDataField, setGasLimitField, setNonceField } from 'actions/transaction/actionCreators';
import {
setDataField,
setGasLimitField,
setGasPriceField,
setNonceField
} from 'actions/transaction/actionCreators';
import {
InputDataAction,
InputGasLimitAction,
InputGasPriceAction,
InputNonceAction,
TypeKeys
} from 'actions/transaction';
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 {
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 }));
}
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 {
const validNonce: boolean = yield call(isValidNonce, payload);
yield put(setNonceField({ raw: payload, value: validNonce ? Nonce(payload) : null }));
@ -29,5 +47,6 @@ export function* handleNonceInput({ payload }: InputNonceAction): SagaIterator {
export const fields = [
takeEvery(TypeKeys.DATA_FIELD_INPUT, handleDataInput),
takeEvery(TypeKeys.GAS_LIMIT_INPUT, handleGasLimitInput),
takeEvery(TypeKeys.GAS_PRICE_INPUT, handleGasPriceInput),
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput)
];

View File

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

View File

@ -8,3 +8,6 @@
@import "./overrides/grid";
@import "./overrides/input-groups";
@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.react": "0.7.2",
"query-string": "5.0.1",
"rc-slider": "^8.5.0",
"react": "16.2.0",
"react-dom": "16.2.0",
"react-markdown": "2.5.1",