Performance Improvements (Pure components, debounced gas slider) (#899)

* PureComponent a ton of non-connected components.

* Debounce gas price slider. Keep gas price in state to reflect changes immediately.

* PureComponent balance sidebar and swap unconnected components.

* Import correct component.

* Move debouncing of gas slider to sagas via gasPriceInputIntent action.

* Remove console log.

* Remove leftover file from merge.
This commit is contained in:
William O'Beirne 2018-01-29 14:13:46 -05:00 committed by Daniel Ternyak
parent ae8b123cfd
commit d1a2c885a2
53 changed files with 177 additions and 107 deletions

View File

@ -6,6 +6,7 @@ import {
SetValueFieldAction, SetValueFieldAction,
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
ResetAction, ResetAction,
@ -25,6 +26,12 @@ const inputGasPrice = (payload: InputGasPriceAction['payload']) => ({
payload payload
}); });
type TInputGasPriceIntent = typeof inputGasPrice;
const inputGasPriceIntent = (payload: InputGasPriceIntentAction['payload']) => ({
type: TypeKeys.GAS_PRICE_INPUT_INTENT,
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,
@ -79,6 +86,7 @@ const reset = (): ResetAction => ({ type: TypeKeys.RESET });
export { export {
TInputGasLimit, TInputGasLimit,
TInputGasPrice, TInputGasPrice,
TInputGasPriceIntent,
TInputNonce, TInputNonce,
TInputData, TInputData,
TSetGasLimitField, TSetGasLimitField,
@ -90,6 +98,7 @@ export {
TReset, TReset,
inputGasLimit, inputGasLimit,
inputGasPrice, inputGasPrice,
inputGasPriceIntent,
inputNonce, inputNonce,
inputData, inputData,
setGasLimitField, setGasLimitField,

View File

@ -10,6 +10,10 @@ interface InputGasPriceAction {
type: TypeKeys.GAS_PRICE_INPUT; type: TypeKeys.GAS_PRICE_INPUT;
payload: string; payload: string;
} }
interface InputGasPriceIntentAction {
type: TypeKeys.GAS_PRICE_INPUT_INTENT;
payload: string;
}
interface InputDataAction { interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT; type: TypeKeys.DATA_FIELD_INPUT;
payload: string; payload: string;
@ -84,6 +88,7 @@ type FieldAction =
export { export {
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction,
InputDataAction, InputDataAction,
InputNonceAction, InputNonceAction,
SetGasLimitFieldAction, SetGasLimitFieldAction,

View File

@ -30,6 +30,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', GAS_PRICE_INPUT = 'GAS_PRICE_INPUT',
GAS_PRICE_INPUT_INTENT = 'GAS_PRICE_INPUT_INTENT',
NONCE_INPUT = 'NONCE_INPUT', NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET', DATA_FIELD_SET = 'DATA_FIELD_SET',

View File

@ -7,7 +7,7 @@ interface State {
isFading: boolean; isFading: boolean;
hasAcknowledged: boolean; hasAcknowledged: boolean;
} }
export default class AlphaAgreement extends React.Component<{}, State> { export default class AlphaAgreement extends React.PureComponent<{}, State> {
public state = { public state = {
hasAcknowledged: !!localStorage.getItem(LS_KEY), hasAcknowledged: !!localStorage.getItem(LS_KEY),
isFading: false isFading: false

View File

@ -23,7 +23,7 @@ interface State {
decimal: string; decimal: string;
} }
export default class AddCustomTokenForm extends React.Component<Props, State> { export default class AddCustomTokenForm extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
tokenSymbolLookup: {}, tokenSymbolLookup: {},
address: '', address: '',

View File

@ -19,7 +19,7 @@ interface State {
trackedTokens: { [symbol: string]: boolean }; trackedTokens: { [symbol: string]: boolean };
showCustomTokenForm: boolean; showCustomTokenForm: boolean;
} }
export default class TokenBalances extends React.Component<Props, State> { export default class TokenBalances extends React.PureComponent<Props, State> {
public state = { public state = {
trackedTokens: {}, trackedTokens: {},
showCustomTokenForm: false showCustomTokenForm: false

View File

@ -19,7 +19,7 @@ interface State {
showLongBalance: boolean; showLongBalance: boolean;
} }
export default class TokenRow extends React.Component<Props, State> { export default class TokenRow extends React.PureComponent<Props, State> {
public state = { public state = {
showLongBalance: false showLongBalance: false
}; };

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import './PreFooter.scss'; import './PreFooter.scss';
const PreFooter = () => { const PreFooter: React.SFC<{}> = () => {
return ( return (
<section className="pre-footer"> <section className="pre-footer">
<div className="container"> <div className="container">

View File

@ -108,7 +108,7 @@ interface State {
isOpen: boolean; isOpen: boolean;
} }
export default class Footer extends React.Component<Props, State> { export default class Footer extends React.PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { isOpen: false }; this.state = { isOpen: false };

View File

@ -36,7 +36,7 @@ interface State {
password: string; password: string;
} }
export default class CustomNodeModal extends React.Component<Props, State> { export default class CustomNodeModal extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
name: '', name: '',
url: '', url: '',

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import NavigationLink from './NavigationLink'; import NavigationLink from './NavigationLink';
import { knowledgeBaseURL } from 'config'; import { knowledgeBaseURL } from 'config';
import './Navigation.scss'; import './Navigation.scss';
@ -58,7 +58,7 @@ interface BorderStyle {
borderTopColor?: string; borderTopColor?: string;
} }
export default class Navigation extends Component<Props, State> { export default class Navigation extends PureComponent<Props, State> {
public state = { public state = {
showLeftArrow: false, showLeftArrow: false,
showRightArrow: false showRightArrow: false

View File

@ -10,7 +10,7 @@ interface Props extends RouteComponentProps<{}> {
isHomepage: boolean; isHomepage: boolean;
} }
class NavigationLink extends React.Component<Props, {}> { class NavigationLink extends React.PureComponent<Props, {}> {
public render() { public render() {
const { link, location, isHomepage } = this.props; const { link, location, isHomepage } = this.props;
const isExternalLink = link.to.includes('http'); const isExternalLink = link.to.includes('http');

View File

@ -7,7 +7,7 @@ import {
} from 'actions/config'; } from 'actions/config';
import logo from 'assets/images/logo-myetherwallet.svg'; import logo from 'assets/images/logo-myetherwallet.svg';
import { Dropdown, ColorDropdown } from 'components/ui'; import { Dropdown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { TSetGasPriceField } from 'actions/transaction'; import { TSetGasPriceField } from 'actions/transaction';
@ -49,7 +49,7 @@ interface State {
isAddingCustomNode: boolean; isAddingCustomNode: boolean;
} }
export default class Header extends Component<Props, State> { export default class Header extends PureComponent<Props, State> {
public state = { public state = {
isAddingCustomNode: false isAddingCustomNode: false
}; };

View File

@ -14,7 +14,7 @@ interface Props {
match: RouteComponentProps<{}>['match']; match: RouteComponentProps<{}>['match'];
} }
export default class SubTabs extends React.Component<Props> { export default class SubTabs extends React.PureComponent<Props> {
public render() { public render() {
const { tabs, match } = this.props; const { tabs, match } = this.props;
const currentPath = match.url; const currentPath = match.url;

View File

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import BN from 'bn.js';
import { translateRaw } from 'translations'; import { translateRaw } from 'translations';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
inputGasPrice, inputGasPrice,
TInputGasPrice, TInputGasPrice,
inputGasPriceIntent,
TInputGasPriceIntent,
getNonceRequested, getNonceRequested,
TGetNonceRequested, TGetNonceRequested,
reset, reset,
@ -12,6 +15,7 @@ import {
import { fetchCCRates, TFetchCCRates } from 'actions/rates'; import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { getNetworkConfig, getOffline } from 'selectors/config'; import { getNetworkConfig, getOffline } from 'selectors/config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { Units } from 'libs/units';
import SimpleGas from './components/SimpleGas'; import SimpleGas from './components/SimpleGas';
import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas'; import AdvancedGas, { AdvancedOptions } from './components/AdvancedGas';
import './TXMetaDataPanel.scss'; import './TXMetaDataPanel.scss';
@ -27,6 +31,7 @@ interface StateProps {
interface DispatchProps { interface DispatchProps {
inputGasPrice: TInputGasPrice; inputGasPrice: TInputGasPrice;
inputGasPriceIntent: TInputGasPriceIntent;
fetchCCRates: TFetchCCRates; fetchCCRates: TFetchCCRates;
getNonceRequested: TGetNonceRequested; getNonceRequested: TGetNonceRequested;
reset: TReset; reset: TReset;
@ -47,6 +52,7 @@ interface OwnProps {
type Props = DispatchProps & OwnProps & StateProps; type Props = DispatchProps & OwnProps & StateProps;
interface State { interface State {
gasPrice: AppState['transaction']['fields']['gasPrice'];
sliderState: SliderStates; sliderState: SliderStates;
} }
@ -56,6 +62,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
}; };
public state: State = { public state: State = {
gasPrice: this.props.gasPrice,
sliderState: (this.props as DefaultProps).initialState sliderState: (this.props as DefaultProps).initialState
}; };
@ -71,10 +78,14 @@ class TXMetaDataPanel extends React.Component<Props, State> {
if (this.props.offline && !nextProps.offline) { if (this.props.offline && !nextProps.offline) {
this.props.fetchCCRates([this.props.network.unit]); this.props.fetchCCRates([this.props.network.unit]);
} }
if (this.props.gasPrice !== nextProps.gasPrice) {
this.setState({ gasPrice: nextProps.gasPrice });
}
} }
public render() { public render() {
const { offline, disableToggle, gasPrice, advancedGasOptions, className = '' } = this.props; const { offline, disableToggle, advancedGasOptions, className = '' } = this.props;
const { gasPrice } = this.state;
const showAdvanced = this.state.sliderState === 'advanced' || offline; const showAdvanced = this.state.sliderState === 'advanced' || offline;
return ( return (
<div className={`Gas col-md-12 ${className}`}> <div className={`Gas col-md-12 ${className}`}>
@ -85,7 +96,7 @@ class TXMetaDataPanel extends React.Component<Props, State> {
options={advancedGasOptions} options={advancedGasOptions}
/> />
) : ( ) : (
<SimpleGas gasPrice={gasPrice} inputGasPrice={this.props.inputGasPrice} /> <SimpleGas gasPrice={gasPrice} inputGasPrice={this.handleGasPriceInput} />
)} )}
{!offline && {!offline &&
@ -107,6 +118,15 @@ class TXMetaDataPanel extends React.Component<Props, State> {
private toggleAdvanced = () => { private toggleAdvanced = () => {
this.setState({ sliderState: this.state.sliderState === 'advanced' ? 'simple' : 'advanced' }); this.setState({ sliderState: this.state.sliderState === 'advanced' ? 'simple' : 'advanced' });
}; };
private handleGasPriceInput = (raw: string) => {
const gasBn = new BN(raw);
const value = gasBn.mul(new BN(Units.gwei));
this.setState({
gasPrice: { raw, value }
});
this.props.inputGasPriceIntent(raw);
};
} }
function mapStateToProps(state: AppState): StateProps { function mapStateToProps(state: AppState): StateProps {
@ -119,6 +139,7 @@ function mapStateToProps(state: AppState): StateProps {
export default connect(mapStateToProps, { export default connect(mapStateToProps, {
inputGasPrice, inputGasPrice,
inputGasPriceIntent,
fetchCCRates, fetchCCRates,
getNonceRequested, getNonceRequested,
reset reset

View File

@ -108,6 +108,7 @@ class AdvancedGas extends React.Component<Props, State> {
{feeSummary && ( {feeSummary && (
<div className="AdvancedGas-fee-summary"> <div className="AdvancedGas-fee-summary">
<FeeSummary <FeeSummary
gasPrice={gasPrice}
render={({ gasPriceWei, gasLimit, fee, usd }) => ( render={({ gasPriceWei, gasLimit, fee, usd }) => (
<span> <span>
{gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>} {gasPriceWei} * {gasLimit} = {fee} {usd && <span>~= ${usd} USD</span>}

View File

@ -14,17 +14,20 @@ interface RenderData {
usd: React.ReactElement<string> | null; usd: React.ReactElement<string> | null;
} }
interface Props { interface ReduxStateProps {
// Redux props
gasPrice: AppState['transaction']['fields']['gasPrice'];
gasLimit: AppState['transaction']['fields']['gasLimit']; gasLimit: AppState['transaction']['fields']['gasLimit'];
rates: AppState['rates']['rates']; rates: AppState['rates']['rates'];
network: AppState['config']['network']; network: AppState['config']['network'];
isOffline: AppState['config']['offline']; isOffline: AppState['config']['offline'];
// Component props }
interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice'];
render(data: RenderData): React.ReactElement<string> | string; render(data: RenderData): React.ReactElement<string> | string;
} }
type Props = OwnProps & ReduxStateProps;
class FeeSummary extends React.Component<Props> { class FeeSummary extends React.Component<Props> {
public render() { public render() {
const { gasPrice, gasLimit, rates, network, isOffline } = this.props; const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
@ -67,9 +70,8 @@ class FeeSummary extends React.Component<Props> {
} }
} }
function mapStateToProps(state: AppState) { function mapStateToProps(state: AppState): ReduxStateProps {
return { return {
gasPrice: state.transaction.fields.gasPrice,
gasLimit: state.transaction.fields.gasLimit, gasLimit: state.transaction.fields.gasLimit,
rates: state.rates.rates, rates: state.rates.rates,
network: getNetworkConfig(state), network: getNetworkConfig(state),

View File

@ -3,7 +3,6 @@ import Slider from 'rc-slider';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { gasPriceDefaults } from 'config'; import { gasPriceDefaults } from 'config';
import FeeSummary from './FeeSummary'; import FeeSummary from './FeeSummary';
import { TInputGasPrice } from 'actions/transaction';
import './SimpleGas.scss'; import './SimpleGas.scss';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getGasLimitEstimationTimedOut } from 'selectors/transaction'; import { getGasLimitEstimationTimedOut } from 'selectors/transaction';
@ -13,7 +12,7 @@ import { getIsWeb3Node } from 'selectors/config';
interface OwnProps { interface OwnProps {
gasPrice: AppState['transaction']['fields']['gasPrice']; gasPrice: AppState['transaction']['fields']['gasPrice'];
inputGasPrice: TInputGasPrice; inputGasPrice(rawGas: string);
} }
interface StateProps { interface StateProps {
@ -41,7 +40,7 @@ class SimpleGas extends React.Component<Props> {
<div className="prompt-toggle-gas-limit"> <div className="prompt-toggle-gas-limit">
<p className="small"> <p className="small">
{isWeb3Node {isWeb3Node
? "Couldn't calculate gas limit, if you know what your doing, try setting manually in Advanced settings" ? "Couldn't calculate gas limit, if you know what you're doing, try setting manually in Advanced settings"
: "Couldn't calculate gas limit, try switching nodes"} : "Couldn't calculate gas limit, try switching nodes"}
</p> </p>
</div> </div>
@ -62,6 +61,7 @@ class SimpleGas extends React.Component<Props> {
</div> </div>
</div> </div>
<FeeSummary <FeeSummary
gasPrice={gasPrice}
render={({ fee, usd }) => ( render={({ fee, usd }) => (
<span> <span>
{fee} {usd && <span>/ ${usd}</span>} {fee} {usd && <span>/ ${usd}</span>}

View File

@ -60,7 +60,7 @@ const customDPath: DPath = {
value: 'custom' value: 'custom'
}; };
class DeterministicWalletsModalClass extends React.Component<Props, State> { class DeterministicWalletsModalClass extends React.PureComponent<Props, State> {
public state: State = { public state: State = {
selectedAddress: '', selectedAddress: '',
selectedAddrIndex: 0, selectedAddrIndex: 0,

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
export class DigitalBitboxDecrypt extends React.Component<{}, {}> { export class DigitalBitboxDecrypt extends React.PureComponent<{}, {}> {
public render() { public render() {
return <strong>Not yet implemented</strong>; return <strong>Not yet implemented</strong>;
} }

View File

@ -1,5 +1,5 @@
import { isKeystorePassRequired } from 'libs/wallet'; import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import { TShowNotification } from 'actions/notifications'; import { TShowNotification } from 'actions/notifications';
@ -25,7 +25,7 @@ function isValidFile(rawFile: File): boolean {
return fileType === '' || fileType === 'application/json'; return fileType === '' || fileType === 'application/json';
} }
export class KeystoreDecrypt extends Component { export class KeystoreDecrypt extends PureComponent {
public props: { public props: {
value: KeystoreValue; value: KeystoreValue;
isWalletPending: boolean; isWalletPending: boolean;

View File

@ -1,5 +1,5 @@
import './LedgerNano.scss'; import './LedgerNano.scss';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import { LedgerWallet } from 'libs/wallet'; import { LedgerWallet } from 'libs/wallet';
@ -31,7 +31,7 @@ interface State {
type Props = OwnProps & StateProps; type Props = OwnProps & StateProps;
class LedgerNanoSDecryptClass extends Component<Props, State> { class LedgerNanoSDecryptClass extends PureComponent<Props, State> {
public state: State = { public state: State = {
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',

View File

@ -1,5 +1,5 @@
import { mnemonicToSeed, validateMnemonic } from 'bip39'; import { mnemonicToSeed, validateMnemonic } from 'bip39';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import DeterministicWalletsModal from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
import { formatMnemonic } from 'utils/formatters'; import { formatMnemonic } from 'utils/formatters';
@ -27,7 +27,7 @@ interface State {
dPath: string; dPath: string;
} }
class MnemonicDecryptClass extends Component<Props & StateProps, State> { class MnemonicDecryptClass extends PureComponent<Props & StateProps, State> {
public state: State = { public state: State = {
phrase: '', phrase: '',
formattedPhrase: '', formattedPhrase: '',

View File

@ -1,6 +1,6 @@
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators'; import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values'; import { stripHexPrefix } from 'libs/values';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import { TogglablePassword } from 'components'; import { TogglablePassword } from 'components';
@ -45,7 +45,7 @@ interface Props {
onUnlock(): void; onUnlock(): void;
} }
export class PrivateKeyDecrypt extends Component<Props> { export class PrivateKeyDecrypt extends PureComponent<Props> {
public render() { public render() {
const { key, password } = this.props.value; const { key, password } = this.props.value;
const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password); const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password);

View File

@ -1,5 +1,5 @@
import { TrezorWallet, TREZOR_MINIMUM_FIRMWARE } from 'libs/wallet'; import { TrezorWallet, TREZOR_MINIMUM_FIRMWARE } from 'libs/wallet';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import TrezorConnect from 'vendor/trezor-connect'; import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal'; import DeterministicWalletsModal from './DeterministicWalletsModal';
@ -32,7 +32,7 @@ interface State {
type Props = OwnProps & StateProps; type Props = OwnProps & StateProps;
class TrezorDecryptClass extends Component<Props, State> { class TrezorDecryptClass extends PureComponent<Props, State> {
public state: State = { public state: State = {
publicKey: '', publicKey: '',
chainCode: '', chainCode: '',

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import { donationAddressMap } from 'config'; import { donationAddressMap } from 'config';
import { isValidETHAddress } from 'libs/validators'; import { isValidETHAddress } from 'libs/validators';
@ -12,7 +12,7 @@ interface State {
address: string; address: string;
} }
export class ViewOnlyDecrypt extends Component<Props, State> { export class ViewOnlyDecrypt extends PureComponent<Props, State> {
public state = { public state = {
address: '' address: ''
}; };

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React from 'react';
import translate from 'translations'; import translate from 'translations';
import { NewTabLink } from 'components/ui'; import { NewTabLink } from 'components/ui';
import './Web3.scss'; import './Web3.scss';
@ -7,27 +7,20 @@ interface Props {
onUnlock(): void; onUnlock(): void;
} }
export class Web3Decrypt extends Component<Props> { export const Web3Decrypt: React.SFC<Props> = ({ onUnlock }) => (
public render() { <div className="Web3Decrypt">
return ( <div>
<div className="Web3Decrypt"> <button className="Web3Decrypt-decrypt btn btn-primary btn-lg btn-block" onClick={onUnlock}>
<div> {translate('ADD_MetaMask')}
<button </button>
className="Web3Decrypt-decrypt btn btn-primary btn-lg btn-block" </div>
onClick={this.props.onUnlock}
>
{translate('ADD_MetaMask')}
</button>
</div>
<div> <div>
<NewTabLink <NewTabLink
className="Web3Decrypt-install btn btn-sm btn-default btn-block" className="Web3Decrypt-install btn btn-sm btn-default btn-block"
content={translate('Download MetaMask')} content={translate('Download MetaMask')}
href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en" href="https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en"
/> />
</div> </div>
</div> </div>
); );
}
}

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import DropdownShell from './DropdownShell'; import DropdownShell from './DropdownShell';
import removeIcon from 'assets/images/icon-remove.svg'; import removeIcon from 'assets/images/icon-remove.svg';
@ -25,7 +25,7 @@ interface Props<T> {
onChange(value: T): void; onChange(value: T): void;
} }
export default class ColorDropdown<T> extends Component<Props<T>, {}> { export default class ColorDropdown<T> extends PureComponent<Props<T>, {}> {
private dropdownShell: DropdownShell | null; private dropdownShell: DropdownShell | null;
public render() { public render() {

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import DropdownShell from './DropdownShell'; import DropdownShell from './DropdownShell';
@ -19,7 +19,7 @@ interface State {
search: string; search: string;
} }
export default class DropdownComponent<T> extends Component<Props<T>, State> { export default class DropdownComponent<T> extends PureComponent<Props<T>, State> {
public state = { public state = {
search: '' search: ''
}; };

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
interface Props { interface Props {
@ -14,7 +14,7 @@ interface State {
expanded: boolean; expanded: boolean;
} }
export default class DropdownComponent extends Component<Props, State> { export default class DropdownComponent extends PureComponent<Props, State> {
public static defaultProps = { public static defaultProps = {
color: 'default', color: 'default',
size: 'sm' size: 'sm'

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
export type ExpandHandler = (ev: React.FormEvent<HTMLAnchorElement>) => void; export type ExpandHandler = (ev: React.FormEvent<HTMLAnchorElement>) => void;
@ -13,7 +13,7 @@ interface State {
const initialState: State = { expanded: false }; const initialState: State = { expanded: false };
export class Expandable extends Component<Props, State> { export class Expandable extends PureComponent<Props, State> {
public state: State = initialState; public state: State = initialState;
public render() { public render() {

View File

@ -1,5 +1,5 @@
import closeIcon from 'assets/images/icon-x.svg'; import closeIcon from 'assets/images/icon-x.svg';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import './Modal.scss'; import './Modal.scss';
export interface IButton { export interface IButton {
@ -17,7 +17,7 @@ interface Props {
handleClose?(): void; handleClose?(): void;
} }
export default class Modal extends Component<Props, {}> { export default class Modal extends PureComponent<Props, {}> {
private modalContent: HTMLElement | null = null; private modalContent: HTMLElement | null = null;
public componentDidMount() { public componentDidMount() {

View File

@ -13,7 +13,7 @@ interface State {
qr?: string; qr?: string;
} }
export default class QRCode extends React.Component<Props, State> { export default class QRCode extends React.PureComponent<Props, State> {
public state: State = {}; public state: State = {};
public componentWillMount() { public componentWillMount() {
// Start generating QR codes immediately // Start generating QR codes immediately

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import Dropdown from './Dropdown'; import Dropdown from './Dropdown';
interface Props { interface Props {
@ -8,7 +8,7 @@ interface Props {
onChange(value: string): void; onChange(value: string): void;
} }
export default class SimpleDropdown extends Component<Props, void> { export default class SimpleDropdown extends PureComponent<Props, void> {
public render() { public render() {
const { options, value, onChange, ariaLabel } = this.props; const { options, value, onChange, ariaLabel } = this.props;

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
interface Props { interface Props {
value?: string; value?: string;
@ -6,7 +6,7 @@ interface Props {
onChange(event: React.FormEvent<HTMLSpanElement>): void; onChange(event: React.FormEvent<HTMLSpanElement>): void;
} }
export default class SimpleSelect extends Component<Props, {}> { export default class SimpleSelect extends PureComponent<Props, {}> {
public render() { public render() {
return ( return (
<select <select

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import './SwapDropdown.scss'; import './SwapDropdown.scss';
import classnames from 'classnames'; import classnames from 'classnames';
@ -16,7 +16,7 @@ interface Props<T> {
onChange(value: T): void; onChange(value: T): void;
} }
class SwapDropdown<T> extends Component<Props<T>, {}> { class SwapDropdown<T> extends PureComponent<Props<T>, {}> {
public state = { public state = {
open: false open: false
}; };

View File

@ -17,7 +17,7 @@ interface State {
isKeystoreModalOpen: boolean; isKeystoreModalOpen: boolean;
} }
export default class WalletInfo extends React.Component<Props, State> { export default class WalletInfo extends React.PureComponent<Props, State> {
public state = { public state = {
address: '', address: '',
privateKey: '', privateKey: '',

View File

@ -1,12 +1,12 @@
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
interface Props { interface Props {
paymentAddress: string | null; paymentAddress: string | null;
destinationAmount: number; destinationAmount: number;
} }
export default class BitcoinQR extends Component<Props, {}> { export default class BitcoinQR extends PureComponent<Props, {}> {
public render() { public render() {
const { paymentAddress, destinationAmount } = this.props; const { paymentAddress, destinationAmount } = this.props;
return ( return (

View File

@ -7,7 +7,7 @@ import {
} from 'reducers/swap/types'; } from 'reducers/swap/types';
import SimpleButton from 'components/ui/SimpleButton'; import SimpleButton from 'components/ui/SimpleButton';
import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from 'config/bity'; import { generateKindMax, generateKindMin, WhitelistedCoins, bityConfig } from 'config/bity';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import { combineAndUpper } from 'utils/formatters'; import { combineAndUpper } from 'utils/formatters';
import { SwapDropdown } from 'components/ui'; import { SwapDropdown } from 'components/ui';
@ -41,7 +41,7 @@ interface State {
type Props = StateProps & ActionProps; type Props = StateProps & ActionProps;
export default class CurrencySwap extends Component<Props, State> { export default class CurrencySwap extends PureComponent<Props, State> {
public state = { public state = {
disabled: true, disabled: true,
origin: { origin: {

View File

@ -7,7 +7,7 @@ import bityLogoWhite from 'assets/images/logo-bity-white.svg';
import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg'; import shapeshiftLogoWhite from 'assets/images/logo-shapeshift.svg';
import Spinner from 'components/ui/Spinner'; import Spinner from 'components/ui/Spinner';
import { bityReferralURL, shapeshiftReferralURL } from 'config'; import { bityReferralURL, shapeshiftReferralURL } from 'config';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import './CurrentRates.scss'; import './CurrentRates.scss';
import { SHAPESHIFT_WHITELIST } from 'api/shapeshift'; import { SHAPESHIFT_WHITELIST } from 'api/shapeshift';
@ -22,7 +22,7 @@ interface Props {
shapeshiftRates: NormalizedShapeshiftRates; shapeshiftRates: NormalizedShapeshiftRates;
} }
export default class CurrentRates extends Component<Props> { export default class CurrentRates extends PureComponent<Props> {
private shapeShiftRateCache = null; private shapeShiftRateCache = null;
public getRandomSSPairData = ( public getRandomSSPairData = (

View File

@ -9,7 +9,7 @@ import {
TStopPollShapeshiftOrderStatus TStopPollShapeshiftOrderStatus
} from 'actions/swap'; } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types'; import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import BitcoinQR from './BitcoinQR'; import BitcoinQR from './BitcoinQR';
import PaymentInfo from './PaymentInfo'; import PaymentInfo from './PaymentInfo';
import SwapProgress from './SwapProgress'; import SwapProgress from './SwapProgress';
@ -39,7 +39,7 @@ interface ReduxActionProps {
showNotification: TShowNotification; showNotification: TShowNotification;
} }
export default class PartThree extends Component<ReduxActionProps & ReduxStateProps, {}> { export default class PartThree extends PureComponent<ReduxActionProps & ReduxStateProps, {}> {
public componentDidMount() { public componentDidMount() {
const { provider } = this.props; const { provider } = this.props;
if (provider === 'shapeshift') { if (provider === 'shapeshift') {

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import { SwapInput } from 'reducers/swap/types'; import { SwapInput } from 'reducers/swap/types';
import './PaymentInfo.scss'; import './PaymentInfo.scss';
@ -8,7 +8,7 @@ export interface Props {
paymentAddress: string | null; paymentAddress: string | null;
} }
export default class PaymentInfo extends Component<Props, {}> { export default class PaymentInfo extends PureComponent<Props, {}> {
public render() { public render() {
const { origin } = this.props; const { origin } = this.props;
return ( return (

View File

@ -10,7 +10,7 @@ import classnames from 'classnames';
import SimpleButton from 'components/ui/SimpleButton'; import SimpleButton from 'components/ui/SimpleButton';
import { donationAddressMap } from 'config'; import { donationAddressMap } from 'config';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators'; import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import { combineAndUpper } from 'utils/formatters'; import { combineAndUpper } from 'utils/formatters';
import './ReceivingAddress.scss'; import './ReceivingAddress.scss';
@ -32,7 +32,7 @@ export interface ActionProps {
shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap; shapeshiftOrderCreateRequestedSwap: TShapeshiftOrderCreateRequestedSwap;
} }
export default class ReceivingAddress extends Component<StateProps & ActionProps, {}> { export default class ReceivingAddress extends PureComponent<StateProps & ActionProps, {}> {
public onChangeDestinationAddress = (event: React.FormEvent<HTMLInputElement>) => { public onChangeDestinationAddress = (event: React.FormEvent<HTMLInputElement>) => {
const value = event.currentTarget.value; const value = event.currentTarget.value;
this.props.destinationAddressSwap(value); this.props.destinationAddressSwap(value);

View File

@ -2,7 +2,7 @@ import React from 'react';
import './ShapeshiftBanner.scss'; import './ShapeshiftBanner.scss';
import shapeshiftSvg from 'assets/images/logo-shapeshift.svg'; import shapeshiftSvg from 'assets/images/logo-shapeshift.svg';
export default () => ( const ShapeshiftBanner: React.SFC<{}> = () => (
<div className="ShapeshiftBanner"> <div className="ShapeshiftBanner">
<div className="ShapeshiftBanner-banner"> <div className="ShapeshiftBanner-banner">
<p> <p>
@ -13,3 +13,5 @@ export default () => (
</div> </div>
</div> </div>
); );
export default ShapeshiftBanner;

View File

@ -14,7 +14,7 @@ interface Props {
bityRates: NormalizedBityRates; bityRates: NormalizedBityRates;
} }
class SupportFooter extends React.Component<Props, {}> { class SupportFooter extends React.PureComponent<Props, {}> {
public state = { public state = {
open: false open: false
}; };

View File

@ -1,6 +1,6 @@
import { RestartSwapAction } from 'actions/swap'; import { RestartSwapAction } from 'actions/swap';
import { SwapInput } from 'reducers/swap/types'; import { SwapInput } from 'reducers/swap/types';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import classnames from 'classnames'; import classnames from 'classnames';
import { toFixedIfLarger } from 'utils/formatters'; import { toFixedIfLarger } from 'utils/formatters';
@ -16,7 +16,7 @@ export interface SwapInfoHeaderProps {
restartSwap(): RestartSwapAction; restartSwap(): RestartSwapAction;
} }
export default class SwapInfoHeader extends Component<SwapInfoHeaderProps, {}> { export default class SwapInfoHeader extends PureComponent<SwapInfoHeaderProps, {}> {
public computedOriginDestinationRatio = () => { public computedOriginDestinationRatio = () => {
const { origin, destination } = this.props; const { origin, destination } = this.props;
if (!origin.amount || !destination.amount) { if (!origin.amount || !destination.amount) {

View File

@ -2,7 +2,7 @@ import { RestartSwapAction } from 'actions/swap';
import bityLogo from 'assets/images/logo-bity.svg'; import bityLogo from 'assets/images/logo-bity.svg';
import shapeshiftLogo from 'assets/images/shapeshift-dark.svg'; import shapeshiftLogo from 'assets/images/shapeshift-dark.svg';
import { bityReferralURL } from 'config'; import { bityReferralURL } from 'config';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate from 'translations'; import translate from 'translations';
import './SwapInfoHeader.scss'; import './SwapInfoHeader.scss';
@ -11,7 +11,7 @@ export interface SwapInfoHeaderTitleProps {
restartSwap(): RestartSwapAction; restartSwap(): RestartSwapAction;
} }
export default class SwapInfoHeaderTitle extends Component<SwapInfoHeaderTitleProps, {}> { export default class SwapInfoHeaderTitle extends PureComponent<SwapInfoHeaderTitleProps, {}> {
public render() { public render() {
const { provider } = this.props; const { provider } = this.props;
const logoToRender = provider === 'shapeshift' ? shapeshiftLogo : bityLogo; const logoToRender = provider === 'shapeshift' ? shapeshiftLogo : bityLogo;

View File

@ -1,6 +1,6 @@
import { TShowNotification } from 'actions/notifications'; import { TShowNotification } from 'actions/notifications';
import { bityConfig } from 'config/bity'; import { bityConfig } from 'config/bity';
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
import './SwapProgress.scss'; import './SwapProgress.scss';
@ -19,7 +19,7 @@ export interface Props {
interface State { interface State {
hasShownViewTx: boolean; hasShownViewTx: boolean;
} }
export default class SwapProgress extends Component<Props, State> { export default class SwapProgress extends PureComponent<Props, State> {
public state = { public state = {
hasShownViewTx: false hasShownViewTx: false
}; };

View File

@ -11,7 +11,7 @@ type Data = Buffer;
export const ETH_DECIMAL = 18; export const ETH_DECIMAL = 18;
const Units = { export const Units = {
wei: '1', wei: '1',
kwei: '1000', kwei: '1000',
ada: '1000', ada: '1000',

View File

@ -1,11 +1,18 @@
import { State, RequestStatus } from './typings'; import { State, RequestStatus } from './typings';
import { TypeKeys as TK, ResetAction, NetworkAction } from 'actions/transaction'; import {
TypeKeys as TK,
ResetAction,
NetworkAction,
InputGasPriceAction,
InputGasPriceIntentAction
} from 'actions/transaction';
import { Action } from 'redux'; import { Action } from 'redux';
const INITIAL_STATE: State = { const INITIAL_STATE: State = {
gasEstimationStatus: null, gasEstimationStatus: null,
getFromStatus: null, getFromStatus: null,
getNonceStatus: null getNonceStatus: null,
gasPriceStatus: null
}; };
const getPostFix = (str: string) => { const getPostFix = (str: string) => {
@ -18,9 +25,17 @@ const nextState = (field: keyof State) => (state: State, action: Action): State
[field]: RequestStatus[getPostFix(action.type)] [field]: RequestStatus[getPostFix(action.type)]
}); });
const setGasPriceStatus = (state: State, gasPriceStatus: RequestStatus) => ({
...state,
gasPriceStatus
});
const reset = () => INITIAL_STATE; const reset = () => INITIAL_STATE;
export const network = (state: State = INITIAL_STATE, action: NetworkAction | ResetAction) => { export const network = (
state: State = INITIAL_STATE,
action: NetworkAction | ResetAction | InputGasPriceAction | InputGasPriceIntentAction
) => {
switch (action.type) { switch (action.type) {
case TK.ESTIMATE_GAS_REQUESTED: case TK.ESTIMATE_GAS_REQUESTED:
return nextState('gasEstimationStatus')(state, action); return nextState('gasEstimationStatus')(state, action);
@ -42,6 +57,14 @@ export const network = (state: State = INITIAL_STATE, action: NetworkAction | Re
return nextState('getNonceStatus')(state, action); return nextState('getNonceStatus')(state, action);
case TK.GET_NONCE_FAILED: case TK.GET_NONCE_FAILED:
return nextState('getNonceStatus')(state, action); return nextState('getNonceStatus')(state, action);
// Not exactly "network" requests, but we want to show pending while
// gas price is subject to change
case TK.GAS_PRICE_INPUT_INTENT:
return setGasPriceStatus(state, RequestStatus.REQUESTED);
case TK.GAS_PRICE_INPUT:
return setGasPriceStatus(state, RequestStatus.SUCCEEDED);
case TK.RESET: case TK.RESET:
return reset(); return reset();
default: default:

View File

@ -8,4 +8,5 @@ export interface State {
gasEstimationStatus: RequestStatus | null; gasEstimationStatus: RequestStatus | null;
getFromStatus: RequestStatus | null; getFromStatus: RequestStatus | null;
getNonceStatus: RequestStatus | null; getNonceStatus: RequestStatus | null;
gasPriceStatus: RequestStatus | null;
} }

View File

@ -1,7 +1,8 @@
import BN from 'bn.js'; import BN from 'bn.js';
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga'; import { SagaIterator, delay } from 'redux-saga';
import { import {
inputGasPrice,
setDataField, setDataField,
setGasLimitField, setGasLimitField,
setGasPriceField, setGasPriceField,
@ -11,6 +12,7 @@ import {
InputDataAction, InputDataAction,
InputGasLimitAction, InputGasLimitAction,
InputGasPriceAction, InputGasPriceAction,
InputGasPriceIntentAction,
InputNonceAction, InputNonceAction,
TypeKeys TypeKeys
} from 'actions/transaction'; } from 'actions/transaction';
@ -38,6 +40,13 @@ export function* handleGasPriceInput({ payload }: InputGasPriceAction): SagaIter
); );
} }
export function* handleGasPriceInputIntent({ payload }: InputGasPriceIntentAction): SagaIterator {
yield call(delay, 300);
// Important to put and not fork handleGasPriceInput, we want
// action to go to reducers.
yield put(inputGasPrice(payload));
}
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 }));
@ -47,5 +56,6 @@ 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.GAS_PRICE_INPUT, handleGasPriceInput),
takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput) takeEvery(TypeKeys.NONCE_INPUT, handleNonceInput),
takeLatest(TypeKeys.GAS_PRICE_INPUT_INTENT, handleGasPriceInputIntent)
]; ];

View File

@ -10,10 +10,12 @@ const getGasLimit = (state: AppState) => getFields(state).gasLimit;
const getGasPrice = (state: AppState) => getFields(state).gasPrice; const getGasPrice = (state: AppState) => getFields(state).gasPrice;
const getValue = (state: AppState) => getFields(state).value; const getValue = (state: AppState) => getFields(state).value;
const getNonce = (state: AppState) => getFields(state).nonce; const getNonce = (state: AppState) => getFields(state).nonce;
const getDataExists = (state: AppState) => { const getDataExists = (state: AppState) => {
const { value } = getData(state); const { value } = getData(state);
return !!value && value.length > 0; return !!value && value.length > 0;
}; };
const getValidGasCost = (state: AppState) => { const getValidGasCost = (state: AppState) => {
const gasCost = getGasCost(state); const gasCost = getGasCost(state);
const etherBalance = getEtherBalance(state); const etherBalance = getEtherBalance(state);