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:
William O'Beirne 2018-01-11 13:04:11 -05:00 committed by Daniel Ternyak
parent 659f218b1c
commit 4f6e83acf4
48 changed files with 441 additions and 319 deletions

View File

@ -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 });
}

View File

@ -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 {

View File

@ -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

View File

@ -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',

View File

@ -40,4 +40,9 @@
text-align: center;
}
}
&-offline {
margin-bottom: 0;
text-align: center;
}
}

View File

@ -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>
<ul className="EquivalentValues-values">{valuesEl}</ul>
{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;
}

View File

@ -41,4 +41,9 @@
color: $gray;
}
}
&-offline {
margin-bottom: 0;
text-align: center;
}
}

View File

@ -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
};
}

View File

@ -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
};
}

View File

@ -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() {
this.props.fetchCCRates([this.props.network.unit]);
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)
};

View File

@ -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);
};
}

View File

@ -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
};
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -130,6 +130,10 @@ $small-size: 900px;
margin-right: 10px;
}
&-online {
margin-right: 6px;
}
&-dropdown {
margin-left: 6px;

View File

@ -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}

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -1,4 +1,5 @@
{
"READ_ONLY": ["view-only"],
"UNABLE_TO_SIGN": ["trezor", "view-only"]
"UNABLE_TO_SIGN": ["trezor", "view-only"],
"ONLINE_ONLY": ["web3", "trezor"]
}

View File

@ -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';

View File

@ -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[];
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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 />

View File

@ -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>

View File

@ -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
};
}

View File

@ -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:

View File

@ -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,20 +67,33 @@ 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
yield put(
showNotification(
'danger',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
Infinity
)
);
// 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',
`Youve lost your connection to the network, check your internet
connection or try changing networks from the dropdown at the
top right of the page.`,
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);

View File

@ -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);

View File

@ -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);

View File

@ -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)
];
}

View File

@ -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);

View File

@ -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;
};

View File

@ -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": {

View File

@ -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: []
};

View File

@ -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();

View File

@ -22,6 +22,7 @@ exports[`render snapshot 1`] = `
destinationAddressSwap={[Function]}
initSwap={[Function]}
isFetchingRates={null}
isOffline={false}
isPostingOrder={false}
loadBityRatesRequestedSwap={[Function]}
loadShapeshiftRatesRequestedSwap={[Function]}

View File

@ -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,

View File

@ -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": "Youve 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,
},
}
`;

View File

@ -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;

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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);
});
});