Better Offline UX (#785)
* Check offline status immediately. * If they start the page offline, show a less severe error message. * Get rid of offline aware header. Disable wallet options when offline. * Add online indicator to the header. * Prevent some components from render, some requests from firing when offline. * Allow for array of elements with typing. * Dont show dollars in fee summary when offline. * Fix up saga tests. * Fix sidebar component offline styles. * Remove force offline. * Dont request rates if offline. * Nonce in advanced, show even of online. * Show invalid advanced props. * Fix up offline poll tests.
This commit is contained in:
parent
659f218b1c
commit
4f6e83acf4
|
@ -15,6 +15,7 @@ import PageNotFound from 'components/PageNotFound';
|
||||||
import LogOutPrompt from 'components/LogOutPrompt';
|
import LogOutPrompt from 'components/LogOutPrompt';
|
||||||
import { Aux } from 'components/ui';
|
import { Aux } from 'components/ui';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
|
import { pollOfflineStatus } from 'actions/config';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -30,6 +31,10 @@ export default class Root extends Component<Props, State> {
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.props.store.dispatch(pollOfflineStatus());
|
||||||
|
}
|
||||||
|
|
||||||
public componentDidCatch(error: Error) {
|
public componentDidCatch(error: Error) {
|
||||||
this.setState({ error });
|
this.setState({ error });
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,6 @@ import * as interfaces from './actionTypes';
|
||||||
import { TypeKeys } from './constants';
|
import { TypeKeys } from './constants';
|
||||||
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data';
|
import { NodeConfig, CustomNodeConfig, NetworkConfig, CustomNetworkConfig } from 'config/data';
|
||||||
|
|
||||||
export type TForceOfflineConfig = typeof forceOfflineConfig;
|
|
||||||
export function forceOfflineConfig(): interfaces.ForceOfflineAction {
|
|
||||||
return {
|
|
||||||
type: TypeKeys.CONFIG_FORCE_OFFLINE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TToggleOfflineConfig = typeof toggleOfflineConfig;
|
export type TToggleOfflineConfig = typeof toggleOfflineConfig;
|
||||||
export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
|
export function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,11 +6,6 @@ export interface ToggleOfflineAction {
|
||||||
type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
|
type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Force Offline ***/
|
|
||||||
export interface ForceOfflineAction {
|
|
||||||
type: TypeKeys.CONFIG_FORCE_OFFLINE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Change Language ***/
|
/*** Change Language ***/
|
||||||
export interface ChangeLanguageAction {
|
export interface ChangeLanguageAction {
|
||||||
type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
|
type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
|
||||||
|
@ -80,7 +75,6 @@ export type ConfigAction =
|
||||||
| ChangeLanguageAction
|
| ChangeLanguageAction
|
||||||
| ToggleOfflineAction
|
| ToggleOfflineAction
|
||||||
| PollOfflineStatus
|
| PollOfflineStatus
|
||||||
| ForceOfflineAction
|
|
||||||
| ChangeNodeIntentAction
|
| ChangeNodeIntentAction
|
||||||
| AddCustomNodeAction
|
| AddCustomNodeAction
|
||||||
| RemoveCustomNodeAction
|
| RemoveCustomNodeAction
|
||||||
|
|
|
@ -3,7 +3,6 @@ export enum TypeKeys {
|
||||||
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
|
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
|
||||||
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
|
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
|
||||||
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
||||||
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
|
|
||||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
||||||
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
||||||
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
||||||
|
|
|
@ -40,4 +40,9 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-offline {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
||||||
ratesError?: State['ratesError'];
|
ratesError?: State['ratesError'];
|
||||||
fetchCCRates: TFetchCCRates;
|
fetchCCRates: TFetchCCRates;
|
||||||
network: NetworkConfig;
|
network: NetworkConfig;
|
||||||
|
isOffline: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CmpState {
|
interface CmpState {
|
||||||
|
@ -44,15 +45,19 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: Props) {
|
public componentWillReceiveProps(nextProps: Props) {
|
||||||
const { balance, tokenBalances } = this.props;
|
const { balance, tokenBalances, isOffline } = this.props;
|
||||||
if (nextProps.balance !== balance || nextProps.tokenBalances !== tokenBalances) {
|
if (
|
||||||
|
nextProps.balance !== balance ||
|
||||||
|
nextProps.tokenBalances !== tokenBalances ||
|
||||||
|
nextProps.isOffline !== isOffline
|
||||||
|
) {
|
||||||
this.makeBalanceLookup(nextProps);
|
this.makeBalanceLookup(nextProps);
|
||||||
this.fetchRates(nextProps);
|
this.fetchRates(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { balance, tokenBalances, rates, ratesError, network } = this.props;
|
const { balance, tokenBalances, rates, ratesError, isOffline, network } = this.props;
|
||||||
const { currency } = this.state;
|
const { currency } = this.state;
|
||||||
|
|
||||||
// There are a bunch of reasons why the incorrect balances might be rendered
|
// There are a bunch of reasons why the incorrect balances might be rendered
|
||||||
|
@ -130,7 +135,13 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
||||||
</select>
|
</select>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
|
{isOffline ? (
|
||||||
|
<div className="EquivalentValues-offline well well-sm">
|
||||||
|
Equivalent values are unavailable offline
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<ul className="EquivalentValues-values">{valuesEl}</ul>
|
<ul className="EquivalentValues-values">{valuesEl}</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -154,8 +165,8 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchRates(props: Props) {
|
private fetchRates(props: Props) {
|
||||||
// Duck out if we haven't gotten balances yet
|
// Duck out if we haven't gotten balances yet, or we're not going to
|
||||||
if (!props.balance || !props.tokenBalances) {
|
if (!props.balance || !props.tokenBalances || props.isOffline) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,4 +41,9 @@
|
||||||
color: $gray;
|
color: $gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-offline {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ interface StateProps {
|
||||||
tokensError: AppState['wallet']['tokensError'];
|
tokensError: AppState['wallet']['tokensError'];
|
||||||
isTokensLoading: AppState['wallet']['isTokensLoading'];
|
isTokensLoading: AppState['wallet']['isTokensLoading'];
|
||||||
hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens'];
|
hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens'];
|
||||||
|
isOffline: AppState['config']['offline'];
|
||||||
}
|
}
|
||||||
interface ActionProps {
|
interface ActionProps {
|
||||||
addCustomToken: TAddCustomToken;
|
addCustomToken: TAddCustomToken;
|
||||||
|
@ -46,13 +47,20 @@ class TokenBalances extends React.Component<Props> {
|
||||||
tokenBalances,
|
tokenBalances,
|
||||||
hasSavedWalletTokens,
|
hasSavedWalletTokens,
|
||||||
isTokensLoading,
|
isTokensLoading,
|
||||||
tokensError
|
tokensError,
|
||||||
|
isOffline
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const walletTokens = walletConfig ? walletConfig.tokens : [];
|
const walletTokens = walletConfig ? walletConfig.tokens : [];
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (tokensError) {
|
if (isOffline) {
|
||||||
|
content = (
|
||||||
|
<div className="TokenBalances-offline well well-sm">
|
||||||
|
Token balances are unavailable offline
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (tokensError) {
|
||||||
content = <h5>{tokensError}</h5>;
|
content = <h5>{tokensError}</h5>;
|
||||||
} else if (isTokensLoading) {
|
} else if (isTokensLoading) {
|
||||||
content = (
|
content = (
|
||||||
|
@ -109,7 +117,8 @@ function mapStateToProps(state: AppState): StateProps {
|
||||||
tokenBalances: getTokenBalances(state),
|
tokenBalances: getTokenBalances(state),
|
||||||
tokensError: state.wallet.tokensError,
|
tokensError: state.wallet.tokensError,
|
||||||
isTokensLoading: state.wallet.isTokensLoading,
|
isTokensLoading: state.wallet.isTokensLoading,
|
||||||
hasSavedWalletTokens: state.wallet.hasSavedWalletTokens
|
hasSavedWalletTokens: state.wallet.hasSavedWalletTokens,
|
||||||
|
isOffline: state.config.offline
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface Props {
|
||||||
rates: AppState['rates']['rates'];
|
rates: AppState['rates']['rates'];
|
||||||
ratesError: AppState['rates']['ratesError'];
|
ratesError: AppState['rates']['ratesError'];
|
||||||
fetchCCRates: TFetchCCRates;
|
fetchCCRates: TFetchCCRates;
|
||||||
|
isOffline: AppState['config']['offline'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Block {
|
interface Block {
|
||||||
|
@ -29,7 +30,7 @@ interface Block {
|
||||||
|
|
||||||
export class BalanceSidebar extends React.Component<Props, {}> {
|
export class BalanceSidebar extends React.Component<Props, {}> {
|
||||||
public render() {
|
public render() {
|
||||||
const { wallet, balance, network, tokenBalances, rates, ratesError } = this.props;
|
const { wallet, balance, network, tokenBalances, rates, ratesError, isOffline } = this.props;
|
||||||
|
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -59,6 +60,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
|
||||||
rates={rates}
|
rates={rates}
|
||||||
ratesError={ratesError}
|
ratesError={ratesError}
|
||||||
fetchCCRates={this.props.fetchCCRates}
|
fetchCCRates={this.props.fetchCCRates}
|
||||||
|
isOffline={isOffline}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +85,8 @@ function mapStateToProps(state: AppState) {
|
||||||
tokenBalances: getShownTokenBalances(state, true),
|
tokenBalances: getShownTokenBalances(state, true),
|
||||||
network: getNetworkConfig(state),
|
network: getNetworkConfig(state),
|
||||||
rates: state.rates.rates,
|
rates: state.rates.rates,
|
||||||
ratesError: state.rates.ratesError
|
ratesError: state.rates.ratesError,
|
||||||
|
isOffline: state.config.offline
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ interface Props {
|
||||||
// Data
|
// Data
|
||||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||||
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
||||||
|
nonce: AppState['transaction']['fields']['nonce'];
|
||||||
offline: AppState['config']['offline'];
|
offline: AppState['config']['offline'];
|
||||||
network: AppState['config']['network'];
|
network: AppState['config']['network'];
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -41,11 +42,19 @@ class GasSlider extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
if (!this.props.offline) {
|
||||||
this.props.fetchCCRates([this.props.network.unit]);
|
this.props.fetchCCRates([this.props.network.unit]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (this.props.offline && !nextProps.offline) {
|
||||||
|
this.props.fetchCCRates([this.props.network.unit]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { gasPrice, gasLimit, offline, disableAdvanced } = this.props;
|
const { gasPrice, gasLimit, nonce, offline, disableAdvanced } = this.props;
|
||||||
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
|
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -54,8 +63,10 @@ class GasSlider extends React.Component<Props, State> {
|
||||||
<AdvancedGas
|
<AdvancedGas
|
||||||
gasPrice={gasPrice.raw}
|
gasPrice={gasPrice.raw}
|
||||||
gasLimit={gasLimit.raw}
|
gasLimit={gasLimit.raw}
|
||||||
|
nonce={nonce.raw}
|
||||||
changeGasPrice={this.props.inputGasPrice}
|
changeGasPrice={this.props.inputGasPrice}
|
||||||
changeGasLimit={this.props.inputGasLimit}
|
changeGasLimit={this.props.inputGasLimit}
|
||||||
|
changeNonce={this.props.inputNonce}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
|
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
|
||||||
|
@ -86,6 +97,7 @@ function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
gasPrice: state.transaction.fields.gasPrice,
|
gasPrice: state.transaction.fields.gasPrice,
|
||||||
gasLimit: state.transaction.fields.gasLimit,
|
gasLimit: state.transaction.fields.gasLimit,
|
||||||
|
nonce: state.transaction.fields.nonce,
|
||||||
offline: state.config.offline,
|
offline: state.config.offline,
|
||||||
network: getNetworkConfig(state)
|
network: getNetworkConfig(state)
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { DataFieldFactory } from 'components/DataFieldFactory';
|
import { DataFieldFactory } from 'components/DataFieldFactory';
|
||||||
import FeeSummary from './FeeSummary';
|
import FeeSummary from './FeeSummary';
|
||||||
|
@ -7,35 +8,53 @@ import './AdvancedGas.scss';
|
||||||
interface Props {
|
interface Props {
|
||||||
gasPrice: string;
|
gasPrice: string;
|
||||||
gasLimit: string;
|
gasLimit: string;
|
||||||
|
nonce: string;
|
||||||
changeGasPrice(gwei: string): void;
|
changeGasPrice(gwei: string): void;
|
||||||
changeGasLimit(wei: string): void;
|
changeGasLimit(wei: string): void;
|
||||||
|
changeNonce(nonce: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AdvancedGas extends React.Component<Props> {
|
export default class AdvancedGas extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
|
// Can't shadow var names for data & fee summary
|
||||||
|
const vals = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="AdvancedGas row form-group">
|
<div className="AdvancedGas row form-group">
|
||||||
<div className="col-md-3 col-sm-6 col-xs-12">
|
<div className="col-md-4 col-sm-6 col-xs-12">
|
||||||
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
|
<label>{translate('OFFLINE_Step2_Label_3')} (gwei)</label>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className={classnames('form-control', !vals.gasPrice && 'is-invalid')}
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.gasPrice}
|
placeholder="e.g. 40"
|
||||||
|
value={vals.gasPrice}
|
||||||
onChange={this.handleGasPriceChange}
|
onChange={this.handleGasPriceChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-3 col-sm-6 col-xs-12">
|
<div className="col-md-4 col-sm-6 col-xs-12">
|
||||||
<label>{translate('OFFLINE_Step2_Label_4')}</label>
|
<label>{translate('OFFLINE_Step2_Label_4')}</label>
|
||||||
<input
|
<input
|
||||||
className="form-control"
|
className={classnames('form-control', !vals.gasLimit && 'is-invalid')}
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.gasLimit}
|
placeholder="e.g. 21000"
|
||||||
|
value={vals.gasLimit}
|
||||||
onChange={this.handleGasLimitChange}
|
onChange={this.handleGasLimitChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-6 col-sm-12">
|
<div className="col-md-4 col-sm-12">
|
||||||
|
<label>{translate('OFFLINE_Step2_Label_5')}</label>
|
||||||
|
<input
|
||||||
|
className={classnames('form-control', !vals.nonce && 'is-invalid')}
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g. 7"
|
||||||
|
value={vals.nonce}
|
||||||
|
onChange={this.handleNonceChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-12">
|
||||||
<label>{translate('OFFLINE_Step2_Label_6')}</label>
|
<label>{translate('OFFLINE_Step2_Label_6')}</label>
|
||||||
<DataFieldFactory
|
<DataFieldFactory
|
||||||
withProps={({ data, onChange }) => (
|
withProps={({ data, onChange }) => (
|
||||||
|
@ -69,4 +88,8 @@ export default class AdvancedGas extends React.Component<Props> {
|
||||||
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||||
this.props.changeGasLimit(ev.currentTarget.value);
|
this.props.changeGasLimit(ev.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleNonceChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
this.props.changeNonce(ev.currentTarget.value);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,14 @@ interface Props {
|
||||||
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'];
|
||||||
// Component props
|
// Component props
|
||||||
render(data: RenderData): React.ReactElement<string> | string;
|
render(data: RenderData): React.ReactElement<string> | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeeSummary extends React.Component<Props> {
|
class FeeSummary extends React.Component<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const { gasPrice, gasLimit, rates, network } = this.props;
|
const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
|
||||||
|
|
||||||
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
|
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
|
||||||
const fee = (
|
const fee = (
|
||||||
|
@ -42,7 +43,7 @@ class FeeSummary extends React.Component<Props> {
|
||||||
const usdBig = network.isTestnet
|
const usdBig = network.isTestnet
|
||||||
? new BN(0)
|
? new BN(0)
|
||||||
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
|
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
|
||||||
const usd = (
|
const usd = isOffline ? null : (
|
||||||
<UnitDisplay
|
<UnitDisplay
|
||||||
value={usdBig}
|
value={usdBig}
|
||||||
unit="ether"
|
unit="ether"
|
||||||
|
@ -71,7 +72,8 @@ function mapStateToProps(state: AppState) {
|
||||||
gasPrice: state.transaction.fields.gasPrice,
|
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),
|
||||||
|
isOffline: state.config.offline
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
@import 'common/sass/variables';
|
||||||
|
@import 'common/sass/mixins';
|
||||||
|
|
||||||
|
@keyframes online-pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.OnlineStatus {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
text-align: center;
|
||||||
|
transition: $transition;
|
||||||
|
border-radius: 100%;
|
||||||
|
transition: background-color 300ms ease;
|
||||||
|
@include show-tooltip-on-hover;
|
||||||
|
|
||||||
|
&.is-online {
|
||||||
|
background: lighten($brand-success, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-offline {
|
||||||
|
background: lighten($brand-danger, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-connecting {
|
||||||
|
background: #FFF;
|
||||||
|
animation: online-pulse 800ms ease infinite;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Tooltip } from 'components/ui';
|
||||||
|
import './OnlineStatus.scss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOffline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnlineStatus: React.SFC<Props> = ({ isOffline }) => (
|
||||||
|
<div className={`OnlineStatus fa-stack ${isOffline ? 'is-offline' : 'is-online'}`}>
|
||||||
|
<Tooltip>{isOffline ? 'Offline' : 'Online'}</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default OnlineStatus;
|
|
@ -130,6 +130,10 @@ $small-size: 900px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-online {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
&-dropdown {
|
&-dropdown {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import GasPriceDropdown from './components/GasPriceDropdown';
|
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||||
import Navigation from './components/Navigation';
|
import Navigation from './components/Navigation';
|
||||||
import CustomNodeModal from './components/CustomNodeModal';
|
import CustomNodeModal from './components/CustomNodeModal';
|
||||||
|
import OnlineStatus from './components/OnlineStatus';
|
||||||
import { getKeyByValue } from 'utils/helpers';
|
import { getKeyByValue } from 'utils/helpers';
|
||||||
import { makeCustomNodeId } from 'utils/node';
|
import { makeCustomNodeId } from 'utils/node';
|
||||||
import { getNetworkConfigFromId } from 'utils/network';
|
import { getNetworkConfigFromId } from 'utils/network';
|
||||||
|
@ -35,6 +36,7 @@ interface Props {
|
||||||
node: NodeConfig;
|
node: NodeConfig;
|
||||||
nodeSelection: string;
|
nodeSelection: string;
|
||||||
isChangingNode: boolean;
|
isChangingNode: boolean;
|
||||||
|
isOffline: boolean;
|
||||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||||
customNodes: CustomNodeConfig[];
|
customNodes: CustomNodeConfig[];
|
||||||
customNetworks: CustomNetworkConfig[];
|
customNetworks: CustomNetworkConfig[];
|
||||||
|
@ -62,6 +64,7 @@ export default class Header extends Component<Props, State> {
|
||||||
node,
|
node,
|
||||||
nodeSelection,
|
nodeSelection,
|
||||||
isChangingNode,
|
isChangingNode,
|
||||||
|
isOffline,
|
||||||
customNodes,
|
customNodes,
|
||||||
customNetworks
|
customNetworks
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -127,6 +130,10 @@ export default class Header extends Component<Props, State> {
|
||||||
<div className="Header-branding-right">
|
<div className="Header-branding-right">
|
||||||
<span className="Header-branding-right-version hidden-xs">v{VERSION}</span>
|
<span className="Header-branding-right-version hidden-xs">v{VERSION}</span>
|
||||||
|
|
||||||
|
<div className="Header-branding-right-online">
|
||||||
|
<OnlineStatus isOffline={isOffline} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="Header-branding-right-dropdown">
|
<div className="Header-branding-right-dropdown">
|
||||||
<GasPriceDropdown
|
<GasPriceDropdown
|
||||||
value={this.props.gasPrice.raw}
|
value={this.props.gasPrice.raw}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Aux } from 'components/ui';
|
||||||
import { Query } from 'components/renderCbs';
|
import { Query } from 'components/renderCbs';
|
||||||
import Help from 'components/ui/Help';
|
import Help from 'components/ui/Help';
|
||||||
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
|
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
|
||||||
import { isAnyOffline } from 'selectors/config';
|
import { getOffline } from 'selectors/config';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
const nonceHelp = (
|
const nonceHelp = (
|
||||||
|
@ -50,6 +50,6 @@ class NonceInputClass extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NonceInput = connect((state: AppState) => ({
|
export const NonceInput = connect((state: AppState) => ({
|
||||||
shouldDisplay: isAnyOffline(state) || nonceRequestFailed(state),
|
shouldDisplay: getOffline(state) || nonceRequestFailed(state),
|
||||||
nonce: getNonce(state)
|
nonce: getNonce(state)
|
||||||
}))(NonceInputClass);
|
}))(NonceInputClass);
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { UnlockHeader } from 'components/ui';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import translate from 'translations';
|
|
||||||
import { isAnyOffline } from 'selectors/config';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { AppState } from 'reducers';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
disabledWallets?: string[];
|
|
||||||
}
|
|
||||||
export const OfflineAwareUnlockHeader: React.SFC<Props> = ({ disabledWallets }) => (
|
|
||||||
<UnlockHeader title={<Title />} disabledWallets={disabledWallets} />
|
|
||||||
);
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
shouldDisplayOffline: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TitleClass extends Component<StateProps> {
|
|
||||||
public render() {
|
|
||||||
const { shouldDisplayOffline } = this.props;
|
|
||||||
const offlineTitle = shouldDisplayOffline ? (
|
|
||||||
<span style={{ color: 'red' }}> (Offline)</span>
|
|
||||||
) : null;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{translate('Account')}
|
|
||||||
{offlineTitle}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Title = connect((state: AppState) => ({
|
|
||||||
shouldDisplayOffline: isAnyOffline(state)
|
|
||||||
}))(TitleClass);
|
|
|
@ -33,14 +33,15 @@ import {
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
||||||
import { IWallet } from 'libs/wallet';
|
import { IWallet } from 'libs/wallet';
|
||||||
|
import DISABLES from './disables.json';
|
||||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||||
|
|
||||||
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
||||||
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
||||||
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
||||||
import MistIcon from 'assets/images/wallets/mist.svg';
|
import MistIcon from 'assets/images/wallets/mist.svg';
|
||||||
import TrezorIcon from 'assets/images/wallets/trezor.svg';
|
import TrezorIcon from 'assets/images/wallets/trezor.svg';
|
||||||
import './WalletDecrypt.scss';
|
import './WalletDecrypt.scss';
|
||||||
type UnlockParams = {} | PrivateKeyValue;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resetTransactionState: TReset;
|
resetTransactionState: TReset;
|
||||||
|
@ -59,6 +60,7 @@ interface Props {
|
||||||
isPasswordPending: AppState['wallet']['isPasswordPending'];
|
isPasswordPending: AppState['wallet']['isPasswordPending'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnlockParams = {} | PrivateKeyValue;
|
||||||
interface State {
|
interface State {
|
||||||
selectedWalletKey: string | null;
|
selectedWalletKey: string | null;
|
||||||
value: UnlockParams | null;
|
value: UnlockParams | null;
|
||||||
|
@ -227,11 +229,6 @@ export class WalletDecrypt extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isOnlineRequiredWalletAndOffline(selectedWalletKey) {
|
|
||||||
const onlineRequiredWallets = ['trezor', 'ledger-nano-s'];
|
|
||||||
return this.props.offline && onlineRequiredWallets.includes(selectedWalletKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildWalletOptions() {
|
public buildWalletOptions() {
|
||||||
const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo;
|
const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo;
|
||||||
|
|
||||||
|
@ -379,6 +376,10 @@ export class WalletDecrypt extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private isWalletDisabled = (walletKey: string) => {
|
private isWalletDisabled = (walletKey: string) => {
|
||||||
|
if (this.props.offline && DISABLES.ONLINE_ONLY.includes(walletKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.props.disabledWallets) {
|
if (!this.props.disabledWallets) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
"READ_ONLY": ["view-only"],
|
"READ_ONLY": ["view-only"],
|
||||||
"UNABLE_TO_SIGN": ["trezor", "view-only"]
|
"UNABLE_TO_SIGN": ["trezor", "view-only"],
|
||||||
|
"ONLINE_ONLY": ["web3", "trezor"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ export * from './CurrentCustomMessage';
|
||||||
export * from './GenerateTransaction';
|
export * from './GenerateTransaction';
|
||||||
export * from './SendButton';
|
export * from './SendButton';
|
||||||
export * from './SigningStatus';
|
export * from './SigningStatus';
|
||||||
export * from './OfflineAwareUnlockHeader';
|
|
||||||
export { default as Header } from './Header';
|
export { default as Header } from './Header';
|
||||||
export { default as Footer } from './Footer';
|
export { default as Footer } from './Footer';
|
||||||
export { default as BalanceSidebar } from './BalanceSidebar';
|
export { default as BalanceSidebar } from './BalanceSidebar';
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { IWallet } from 'libs/wallet/IWallet';
|
||||||
import './UnlockHeader.scss';
|
import './UnlockHeader.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: React.ReactElement<any>;
|
title: React.ReactElement<string> | string;
|
||||||
wallet: IWallet;
|
wallet: IWallet;
|
||||||
disabledWallets?: string[];
|
disabledWallets?: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
@import 'common/sass/variables';
|
||||||
|
|
||||||
|
@keyframes ban-wifi {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.OfflineTab {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
.fa-ban {
|
||||||
|
color: $brand-danger;
|
||||||
|
animation: ban-wifi 500ms ease 200ms 1;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './OfflineTab.scss';
|
||||||
|
|
||||||
|
const OfflineTab: React.SFC<{}> = () => (
|
||||||
|
<section className="OfflineTab Tab-content swap-tab">
|
||||||
|
<div className="Tab-content-pane">
|
||||||
|
<div className="OfflineTab-icon fa-stack fa-4x">
|
||||||
|
<i className="fa fa-wifi fa-stack-1x" />
|
||||||
|
<i className="fa fa-ban fa-stack-2x" />
|
||||||
|
</div>
|
||||||
|
<h1 className="OfflineTab-message">This feature is unavailable while offline</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default OfflineTab;
|
|
@ -16,6 +16,7 @@ import { TSetGasPriceField, setGasPriceField as dSetGasPriceField } from 'action
|
||||||
import { AlphaAgreement, Footer, Header } from 'components';
|
import { AlphaAgreement, Footer, Header } from 'components';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import Notifications from './Notifications';
|
import Notifications from './Notifications';
|
||||||
|
import OfflineTab from './OfflineTab';
|
||||||
import { getGasPrice } from 'selectors/transaction';
|
import { getGasPrice } from 'selectors/transaction';
|
||||||
|
|
||||||
interface ReduxProps {
|
interface ReduxProps {
|
||||||
|
@ -23,6 +24,7 @@ interface ReduxProps {
|
||||||
node: AppState['config']['node'];
|
node: AppState['config']['node'];
|
||||||
nodeSelection: AppState['config']['nodeSelection'];
|
nodeSelection: AppState['config']['nodeSelection'];
|
||||||
isChangingNode: AppState['config']['isChangingNode'];
|
isChangingNode: AppState['config']['isChangingNode'];
|
||||||
|
isOffline: AppState['config']['offline'];
|
||||||
customNodes: AppState['config']['customNodes'];
|
customNodes: AppState['config']['customNodes'];
|
||||||
customNetworks: AppState['config']['customNetworks'];
|
customNetworks: AppState['config']['customNetworks'];
|
||||||
latestBlock: AppState['config']['latestBlock'];
|
latestBlock: AppState['config']['latestBlock'];
|
||||||
|
@ -39,19 +41,21 @@ interface ActionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// FIXME
|
isUnavailableOffline?: boolean;
|
||||||
children: any;
|
children: string | React.ReactElement<string> | React.ReactElement<string>[];
|
||||||
} & ReduxProps &
|
} & ReduxProps &
|
||||||
ActionProps;
|
ActionProps;
|
||||||
|
|
||||||
class TabSection extends Component<Props, {}> {
|
class TabSection extends Component<Props, {}> {
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
|
isUnavailableOffline,
|
||||||
children,
|
children,
|
||||||
// APP
|
// APP
|
||||||
node,
|
node,
|
||||||
nodeSelection,
|
nodeSelection,
|
||||||
isChangingNode,
|
isChangingNode,
|
||||||
|
isOffline,
|
||||||
languageSelection,
|
languageSelection,
|
||||||
customNodes,
|
customNodes,
|
||||||
customNetworks,
|
customNetworks,
|
||||||
|
@ -70,6 +74,7 @@ class TabSection extends Component<Props, {}> {
|
||||||
node,
|
node,
|
||||||
nodeSelection,
|
nodeSelection,
|
||||||
isChangingNode,
|
isChangingNode,
|
||||||
|
isOffline,
|
||||||
gasPrice,
|
gasPrice,
|
||||||
customNodes,
|
customNodes,
|
||||||
customNetworks,
|
customNetworks,
|
||||||
|
@ -85,7 +90,9 @@ class TabSection extends Component<Props, {}> {
|
||||||
<div className="page-layout">
|
<div className="page-layout">
|
||||||
<main>
|
<main>
|
||||||
<Header {...headerProps} />
|
<Header {...headerProps} />
|
||||||
<div className="Tab container">{children}</div>
|
<div className="Tab container">
|
||||||
|
{isUnavailableOffline && isOffline ? <OfflineTab /> : children}
|
||||||
|
</div>
|
||||||
<Footer latestBlock={latestBlock} />
|
<Footer latestBlock={latestBlock} />
|
||||||
</main>
|
</main>
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
@ -100,6 +107,7 @@ function mapStateToProps(state: AppState): ReduxProps {
|
||||||
node: state.config.node,
|
node: state.config.node,
|
||||||
nodeSelection: state.config.nodeSelection,
|
nodeSelection: state.config.nodeSelection,
|
||||||
isChangingNode: state.config.isChangingNode,
|
isChangingNode: state.config.isChangingNode,
|
||||||
|
isOffline: state.config.offline,
|
||||||
languageSelection: state.config.languageSelection,
|
languageSelection: state.config.languageSelection,
|
||||||
gasPrice: getGasPrice(state),
|
gasPrice: getGasPrice(state),
|
||||||
customNodes: state.config.customNodes,
|
customNodes: state.config.customNodes,
|
||||||
|
|
|
@ -43,7 +43,7 @@ class BroadcastTx extends Component<DispatchProps & StateProps> {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabSection>
|
<TabSection isUnavailableOffline={true}>
|
||||||
<div className="Tab-content-pane row block text-center">
|
<div className="Tab-content-pane row block text-center">
|
||||||
<div className="BroadcastTx">
|
<div className="BroadcastTx">
|
||||||
<h1 className="BroadcastTx-title">Broadcast Signed Transaction</h1>
|
<h1 className="BroadcastTx-title">Broadcast Signed Transaction</h1>
|
||||||
|
|
|
@ -43,7 +43,7 @@ class Contracts extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabSection>
|
<TabSection isUnavailableOffline={true}>
|
||||||
<section className="Tab-content Contracts">
|
<section className="Tab-content Contracts">
|
||||||
<div className="Tab-content-pane">
|
<div className="Tab-content-pane">
|
||||||
<h1 className="Contracts-header">
|
<h1 className="Contracts-header">
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface ContainerTabPaneActiveProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContainerTabPaneActive = ({ children }: ContainerTabPaneActiveProps) => (
|
const ContainerTabPaneActive = ({ children }: ContainerTabPaneActiveProps) => (
|
||||||
<TabSection>
|
<TabSection isUnavailableOffline={true}>
|
||||||
<section className="container">
|
<section className="container">
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
<main className="tab-pane active">
|
<main className="tab-pane active">
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { isAnyOfflineWithWeb3 } from 'selectors/derived';
|
import { isAnyOfflineWithWeb3 } from 'selectors/derived';
|
||||||
import {
|
import {
|
||||||
NonceField,
|
|
||||||
AddressField,
|
AddressField,
|
||||||
AmountField,
|
AmountField,
|
||||||
GasSlider,
|
GasSlider,
|
||||||
|
@ -33,11 +32,6 @@ const content = (
|
||||||
<GasSlider />
|
<GasSlider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row form-group">
|
|
||||||
<div className="col-xs-12">
|
|
||||||
<NonceField />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CurrentCustomMessage />
|
<CurrentCustomMessage />
|
||||||
<NonStandardTransaction />
|
<NonStandardTransaction />
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import TabSection from 'containers/TabSection';
|
|
||||||
import { OfflineAwareUnlockHeader } from 'components';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import translate from 'translations';
|
||||||
|
import TabSection from 'containers/TabSection';
|
||||||
|
import { UnlockHeader } from 'components/ui';
|
||||||
import { SideBar } from './components/index';
|
import { SideBar } from './components/index';
|
||||||
import { IReadOnlyWallet, IFullWallet } from 'libs/wallet';
|
import { IReadOnlyWallet, IFullWallet } from 'libs/wallet';
|
||||||
import { getWalletInst } from 'selectors/wallet';
|
import { getWalletInst } from 'selectors/wallet';
|
||||||
|
@ -52,7 +53,7 @@ class SendTransaction extends React.Component<Props> {
|
||||||
return (
|
return (
|
||||||
<TabSection>
|
<TabSection>
|
||||||
<section className="Tab-content">
|
<section className="Tab-content">
|
||||||
<OfflineAwareUnlockHeader />
|
<UnlockHeader title={translate('Account')} />
|
||||||
{wallet && <WalletTabs {...tabProps} />}
|
{wallet && <WalletTabs {...tabProps} />}
|
||||||
</section>
|
</section>
|
||||||
</TabSection>
|
</TabSection>
|
||||||
|
|
|
@ -73,6 +73,7 @@ interface ReduxStateProps {
|
||||||
bityOrderStatus: string | null;
|
bityOrderStatus: string | null;
|
||||||
shapeshiftOrderStatus: string | null;
|
shapeshiftOrderStatus: string | null;
|
||||||
paymentAddress: string | null;
|
paymentAddress: string | null;
|
||||||
|
isOffline: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReduxActionProps {
|
interface ReduxActionProps {
|
||||||
|
@ -98,8 +99,15 @@ interface ReduxActionProps {
|
||||||
|
|
||||||
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.props.loadBityRatesRequestedSwap();
|
if (!this.props.isOffline) {
|
||||||
this.props.loadShapeshiftRatesRequestedSwap();
|
this.loadRates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillReceiveProps(nextProps: ReduxStateProps) {
|
||||||
|
if (this.props.isOffline && !nextProps.isOffline) {
|
||||||
|
this.loadRates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -107,6 +115,11 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
||||||
this.props.stopLoadShapeshiftRatesSwap();
|
this.props.stopLoadShapeshiftRatesSwap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public loadRates() {
|
||||||
|
this.props.loadBityRatesRequestedSwap();
|
||||||
|
this.props.loadShapeshiftRatesRequestedSwap();
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
// STATE
|
// STATE
|
||||||
|
@ -222,7 +235,7 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
||||||
const CurrentRatesProps = { provider, bityRates, shapeshiftRates };
|
const CurrentRatesProps = { provider, bityRates, shapeshiftRates };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabSection>
|
<TabSection isUnavailableOffline={true}>
|
||||||
<section className="Tab-content swap-tab">
|
<section className="Tab-content swap-tab">
|
||||||
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
|
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
|
||||||
{step === 1 && <ShapeshiftBanner />}
|
{step === 1 && <ShapeshiftBanner />}
|
||||||
|
@ -257,7 +270,8 @@ function mapStateToProps(state: AppState) {
|
||||||
isPostingOrder: state.swap.isPostingOrder,
|
isPostingOrder: state.swap.isPostingOrder,
|
||||||
bityOrderStatus: state.swap.bityOrderStatus,
|
bityOrderStatus: state.swap.bityOrderStatus,
|
||||||
shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
|
shapeshiftOrderStatus: state.swap.shapeshiftOrderStatus,
|
||||||
paymentAddress: state.swap.paymentAddress
|
paymentAddress: state.swap.paymentAddress,
|
||||||
|
isOffline: state.config.offline
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ export interface State {
|
||||||
network: NetworkConfig;
|
network: NetworkConfig;
|
||||||
isChangingNode: boolean;
|
isChangingNode: boolean;
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
forceOffline: boolean;
|
|
||||||
customNodes: CustomNodeConfig[];
|
customNodes: CustomNodeConfig[];
|
||||||
customNetworks: CustomNetworkConfig[];
|
customNetworks: CustomNetworkConfig[];
|
||||||
latestBlock: string;
|
latestBlock: string;
|
||||||
|
@ -42,7 +41,6 @@ export const INITIAL_STATE: State = {
|
||||||
network: NETWORKS[NODES[defaultNode].network],
|
network: NETWORKS[NODES[defaultNode].network],
|
||||||
isChangingNode: false,
|
isChangingNode: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
forceOffline: false,
|
|
||||||
customNodes: [],
|
customNodes: [],
|
||||||
customNetworks: [],
|
customNetworks: [],
|
||||||
latestBlock: '???'
|
latestBlock: '???'
|
||||||
|
@ -79,13 +77,6 @@ function toggleOffline(state: State): State {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceOffline(state: State): State {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
forceOffline: !state.forceOffline
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCustomNode(state: State, action: AddCustomNodeAction): State {
|
function addCustomNode(state: State, action: AddCustomNodeAction): State {
|
||||||
const newId = makeCustomNodeId(action.payload);
|
const newId = makeCustomNodeId(action.payload);
|
||||||
return {
|
return {
|
||||||
|
@ -141,8 +132,6 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat
|
||||||
return changeNodeIntent(state);
|
return changeNodeIntent(state);
|
||||||
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
|
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
|
||||||
return toggleOffline(state);
|
return toggleOffline(state);
|
||||||
case TypeKeys.CONFIG_FORCE_OFFLINE:
|
|
||||||
return forceOffline(state);
|
|
||||||
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
|
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
|
||||||
return addCustomNode(state, action);
|
return addCustomNode(state, action);
|
||||||
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
|
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
|
||||||
|
|
|
@ -22,8 +22,7 @@ import {
|
||||||
getNodeConfig,
|
getNodeConfig,
|
||||||
getCustomNodeConfigs,
|
getCustomNodeConfigs,
|
||||||
getCustomNetworkConfigs,
|
getCustomNetworkConfigs,
|
||||||
getOffline,
|
getOffline
|
||||||
getForceOffline
|
|
||||||
} from 'selectors/config';
|
} from 'selectors/config';
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { TypeKeys } from 'actions/config/constants';
|
import { TypeKeys } from 'actions/config/constants';
|
||||||
|
@ -50,19 +49,11 @@ export function* pollOfflineStatus(): SagaIterator {
|
||||||
while (true) {
|
while (true) {
|
||||||
const node: NodeConfig = yield select(getNodeConfig);
|
const node: NodeConfig = yield select(getNodeConfig);
|
||||||
const isOffline: boolean = yield select(getOffline);
|
const isOffline: boolean = yield select(getOffline);
|
||||||
const isForcedOffline: boolean = yield select(getForceOffline);
|
|
||||||
|
|
||||||
// If they're forcing themselves offline, exit the loop. It will be
|
|
||||||
// kicked off again if they toggle it in handleTogglePollOfflineStatus.
|
|
||||||
if (isForcedOffline) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our offline state disagrees with the browser, run a check
|
// If our offline state disagrees with the browser, run a check
|
||||||
// Don't check if the user is in another tab or window
|
// Don't check if the user is in another tab or window
|
||||||
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
|
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
|
||||||
if (shouldPing && !document.hidden) {
|
if (shouldPing && !document.hidden) {
|
||||||
hasCheckedOnline = true;
|
|
||||||
const { pingSucceeded } = yield race({
|
const { pingSucceeded } = yield race({
|
||||||
pingSucceeded: call(node.lib.ping.bind(node.lib)),
|
pingSucceeded: call(node.lib.ping.bind(node.lib)),
|
||||||
timeout: call(delay, 5000)
|
timeout: call(delay, 5000)
|
||||||
|
@ -76,6 +67,9 @@ export function* pollOfflineStatus(): SagaIterator {
|
||||||
yield put(toggleOfflineConfig());
|
yield put(toggleOfflineConfig());
|
||||||
} else if (!pingSucceeded && !isOffline) {
|
} else if (!pingSucceeded && !isOffline) {
|
||||||
// If we were unable to ping but redux says we're online, mark offline
|
// If we were unable to ping but redux says we're online, mark offline
|
||||||
|
// If they had been online, show an error.
|
||||||
|
// If they hadn't been online, just inform them with a warning.
|
||||||
|
if (hasCheckedOnline) {
|
||||||
yield put(
|
yield put(
|
||||||
showNotification(
|
showNotification(
|
||||||
'danger',
|
'danger',
|
||||||
|
@ -85,11 +79,21 @@ export function* pollOfflineStatus(): SagaIterator {
|
||||||
Infinity
|
Infinity
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
yield put(
|
||||||
|
showNotification(
|
||||||
|
'info',
|
||||||
|
'You are currently offline. Some features will be unavailable.',
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
yield put(toggleOfflineConfig());
|
yield put(toggleOfflineConfig());
|
||||||
} else {
|
} else {
|
||||||
// If neither case was true, try again in 5s
|
// If neither case was true, try again in 5s
|
||||||
yield call(delay, 5000);
|
yield call(delay, 5000);
|
||||||
}
|
}
|
||||||
|
hasCheckedOnline = true;
|
||||||
} else {
|
} else {
|
||||||
yield call(delay, 1000);
|
yield call(delay, 1000);
|
||||||
}
|
}
|
||||||
|
@ -103,15 +107,6 @@ export function* handlePollOfflineStatus(): SagaIterator {
|
||||||
yield cancel(pollOfflineStatusTask);
|
yield cancel(pollOfflineStatusTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* handleTogglePollOfflineStatus(): SagaIterator {
|
|
||||||
const isForcedOffline: boolean = yield select(getForceOffline);
|
|
||||||
if (isForcedOffline) {
|
|
||||||
yield fork(handlePollOfflineStatus);
|
|
||||||
} else {
|
|
||||||
yield call(handlePollOfflineStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @HACK For now we reload the app when doing a language swap to force non-connected
|
// @HACK For now we reload the app when doing a language swap to force non-connected
|
||||||
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||||
export function* reload(): SagaIterator {
|
export function* reload(): SagaIterator {
|
||||||
|
@ -251,7 +246,6 @@ export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => {
|
||||||
|
|
||||||
export default function* configSaga(): SagaIterator {
|
export default function* configSaga(): SagaIterator {
|
||||||
yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus);
|
yield takeLatest(TypeKeys.CONFIG_POLL_OFFLINE_STATUS, handlePollOfflineStatus);
|
||||||
yield takeEvery(TypeKeys.CONFIG_FORCE_OFFLINE, handleTogglePollOfflineStatus);
|
|
||||||
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
yield takeEvery(TypeKeys.CONFIG_NODE_CHANGE_INTENT, handleNodeChangeIntent);
|
||||||
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload);
|
||||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SagaIterator, buffers, delay } from 'redux-saga';
|
import { SagaIterator, buffers, delay } from 'redux-saga';
|
||||||
import { apply, put, select, take, actionChannel, call, fork } from 'redux-saga/effects';
|
import { apply, put, select, take, actionChannel, call, fork } from 'redux-saga/effects';
|
||||||
import { INode } from 'libs/nodes/INode';
|
import { INode } from 'libs/nodes/INode';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import { getNodeLib, getOffline } from 'selectors/config';
|
||||||
import { getWalletInst } from 'selectors/wallet';
|
import { getWalletInst } from 'selectors/wallet';
|
||||||
import { getTransaction, IGetTransaction } from 'selectors/transaction';
|
import { getTransaction, IGetTransaction } from 'selectors/transaction';
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +22,11 @@ import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/
|
||||||
|
|
||||||
export function* shouldEstimateGas(): SagaIterator {
|
export function* shouldEstimateGas(): SagaIterator {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const action:
|
const action:
|
||||||
| SetToFieldAction
|
| SetToFieldAction
|
||||||
| SetDataFieldAction
|
| SetDataFieldAction
|
||||||
|
@ -59,6 +64,11 @@ export function* estimateGas(): SagaIterator {
|
||||||
const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1));
|
const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1));
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const { payload }: EstimateGasRequestedAction = yield take(requestChan);
|
const { payload }: EstimateGasRequestedAction = yield take(requestChan);
|
||||||
// debounce 250 ms
|
// debounce 250 ms
|
||||||
yield call(delay, 250);
|
yield call(delay, 250);
|
||||||
|
|
|
@ -12,9 +12,13 @@ import { Nonce } from 'libs/units';
|
||||||
export function* handleNonceRequest(): SagaIterator {
|
export function* handleNonceRequest(): SagaIterator {
|
||||||
const nodeLib: INode = yield select(getNodeLib);
|
const nodeLib: INode = yield select(getNodeLib);
|
||||||
const walletInst: AppState['wallet']['inst'] = yield select(getWalletInst);
|
const walletInst: AppState['wallet']['inst'] = yield select(getWalletInst);
|
||||||
const offline: boolean = yield select(getOffline);
|
const isOffline: boolean = yield select(getOffline);
|
||||||
try {
|
try {
|
||||||
if (!walletInst || offline) {
|
if (isOffline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!walletInst) {
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
const fromAddress: string = yield apply(walletInst, walletInst.getAddressString);
|
const fromAddress: string = yield apply(walletInst, walletInst.getAddressString);
|
||||||
|
|
|
@ -39,7 +39,7 @@ import {
|
||||||
import { NODES, initWeb3Node, Token } from 'config/data';
|
import { NODES, initWeb3Node, Token } from 'config/data';
|
||||||
import { SagaIterator, delay, Task } from 'redux-saga';
|
import { SagaIterator, delay, Task } from 'redux-saga';
|
||||||
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
|
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
|
||||||
import { getNodeLib, getAllTokens } from 'selectors/config';
|
import { getNodeLib, getAllTokens, getOffline } from 'selectors/config';
|
||||||
import {
|
import {
|
||||||
getTokens,
|
getTokens,
|
||||||
getWalletInst,
|
getWalletInst,
|
||||||
|
@ -58,6 +58,11 @@ export interface TokenBalanceLookup {
|
||||||
|
|
||||||
export function* updateAccountBalance(): SagaIterator {
|
export function* updateAccountBalance(): SagaIterator {
|
||||||
try {
|
try {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
yield put(setBalancePending());
|
yield put(setBalancePending());
|
||||||
const wallet: null | IWallet = yield select(getWalletInst);
|
const wallet: null | IWallet = yield select(getWalletInst);
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
|
@ -75,6 +80,11 @@ export function* updateAccountBalance(): SagaIterator {
|
||||||
|
|
||||||
export function* updateTokenBalances(): SagaIterator {
|
export function* updateTokenBalances(): SagaIterator {
|
||||||
try {
|
try {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wallet: null | IWallet = yield select(getWalletInst);
|
const wallet: null | IWallet = yield select(getWalletInst);
|
||||||
const tokens: MergedToken[] = yield select(getWalletConfigTokens);
|
const tokens: MergedToken[] = yield select(getWalletConfigTokens);
|
||||||
if (!wallet || !tokens.length) {
|
if (!wallet || !tokens.length) {
|
||||||
|
@ -91,6 +101,11 @@ export function* updateTokenBalances(): SagaIterator {
|
||||||
|
|
||||||
export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
|
export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
|
||||||
try {
|
try {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wallet: null | IWallet = yield select(getWalletInst);
|
const wallet: null | IWallet = yield select(getWalletInst);
|
||||||
const { tokenSymbol } = action.payload;
|
const { tokenSymbol } = action.payload;
|
||||||
const allTokens: Token[] = yield select(getAllTokens);
|
const allTokens: Token[] = yield select(getAllTokens);
|
||||||
|
@ -115,6 +130,11 @@ export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaI
|
||||||
|
|
||||||
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
|
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
|
||||||
try {
|
try {
|
||||||
|
const isOffline = yield select(getOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wallet = action.payload;
|
const wallet = action.payload;
|
||||||
const tokens: MergedToken[] = yield select(getTokens);
|
const tokens: MergedToken[] = yield select(getTokens);
|
||||||
yield put(setTokenBalancesPending());
|
yield put(setTokenBalancesPending());
|
||||||
|
@ -288,7 +308,9 @@ export default function* walletSaga(): SagaIterator {
|
||||||
takeEvery(TypeKeys.WALLET_SET, handleNewWallet),
|
takeEvery(TypeKeys.WALLET_SET, handleNewWallet),
|
||||||
takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens),
|
takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens),
|
||||||
takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens),
|
takeEvery(TypeKeys.WALLET_SET_WALLET_TOKENS, handleSetWalletTokens),
|
||||||
takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd),
|
takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance),
|
||||||
takeEvery(TypeKeys.WALLET_SET_TOKEN_BALANCE_PENDING, updateTokenBalance)
|
// Foreign actions
|
||||||
|
takeEvery(ConfigTypeKeys.CONFIG_TOGGLE_OFFLINE, updateBalances),
|
||||||
|
takeEvery(CustomTokenTypeKeys.CUSTOM_TOKEN_ADD, handleCustomTokenAdd)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,12 +86,6 @@ export function getOffline(state: AppState): boolean {
|
||||||
return state.config.offline;
|
return state.config.offline;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getForceOffline(state: AppState): boolean {
|
|
||||||
return state.config.forceOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isAnyOffline = (state: AppState) => getOffline(state) || getForceOffline(state);
|
|
||||||
|
|
||||||
export function isSupportedUnit(state: AppState, unit: string) {
|
export function isSupportedUnit(state: AppState, unit: string) {
|
||||||
const isToken: boolean = tokenExists(state, unit);
|
const isToken: boolean = tokenExists(state, unit);
|
||||||
const isEther: boolean = isEtherUnit(unit);
|
const isEther: boolean = isEtherUnit(unit);
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { getWalletType } from 'selectors/wallet';
|
import { getWalletType } from 'selectors/wallet';
|
||||||
import { getOffline, getForceOffline } from 'selectors/config';
|
import { getOffline } from 'selectors/config';
|
||||||
|
|
||||||
export const isAnyOfflineWithWeb3 = (state: AppState): boolean => {
|
export const isAnyOfflineWithWeb3 = (state: AppState): boolean => {
|
||||||
const { isWeb3Wallet } = getWalletType(state);
|
const { isWeb3Wallet } = getWalletType(state);
|
||||||
const offline = getOffline(state);
|
const offline = getOffline(state);
|
||||||
const forceOffline = getForceOffline(state);
|
return offline && isWeb3Wallet;
|
||||||
const anyOffline = offline || forceOffline;
|
|
||||||
const anyOfflineAndWeb3 = anyOffline && isWeb3Wallet;
|
|
||||||
return anyOfflineAndWeb3;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -143,10 +143,8 @@
|
||||||
"tscheck": "tsc --noEmit",
|
"tscheck": "tsc --noEmit",
|
||||||
"start": "npm run dev",
|
"start": "npm run dev",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
"formatAll":
|
"formatAll": "find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
|
||||||
"find ./common/ -name '*.ts*' | xargs prettier --write --config ./.prettierrc --config-precedence file-override",
|
"prettier:diff": "prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
|
||||||
"prettier:diff":
|
|
||||||
"prettier --write --config ./.prettierrc --list-different \"common/**/*.ts\" \"common/**/*.tsx\"",
|
|
||||||
"prepush": "npm run tslint && npm run tscheck"
|
"prepush": "npm run tslint && npm run tscheck"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|
|
@ -17,8 +17,7 @@ it('render snapshot', () => {
|
||||||
nodeSelection: testNode,
|
nodeSelection: testNode,
|
||||||
node: NODES[testNode],
|
node: NODES[testNode],
|
||||||
gasPriceGwei: 21,
|
gasPriceGwei: 21,
|
||||||
offline: false,
|
offline: false
|
||||||
forceOffline: false
|
|
||||||
};
|
};
|
||||||
const testState = {
|
const testState = {
|
||||||
wallet: {},
|
wallet: {},
|
||||||
|
@ -31,7 +30,6 @@ it('render snapshot', () => {
|
||||||
gasPrice: {},
|
gasPrice: {},
|
||||||
transactions: {},
|
transactions: {},
|
||||||
offline: {},
|
offline: {},
|
||||||
forceOffline: {},
|
|
||||||
config: testStateConfig,
|
config: testStateConfig,
|
||||||
customTokens: []
|
customTokens: []
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,12 +4,13 @@ import Adapter from 'enzyme-adapter-react-16';
|
||||||
import Swap from 'containers/Tabs/Swap';
|
import Swap from 'containers/Tabs/Swap';
|
||||||
import shallowWithStore from '../utils/shallowWithStore';
|
import shallowWithStore from '../utils/shallowWithStore';
|
||||||
import { createMockStore } from 'redux-test-utils';
|
import { createMockStore } from 'redux-test-utils';
|
||||||
import { INITIAL_STATE } from 'reducers/swap';
|
import { INITIAL_STATE as swap } from 'reducers/swap';
|
||||||
|
import { INITIAL_STATE as config } from 'reducers/config';
|
||||||
|
|
||||||
Enzyme.configure({ adapter: new Adapter() });
|
Enzyme.configure({ adapter: new Adapter() });
|
||||||
|
|
||||||
it('render snapshot', () => {
|
it('render snapshot', () => {
|
||||||
const store = createMockStore({ swap: INITIAL_STATE });
|
const store = createMockStore({ swap, config });
|
||||||
const component = shallowWithStore(<Swap />, store);
|
const component = shallowWithStore(<Swap />, store);
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
|
|
|
@ -22,6 +22,7 @@ exports[`render snapshot 1`] = `
|
||||||
destinationAddressSwap={[Function]}
|
destinationAddressSwap={[Function]}
|
||||||
initSwap={[Function]}
|
initSwap={[Function]}
|
||||||
isFetchingRates={null}
|
isFetchingRates={null}
|
||||||
|
isOffline={false}
|
||||||
isPostingOrder={false}
|
isPostingOrder={false}
|
||||||
loadBityRatesRequestedSwap={[Function]}
|
loadBityRatesRequestedSwap={[Function]}
|
||||||
loadShapeshiftRatesRequestedSwap={[Function]}
|
loadShapeshiftRatesRequestedSwap={[Function]}
|
||||||
|
|
|
@ -53,28 +53,6 @@ describe('config reducer', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle CONFIG_FORCE_OFFLINE', () => {
|
|
||||||
const forceOfflineTrue = {
|
|
||||||
...INITIAL_STATE,
|
|
||||||
forceOffline: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceOfflineFalse = {
|
|
||||||
...INITIAL_STATE,
|
|
||||||
forceOffline: false
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(config(forceOfflineTrue, configActions.forceOfflineConfig())).toEqual({
|
|
||||||
...forceOfflineTrue,
|
|
||||||
forceOffline: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(config(forceOfflineFalse, configActions.forceOfflineConfig())).toEqual({
|
|
||||||
...forceOfflineFalse,
|
|
||||||
forceOffline: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle CONFIG_ADD_CUSTOM_NODE', () => {
|
it('should handle CONFIG_ADD_CUSTOM_NODE', () => {
|
||||||
expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({
|
expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({
|
||||||
...INITIAL_STATE,
|
...INITIAL_STATE,
|
||||||
|
|
|
@ -52,26 +52,6 @@ Object {
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`pollOfflineStatus* should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline 1`] = `
|
|
||||||
Object {
|
|
||||||
"@@redux-saga/IO": true,
|
|
||||||
"PUT": Object {
|
|
||||||
"action": Object {
|
|
||||||
"payload": Object {
|
|
||||||
"duration": Infinity,
|
|
||||||
"id": 0.001,
|
|
||||||
"level": "danger",
|
|
||||||
"msg": "You’ve lost your connection to the network, check your internet
|
|
||||||
connection or try changing networks from the dropdown at the
|
|
||||||
top right of the page.",
|
|
||||||
},
|
|
||||||
"type": "SHOW_NOTIFICATION",
|
|
||||||
},
|
|
||||||
"channel": null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`pollOfflineStatus* should race pingSucceeded and timeout 1`] = `
|
exports[`pollOfflineStatus* should race pingSucceeded and timeout 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"@@redux-saga/IO": true,
|
"@@redux-saga/IO": true,
|
||||||
|
@ -97,3 +77,21 @@ Object {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`pollOfflineStatus* should toggle offline and show notification if navigator agrees with isOffline and ping fails 1`] = `
|
||||||
|
Object {
|
||||||
|
"@@redux-saga/IO": true,
|
||||||
|
"PUT": Object {
|
||||||
|
"action": Object {
|
||||||
|
"payload": Object {
|
||||||
|
"duration": 5000,
|
||||||
|
"id": 0.001,
|
||||||
|
"level": "info",
|
||||||
|
"msg": "You are currently offline. Some features will be unavailable.",
|
||||||
|
},
|
||||||
|
"type": "SHOW_NOTIFICATION",
|
||||||
|
},
|
||||||
|
"channel": null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
pollOfflineStatus,
|
pollOfflineStatus,
|
||||||
handlePollOfflineStatus,
|
handlePollOfflineStatus,
|
||||||
handleNodeChangeIntent,
|
handleNodeChangeIntent,
|
||||||
handleTogglePollOfflineStatus,
|
|
||||||
reload,
|
reload,
|
||||||
unsetWeb3Node,
|
unsetWeb3Node,
|
||||||
unsetWeb3NodeOnWalletEvent,
|
unsetWeb3NodeOnWalletEvent,
|
||||||
|
@ -18,7 +17,6 @@ import {
|
||||||
getNode,
|
getNode,
|
||||||
getNodeConfig,
|
getNodeConfig,
|
||||||
getOffline,
|
getOffline,
|
||||||
getForceOffline,
|
|
||||||
getCustomNodeConfigs,
|
getCustomNodeConfigs,
|
||||||
getCustomNetworkConfigs
|
getCustomNetworkConfigs
|
||||||
} from 'selectors/config';
|
} from 'selectors/config';
|
||||||
|
@ -43,12 +41,13 @@ describe('pollOfflineStatus*', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const isOffline = true;
|
const isOffline = true;
|
||||||
const isForcedOffline = true;
|
|
||||||
const raceSuccess = {
|
const raceSuccess = {
|
||||||
pingSucceeded: true
|
pingSucceeded: true,
|
||||||
|
timeout: false
|
||||||
};
|
};
|
||||||
const raceFailure = {
|
const raceFailure = {
|
||||||
pingSucceeded: false
|
pingSucceeded: false,
|
||||||
|
timeout: true
|
||||||
};
|
};
|
||||||
|
|
||||||
let originalHidden;
|
let originalHidden;
|
||||||
|
@ -88,49 +87,32 @@ describe('pollOfflineStatus*', () => {
|
||||||
expect(data.gen.next(node).value).toEqual(select(getOffline));
|
expect(data.gen.next(node).value).toEqual(select(getOffline));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select getForceOffline', () => {
|
|
||||||
data.isOfflineClone = data.gen.clone();
|
|
||||||
expect(data.gen.next(isOffline).value).toEqual(select(getForceOffline));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be done if isForcedOffline', () => {
|
|
||||||
data.clone1 = data.gen.clone();
|
|
||||||
expect(data.clone1.next(isForcedOffline).done).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call delay if document is hidden', () => {
|
it('should call delay if document is hidden', () => {
|
||||||
data.clone2 = data.gen.clone();
|
data.hiddenDoc = data.gen.clone();
|
||||||
doc.hidden = true;
|
doc.hidden = true;
|
||||||
|
expect(data.hiddenDoc.next(!isOffline).value).toEqual(call(delay, 1000));
|
||||||
expect(data.clone2.next(!isForcedOffline).value).toEqual(call(delay, 1000));
|
doc.hidden = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should race pingSucceeded and timeout', () => {
|
it('should race pingSucceeded and timeout', () => {
|
||||||
doc.hidden = false;
|
data.isOfflineClone = data.gen.clone();
|
||||||
expect(data.gen.next(!isForcedOffline).value).toMatchSnapshot();
|
data.shouldDelayClone = data.gen.clone();
|
||||||
|
expect(data.gen.next(isOffline).value).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put showNotification and put toggleOfflineConfig if pingSucceeded && isOffline', () => {
|
it('should toggle offline and show notification if navigator disagrees with isOffline and ping succeeds', () => {
|
||||||
expect(data.gen.next(raceSuccess).value).toEqual(
|
expect(data.gen.next(raceSuccess).value).toEqual(
|
||||||
put(showNotification('success', 'Your connection to the network has been restored!', 3000))
|
put(showNotification('success', 'Your connection to the network has been restored!', 3000))
|
||||||
);
|
);
|
||||||
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
|
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline', () => {
|
it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => {
|
||||||
nav.onLine = !isOffline;
|
nav.onLine = isOffline;
|
||||||
|
expect(data.isOfflineClone.next(!isOffline));
|
||||||
data.isOfflineClone.next(!isOffline);
|
|
||||||
data.isOfflineClone.next(!isForcedOffline);
|
|
||||||
|
|
||||||
data.clone3 = data.isOfflineClone.clone();
|
|
||||||
|
|
||||||
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
|
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
|
||||||
expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig()));
|
expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig()));
|
||||||
});
|
nav.onLine = !isOffline;
|
||||||
|
|
||||||
it('should call delay when neither case is true', () => {
|
|
||||||
expect(data.clone3.next(raceSuccess).value).toEqual(call(delay, 5000));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -152,30 +134,6 @@ describe('handlePollOfflineStatus*', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleTogglePollOfflineStatus*', () => {
|
|
||||||
const data = {} as any;
|
|
||||||
data.gen = cloneableGenerator(handleTogglePollOfflineStatus)();
|
|
||||||
const isForcedOffline = true;
|
|
||||||
|
|
||||||
it('should select getForceOffline', () => {
|
|
||||||
expect(data.gen.next().value).toEqual(select(getForceOffline));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fork handlePollOfflineStatus when isForcedOffline', () => {
|
|
||||||
data.clone = data.gen.clone();
|
|
||||||
expect(data.gen.next(isForcedOffline).value).toEqual(fork(handlePollOfflineStatus));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call handlePollOfflineStatus when !isForcedOffline', () => {
|
|
||||||
expect(data.clone.next(!isForcedOffline).value).toEqual(call(handlePollOfflineStatus));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be done', () => {
|
|
||||||
expect(data.gen.next().done).toEqual(true);
|
|
||||||
expect(data.clone.next().done).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleNodeChangeIntent*', () => {
|
describe('handleNodeChangeIntent*', () => {
|
||||||
let originalRandom;
|
let originalRandom;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { buffers, delay } from 'redux-saga';
|
import { buffers, delay } from 'redux-saga';
|
||||||
import { apply, put, select, take, actionChannel, call } from 'redux-saga/effects';
|
import { apply, put, select, take, actionChannel, call } from 'redux-saga/effects';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import { getNodeLib, getOffline } from 'selectors/config';
|
||||||
import { getWalletInst } from 'selectors/wallet';
|
import { getWalletInst } from 'selectors/wallet';
|
||||||
import { getTransaction } from 'selectors/transaction';
|
import { getTransaction } from 'selectors/transaction';
|
||||||
import {
|
import {
|
||||||
|
@ -16,6 +16,7 @@ import { cloneableGenerator } from 'redux-saga/utils';
|
||||||
import { Wei } from 'libs/units';
|
import { Wei } from 'libs/units';
|
||||||
|
|
||||||
describe('shouldEstimateGas*', () => {
|
describe('shouldEstimateGas*', () => {
|
||||||
|
const offline = false;
|
||||||
const transaction: any = 'transaction';
|
const transaction: any = 'transaction';
|
||||||
const tx = { transaction };
|
const tx = { transaction };
|
||||||
const rest: any = {
|
const rest: any = {
|
||||||
|
@ -39,8 +40,12 @@ describe('shouldEstimateGas*', () => {
|
||||||
|
|
||||||
const gen = shouldEstimateGas();
|
const gen = shouldEstimateGas();
|
||||||
|
|
||||||
|
it('should select getOffline', () => {
|
||||||
|
expect(gen.next().value).toEqual(select(getOffline));
|
||||||
|
});
|
||||||
|
|
||||||
it('should take expected types', () => {
|
it('should take expected types', () => {
|
||||||
expect(gen.next().value).toEqual(
|
expect(gen.next(offline).value).toEqual(
|
||||||
take([
|
take([
|
||||||
TypeKeys.TO_FIELD_SET,
|
TypeKeys.TO_FIELD_SET,
|
||||||
TypeKeys.DATA_FIELD_SET,
|
TypeKeys.DATA_FIELD_SET,
|
||||||
|
@ -65,6 +70,7 @@ describe('shouldEstimateGas*', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('estimateGas*', () => {
|
describe('estimateGas*', () => {
|
||||||
|
const offline = false;
|
||||||
const requestChan = 'requestChan';
|
const requestChan = 'requestChan';
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
mock1: 'mock1',
|
mock1: 'mock1',
|
||||||
|
@ -102,8 +108,12 @@ describe('estimateGas*', () => {
|
||||||
expect(expected).toEqual(result);
|
expect(expected).toEqual(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should select getOffline', () => {
|
||||||
|
expect(gens.gen.next(requestChan).value).toEqual(select(getOffline));
|
||||||
|
});
|
||||||
|
|
||||||
it('should take requestChan', () => {
|
it('should take requestChan', () => {
|
||||||
expect(gens.gen.next(requestChan).value).toEqual(take(requestChan));
|
expect(gens.gen.next(offline).value).toEqual(take(requestChan));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call delay', () => {
|
it('should call delay', () => {
|
||||||
|
|
|
@ -40,18 +40,23 @@ describe('handleNonceRequest*', () => {
|
||||||
expect(gens.gen.next(nodeLib).value).toEqual(select(getWalletInst));
|
expect(gens.gen.next(nodeLib).value).toEqual(select(getWalletInst));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle being called without wallet inst correctly', () => {
|
||||||
|
gens.noWallet = gens.gen.clone();
|
||||||
|
gens.noWallet.next();
|
||||||
|
expect(gens.noWallet.next(offline).value).toEqual(
|
||||||
|
put(showNotification('warning', 'Your addresses nonce could not be fetched'))
|
||||||
|
);
|
||||||
|
expect(gens.noWallet.next().value).toEqual(put(getNonceFailed()));
|
||||||
|
expect(gens.noWallet.next().done).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should select getOffline', () => {
|
it('should select getOffline', () => {
|
||||||
gens.clone = gens.gen.clone();
|
|
||||||
expect(gens.gen.next(walletInst).value).toEqual(select(getOffline));
|
expect(gens.gen.next(walletInst).value).toEqual(select(getOffline));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle errors correctly', () => {
|
it('should exit if being called while offline', () => {
|
||||||
gens.clone.next();
|
gens.offline = gens.gen.clone();
|
||||||
expect(gens.clone.next().value).toEqual(
|
expect(gens.offline.next(true).done).toEqual(true);
|
||||||
put(showNotification('warning', 'Your addresses nonce could not be fetched'))
|
|
||||||
);
|
|
||||||
expect(gens.clone.next().value).toEqual(put(getNonceFailed()));
|
|
||||||
expect(gens.clone.next().done).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply walletInst.getAddressString', () => {
|
it('should apply walletInst.getAddressString', () => {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { changeNodeIntent, web3UnsetNode } from 'actions/config';
|
||||||
import { INode } from 'libs/nodes/INode';
|
import { INode } from 'libs/nodes/INode';
|
||||||
import { initWeb3Node, Token, N_FACTOR } from 'config/data';
|
import { initWeb3Node, Token, N_FACTOR } from 'config/data';
|
||||||
import { apply, call, fork, put, select, take } from 'redux-saga/effects';
|
import { apply, call, fork, put, select, take } from 'redux-saga/effects';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import { getNodeLib, getOffline } from 'selectors/config';
|
||||||
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';
|
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';
|
||||||
import {
|
import {
|
||||||
updateAccountBalance,
|
updateAccountBalance,
|
||||||
|
@ -39,7 +39,7 @@ import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
||||||
|
|
||||||
// init module
|
// init module
|
||||||
configuredStore.getState();
|
configuredStore.getState();
|
||||||
|
const offline = false;
|
||||||
const pkey = '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344';
|
const pkey = '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344';
|
||||||
const wallet = PrivKeyWallet(Buffer.from(pkey, 'hex'));
|
const wallet = PrivKeyWallet(Buffer.from(pkey, 'hex'));
|
||||||
const address = '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854';
|
const address = '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854';
|
||||||
|
@ -83,90 +83,108 @@ const utcKeystore = {
|
||||||
// necessary so we can later inject a mocked web3 to the window
|
// necessary so we can later inject a mocked web3 to the window
|
||||||
|
|
||||||
describe('updateAccountBalance*', () => {
|
describe('updateAccountBalance*', () => {
|
||||||
const gen1 = updateAccountBalance();
|
const gen = updateAccountBalance();
|
||||||
const gen2 = updateAccountBalance();
|
|
||||||
|
it('should select offline', () => {
|
||||||
|
expect(gen.next().value).toEqual(select(getOffline));
|
||||||
|
});
|
||||||
|
|
||||||
it('should put setBalancePending', () => {
|
it('should put setBalancePending', () => {
|
||||||
expect(gen1.next().value).toEqual(put(setBalancePending()));
|
expect(gen.next(false).value).toEqual(put(setBalancePending()));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select getWalletInst', () => {
|
it('should select getWalletInst', () => {
|
||||||
expect(gen1.next().value).toEqual(select(getWalletInst));
|
expect(gen.next(false).value).toEqual(select(getWalletInst));
|
||||||
});
|
|
||||||
|
|
||||||
it('should return if wallet is falsey', () => {
|
|
||||||
gen2.next();
|
|
||||||
gen2.next();
|
|
||||||
gen2.next(null);
|
|
||||||
expect(gen2.next().done).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select getNodeLib', () => {
|
it('should select getNodeLib', () => {
|
||||||
expect(gen1.next(wallet).value).toEqual(select(getNodeLib));
|
expect(gen.next(wallet).value).toEqual(select(getNodeLib));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply wallet.getAddressString', () => {
|
it('should apply wallet.getAddressString', () => {
|
||||||
expect(gen1.next(node).value).toEqual(apply(wallet, wallet.getAddressString));
|
expect(gen.next(node).value).toEqual(apply(wallet, wallet.getAddressString));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply node.getBalance', () => {
|
it('should apply node.getBalance', () => {
|
||||||
expect(gen1.next(address).value).toEqual(apply(node, node.getBalance, [address]));
|
expect(gen.next(address).value).toEqual(apply(node, node.getBalance, [address]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put setBalanceFulfilled', () => {
|
it('should put setBalanceFulfilled', () => {
|
||||||
expect(gen1.next(balance).value).toEqual(put(setBalanceFullfilled(balance)));
|
expect(gen.next(balance).value).toEqual(put(setBalanceFullfilled(balance)));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be done', () => {
|
it('should be done', () => {
|
||||||
expect(gen1.next().done).toEqual(true);
|
expect(gen.next().done).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bail out if offline', () => {
|
||||||
|
const offlineGen = updateAccountBalance();
|
||||||
|
offlineGen.next();
|
||||||
|
expect(offlineGen.next(true).done).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bail out if wallet inst is missing', () => {
|
||||||
|
const noWalletGen = updateAccountBalance();
|
||||||
|
noWalletGen.next();
|
||||||
|
noWalletGen.next(false);
|
||||||
|
noWalletGen.next(false);
|
||||||
|
expect(noWalletGen.next(null).done).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateTokenBalances*', () => {
|
describe('updateTokenBalances*', () => {
|
||||||
const gen1 = cloneableGenerator(updateTokenBalances)();
|
const gen = cloneableGenerator(updateTokenBalances)();
|
||||||
const gen2 = updateTokenBalances();
|
|
||||||
const gen3 = updateTokenBalances();
|
|
||||||
|
|
||||||
it('should select getWalletInst', () => {
|
it('should bail out if offline', () => {
|
||||||
expect(gen1.next().value).toEqual(select(getWalletInst));
|
const offlineGen = gen.clone();
|
||||||
|
expect(offlineGen.next());
|
||||||
|
expect(offlineGen.next(true).done).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select getWalletConfigTokens', () => {
|
it('should select getOffline', () => {
|
||||||
expect(gen1.next(wallet).value).toEqual(select(getWalletConfigTokens));
|
expect(gen.next().value).toEqual(select(getOffline));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select getWalletInst', () => {
|
||||||
|
expect(gen.next(offline).value).toEqual(select(getWalletInst));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return if wallet is falsey', () => {
|
it('should return if wallet is falsey', () => {
|
||||||
gen2.next();
|
const noWalletGen = gen.clone();
|
||||||
gen2.next(null);
|
noWalletGen.next(null);
|
||||||
expect(gen2.next().done).toEqual(true);
|
expect(noWalletGen.next().done).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return if tokens are falsey', () => {
|
it('should select getWalletConfigTokens', () => {
|
||||||
gen3.next();
|
expect(gen.next(wallet).value).toEqual(select(getWalletConfigTokens));
|
||||||
gen3.next(wallet);
|
});
|
||||||
expect(gen3.next({}).done).toEqual(true);
|
|
||||||
|
it('should return if no tokens are requested', () => {
|
||||||
|
const noTokensGen = gen.clone();
|
||||||
|
noTokensGen.next({});
|
||||||
|
expect(noTokensGen.next().done).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put setTokenBalancesPending', () => {
|
it('should put setTokenBalancesPending', () => {
|
||||||
expect(gen1.next(tokens).value).toEqual(put(setTokenBalancesPending()));
|
expect(gen.next(tokens).value).toEqual(put(setTokenBalancesPending()));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw and put setTokenBalancesRejected', () => {
|
it('should put setTokenBalancesRejected on throw', () => {
|
||||||
const gen4 = gen1.clone();
|
const throwGen = gen.clone();
|
||||||
if (gen4.throw) {
|
if (throwGen.throw) {
|
||||||
expect(gen4.throw().value).toEqual(put(setTokenBalancesRejected()));
|
expect(throwGen.throw().value).toEqual(put(setTokenBalancesRejected()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getTokenBalances', () => {
|
it('should call getTokenBalances', () => {
|
||||||
expect(gen1.next().value).toEqual(call(getTokenBalances, wallet, tokens));
|
expect(gen.next().value).toEqual(call(getTokenBalances, wallet, tokens));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put setTokenBalancesFufilled', () => {
|
it('should put setTokenBalancesFufilled', () => {
|
||||||
expect(gen1.next({}).value).toEqual(put(setTokenBalancesFulfilled({})));
|
expect(gen.next({}).value).toEqual(put(setTokenBalancesFulfilled({})));
|
||||||
});
|
});
|
||||||
it('should be done', () => {
|
it('should be done', () => {
|
||||||
expect(gen1.next().done).toEqual(true);
|
expect(gen.next().done).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue