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

View File

@ -12,16 +12,6 @@
}
}
@keyframes wallet-button-enter-disabled {
0% {
opacity: 0;
}
30%,
100% {
opacity: 0.3;
}
}
.WalletButton {
position: relative;
flex: 1;
@ -70,10 +60,17 @@
}
&.is-disabled {
opacity: 0.3 !important;
outline: none;
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 {

View File

@ -16,6 +16,7 @@ interface OwnProps {
isSecure?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
disableReason?: string;
onClick(walletType: string): void;
}
@ -23,6 +24,12 @@ interface StateProps {
isFormatDisabled?: boolean;
}
interface Icon {
icon: string;
tooltip: string;
href?: string;
}
type Props = OwnProps & StateProps;
export class WalletButton extends React.PureComponent<Props> {
@ -35,9 +42,37 @@ export class WalletButton extends React.PureComponent<Props> {
helpLink,
isSecure,
isReadOnly,
isDisabled
isDisabled,
disableReason
} = 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 (
<div
className={classnames({
@ -49,42 +84,32 @@ export class WalletButton extends React.PureComponent<Props> {
tabIndex={isDisabled ? -1 : 0}
aria-disabled={isDisabled}
>
<div className="WalletButton-title">
{icon && <img className="WalletButton-title-icon" src={icon} />}
<span>{name}</span>
<div className="WalletButton-inner">
<div className="WalletButton-title">
{icon && <img className="WalletButton-title-icon" src={icon} />}
<span>{name}</span>
</div>
{description && <div className="WalletButton-description">{description}</div>}
{example && <div className="WalletButton-example">{example}</div>}
<div className="WalletButton-icons">
{icons.map(i => (
<span className="WalletButton-icons-icon" key={i.icon} onClick={this.stopPropogation}>
{i.href ? (
<NewTabLink href={i.href} onClick={this.stopPropogation}>
<i className={`fa fa-${i.icon}`} />
</NewTabLink>
) : (
<i className={`fa fa-${i.icon}`} />
)}
{!isDisabled && <Tooltip size="sm">{i.tooltip}</Tooltip>}
</span>
))}
</div>
</div>
{description && <div className="WalletButton-description">{description}</div>}
{example && <div className="WalletButton-example">{example}</div>}
<div className="WalletButton-icons">
{isSecure ? (
<span className="WalletButton-icons-icon" onClick={this.stopPropogation}>
<i className="fa fa-shield" />
<Tooltip>{translateRaw('This wallet type is secure')}</Tooltip>
</span>
) : (
<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>
<Tooltip>{translateRaw('NAV_Help')}</Tooltip>
</span>
)}
</div>
{isDisabled && disableReason && <Tooltip>{disableReason}</Tooltip>}
</div>
);
}

View File

@ -1,15 +1,31 @@
import { MiscWalletName, SecureWalletName, WalletName } from 'config';
export interface DisabledWallets {
wallets: WalletName[];
reasons: {
[key: string]: string;
};
}
enum WalletMode {
READ_ONLY = 'READ_ONLY',
UNABLE_TO_SIGN = 'UNABLE_TO_SIGN',
ONLINE_ONLY = 'ONLINE_ONLY'
UNABLE_TO_SIGN = 'UNABLE_TO_SIGN'
}
const walletModes: { [key in WalletMode]: WalletName[] } = {
[WalletMode.READ_ONLY]: [MiscWalletName.VIEW_ONLY],
[WalletMode.UNABLE_TO_SIGN]: [SecureWalletName.TREZOR, MiscWalletName.VIEW_ONLY],
[WalletMode.ONLINE_ONLY]: [SecureWalletName.WEB3, SecureWalletName.TREZOR]
// Duplicating reasons is kind of tedious, but saves having to run through a
// bunch of loops to format it differently
export const DISABLE_WALLETS: { [key in WalletMode]: DisabledWallets } = {
[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';
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/mixins';
$tooltip-bg: rgba(#222, 0.95);
.Tooltip {
position: absolute;
top: 0;
left: 50%;
width: 220px;
color: #FFF;
font-size: $font-size-xs;
font-size: $font-size-small;
font-family: $font-family-sans-serif;
pointer-events: none;
opacity: 0;
@ -20,9 +22,9 @@
> span {
display: inline-block;
background: rgba(#000, 0.9);
border-radius: 2px;
padding: 4px 8px;
background: $tooltip-bg;
border-radius: 3px;
padding: 6px 10px;
&:after {
position: absolute;
@ -30,7 +32,36 @@
bottom: 0;
left: 50%;
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 classnames from 'classnames';
import './Tooltip.scss';
interface Props {
children: React.ReactElement<string> | string;
size?: 'sm' | 'md' | 'lg';
}
const Tooltip: React.SFC<Props> = ({ children }) => (
<div className="Tooltip">
const Tooltip: React.SFC<Props> = ({ size, children }) => (
<div
className={classnames({
Tooltip: true,
[`is-size-${size}`]: !!size
})}
>
<span className="Tooltip-text">{children}</span>
</div>
);

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { TokenValue, Wei } from 'libs/units';
import { Token } from 'config';
import { Token, SecureWalletName, WalletName } from 'config';
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 { isEtherTransaction, getUnit } from './transaction';
import { unSupportedWalletFormatsOnNetwork } from 'utils/network';
import { DisabledWallets } from 'components/WalletDecrypt';
export function getWalletInst(state: AppState): IWallet | null | undefined {
return state.wallet.inst;
@ -139,3 +141,56 @@ export function getShownTokenBalances(
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;
}