Logout Prompt on Navigation (#540)

* Update TODO comments & Remove old TODO comments

* Add NavigationPrompts to WalletDecrypt

* Remove commented code

* Move NavigationPrompt & Remove formatting diffs

* Remove formating diff

* Bind WalletDecrypt action creators & Add new selector for readonly wallet
This commit is contained in:
James Prado 2017-12-11 15:17:44 -05:00 committed by Daniel Ternyak
parent 72e30643a9
commit e6a958d6c1
10 changed files with 215 additions and 266 deletions

View File

@ -116,7 +116,6 @@ export default class Header extends Component<Props, State> {
<section className="Header-branding">
<section className="Header-branding-inner container">
<Link to="/" className="Header-branding-title" aria-label="Go to homepage">
{/* TODO - don't hardcode image path*/}
<img
className="Header-branding-title-logo"
src={logo}

View File

@ -35,10 +35,7 @@ class NavigationPrompt extends React.Component<Props, State> {
public setupUnblock() {
this.unblock = this.injected.history.block(nextLocation => {
if (
this.props.when &&
nextLocation.pathname !== this.injected.location.pathname
) {
if (this.props.when && nextLocation.pathname !== this.injected.location.pathname) {
this.setState({
openModal: true,
nextLocation
@ -92,9 +89,7 @@ class NavigationPrompt extends React.Component<Props, State> {
handleClose={this.onCancel}
buttons={buttons}
>
<p>
Leaving this page will log you out. Are you sure you want to continue?
</p>
<p>Leaving this page will log you out. Are you sure you want to continue?</p>
</Modal>
);
}

View File

@ -1,18 +1,21 @@
import {
setWallet,
TSetWallet,
unlockKeystore,
UnlockKeystoreAction,
TUnlockKeystore,
unlockMnemonic,
UnlockMnemonicAction,
TUnlockMnemonic,
unlockPrivateKey,
UnlockPrivateKeyAction,
unlockWeb3
TUnlockPrivateKey,
unlockWeb3,
TUnlockWeb3,
resetWallet,
TResetWallet
} from 'actions/wallet';
import isEmpty from 'lodash/isEmpty';
import map from 'lodash/map';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import translate from 'translations';
import KeystoreDecrypt from './Keystore';
import LedgerNanoSDecrypt from './LedgerNano';
@ -24,77 +27,20 @@ import { AppState } from 'reducers';
import Web3Decrypt from './Web3';
import Help from 'components/ui/Help';
import { knowledgeBaseURL } from 'config/data';
const WALLETS = {
web3: {
lid: 'x_MetaMask',
component: Web3Decrypt,
initialParams: {},
unlock: unlockWeb3,
helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask`
},
'ledger-nano-s': {
lid: 'x_Ledger',
component: LedgerNanoSDecrypt,
initialParams: {},
unlock: setWallet,
helpLink:
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
},
trezor: {
lid: 'x_Trezor',
component: TrezorDecrypt,
initialParams: {},
unlock: setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
},
'keystore-file': {
lid: 'x_Keystore2',
component: KeystoreDecrypt,
initialParams: {
file: '',
password: ''
},
unlock: unlockKeystore,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
initialParams: {},
unlock: unlockMnemonic,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'private-key': {
lid: 'x_PrivKey2',
component: PrivateKeyDecrypt,
initialParams: {
key: '',
password: ''
},
unlock: unlockPrivateKey,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'view-only': {
lid: 'View with Address Only',
component: ViewOnlyDecrypt,
initialParams: {},
unlock: setWallet,
helpLink: ''
}
};
import NavigationPrompt from './NavigationPrompt';
import { IWallet } from 'libs/wallet';
type UnlockParams = {} | PrivateKeyValue;
interface Props {
// FIXME
dispatch: Dispatch<UnlockKeystoreAction | UnlockMnemonicAction | UnlockPrivateKeyAction>;
unlockKeystore: TUnlockKeystore;
unlockMnemonic: TUnlockMnemonic;
unlockPrivateKey: TUnlockPrivateKey;
setWallet: TSetWallet;
unlockWeb3: TUnlockWeb3;
resetWallet: TResetWallet;
wallet: IWallet;
hidden?: boolean;
offline: boolean;
allowReadOnly?: boolean;
}
@ -105,19 +51,82 @@ interface State {
}
export class WalletDecrypt extends Component<Props, State> {
public WALLETS = {
web3: {
lid: 'x_MetaMask',
component: Web3Decrypt,
initialParams: {},
unlock: this.props.unlockWeb3,
helpLink: `${knowledgeBaseURL}/migration/moving-from-private-key-to-metamask`
},
'ledger-nano-s': {
lid: 'x_Ledger',
component: LedgerNanoSDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink:
'https://ledger.zendesk.com/hc/en-us/articles/115005200009-How-to-use-MyEtherWallet-with-Ledger'
},
trezor: {
lid: 'x_Trezor',
component: TrezorDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: 'https://doc.satoshilabs.com/trezor-apps/mew.html'
},
'keystore-file': {
lid: 'x_Keystore2',
component: KeystoreDecrypt,
initialParams: {
file: '',
password: ''
},
unlock: this.props.unlockKeystore,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
initialParams: {},
unlock: this.props.unlockMnemonic,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'private-key': {
lid: 'x_PrivKey2',
component: PrivateKeyDecrypt,
initialParams: {
key: '',
password: ''
},
unlock: this.props.unlockPrivateKey,
helpLink: `${
knowledgeBaseURL
}/private-keys-passwords/difference-beween-private-key-and-keystore-file.html`
},
'view-only': {
lid: 'View with Address Only',
component: ViewOnlyDecrypt,
initialParams: {},
unlock: this.props.setWallet,
helpLink: ''
}
};
public state: State = {
selectedWalletKey: 'keystore-file',
value: WALLETS['keystore-file'].initialParams
value: this.WALLETS['keystore-file'].initialParams
};
public getDecryptionComponent() {
const { selectedWalletKey, value } = this.state;
const selectedWallet = WALLETS[selectedWalletKey];
const selectedWallet = this.WALLETS[selectedWalletKey];
if (!selectedWallet) {
return null;
}
return (
<selectedWallet.component value={value} onChange={this.onChange} onUnlock={this.onUnlock} />
);
@ -129,7 +138,7 @@ export class WalletDecrypt extends Component<Props, State> {
}
public buildWalletOptions() {
return map(WALLETS, (wallet, key) => {
return map(this.WALLETS, (wallet, key) => {
const { helpLink } = wallet;
const isSelected = this.state.selectedWalletKey === key;
const isDisabled =
@ -156,7 +165,7 @@ export class WalletDecrypt extends Component<Props, State> {
}
public handleDecryptionChoiceChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const wallet = WALLETS[(event.target as HTMLInputElement).value];
const wallet = this.WALLETS[(event.target as HTMLInputElement).value];
if (!wallet) {
return;
@ -169,33 +178,37 @@ export class WalletDecrypt extends Component<Props, State> {
};
public render() {
const { wallet, hidden } = this.props;
const decryptionComponent = this.getDecryptionComponent();
const unlocked = !!wallet;
return (
<article className="Tab-content-pane row">
<section className="col-md-4 col-sm-6">
<h4>{translate('decrypt_Access')}</h4>
{this.buildWalletOptions()}
</section>
{decryptionComponent}
{!!(this.state.value as PrivateKeyValue).valid && (
<div>
<NavigationPrompt when={unlocked} onConfirm={this.props.resetWallet} />
<article hidden={hidden} className="Tab-content-pane row">
<section className="col-md-4 col-sm-6">
<h4 id="uploadbtntxt-wallet">{translate('ADD_Label_6')}</h4>
<div className="form-group">
<a
tabIndex={0}
role="button"
className="btn btn-primary btn-block"
onClick={this.onUnlock}
>
{translate('ADD_Label_6_short')}
</a>
</div>
<h4>{translate('decrypt_Access')}</h4>
{this.buildWalletOptions()}
</section>
)}
</article>
{decryptionComponent}
{!!(this.state.value as PrivateKeyValue).valid && (
<section className="col-md-4 col-sm-6">
<h4 id="uploadbtntxt-wallet">{translate('ADD_Label_6')}</h4>
<div className="form-group">
<a
tabIndex={0}
role="button"
className="btn btn-primary btn-block"
onClick={this.onUnlock}
>
{translate('ADD_Label_6_short')}
</a>
</div>
</section>
)}
</article>
</div>
);
}
@ -207,14 +220,22 @@ export class WalletDecrypt extends Component<Props, State> {
// some components (TrezorDecrypt) don't take an onChange prop, and thus this.state.value will remain unpopulated.
// in this case, we can expect the payload to contain the unlocked wallet info.
const unlockValue = this.state.value && !isEmpty(this.state.value) ? this.state.value : payload;
this.props.dispatch(WALLETS[this.state.selectedWalletKey].unlock(unlockValue));
this.WALLETS[this.state.selectedWalletKey].unlock(unlockValue);
};
}
function mapStateToProps(state: AppState) {
return {
offline: state.config.offline
offline: state.config.offline,
wallet: state.wallet.inst
};
}
export default connect(mapStateToProps)(WalletDecrypt);
export default connect(mapStateToProps, {
unlockKeystore,
unlockMnemonic,
unlockPrivateKey,
unlockWeb3,
setWallet,
resetWallet
})(WalletDecrypt);

View File

@ -4,13 +4,7 @@ import Spinner from './Spinner';
const DEFAULT_BUTTON_TYPE = 'primary';
const DEFAULT_BUTTON_SIZE = 'lg';
type ButtonType =
| 'default'
| 'primary'
| 'success'
| 'info'
| 'warning'
| 'danger';
type ButtonType = 'default' | 'primary' | 'success' | 'info' | 'warning' | 'danger';
type ButtonSize = 'lg' | 'sm' | 'xs';
interface Props {
@ -25,19 +19,15 @@ interface Props {
export default class SimpleButton extends Component<Props, {}> {
public computedClass = () => {
return `btn btn-${this.props.size || DEFAULT_BUTTON_TYPE} btn-${this.props
.type || DEFAULT_BUTTON_SIZE}`;
return `btn btn-${this.props.size || DEFAULT_BUTTON_TYPE} btn-${this.props.type ||
DEFAULT_BUTTON_SIZE}`;
};
public render() {
const { loading, disabled, loadingText, text, onClick } = this.props;
return (
<div>
<button
onClick={onClick}
disabled={loading || disabled}
className={this.computedClass()}
>
<button onClick={onClick} disabled={loading || disabled} className={this.computedClass()}>
{loading ? (
<div>
<Spinner /> {loadingText || text}

View File

@ -14,17 +14,12 @@ interface State {
}
export class UnlockHeader extends React.Component<Props, State> {
public state = {
expanded: !this.props.wallet
expanded: !!this.props.wallet
};
public componentDidUpdate(prevProps: Props) {
if (this.props.wallet && this.props.wallet !== prevProps.wallet) {
this.setState({ expanded: false });
}
// not sure if could happen
if (!this.props.wallet && this.props.wallet !== prevProps.wallet) {
this.setState({ expanded: true });
this.setState({ expanded: !this.state.expanded });
}
}
@ -38,8 +33,7 @@ export class UnlockHeader extends React.Component<Props, State> {
</a>
<h1>{title}</h1>
</div>
{this.state.expanded && <WalletDecrypt allowReadOnly={allowReadOnly} />}
{this.state.expanded && <hr />}
<WalletDecrypt hidden={this.state.expanded} allowReadOnly={allowReadOnly} />
</article>
);
}

View File

@ -14,9 +14,7 @@ export interface Props {
txCompare: React.ReactElement<TTxCompare> | null;
displayModal: boolean;
deployModal: React.ReactElement<TTxModal> | null;
handleInput(
input: string
): (ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
handleInput(input: string): (ev: React.FormEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
handleSignTx(): Promise<void>;
handleDeploy(): void;
}
@ -40,9 +38,7 @@ const Deploy = (props: Props) => {
<div className="Deploy">
<section>
<label className="Deploy-field form-group">
<h4 className="Deploy-field-label">
{translate('CONTRACT_ByteCode')}
</h4>
<h4 className="Deploy-field-label">{translate('CONTRACT_ByteCode')}</h4>
<textarea
name="byteCode"
placeholder="0x8f87a973e..."
@ -67,7 +63,7 @@ const Deploy = (props: Props) => {
/>
</label>
{walletExists ? (
{walletExists && (
<button
className="Sign-submit btn btn-primary"
disabled={!showSignTxButton}
@ -75,17 +71,13 @@ const Deploy = (props: Props) => {
>
{translate('DEP_signtx')}
</button>
) : (
<WalletDecrypt />
)}
<WalletDecrypt hidden={walletExists} />
{txCompare ? (
<section>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={handleDeploy}
>
<button className="Deploy-submit btn btn-primary" onClick={handleDeploy}>
{translate('NAV_DeployContract')}
</button>
</section>

View File

@ -49,12 +49,7 @@ export default class InteractExplorer extends Component<Props, State> {
};
public render() {
const {
inputs,
outputs,
selectedFunction,
selectedFunctionName
} = this.state;
const { inputs, outputs, selectedFunction, selectedFunctionName } = this.state;
const {
address,
@ -96,15 +91,10 @@ export default class InteractExplorer extends Component<Props, State> {
const { type, name } = input;
return (
<label
key={name}
className="InteractExplorer-func-in form-group"
>
<label key={name} className="InteractExplorer-func-in form-group">
<h4 className="InteractExplorer-func-in-label">
{name}
<span className="InteractExplorer-func-in-label-type">
{type}
</span>
<span className="InteractExplorer-func-in-label-type">{type}</span>
</h4>
<input
className="InteractExplorer-func-in-input form-control"
@ -120,15 +110,10 @@ export default class InteractExplorer extends Component<Props, State> {
const parsedName = name === '' ? index : name;
return (
<label
key={parsedName}
className="InteractExplorer-func-out form-group"
>
<label key={parsedName} className="InteractExplorer-func-out form-group">
<h4 className="InteractExplorer-func-out-label">
{name}
<span className="InteractExplorer-func-out-label-type">
{type}
</span>
<span className="InteractExplorer-func-out-label-type">{type}</span>
</h4>
<input
className="InteractExplorer-func-out-input form-control"
@ -146,69 +131,52 @@ export default class InteractExplorer extends Component<Props, State> {
>
{translate('CONTRACT_Read')}
</button>
) : walletDecrypted ? (
!txGenerated ? (
<Aux>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit}
onChange={handleInput('gasLimit')}
className={classnames(
'InteractExplorer-field-input',
'form-control',
{
'is-invalid': !validGasLimit
}
)}
/>
</label>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4>
<UnitConverter
decimal={getDecimal('ether')}
onChange={handleInput('value')}
>
{({ convertedUnit, onUserInput }) => (
<input
name="value"
value={convertedUnit}
onChange={onUserInput}
placeholder="0"
className={classnames(
'InteractExplorer-field-input',
'form-control',
{
'is-invalid': !validValue
}
)}
/>
)}
</UnitConverter>
</label>
<button
className="InteractExplorer-func-submit btn btn-primary"
disabled={!showContractWrite}
onClick={handleFunctionSend(selectedFunction, inputs)}
>
{translate('CONTRACT_Write')}
</button>
</Aux>
) : (
<Aux>
{txCompare}
<button
className="Deploy-submit btn btn-primary"
onClick={toggleModal}
>
{translate('SEND_trans')}
</button>
</Aux>
)
) : !txGenerated ? (
<Aux>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Gas Limit</h4>
<input
name="gasLimit"
value={gasLimit}
onChange={handleInput('gasLimit')}
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !validGasLimit
})}
/>
</label>
<label className="InteractExplorer-field form-group">
<h4 className="InteractExplorer-field-label">Value</h4>
<UnitConverter decimal={getDecimal('ether')} onChange={handleInput('value')}>
{({ convertedUnit, onUserInput }) => (
<input
name="value"
value={convertedUnit}
onChange={onUserInput}
placeholder="0"
className={classnames('InteractExplorer-field-input', 'form-control', {
'is-invalid': !validValue
})}
/>
)}
</UnitConverter>
</label>
<button
className="InteractExplorer-func-submit btn btn-primary"
disabled={!showContractWrite}
onClick={handleFunctionSend(selectedFunction, inputs)}
>
{translate('CONTRACT_Write')}
</button>
</Aux>
) : (
<WalletDecrypt />
<Aux>
{txCompare}
<button className="Deploy-submit btn btn-primary" onClick={toggleModal}>
{translate('SEND_trans')}
</button>
</Aux>
)}
{<WalletDecrypt hidden={walletDecrypted} />}
</div>
)}
{displayModal && txModal}
@ -240,8 +208,7 @@ export default class InteractExplorer extends Component<Props, State> {
} catch (e) {
this.props.showNotification(
'warning',
`Function call error: ${(e as Error).message}` ||
'Invalid input parameters',
`Function call error: ${(e as Error).message}` || 'Invalid input parameters',
5000
);
}

View File

@ -13,7 +13,6 @@ import {
GasField
} from './components';
import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded';
import NavigationPrompt from './components/NavigationPrompt';
// CONFIG
import { donationAddressMap, NetworkConfig } from 'config/data';
// LIBS
@ -160,9 +159,7 @@ export class SendTransaction extends React.Component<Props, State> {
}
public shouldReEstimateGas(prevState) {
// TODO listen to gas price changes here
// TODO debounce the call
// handle gas estimation
// TODO listen to gas price changes here, debounce the call, and handle gas estimation
return (
// if any relevant fields changed
this.haveFieldsChanged(prevState) &&
@ -287,10 +284,6 @@ export class SendTransaction extends React.Component<Props, State> {
}
allowReadOnly={true}
/>
<NavigationPrompt
when={unlocked}
onConfirm={this.props.resetWallet}
/>
<div className="row">
{/* Send Form */}

View File

@ -6,12 +6,15 @@ import translate from 'translations';
import { showNotification, TShowNotification } from 'actions/notifications';
import { ISignedMessage } from 'libs/signing';
import { IFullWallet } from 'libs/wallet';
import FullWalletOnly from 'components/renderCbs/FullWalletOnly';
import { AppState } from 'reducers';
import SignButton from './SignButton';
import { isWalletFullyUnlocked } from 'selectors/wallet';
import './index.scss';
interface Props {
showNotification: TShowNotification;
wallet: IFullWallet;
unlocked: boolean;
}
interface State {
@ -31,8 +34,8 @@ export class SignMessage extends Component<Props, State> {
public state: State = initialState;
public render() {
const { wallet, unlocked } = this.props;
const { message, signedMessage } = this.state;
const messageBoxClass = classnames([
'SignMessage-inputBox',
'form-control',
@ -53,10 +56,15 @@ export class SignMessage extends Component<Props, State> {
<div className="SignMessage-help">{translate('MSG_info2')}</div>
</div>
<FullWalletOnly
withFullWallet={this.renderSignButton}
withoutFullWallet={this.renderUnlock}
/>
{unlocked && (
<SignButton
wallet={wallet}
message={this.state.message}
showNotification={this.props.showNotification}
onSignMessage={this.onSignMessage}
/>
)}
<WalletDecrypt hidden={unlocked} />
{!!signedMessage && (
<div>
@ -84,21 +92,11 @@ export class SignMessage extends Component<Props, State> {
private onSignMessage = (signedMessage: ISignedMessage) => {
this.setState({ signedMessage });
};
private renderSignButton = (fullWallet: IFullWallet) => {
return (
<SignButton
wallet={fullWallet}
message={this.state.message}
showNotification={this.props.showNotification}
onSignMessage={this.onSignMessage}
/>
);
};
private renderUnlock() {
return <WalletDecrypt />;
}
}
export default connect(null, { showNotification })(SignMessage);
const mapStateToProps = (state: AppState) => ({
wallet: state.wallet.inst,
unlocked: isWalletFullyUnlocked(state)
});
export default connect(mapStateToProps, { showNotification })(SignMessage);

View File

@ -9,6 +9,10 @@ export function getWalletInst(state: AppState): IWallet | null | undefined {
return state.wallet.inst;
}
export function isWalletFullyUnlocked(state: AppState): boolean | null | undefined {
return state.wallet.inst && !state.wallet.inst.isReadOnly;
}
export interface TokenBalance {
symbol: string;
balance: TokenValue;
@ -42,9 +46,7 @@ export function getTokenBalances(state: AppState): TokenBalance[] {
balance: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol].balance
: TokenValue('0'),
error: state.wallet.tokens[t.symbol]
? state.wallet.tokens[t.symbol].error
: null,
error: state.wallet.tokens[t.symbol] ? state.wallet.tokens[t.symbol].error : null,
custom: t.custom,
decimal: t.decimal
}));
@ -62,8 +64,6 @@ export function getTxFromBroadcastTransactionStatus(
transactions: BroadcastTransactionStatus[],
signedTx: string
): BroadcastTransactionStatus | null {
const tx = transactions.find(
transaction => transaction.signedTx === signedTx
);
const tx = transactions.find(transaction => transaction.signedTx === signedTx);
return tx || null;
}