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 { Aux } from 'components/ui';
|
||||
import { Store } from 'redux';
|
||||
import { pollOfflineStatus } from 'actions/config';
|
||||
import { AppState } from 'reducers';
|
||||
|
||||
interface Props {
|
||||
|
@ -30,6 +31,10 @@ export default class Root extends Component<Props, State> {
|
|||
error: null
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.store.dispatch(pollOfflineStatus());
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error) {
|
||||
this.setState({ error });
|
||||
}
|
||||
|
|
|
@ -2,13 +2,6 @@ import * as interfaces from './actionTypes';
|
|||
import { TypeKeys } from './constants';
|
||||
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 function toggleOfflineConfig(): interfaces.ToggleOfflineAction {
|
||||
return {
|
||||
|
|
|
@ -6,11 +6,6 @@ export interface ToggleOfflineAction {
|
|||
type: TypeKeys.CONFIG_TOGGLE_OFFLINE;
|
||||
}
|
||||
|
||||
/*** Force Offline ***/
|
||||
export interface ForceOfflineAction {
|
||||
type: TypeKeys.CONFIG_FORCE_OFFLINE;
|
||||
}
|
||||
|
||||
/*** Change Language ***/
|
||||
export interface ChangeLanguageAction {
|
||||
type: TypeKeys.CONFIG_LANGUAGE_CHANGE;
|
||||
|
@ -80,7 +75,6 @@ export type ConfigAction =
|
|||
| ChangeLanguageAction
|
||||
| ToggleOfflineAction
|
||||
| PollOfflineStatus
|
||||
| ForceOfflineAction
|
||||
| ChangeNodeIntentAction
|
||||
| AddCustomNodeAction
|
||||
| RemoveCustomNodeAction
|
||||
|
|
|
@ -3,7 +3,6 @@ export enum TypeKeys {
|
|||
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
|
||||
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
|
||||
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
|
||||
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
|
||||
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',
|
||||
CONFIG_ADD_CUSTOM_NODE = 'CONFIG_ADD_CUSTOM_NODE',
|
||||
CONFIG_REMOVE_CUSTOM_NODE = 'CONFIG_REMOVE_CUSTOM_NODE',
|
||||
|
|
|
@ -40,4 +40,9 @@
|
|||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-offline {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
|||
ratesError?: State['ratesError'];
|
||||
fetchCCRates: TFetchCCRates;
|
||||
network: NetworkConfig;
|
||||
isOffline: boolean;
|
||||
}
|
||||
|
||||
interface CmpState {
|
||||
|
@ -44,15 +45,19 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
|||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
const { balance, tokenBalances } = this.props;
|
||||
if (nextProps.balance !== balance || nextProps.tokenBalances !== tokenBalances) {
|
||||
const { balance, tokenBalances, isOffline } = this.props;
|
||||
if (
|
||||
nextProps.balance !== balance ||
|
||||
nextProps.tokenBalances !== tokenBalances ||
|
||||
nextProps.isOffline !== isOffline
|
||||
) {
|
||||
this.makeBalanceLookup(nextProps);
|
||||
this.fetchRates(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { balance, tokenBalances, rates, ratesError, network } = this.props;
|
||||
const { balance, tokenBalances, rates, ratesError, isOffline, network } = this.props;
|
||||
const { currency } = this.state;
|
||||
|
||||
// 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>
|
||||
</h5>
|
||||
|
||||
{isOffline ? (
|
||||
<div className="EquivalentValues-offline well well-sm">
|
||||
Equivalent values are unavailable offline
|
||||
</div>
|
||||
) : (
|
||||
<ul className="EquivalentValues-values">{valuesEl}</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -154,8 +165,8 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
|||
}
|
||||
|
||||
private fetchRates(props: Props) {
|
||||
// Duck out if we haven't gotten balances yet
|
||||
if (!props.balance || !props.tokenBalances) {
|
||||
// Duck out if we haven't gotten balances yet, or we're not going to
|
||||
if (!props.balance || !props.tokenBalances || props.isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,4 +41,9 @@
|
|||
color: $gray;
|
||||
}
|
||||
}
|
||||
|
||||
&-offline {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ interface StateProps {
|
|||
tokensError: AppState['wallet']['tokensError'];
|
||||
isTokensLoading: AppState['wallet']['isTokensLoading'];
|
||||
hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens'];
|
||||
isOffline: AppState['config']['offline'];
|
||||
}
|
||||
interface ActionProps {
|
||||
addCustomToken: TAddCustomToken;
|
||||
|
@ -46,13 +47,20 @@ class TokenBalances extends React.Component<Props> {
|
|||
tokenBalances,
|
||||
hasSavedWalletTokens,
|
||||
isTokensLoading,
|
||||
tokensError
|
||||
tokensError,
|
||||
isOffline
|
||||
} = this.props;
|
||||
|
||||
const walletTokens = walletConfig ? walletConfig.tokens : [];
|
||||
|
||||
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>;
|
||||
} else if (isTokensLoading) {
|
||||
content = (
|
||||
|
@ -109,7 +117,8 @@ function mapStateToProps(state: AppState): StateProps {
|
|||
tokenBalances: getTokenBalances(state),
|
||||
tokensError: state.wallet.tokensError,
|
||||
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'];
|
||||
ratesError: AppState['rates']['ratesError'];
|
||||
fetchCCRates: TFetchCCRates;
|
||||
isOffline: AppState['config']['offline'];
|
||||
}
|
||||
|
||||
interface Block {
|
||||
|
@ -29,7 +30,7 @@ interface Block {
|
|||
|
||||
export class BalanceSidebar extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const { wallet, balance, network, tokenBalances, rates, ratesError } = this.props;
|
||||
const { wallet, balance, network, tokenBalances, rates, ratesError, isOffline } = this.props;
|
||||
|
||||
if (!wallet) {
|
||||
return null;
|
||||
|
@ -59,6 +60,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
|
|||
rates={rates}
|
||||
ratesError={ratesError}
|
||||
fetchCCRates={this.props.fetchCCRates}
|
||||
isOffline={isOffline}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -83,7 +85,8 @@ function mapStateToProps(state: AppState) {
|
|||
tokenBalances: getShownTokenBalances(state, true),
|
||||
network: getNetworkConfig(state),
|
||||
rates: state.rates.rates,
|
||||
ratesError: state.rates.ratesError
|
||||
ratesError: state.rates.ratesError,
|
||||
isOffline: state.config.offline
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ interface Props {
|
|||
// Data
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
gasLimit: AppState['transaction']['fields']['gasLimit'];
|
||||
nonce: AppState['transaction']['fields']['nonce'];
|
||||
offline: AppState['config']['offline'];
|
||||
network: AppState['config']['network'];
|
||||
// Actions
|
||||
|
@ -41,11 +42,19 @@ class GasSlider extends React.Component<Props, State> {
|
|||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this.props.offline) {
|
||||
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() {
|
||||
const { gasPrice, gasLimit, offline, disableAdvanced } = this.props;
|
||||
const { gasPrice, gasLimit, nonce, offline, disableAdvanced } = this.props;
|
||||
const showAdvanced = (this.state.showAdvanced || offline) && !disableAdvanced;
|
||||
|
||||
return (
|
||||
|
@ -54,8 +63,10 @@ class GasSlider extends React.Component<Props, State> {
|
|||
<AdvancedGas
|
||||
gasPrice={gasPrice.raw}
|
||||
gasLimit={gasLimit.raw}
|
||||
nonce={nonce.raw}
|
||||
changeGasPrice={this.props.inputGasPrice}
|
||||
changeGasLimit={this.props.inputGasLimit}
|
||||
changeNonce={this.props.inputNonce}
|
||||
/>
|
||||
) : (
|
||||
<SimpleGas gasPrice={gasPrice.raw} changeGasPrice={this.props.inputGasPrice} />
|
||||
|
@ -86,6 +97,7 @@ function mapStateToProps(state: AppState) {
|
|||
return {
|
||||
gasPrice: state.transaction.fields.gasPrice,
|
||||
gasLimit: state.transaction.fields.gasLimit,
|
||||
nonce: state.transaction.fields.nonce,
|
||||
offline: state.config.offline,
|
||||
network: getNetworkConfig(state)
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import translate from 'translations';
|
||||
import { DataFieldFactory } from 'components/DataFieldFactory';
|
||||
import FeeSummary from './FeeSummary';
|
||||
|
@ -7,35 +8,53 @@ import './AdvancedGas.scss';
|
|||
interface Props {
|
||||
gasPrice: string;
|
||||
gasLimit: string;
|
||||
nonce: string;
|
||||
changeGasPrice(gwei: string): void;
|
||||
changeGasLimit(wei: string): void;
|
||||
changeNonce(nonce: string): void;
|
||||
}
|
||||
|
||||
export default class AdvancedGas extends React.Component<Props> {
|
||||
public render() {
|
||||
// Can't shadow var names for data & fee summary
|
||||
const vals = this.props;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<input
|
||||
className="form-control"
|
||||
className={classnames('form-control', !vals.gasPrice && 'is-invalid')}
|
||||
type="number"
|
||||
value={this.props.gasPrice}
|
||||
placeholder="e.g. 40"
|
||||
value={vals.gasPrice}
|
||||
onChange={this.handleGasPriceChange}
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
className="form-control"
|
||||
className={classnames('form-control', !vals.gasLimit && 'is-invalid')}
|
||||
type="number"
|
||||
value={this.props.gasLimit}
|
||||
placeholder="e.g. 21000"
|
||||
value={vals.gasLimit}
|
||||
onChange={this.handleGasLimitChange}
|
||||
/>
|
||||
</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>
|
||||
<DataFieldFactory
|
||||
withProps={({ data, onChange }) => (
|
||||
|
@ -69,4 +88,8 @@ export default class AdvancedGas extends React.Component<Props> {
|
|||
private handleGasLimitChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
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'];
|
||||
rates: AppState['rates']['rates'];
|
||||
network: AppState['config']['network'];
|
||||
isOffline: AppState['config']['offline'];
|
||||
// Component props
|
||||
render(data: RenderData): React.ReactElement<string> | string;
|
||||
}
|
||||
|
||||
class FeeSummary extends React.Component<Props> {
|
||||
public render() {
|
||||
const { gasPrice, gasLimit, rates, network } = this.props;
|
||||
const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
|
||||
|
||||
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
|
||||
const fee = (
|
||||
|
@ -42,7 +43,7 @@ class FeeSummary extends React.Component<Props> {
|
|||
const usdBig = network.isTestnet
|
||||
? new BN(0)
|
||||
: feeBig && rates[network.unit] && feeBig.muln(rates[network.unit].USD);
|
||||
const usd = (
|
||||
const usd = isOffline ? null : (
|
||||
<UnitDisplay
|
||||
value={usdBig}
|
||||
unit="ether"
|
||||
|
@ -71,7 +72,8 @@ function mapStateToProps(state: AppState) {
|
|||
gasPrice: state.transaction.fields.gasPrice,
|
||||
gasLimit: state.transaction.fields.gasLimit,
|
||||
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;
|
||||
}
|
||||
|
||||
&-online {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&-dropdown {
|
||||
margin-left: 6px;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import GasPriceDropdown from './components/GasPriceDropdown';
|
||||
import Navigation from './components/Navigation';
|
||||
import CustomNodeModal from './components/CustomNodeModal';
|
||||
import OnlineStatus from './components/OnlineStatus';
|
||||
import { getKeyByValue } from 'utils/helpers';
|
||||
import { makeCustomNodeId } from 'utils/node';
|
||||
import { getNetworkConfigFromId } from 'utils/network';
|
||||
|
@ -35,6 +36,7 @@ interface Props {
|
|||
node: NodeConfig;
|
||||
nodeSelection: string;
|
||||
isChangingNode: boolean;
|
||||
isOffline: boolean;
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
|
@ -62,6 +64,7 @@ export default class Header extends Component<Props, State> {
|
|||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
isOffline,
|
||||
customNodes,
|
||||
customNetworks
|
||||
} = this.props;
|
||||
|
@ -127,6 +130,10 @@ export default class Header extends Component<Props, State> {
|
|||
<div className="Header-branding-right">
|
||||
<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">
|
||||
<GasPriceDropdown
|
||||
value={this.props.gasPrice.raw}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Aux } from 'components/ui';
|
|||
import { Query } from 'components/renderCbs';
|
||||
import Help from 'components/ui/Help';
|
||||
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
|
||||
import { isAnyOffline } from 'selectors/config';
|
||||
import { getOffline } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { connect } from 'react-redux';
|
||||
const nonceHelp = (
|
||||
|
@ -50,6 +50,6 @@ class NonceInputClass extends Component<Props> {
|
|||
}
|
||||
|
||||
export const NonceInput = connect((state: AppState) => ({
|
||||
shouldDisplay: isAnyOffline(state) || nonceRequestFailed(state),
|
||||
shouldDisplay: getOffline(state) || nonceRequestFailed(state),
|
||||
nonce: getNonce(state)
|
||||
}))(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 { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import DISABLES from './disables.json';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
|
||||
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
||||
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
||||
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
||||
import MistIcon from 'assets/images/wallets/mist.svg';
|
||||
import TrezorIcon from 'assets/images/wallets/trezor.svg';
|
||||
import './WalletDecrypt.scss';
|
||||
type UnlockParams = {} | PrivateKeyValue;
|
||||
|
||||
interface Props {
|
||||
resetTransactionState: TReset;
|
||||
|
@ -59,6 +60,7 @@ interface Props {
|
|||
isPasswordPending: AppState['wallet']['isPasswordPending'];
|
||||
}
|
||||
|
||||
type UnlockParams = {} | PrivateKeyValue;
|
||||
interface State {
|
||||
selectedWalletKey: string | 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() {
|
||||
const viewOnly = this.WALLETS['view-only'] as InsecureWalletInfo;
|
||||
|
||||
|
@ -379,6 +376,10 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private isWalletDisabled = (walletKey: string) => {
|
||||
if (this.props.offline && DISABLES.ONLINE_ONLY.includes(walletKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.props.disabledWallets) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"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 './SendButton';
|
||||
export * from './SigningStatus';
|
||||
export * from './OfflineAwareUnlockHeader';
|
||||
export { default as Header } from './Header';
|
||||
export { default as Footer } from './Footer';
|
||||
export { default as BalanceSidebar } from './BalanceSidebar';
|
||||
|
|
|
@ -7,7 +7,7 @@ import { IWallet } from 'libs/wallet/IWallet';
|
|||
import './UnlockHeader.scss';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactElement<any>;
|
||||
title: React.ReactElement<string> | string;
|
||||
wallet: IWallet;
|
||||
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 { AppState } from 'reducers';
|
||||
import Notifications from './Notifications';
|
||||
import OfflineTab from './OfflineTab';
|
||||
import { getGasPrice } from 'selectors/transaction';
|
||||
|
||||
interface ReduxProps {
|
||||
|
@ -23,6 +24,7 @@ interface ReduxProps {
|
|||
node: AppState['config']['node'];
|
||||
nodeSelection: AppState['config']['nodeSelection'];
|
||||
isChangingNode: AppState['config']['isChangingNode'];
|
||||
isOffline: AppState['config']['offline'];
|
||||
customNodes: AppState['config']['customNodes'];
|
||||
customNetworks: AppState['config']['customNetworks'];
|
||||
latestBlock: AppState['config']['latestBlock'];
|
||||
|
@ -39,19 +41,21 @@ interface ActionProps {
|
|||
}
|
||||
|
||||
type Props = {
|
||||
// FIXME
|
||||
children: any;
|
||||
isUnavailableOffline?: boolean;
|
||||
children: string | React.ReactElement<string> | React.ReactElement<string>[];
|
||||
} & ReduxProps &
|
||||
ActionProps;
|
||||
|
||||
class TabSection extends Component<Props, {}> {
|
||||
public render() {
|
||||
const {
|
||||
isUnavailableOffline,
|
||||
children,
|
||||
// APP
|
||||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
isOffline,
|
||||
languageSelection,
|
||||
customNodes,
|
||||
customNetworks,
|
||||
|
@ -70,6 +74,7 @@ class TabSection extends Component<Props, {}> {
|
|||
node,
|
||||
nodeSelection,
|
||||
isChangingNode,
|
||||
isOffline,
|
||||
gasPrice,
|
||||
customNodes,
|
||||
customNetworks,
|
||||
|
@ -85,7 +90,9 @@ class TabSection extends Component<Props, {}> {
|
|||
<div className="page-layout">
|
||||
<main>
|
||||
<Header {...headerProps} />
|
||||
<div className="Tab container">{children}</div>
|
||||
<div className="Tab container">
|
||||
{isUnavailableOffline && isOffline ? <OfflineTab /> : children}
|
||||
</div>
|
||||
<Footer latestBlock={latestBlock} />
|
||||
</main>
|
||||
<Notifications />
|
||||
|
@ -100,6 +107,7 @@ function mapStateToProps(state: AppState): ReduxProps {
|
|||
node: state.config.node,
|
||||
nodeSelection: state.config.nodeSelection,
|
||||
isChangingNode: state.config.isChangingNode,
|
||||
isOffline: state.config.offline,
|
||||
languageSelection: state.config.languageSelection,
|
||||
gasPrice: getGasPrice(state),
|
||||
customNodes: state.config.customNodes,
|
||||
|
|
|
@ -43,7 +43,7 @@ class BroadcastTx extends Component<DispatchProps & StateProps> {
|
|||
});
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<TabSection isUnavailableOffline={true}>
|
||||
<div className="Tab-content-pane row block text-center">
|
||||
<div className="BroadcastTx">
|
||||
<h1 className="BroadcastTx-title">Broadcast Signed Transaction</h1>
|
||||
|
|
|
@ -43,7 +43,7 @@ class Contracts extends Component<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<TabSection isUnavailableOffline={true}>
|
||||
<section className="Tab-content Contracts">
|
||||
<div className="Tab-content-pane">
|
||||
<h1 className="Contracts-header">
|
||||
|
|
|
@ -9,7 +9,7 @@ interface ContainerTabPaneActiveProps {
|
|||
}
|
||||
|
||||
const ContainerTabPaneActive = ({ children }: ContainerTabPaneActiveProps) => (
|
||||
<TabSection>
|
||||
<TabSection isUnavailableOffline={true}>
|
||||
<section className="container">
|
||||
<div className="tab-content">
|
||||
<main className="tab-pane active">
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { isAnyOfflineWithWeb3 } from 'selectors/derived';
|
||||
import {
|
||||
NonceField,
|
||||
AddressField,
|
||||
AmountField,
|
||||
GasSlider,
|
||||
|
@ -33,11 +32,6 @@ const content = (
|
|||
<GasSlider />
|
||||
</div>
|
||||
</div>
|
||||
<div className="row form-group">
|
||||
<div className="col-xs-12">
|
||||
<NonceField />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CurrentCustomMessage />
|
||||
<NonStandardTransaction />
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import TabSection from 'containers/TabSection';
|
||||
import { OfflineAwareUnlockHeader } from 'components';
|
||||
import React from 'react';
|
||||
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 { IReadOnlyWallet, IFullWallet } from 'libs/wallet';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
|
@ -52,7 +53,7 @@ class SendTransaction extends React.Component<Props> {
|
|||
return (
|
||||
<TabSection>
|
||||
<section className="Tab-content">
|
||||
<OfflineAwareUnlockHeader />
|
||||
<UnlockHeader title={translate('Account')} />
|
||||
{wallet && <WalletTabs {...tabProps} />}
|
||||
</section>
|
||||
</TabSection>
|
||||
|
|
|
@ -73,6 +73,7 @@ interface ReduxStateProps {
|
|||
bityOrderStatus: string | null;
|
||||
shapeshiftOrderStatus: string | null;
|
||||
paymentAddress: string | null;
|
||||
isOffline: boolean;
|
||||
}
|
||||
|
||||
interface ReduxActionProps {
|
||||
|
@ -98,8 +99,15 @@ interface ReduxActionProps {
|
|||
|
||||
class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
||||
public componentDidMount() {
|
||||
this.props.loadBityRatesRequestedSwap();
|
||||
this.props.loadShapeshiftRatesRequestedSwap();
|
||||
if (!this.props.isOffline) {
|
||||
this.loadRates();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: ReduxStateProps) {
|
||||
if (this.props.isOffline && !nextProps.isOffline) {
|
||||
this.loadRates();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -107,6 +115,11 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
|||
this.props.stopLoadShapeshiftRatesSwap();
|
||||
}
|
||||
|
||||
public loadRates() {
|
||||
this.props.loadBityRatesRequestedSwap();
|
||||
this.props.loadShapeshiftRatesRequestedSwap();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
// STATE
|
||||
|
@ -222,7 +235,7 @@ class Swap extends Component<ReduxActionProps & ReduxStateProps, {}> {
|
|||
const CurrentRatesProps = { provider, bityRates, shapeshiftRates };
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<TabSection isUnavailableOffline={true}>
|
||||
<section className="Tab-content swap-tab">
|
||||
{step === 1 && <CurrentRates {...CurrentRatesProps} />}
|
||||
{step === 1 && <ShapeshiftBanner />}
|
||||
|
@ -257,7 +270,8 @@ function mapStateToProps(state: AppState) {
|
|||
isPostingOrder: state.swap.isPostingOrder,
|
||||
bityOrderStatus: state.swap.bityOrderStatus,
|
||||
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;
|
||||
isChangingNode: boolean;
|
||||
offline: boolean;
|
||||
forceOffline: boolean;
|
||||
customNodes: CustomNodeConfig[];
|
||||
customNetworks: CustomNetworkConfig[];
|
||||
latestBlock: string;
|
||||
|
@ -42,7 +41,6 @@ export const INITIAL_STATE: State = {
|
|||
network: NETWORKS[NODES[defaultNode].network],
|
||||
isChangingNode: false,
|
||||
offline: false,
|
||||
forceOffline: false,
|
||||
customNodes: [],
|
||||
customNetworks: [],
|
||||
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 {
|
||||
const newId = makeCustomNodeId(action.payload);
|
||||
return {
|
||||
|
@ -141,8 +132,6 @@ export function config(state: State = INITIAL_STATE, action: ConfigAction): Stat
|
|||
return changeNodeIntent(state);
|
||||
case TypeKeys.CONFIG_TOGGLE_OFFLINE:
|
||||
return toggleOffline(state);
|
||||
case TypeKeys.CONFIG_FORCE_OFFLINE:
|
||||
return forceOffline(state);
|
||||
case TypeKeys.CONFIG_ADD_CUSTOM_NODE:
|
||||
return addCustomNode(state, action);
|
||||
case TypeKeys.CONFIG_REMOVE_CUSTOM_NODE:
|
||||
|
|
|
@ -22,8 +22,7 @@ import {
|
|||
getNodeConfig,
|
||||
getCustomNodeConfigs,
|
||||
getCustomNetworkConfigs,
|
||||
getOffline,
|
||||
getForceOffline
|
||||
getOffline
|
||||
} from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { TypeKeys } from 'actions/config/constants';
|
||||
|
@ -50,19 +49,11 @@ export function* pollOfflineStatus(): SagaIterator {
|
|||
while (true) {
|
||||
const node: NodeConfig = yield select(getNodeConfig);
|
||||
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
|
||||
// Don't check if the user is in another tab or window
|
||||
const shouldPing = !hasCheckedOnline || navigator.onLine === isOffline;
|
||||
if (shouldPing && !document.hidden) {
|
||||
hasCheckedOnline = true;
|
||||
const { pingSucceeded } = yield race({
|
||||
pingSucceeded: call(node.lib.ping.bind(node.lib)),
|
||||
timeout: call(delay, 5000)
|
||||
|
@ -76,6 +67,9 @@ export function* pollOfflineStatus(): SagaIterator {
|
|||
yield put(toggleOfflineConfig());
|
||||
} else if (!pingSucceeded && !isOffline) {
|
||||
// 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(
|
||||
showNotification(
|
||||
'danger',
|
||||
|
@ -85,11 +79,21 @@ export function* pollOfflineStatus(): SagaIterator {
|
|||
Infinity
|
||||
)
|
||||
);
|
||||
} else {
|
||||
yield put(
|
||||
showNotification(
|
||||
'info',
|
||||
'You are currently offline. Some features will be unavailable.',
|
||||
5000
|
||||
)
|
||||
);
|
||||
}
|
||||
yield put(toggleOfflineConfig());
|
||||
} else {
|
||||
// If neither case was true, try again in 5s
|
||||
yield call(delay, 5000);
|
||||
}
|
||||
hasCheckedOnline = true;
|
||||
} else {
|
||||
yield call(delay, 1000);
|
||||
}
|
||||
|
@ -103,15 +107,6 @@ export function* handlePollOfflineStatus(): SagaIterator {
|
|||
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
|
||||
// data to reload. Also the use of timeout to avoid using additional actions for now.
|
||||
export function* reload(): SagaIterator {
|
||||
|
@ -251,7 +246,6 @@ export const equivalentNodeOrDefault = (nodeConfig: NodeConfig) => {
|
|||
|
||||
export default function* configSaga(): SagaIterator {
|
||||
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_LANGUAGE_CHANGE, reload);
|
||||
yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SagaIterator, buffers, delay } from 'redux-saga';
|
||||
import { apply, put, select, take, actionChannel, call, fork } from 'redux-saga/effects';
|
||||
import { INode } from 'libs/nodes/INode';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { getNodeLib, getOffline } from 'selectors/config';
|
||||
import { getWalletInst } from 'selectors/wallet';
|
||||
import { getTransaction, IGetTransaction } from 'selectors/transaction';
|
||||
import {
|
||||
|
@ -22,6 +22,11 @@ import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/
|
|||
|
||||
export function* shouldEstimateGas(): SagaIterator {
|
||||
while (true) {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const action:
|
||||
| SetToFieldAction
|
||||
| SetDataFieldAction
|
||||
|
@ -59,6 +64,11 @@ export function* estimateGas(): SagaIterator {
|
|||
const requestChan = yield actionChannel(TypeKeys.ESTIMATE_GAS_REQUESTED, buffers.sliding(1));
|
||||
|
||||
while (true) {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { payload }: EstimateGasRequestedAction = yield take(requestChan);
|
||||
// debounce 250 ms
|
||||
yield call(delay, 250);
|
||||
|
|
|
@ -12,9 +12,13 @@ import { Nonce } from 'libs/units';
|
|||
export function* handleNonceRequest(): SagaIterator {
|
||||
const nodeLib: INode = yield select(getNodeLib);
|
||||
const walletInst: AppState['wallet']['inst'] = yield select(getWalletInst);
|
||||
const offline: boolean = yield select(getOffline);
|
||||
const isOffline: boolean = yield select(getOffline);
|
||||
try {
|
||||
if (!walletInst || offline) {
|
||||
if (isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!walletInst) {
|
||||
throw Error();
|
||||
}
|
||||
const fromAddress: string = yield apply(walletInst, walletInst.getAddressString);
|
||||
|
|
|
@ -39,7 +39,7 @@ import {
|
|||
import { NODES, initWeb3Node, Token } from 'config/data';
|
||||
import { SagaIterator, delay, Task } from 'redux-saga';
|
||||
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 {
|
||||
getTokens,
|
||||
getWalletInst,
|
||||
|
@ -58,6 +58,11 @@ export interface TokenBalanceLookup {
|
|||
|
||||
export function* updateAccountBalance(): SagaIterator {
|
||||
try {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(setBalancePending());
|
||||
const wallet: null | IWallet = yield select(getWalletInst);
|
||||
if (!wallet) {
|
||||
|
@ -75,6 +80,11 @@ export function* updateAccountBalance(): SagaIterator {
|
|||
|
||||
export function* updateTokenBalances(): SagaIterator {
|
||||
try {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wallet: null | IWallet = yield select(getWalletInst);
|
||||
const tokens: MergedToken[] = yield select(getWalletConfigTokens);
|
||||
if (!wallet || !tokens.length) {
|
||||
|
@ -91,6 +101,11 @@ export function* updateTokenBalances(): SagaIterator {
|
|||
|
||||
export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaIterator {
|
||||
try {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wallet: null | IWallet = yield select(getWalletInst);
|
||||
const { tokenSymbol } = action.payload;
|
||||
const allTokens: Token[] = yield select(getAllTokens);
|
||||
|
@ -115,6 +130,11 @@ export function* updateTokenBalance(action: SetTokenBalancePendingAction): SagaI
|
|||
|
||||
export function* scanWalletForTokens(action: ScanWalletForTokensAction): SagaIterator {
|
||||
try {
|
||||
const isOffline = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wallet = action.payload;
|
||||
const tokens: MergedToken[] = yield select(getTokens);
|
||||
yield put(setTokenBalancesPending());
|
||||
|
@ -288,7 +308,9 @@ export default function* walletSaga(): SagaIterator {
|
|||
takeEvery(TypeKeys.WALLET_SET, handleNewWallet),
|
||||
takeEvery(TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS, scanWalletForTokens),
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const isToken: boolean = tokenExists(state, unit);
|
||||
const isEther: boolean = isEtherUnit(unit);
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { AppState } from 'reducers';
|
||||
import { getWalletType } from 'selectors/wallet';
|
||||
import { getOffline, getForceOffline } from 'selectors/config';
|
||||
import { getOffline } from 'selectors/config';
|
||||
|
||||
export const isAnyOfflineWithWeb3 = (state: AppState): boolean => {
|
||||
const { isWeb3Wallet } = getWalletType(state);
|
||||
const offline = getOffline(state);
|
||||
const forceOffline = getForceOffline(state);
|
||||
const anyOffline = offline || forceOffline;
|
||||
const anyOfflineAndWeb3 = anyOffline && isWeb3Wallet;
|
||||
return anyOfflineAndWeb3;
|
||||
return offline && isWeb3Wallet;
|
||||
};
|
||||
|
|
|
@ -143,10 +143,8 @@
|
|||
"tscheck": "tsc --noEmit",
|
||||
"start": "npm run dev",
|
||||
"precommit": "lint-staged",
|
||||
"formatAll":
|
||||
"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\"",
|
||||
"formatAll": "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\"",
|
||||
"prepush": "npm run tslint && npm run tscheck"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
@ -17,8 +17,7 @@ it('render snapshot', () => {
|
|||
nodeSelection: testNode,
|
||||
node: NODES[testNode],
|
||||
gasPriceGwei: 21,
|
||||
offline: false,
|
||||
forceOffline: false
|
||||
offline: false
|
||||
};
|
||||
const testState = {
|
||||
wallet: {},
|
||||
|
@ -31,7 +30,6 @@ it('render snapshot', () => {
|
|||
gasPrice: {},
|
||||
transactions: {},
|
||||
offline: {},
|
||||
forceOffline: {},
|
||||
config: testStateConfig,
|
||||
customTokens: []
|
||||
};
|
||||
|
|
|
@ -4,12 +4,13 @@ import Adapter from 'enzyme-adapter-react-16';
|
|||
import Swap from 'containers/Tabs/Swap';
|
||||
import shallowWithStore from '../utils/shallowWithStore';
|
||||
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() });
|
||||
|
||||
it('render snapshot', () => {
|
||||
const store = createMockStore({ swap: INITIAL_STATE });
|
||||
const store = createMockStore({ swap, config });
|
||||
const component = shallowWithStore(<Swap />, store);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
|
|
|
@ -22,6 +22,7 @@ exports[`render snapshot 1`] = `
|
|||
destinationAddressSwap={[Function]}
|
||||
initSwap={[Function]}
|
||||
isFetchingRates={null}
|
||||
isOffline={false}
|
||||
isPostingOrder={false}
|
||||
loadBityRatesRequestedSwap={[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', () => {
|
||||
expect(config(undefined, configActions.addCustomNode(custNode))).toEqual({
|
||||
...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`] = `
|
||||
Object {
|
||||
"@@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,
|
||||
handlePollOfflineStatus,
|
||||
handleNodeChangeIntent,
|
||||
handleTogglePollOfflineStatus,
|
||||
reload,
|
||||
unsetWeb3Node,
|
||||
unsetWeb3NodeOnWalletEvent,
|
||||
|
@ -18,7 +17,6 @@ import {
|
|||
getNode,
|
||||
getNodeConfig,
|
||||
getOffline,
|
||||
getForceOffline,
|
||||
getCustomNodeConfigs,
|
||||
getCustomNetworkConfigs
|
||||
} from 'selectors/config';
|
||||
|
@ -43,12 +41,13 @@ describe('pollOfflineStatus*', () => {
|
|||
}
|
||||
};
|
||||
const isOffline = true;
|
||||
const isForcedOffline = true;
|
||||
const raceSuccess = {
|
||||
pingSucceeded: true
|
||||
pingSucceeded: true,
|
||||
timeout: false
|
||||
};
|
||||
const raceFailure = {
|
||||
pingSucceeded: false
|
||||
pingSucceeded: false,
|
||||
timeout: true
|
||||
};
|
||||
|
||||
let originalHidden;
|
||||
|
@ -88,49 +87,32 @@ describe('pollOfflineStatus*', () => {
|
|||
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', () => {
|
||||
data.clone2 = data.gen.clone();
|
||||
data.hiddenDoc = data.gen.clone();
|
||||
doc.hidden = true;
|
||||
|
||||
expect(data.clone2.next(!isForcedOffline).value).toEqual(call(delay, 1000));
|
||||
expect(data.hiddenDoc.next(!isOffline).value).toEqual(call(delay, 1000));
|
||||
doc.hidden = false;
|
||||
});
|
||||
|
||||
it('should race pingSucceeded and timeout', () => {
|
||||
doc.hidden = false;
|
||||
expect(data.gen.next(!isForcedOffline).value).toMatchSnapshot();
|
||||
data.isOfflineClone = data.gen.clone();
|
||||
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(
|
||||
put(showNotification('success', 'Your connection to the network has been restored!', 3000))
|
||||
);
|
||||
expect(data.gen.next().value).toEqual(put(toggleOfflineConfig()));
|
||||
});
|
||||
|
||||
it('should put showNotification and put toggleOfflineConfig if !pingSucceeded && !isOffline', () => {
|
||||
nav.onLine = !isOffline;
|
||||
|
||||
data.isOfflineClone.next(!isOffline);
|
||||
data.isOfflineClone.next(!isForcedOffline);
|
||||
|
||||
data.clone3 = data.isOfflineClone.clone();
|
||||
|
||||
it('should toggle offline and show notification if navigator agrees with isOffline and ping fails', () => {
|
||||
nav.onLine = isOffline;
|
||||
expect(data.isOfflineClone.next(!isOffline));
|
||||
expect(data.isOfflineClone.next(raceFailure).value).toMatchSnapshot();
|
||||
expect(data.isOfflineClone.next().value).toEqual(put(toggleOfflineConfig()));
|
||||
});
|
||||
|
||||
it('should call delay when neither case is true', () => {
|
||||
expect(data.clone3.next(raceSuccess).value).toEqual(call(delay, 5000));
|
||||
nav.onLine = !isOffline;
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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*', () => {
|
||||
let originalRandom;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { buffers, delay } from 'redux-saga';
|
||||
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 { getTransaction } from 'selectors/transaction';
|
||||
import {
|
||||
|
@ -16,6 +16,7 @@ import { cloneableGenerator } from 'redux-saga/utils';
|
|||
import { Wei } from 'libs/units';
|
||||
|
||||
describe('shouldEstimateGas*', () => {
|
||||
const offline = false;
|
||||
const transaction: any = 'transaction';
|
||||
const tx = { transaction };
|
||||
const rest: any = {
|
||||
|
@ -39,8 +40,12 @@ describe('shouldEstimateGas*', () => {
|
|||
|
||||
const gen = shouldEstimateGas();
|
||||
|
||||
it('should select getOffline', () => {
|
||||
expect(gen.next().value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('should take expected types', () => {
|
||||
expect(gen.next().value).toEqual(
|
||||
expect(gen.next(offline).value).toEqual(
|
||||
take([
|
||||
TypeKeys.TO_FIELD_SET,
|
||||
TypeKeys.DATA_FIELD_SET,
|
||||
|
@ -65,6 +70,7 @@ describe('shouldEstimateGas*', () => {
|
|||
});
|
||||
|
||||
describe('estimateGas*', () => {
|
||||
const offline = false;
|
||||
const requestChan = 'requestChan';
|
||||
const payload: any = {
|
||||
mock1: 'mock1',
|
||||
|
@ -102,8 +108,12 @@ describe('estimateGas*', () => {
|
|||
expect(expected).toEqual(result);
|
||||
});
|
||||
|
||||
it('should select getOffline', () => {
|
||||
expect(gens.gen.next(requestChan).value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
|
@ -40,18 +40,23 @@ describe('handleNonceRequest*', () => {
|
|||
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', () => {
|
||||
gens.clone = gens.gen.clone();
|
||||
expect(gens.gen.next(walletInst).value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('should handle errors correctly', () => {
|
||||
gens.clone.next();
|
||||
expect(gens.clone.next().value).toEqual(
|
||||
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 exit if being called while offline', () => {
|
||||
gens.offline = gens.gen.clone();
|
||||
expect(gens.offline.next(true).done).toEqual(true);
|
||||
});
|
||||
|
||||
it('should apply walletInst.getAddressString', () => {
|
||||
|
|
|
@ -15,7 +15,7 @@ import { changeNodeIntent, web3UnsetNode } from 'actions/config';
|
|||
import { INode } from 'libs/nodes/INode';
|
||||
import { initWeb3Node, Token, N_FACTOR } from 'config/data';
|
||||
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 {
|
||||
updateAccountBalance,
|
||||
|
@ -39,7 +39,7 @@ import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
|||
|
||||
// init module
|
||||
configuredStore.getState();
|
||||
|
||||
const offline = false;
|
||||
const pkey = '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344';
|
||||
const wallet = PrivKeyWallet(Buffer.from(pkey, 'hex'));
|
||||
const address = '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854';
|
||||
|
@ -83,90 +83,108 @@ const utcKeystore = {
|
|||
// necessary so we can later inject a mocked web3 to the window
|
||||
|
||||
describe('updateAccountBalance*', () => {
|
||||
const gen1 = updateAccountBalance();
|
||||
const gen2 = updateAccountBalance();
|
||||
const gen = updateAccountBalance();
|
||||
|
||||
it('should select offline', () => {
|
||||
expect(gen.next().value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('should put setBalancePending', () => {
|
||||
expect(gen1.next().value).toEqual(put(setBalancePending()));
|
||||
expect(gen.next(false).value).toEqual(put(setBalancePending()));
|
||||
});
|
||||
|
||||
it('should select getWalletInst', () => {
|
||||
expect(gen1.next().value).toEqual(select(getWalletInst));
|
||||
});
|
||||
|
||||
it('should return if wallet is falsey', () => {
|
||||
gen2.next();
|
||||
gen2.next();
|
||||
gen2.next(null);
|
||||
expect(gen2.next().done).toBe(true);
|
||||
expect(gen.next(false).value).toEqual(select(getWalletInst));
|
||||
});
|
||||
|
||||
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', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
expect(gen1.next(balance).value).toEqual(put(setBalanceFullfilled(balance)));
|
||||
expect(gen.next(balance).value).toEqual(put(setBalanceFullfilled(balance)));
|
||||
});
|
||||
|
||||
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*', () => {
|
||||
const gen1 = cloneableGenerator(updateTokenBalances)();
|
||||
const gen2 = updateTokenBalances();
|
||||
const gen3 = updateTokenBalances();
|
||||
const gen = cloneableGenerator(updateTokenBalances)();
|
||||
|
||||
it('should select getWalletInst', () => {
|
||||
expect(gen1.next().value).toEqual(select(getWalletInst));
|
||||
it('should bail out if offline', () => {
|
||||
const offlineGen = gen.clone();
|
||||
expect(offlineGen.next());
|
||||
expect(offlineGen.next(true).done).toBe(true);
|
||||
});
|
||||
|
||||
it('should select getWalletConfigTokens', () => {
|
||||
expect(gen1.next(wallet).value).toEqual(select(getWalletConfigTokens));
|
||||
it('should select getOffline', () => {
|
||||
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', () => {
|
||||
gen2.next();
|
||||
gen2.next(null);
|
||||
expect(gen2.next().done).toEqual(true);
|
||||
const noWalletGen = gen.clone();
|
||||
noWalletGen.next(null);
|
||||
expect(noWalletGen.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return if tokens are falsey', () => {
|
||||
gen3.next();
|
||||
gen3.next(wallet);
|
||||
expect(gen3.next({}).done).toEqual(true);
|
||||
it('should select getWalletConfigTokens', () => {
|
||||
expect(gen.next(wallet).value).toEqual(select(getWalletConfigTokens));
|
||||
});
|
||||
|
||||
it('should return if no tokens are requested', () => {
|
||||
const noTokensGen = gen.clone();
|
||||
noTokensGen.next({});
|
||||
expect(noTokensGen.next().done).toEqual(true);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const gen4 = gen1.clone();
|
||||
if (gen4.throw) {
|
||||
expect(gen4.throw().value).toEqual(put(setTokenBalancesRejected()));
|
||||
it('should put setTokenBalancesRejected on throw', () => {
|
||||
const throwGen = gen.clone();
|
||||
if (throwGen.throw) {
|
||||
expect(throwGen.throw().value).toEqual(put(setTokenBalancesRejected()));
|
||||
}
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expect(gen1.next({}).value).toEqual(put(setTokenBalancesFulfilled({})));
|
||||
expect(gen.next({}).value).toEqual(put(setTokenBalancesFulfilled({})));
|
||||
});
|
||||
it('should be done', () => {
|
||||
expect(gen1.next().done).toEqual(true);
|
||||
expect(gen.next().done).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue