Disable more wallets conditionally + explain why (#924)

* Add more wallet disables, move all into selector.

* Add reasons for disabled wallets.

* Disable read only in lite send.

* Fix view address showing insecure icon.
This commit is contained in:
William O'Beirne 2018-01-26 15:08:39 -05:00 committed by Daniel Ternyak
parent df52521c17
commit 2309d05c06
10 changed files with 227 additions and 87 deletions

View File

@ -32,8 +32,9 @@ import {
InsecureWalletWarning InsecureWalletWarning
} from './components'; } from './components';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import DISABLES from './disables';
import { showNotification, TShowNotification } from 'actions/notifications'; import { showNotification, TShowNotification } from 'actions/notifications';
import { getDisabledWallets } from 'selectors/wallet';
import { DisabledWallets } from './disables';
import LedgerIcon from 'assets/images/wallets/ledger.svg'; import LedgerIcon from 'assets/images/wallets/ledger.svg';
import MetamaskIcon from 'assets/images/wallets/metamask.svg'; import MetamaskIcon from 'assets/images/wallets/metamask.svg';
@ -48,12 +49,10 @@ import {
isWeb3NodeAvailable, isWeb3NodeAvailable,
knowledgeBaseURL knowledgeBaseURL
} from 'config'; } from 'config';
import { unSupportedWalletFormatsOnNetwork } from 'utils/network';
import { getNetworkConfig } from '../../selectors/config';
interface OwnProps { interface OwnProps {
hidden?: boolean; hidden?: boolean;
disabledWallets?: WalletName[]; disabledWallets?: DisabledWallets;
showGenerateLink?: boolean; showGenerateLink?: boolean;
} }
@ -69,8 +68,7 @@ interface DispatchProps {
} }
interface StateProps { interface StateProps {
computedDisabledWallets: WalletName[]; computedDisabledWallets: DisabledWallets;
offline: boolean;
isWalletPending: AppState['wallet']['isWalletPending']; isWalletPending: AppState['wallet']['isWalletPending'];
isPasswordPending: AppState['wallet']['isPasswordPending']; isPasswordPending: AppState['wallet']['isPasswordPending'];
} }
@ -282,6 +280,9 @@ export class WalletDecrypt extends Component<Props, State> {
}; };
public buildWalletOptions() { public buildWalletOptions() {
const { computedDisabledWallets } = this.props;
const { reasons } = computedDisabledWallets;
return ( return (
<div className="WalletDecrypt-wallets"> <div className="WalletDecrypt-wallets">
<h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2> <h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2>
@ -299,6 +300,7 @@ export class WalletDecrypt extends Component<Props, State> {
walletType={walletType} walletType={walletType}
isSecure={true} isSecure={true}
isDisabled={this.isWalletDisabled(walletType)} isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice} onClick={this.handleWalletChoice}
/> />
); );
@ -316,6 +318,7 @@ export class WalletDecrypt extends Component<Props, State> {
walletType={walletType} walletType={walletType}
isSecure={false} isSecure={false}
isDisabled={this.isWalletDisabled(walletType)} isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice} onClick={this.handleWalletChoice}
/> />
); );
@ -332,6 +335,7 @@ export class WalletDecrypt extends Component<Props, State> {
walletType={walletType} walletType={walletType}
isReadOnly={true} isReadOnly={true}
isDisabled={this.isWalletDisabled(walletType)} isDisabled={this.isWalletDisabled(walletType)}
disableReason={reasons[walletType]}
onClick={this.handleWalletChoice} onClick={this.handleWalletChoice}
/> />
); );
@ -426,24 +430,26 @@ export class WalletDecrypt extends Component<Props, State> {
}; };
private isWalletDisabled = (walletKey: WalletName) => { private isWalletDisabled = (walletKey: WalletName) => {
if (this.props.offline && DISABLES.ONLINE_ONLY.includes(walletKey)) { return this.props.computedDisabledWallets.wallets.indexOf(walletKey) !== -1;
return true;
}
return this.props.computedDisabledWallets.indexOf(walletKey) !== -1;
}; };
} }
function mapStateToProps(state: AppState, ownProps: Props) { function mapStateToProps(state: AppState, ownProps: Props) {
const { disabledWallets } = ownProps; const { disabledWallets } = ownProps;
const network = getNetworkConfig(state); let computedDisabledWallets = getDisabledWallets(state);
const networkDisabledFormats = unSupportedWalletFormatsOnNetwork(network);
const computedDisabledWallets = disabledWallets if (disabledWallets) {
? disabledWallets.concat(networkDisabledFormats) computedDisabledWallets = {
: networkDisabledFormats; wallets: [...computedDisabledWallets.wallets, ...disabledWallets.wallets],
reasons: {
...computedDisabledWallets.reasons,
...disabledWallets.reasons
}
};
}
return { return {
computedDisabledWallets, computedDisabledWallets,
offline: state.config.offline,
isWalletPending: state.wallet.isWalletPending, isWalletPending: state.wallet.isWalletPending,
isPasswordPending: state.wallet.isPasswordPending isPasswordPending: state.wallet.isPasswordPending
}; };

View File

@ -12,16 +12,6 @@
} }
} }
@keyframes wallet-button-enter-disabled {
0% {
opacity: 0;
}
30%,
100% {
opacity: 0.3;
}
}
.WalletButton { .WalletButton {
position: relative; position: relative;
flex: 1; flex: 1;
@ -70,10 +60,17 @@
} }
&.is-disabled { &.is-disabled {
opacity: 0.3 !important;
outline: none; outline: none;
cursor: not-allowed; cursor: not-allowed;
animation-name: wallet-button-enter, wallet-button-enter-disabled; @include show-tooltip-on-hover;
.WalletButton-inner {
opacity: 0.3;
}
}
&-inner {
transition: opacity 200ms ease;
} }
&-title { &-title {

View File

@ -16,6 +16,7 @@ interface OwnProps {
isSecure?: boolean; isSecure?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
disableReason?: string;
onClick(walletType: string): void; onClick(walletType: string): void;
} }
@ -23,6 +24,12 @@ interface StateProps {
isFormatDisabled?: boolean; isFormatDisabled?: boolean;
} }
interface Icon {
icon: string;
tooltip: string;
href?: string;
}
type Props = OwnProps & StateProps; type Props = OwnProps & StateProps;
export class WalletButton extends React.PureComponent<Props> { export class WalletButton extends React.PureComponent<Props> {
@ -35,9 +42,37 @@ export class WalletButton extends React.PureComponent<Props> {
helpLink, helpLink,
isSecure, isSecure,
isReadOnly, isReadOnly,
isDisabled isDisabled,
disableReason
} = this.props; } = this.props;
const icons: Icon[] = [];
if (isReadOnly) {
icons.push({
icon: 'eye',
tooltip: translateRaw('You cannot send using address only')
});
} else {
if (isSecure) {
icons.push({
icon: 'shield',
tooltip: translateRaw('This wallet type is secure')
});
} else {
icons.push({
icon: 'exclamation-triangle',
tooltip: translateRaw('This wallet type is insecure')
});
}
}
if (helpLink) {
icons.push({
icon: 'question-circle',
tooltip: translateRaw('NAV_Help'),
href: helpLink
});
}
return ( return (
<div <div
className={classnames({ className={classnames({
@ -49,6 +84,7 @@ export class WalletButton extends React.PureComponent<Props> {
tabIndex={isDisabled ? -1 : 0} tabIndex={isDisabled ? -1 : 0}
aria-disabled={isDisabled} aria-disabled={isDisabled}
> >
<div className="WalletButton-inner">
<div className="WalletButton-title"> <div className="WalletButton-title">
{icon && <img className="WalletButton-title-icon" src={icon} />} {icon && <img className="WalletButton-title-icon" src={icon} />}
<span>{name}</span> <span>{name}</span>
@ -58,34 +94,23 @@ export class WalletButton extends React.PureComponent<Props> {
{example && <div className="WalletButton-example">{example}</div>} {example && <div className="WalletButton-example">{example}</div>}
<div className="WalletButton-icons"> <div className="WalletButton-icons">
{isSecure ? ( {icons.map(i => (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}> <span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
<i className="fa fa-shield" /> {i.href ? (
<Tooltip>{translateRaw('This wallet type is secure')}</Tooltip> <NewTabLink href={i.href} onClick={this.stopPropogation}>
</span> <i className={`fa fa-${i.icon}`} />
) : (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-exclamation-triangle" />
<Tooltip>{translateRaw('This wallet type is insecure')}</Tooltip>
</span>
)}
{isReadOnly && (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-eye" />
<Tooltip>{translateRaw('You cannot send using address only')}</Tooltip>
</span>
)}
{helpLink && (
<span className="WalletButton-icons-icon">
<NewTabLink href={helpLink} onClick={this.stopPropogation}>
<i className="fa fa-question-circle" />
</NewTabLink> </NewTabLink>
<Tooltip>{translateRaw('NAV_Help')}</Tooltip> ) : (
</span> <i className={`fa fa-${i.icon}`} />
)} )}
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
</span>
))}
</div> </div>
</div> </div>
{isDisabled && disableReason && <Tooltip>{disableReason}</Tooltip>}
</div>
); );
} }

View File

@ -1,15 +1,31 @@
import { MiscWalletName, SecureWalletName, WalletName } from 'config'; import { MiscWalletName, SecureWalletName, WalletName } from 'config';
export interface DisabledWallets {
wallets: WalletName[];
reasons: {
[key: string]: string;
};
}
enum WalletMode { enum WalletMode {
READ_ONLY = 'READ_ONLY', READ_ONLY = 'READ_ONLY',
UNABLE_TO_SIGN = 'UNABLE_TO_SIGN', UNABLE_TO_SIGN = 'UNABLE_TO_SIGN'
ONLINE_ONLY = 'ONLINE_ONLY'
} }
const walletModes: { [key in WalletMode]: WalletName[] } = { // Duplicating reasons is kind of tedious, but saves having to run through a
[WalletMode.READ_ONLY]: [MiscWalletName.VIEW_ONLY], // bunch of loops to format it differently
[WalletMode.UNABLE_TO_SIGN]: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY], export const DISABLE_WALLETS: { [key in WalletMode]: DisabledWallets } = {
[WalletMode.ONLINE_ONLY]: [SecureWalletName.WEB3, SecureWalletName.TREZOR] [WalletMode.READ_ONLY]: {
wallets: [MiscWalletName.VIEW_ONLY],
reasons: {
[MiscWalletName.VIEW_ONLY]: 'Read only is not allowed'
}
},
[WalletMode.UNABLE_TO_SIGN]: {
wallets: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
reasons: {
[SecureWalletName.TREZOR]: 'This wallet cant sign messages',
[MiscWalletName.VIEW_ONLY]: 'This wallet cant sign messages'
}
}
}; };
export default walletModes;

View File

@ -1,3 +1,3 @@
import WalletDecrypt from './WalletDecrypt'; import WalletDecrypt from './WalletDecrypt';
export default WalletDecrypt; export default WalletDecrypt;
export { default as DISABLE_WALLETS } from './disables'; export * from './disables';

View File

@ -1,13 +1,15 @@
@import 'common/sass/variables'; @import 'common/sass/variables';
@import 'common/sass/mixins'; @import 'common/sass/mixins';
$tooltip-bg: rgba(#222, 0.95);
.Tooltip { .Tooltip {
position: absolute; position: absolute;
top: 0; top: 0;
left: 50%; left: 50%;
width: 220px; width: 220px;
color: #FFF; color: #FFF;
font-size: $font-size-xs; font-size: $font-size-small;
font-family: $font-family-sans-serif; font-family: $font-family-sans-serif;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
@ -20,9 +22,9 @@
> span { > span {
display: inline-block; display: inline-block;
background: rgba(#000, 0.9); background: $tooltip-bg;
border-radius: 2px; border-radius: 3px;
padding: 4px 8px; padding: 6px 10px;
&:after { &:after {
position: absolute; position: absolute;
@ -30,7 +32,36 @@
bottom: 0; bottom: 0;
left: 50%; left: 50%;
transform: translate(-50%, 100%); transform: translate(-50%, 100%);
@include triangle(8px, rgba(#000, 0.9), down); @include triangle(10px, $tooltip-bg, down);
}
}
// Sizing, medium is default
&.is-size-sm {
width: 200px;
font-size: $font-size-xs;
> span {
padding: 4px 8px;
border-radius: 2px;
&:after {
@include triangle(8px, $tooltip-bg, down);
}
}
}
&.is-size-lg {
width: 240px;
font-size: $font-size-base;
> span {
padding: 8px 12px;
border-radius: 4px;
&:after {
@include triangle(12px, $tooltip-bg, down);
}
} }
} }
} }

View File

@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import './Tooltip.scss'; import './Tooltip.scss';
interface Props { interface Props {
children: React.ReactElement<string> | string; children: React.ReactElement<string> | string;
size?: 'sm' | 'md' | 'lg';
} }
const Tooltip: React.SFC<Props> = ({ children }) => ( const Tooltip: React.SFC<Props> = ({ size, children }) => (
<div className="Tooltip"> <div
className={classnames({
Tooltip: true,
[`is-size-${size}`]: !!size
})}
>
<span className="Tooltip-text">{children}</span> <span className="Tooltip-text">{children}</span>
</div> </div>
); );

View File

@ -2,15 +2,14 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import translate, { TranslateType } from 'translations'; import translate, { TranslateType } from 'translations';
import WalletDecrypt from 'components/WalletDecrypt'; import WalletDecrypt, { DisabledWallets } from 'components/WalletDecrypt';
import { IWallet } from 'libs/wallet/IWallet'; import { IWallet } from 'libs/wallet/IWallet';
import './UnlockHeader.scss'; import './UnlockHeader.scss';
import { WalletName } from 'config';
interface Props { interface Props {
title: TranslateType; title: TranslateType;
wallet: IWallet; wallet: IWallet;
disabledWallets?: WalletName[]; disabledWallets?: DisabledWallets;
showGenerateLink?: boolean; showGenerateLink?: boolean;
} }

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import WalletDecrypt from 'components/WalletDecrypt'; import WalletDecrypt, { DISABLE_WALLETS } from 'components/WalletDecrypt';
import { OnlyUnlocked } from 'components/renderCbs'; import { OnlyUnlocked } from 'components/renderCbs';
import { Fields } from './Fields'; import { Fields } from './Fields';
import { isUnlocked as isUnlockedSelector } from 'selectors/wallet'; import { isUnlocked as isUnlockedSelector } from 'selectors/wallet';
@ -41,7 +41,11 @@ class LiteSendClass extends Component<Props> {
</div> </div>
); );
} else { } else {
renderMe = isUnlocked ? <OnlyUnlocked whenUnlocked={<Fields />} /> : <WalletDecrypt />; renderMe = isUnlocked ? (
<OnlyUnlocked whenUnlocked={<Fields />} />
) : (
<WalletDecrypt disabledWallets={DISABLE_WALLETS.READ_ONLY} />
);
} }
return <React.Fragment>{renderMe}</React.Fragment>; return <React.Fragment>{renderMe}</React.Fragment>;

View File

@ -1,9 +1,11 @@
import { TokenValue, Wei } from 'libs/units'; import { TokenValue, Wei } from 'libs/units';
import { Token } from 'config'; import { Token, SecureWalletName, WalletName } from 'config';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config'; import { getNetworkConfig, getOffline } from 'selectors/config';
import { IWallet, Web3Wallet, LedgerWallet, TrezorWallet, WalletConfig } from 'libs/wallet'; import { IWallet, Web3Wallet, LedgerWallet, TrezorWallet, WalletConfig } from 'libs/wallet';
import { isEtherTransaction, getUnit } from './transaction'; import { isEtherTransaction, getUnit } from './transaction';
import { unSupportedWalletFormatsOnNetwork } from 'utils/network';
import { DisabledWallets } from 'components/WalletDecrypt';
export function getWalletInst(state: AppState): IWallet | null | undefined { export function getWalletInst(state: AppState): IWallet | null | undefined {
return state.wallet.inst; return state.wallet.inst;
@ -139,3 +141,56 @@ export function getShownTokenBalances(
return tokenBalances.filter(t => walletTokens.includes(t.symbol)); return tokenBalances.filter(t => walletTokens.includes(t.symbol));
} }
// TODO: Convert to reselect selector (Issue #884)
export function getDisabledWallets(state: AppState): DisabledWallets {
const network = getNetworkConfig(state);
const isOffline = getOffline(state);
const disabledWallets: DisabledWallets = {
wallets: [],
reasons: {}
};
const addReason = (wallets: WalletName[], reason: string) => {
if (!wallets.length) {
return;
}
disabledWallets.wallets = disabledWallets.wallets.concat(wallets);
wallets.forEach(wallet => {
disabledWallets.reasons[wallet] = reason;
});
};
// Some wallets don't support some networks
addReason(
unSupportedWalletFormatsOnNetwork(network),
`${network.name} does not support this wallet`
);
// Some wallets are unavailable offline
if (isOffline) {
addReason(
[SecureWalletName.WEB3, SecureWalletName.TREZOR],
'This wallet cannot be accessed offline'
);
}
// Some wallets are disabled on certain platforms
if (process.env.BUILD_DOWNLOADABLE) {
addReason(
[SecureWalletName.LEDGER_NANO_S],
'This wallet is only supported at MyEtherWallet.com'
);
}
if (process.env.BUILD_ELECTRON) {
addReason([SecureWalletName.WEB3], 'This wallet is not supported in the MyEtherWallet app');
}
// Dedupe and sort for consistency
disabledWallets.wallets = disabledWallets.wallets
.filter((name, idx) => disabledWallets.wallets.indexOf(name) === idx)
.sort();
return disabledWallets;
}