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:
parent
98afc22537
commit
edda9f71ea
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
|
@ -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} />
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './GasFieldFactory';
|
|
@ -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'}`}
|
|
@ -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 };
|
|
@ -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
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from './GasLimitFieldFactory';
|
|
@ -0,0 +1,10 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.GasSlider {
|
||||
&-toggle {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-top: $space-sm;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,4 @@
|
|||
.AdvancedGas {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
};
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import GasSlider from './GasSlider';
|
||||
export default GasSlider;
|
|
@ -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']}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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) }
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -8,3 +8,6 @@
|
|||
@import "./overrides/grid";
|
||||
@import "./overrides/input-groups";
|
||||
@import "./overrides/type";
|
||||
|
||||
// And an override for rc-slider
|
||||
@import "./overrides/rc-slider";
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue