Generate Mnemonic Wallet (#659)

* Initial work at splitting out generate into two flows.

* Finish mnemonic flow.

* Convert keystore to state-based component. Remove all redux generate stuff. Remove generate help section. Fix styles.

* Add back button, switch to routing instead of state for generate pages.

* PR feedback.

* Alertify warning at generate. Linkify alternatives. Fix some alert link styles.
This commit is contained in:
William O'Beirne 2017-12-28 14:54:07 -05:00 committed by Daniel Ternyak
parent a9af8b6caf
commit 6513acd03d
45 changed files with 1013 additions and 640 deletions

View File

@ -45,6 +45,10 @@ export default class Root extends Component<Props, State> {
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/generate" component={GenerateWallet}>
<Route path="keystore" component={GenerateWallet} />
<Route path="mnemonic" component={GenerateWallet} />
</Route>
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/account" component={SendTransaction}>

View File

@ -1,22 +0,0 @@
import { generate } from 'ethereumjs-wallet';
import * as interfaces from './actionTypes';
import { TypeKeys } from './constants';
export type TGenerateNewWallet = typeof generateNewWallet;
export function generateNewWallet(password: string): interfaces.GenerateNewWalletAction {
return {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET,
wallet: generate(),
password
};
}
export type TContinueToPaper = typeof continueToPaper;
export function continueToPaper(): interfaces.ContinueToPaperAction {
return { type: TypeKeys.GENERATE_WALLET_CONTINUE_TO_PAPER };
}
export type TResetGenerateWallet = typeof resetGenerateWallet;
export function resetGenerateWallet(): interfaces.ResetGenerateWalletAction {
return { type: TypeKeys.GENERATE_WALLET_RESET };
}

View File

@ -1,25 +0,0 @@
import { IFullWallet } from 'ethereumjs-wallet';
import { TypeKeys } from './constants';
/*** Generate Wallet File ***/
export interface GenerateNewWalletAction {
type: TypeKeys.GENERATE_WALLET_GENERATE_WALLET;
wallet: IFullWallet;
password: string;
}
/*** Reset Generate Wallet ***/
export interface ResetGenerateWalletAction {
type: TypeKeys.GENERATE_WALLET_RESET;
}
/*** Confirm Continue To Paper ***/
export interface ContinueToPaperAction {
type: TypeKeys.GENERATE_WALLET_CONTINUE_TO_PAPER;
}
/*** Action Union ***/
export type GenerateWalletAction =
| GenerateNewWalletAction
| ContinueToPaperAction
| ResetGenerateWalletAction;

View File

@ -1,5 +0,0 @@
export enum TypeKeys {
GENERATE_WALLET_GENERATE_WALLET = 'GENERATE_WALLET_GENERATE_WALLET',
GENERATE_WALLET_CONTINUE_TO_PAPER = 'GENERATE_WALLET_CONTINUE_TO_PAPER',
GENERATE_WALLET_RESET = 'GENERATE_WALLET_RESET'
}

View File

@ -1,3 +0,0 @@
export * from './constants';
export * from './actionTypes';
export * from './actionCreators';

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -7,7 +7,7 @@ import './Navigation.scss';
const tabs = [
{
name: 'NAV_GenerateWallet',
to: '/'
to: '/generate'
},
{
@ -90,7 +90,7 @@ export default class Navigation extends Component<Props, State> {
<div className="Navigation-scroll container">
<ul className="Navigation-links">
{tabs.map(link => {
return <NavigationLink key={link.name} link={link} />;
return <NavigationLink key={link.name} link={link} isHomepage={link === tabs[0]} />;
})}
</ul>
</div>

View File

@ -10,16 +10,25 @@ interface Props extends RouteComponentProps<{}> {
to?: string;
external?: boolean;
};
isHomepage: boolean;
}
class NavigationLink extends React.Component<Props, {}> {
public render() {
const { link, location } = this.props;
const { link, location, isHomepage } = this.props;
// isActive if
// 1) Current path is the same as link
// 2) the first path is the same for both links (/account and /account/send)
// 3) we're at the root path and this is the "homepage" nav item
const isActive =
location.pathname === link.to ||
(link.to && location.pathname.split('/')[1] === link.to.split('/')[1]) ||
(isHomepage && location.pathname === '/');
const linkClasses = classnames({
'NavigationLink-link': true,
'is-disabled': !link.to,
'is-active': location.pathname === link.to
'is-active': isActive
});
const linkLabel = `nav item: ${translateRaw(link.name)}`;

View File

@ -1,7 +1,7 @@
import { PaperWallet } from 'components';
import { IFullWallet } from 'ethereumjs-wallet';
import React from 'react';
import translate from 'translations';
import { translateRaw } from 'translations';
import printElement from 'utils/printElement';
import { stripHexPrefix } from 'libs/values';
@ -39,13 +39,13 @@ const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
<PaperWallet address={address} privateKey={privateKey} />
<a
role="button"
aria-label={translate('x_Print')}
aria-label={translateRaw('x_Print')}
aria-describedby="x_PrintDesc"
className={'btn btn-lg btn-primary'}
className="btn btn-lg btn-primary btn-block"
onClick={print(address, privateKey)}
style={{ marginTop: 10 }}
style={{ margin: '10px auto 0', maxWidth: '260px' }}
>
{translate('x_Print')}
{translateRaw('x_Print')}
</a>
</div>
);

View File

@ -36,7 +36,7 @@ interface NewTabLinkProps extends AAttributes {
const NewTabLink = ({ content, children, ...rest }: NewTabLinkProps) => (
<a target="_blank" rel="noopener" {...rest}>
{content || children} {/* Keep content for short-hand text insertion */}
{content || children}
</a>
);

View File

@ -0,0 +1,37 @@
import React, { Component } from 'react';
import Keystore from './components/Keystore';
import Mnemonic from './components/Mnemonic';
import WalletTypes from './components/WalletTypes';
import CryptoWarning from './components/CryptoWarning';
import TabSection from 'containers/TabSection';
import { RouteComponentProps } from 'react-router-dom';
export enum WalletType {
Keystore = 'keystore',
Mnemonic = 'mnemonic'
}
export default class GenerateWallet extends Component<RouteComponentProps<{}>> {
public render() {
const walletType = this.props.location.pathname.split('/')[2];
let content;
if (window.crypto) {
if (walletType === WalletType.Mnemonic) {
content = <Mnemonic />;
} else if (walletType === WalletType.Keystore) {
content = <Keystore />;
} else {
content = <WalletTypes />;
}
} else {
content = <CryptoWarning />;
}
return (
<TabSection>
<section className="Tab-content">{content}</section>
</TabSection>
);
}
}

View File

@ -1,135 +0,0 @@
import { ContinueToPaperAction } from 'actions/generateWallet';
import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
import { toChecksumAddress } from 'ethereumjs-util';
import { NewTabLink } from 'components/ui';
import React, { Component } from 'react';
import translate from 'translations';
import { makeBlob } from 'utils/blob';
import './DownloadWallet.scss';
import Template from './Template';
import { N_FACTOR, knowledgeBaseURL } from 'config/data';
interface Props {
wallet: IFullWallet;
password: string;
continueToPaper(): ContinueToPaperAction;
}
interface State {
hasDownloadedWallet: boolean;
keystore: IV3Wallet | null;
}
export default class DownloadWallet extends Component<Props, State> {
public state: State = {
hasDownloadedWallet: false,
keystore: null
};
public componentWillMount() {
this.setWallet(this.props.wallet, this.props.password);
}
public componentWillUpdate(nextProps: Props) {
if (this.props.wallet !== nextProps.wallet) {
this.setWallet(nextProps.wallet, nextProps.password);
}
}
public render() {
const { hasDownloadedWallet } = this.state;
const filename = this.props.wallet.getV3Filename();
const content = (
<div className="DlWallet">
<h1 className="DlWallet-title">{translate('GEN_Label_2')}</h1>
<a
role="button"
className="DlWallet-download btn btn-primary btn-lg"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby={translate('x_KeystoreDesc')}
download={filename}
href={this.getBlob()}
onClick={this.handleDownloadKeystore}
>
{translate('x_Download')} {translate('x_Keystore2')}
</a>
<div className="DlWallet-warning">
<p>
<strong>Do not lose it!</strong> It cannot be recovered if you lose it.
</p>
<p>
<strong>Do not share it!</strong> Your funds will be stolen if you use this file on a
malicious/phishing site.
</p>
<p>
<strong>Make a backup!</strong> Secure it like the millions of dollars it may one day be
worth.
</p>
</div>
<button
className="DlWallet-continue btn btn-danger"
role="button"
onClick={this.handleContinue}
disabled={!hasDownloadedWallet}
>
I understand. Continue.
</button>
</div>
);
const help = (
<div>
<h4>{translate('GEN_Help_8')}</h4>
<ul>
<li>{translate('GEN_Help_9')}</li>
<li> {translate('GEN_Help_10')}</li>
<input value={filename} className="form-control input-sm" disabled={true} />
</ul>
<h4>{translate('GEN_Help_11')}</h4>
<ul>
<li>{translate('GEN_Help_12')}</li>
</ul>
<h4>{translate('GEN_Help_4')}</h4>
<ul>
<li>
<NewTabLink href={`${knowledgeBaseURL}/getting-started/backing-up-your-new-wallet`}>
<strong>{translate('GEN_Help_13')}</strong>
</NewTabLink>
</li>
<li>
<NewTabLink
href={`${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file`}
>
<strong>{translate('GEN_Help_14')}</strong>
</NewTabLink>
</li>
</ul>
</div>
);
return <Template content={content} help={help} />;
}
public getBlob = () =>
(this.state.keystore && makeBlob('text/json;charset=UTF-8', this.state.keystore)) || undefined;
private markDownloaded = () =>
this.state.keystore && this.setState({ hasDownloadedWallet: true });
private handleContinue = () => this.state.hasDownloadedWallet && this.props.continueToPaper();
private setWallet(wallet: IFullWallet, password: string) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
keystore.address = toChecksumAddress(keystore.address);
this.setState({ keystore });
}
private handleDownloadKeystore = (e: React.FormEvent<HTMLAnchorElement>) =>
this.state.keystore ? this.markDownloaded() : e.preventDefault();
}

View File

@ -1,137 +0,0 @@
import { GenerateNewWalletAction } from 'actions/generateWallet';
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import translate from 'translations';
import { knowledgeBaseURL, MINIMUM_PASSWORD_LENGTH } from 'config/data';
import './EnterPassword.scss';
import PasswordInput from './PasswordInput';
import Template from './Template';
interface Props {
generateNewWallet(pw: string): GenerateNewWalletAction;
}
interface State {
fileName: null | string;
blobURI: null | string;
password: string;
isPasswordValid: boolean;
isPasswordVisible: boolean;
}
export default class EnterPassword extends Component<Props, State> {
public state = {
fileName: null,
blobURI: null,
password: '',
isPasswordValid: false,
isPasswordVisible: false
};
public render() {
const { password, isPasswordValid, isPasswordVisible } = this.state;
const content = (
<div className="EnterPw">
<h1 className="EnterPw-title" aria-live="polite">
{translate('NAV_GenerateWallet')}
</h1>
<label className="EnterPw-password">
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
<PasswordInput
password={password}
onPasswordChange={this.onPasswordChange}
isPasswordVisible={isPasswordVisible}
togglePassword={this.togglePassword}
isPasswordValid={isPasswordValid}
/>
</label>
<button
onClick={this.onClickGenerateFile}
disabled={!isPasswordValid}
className="EnterPw-submit btn btn-primary btn-block"
>
{translate('NAV_GenerateWallet')}
</button>
<p className="EnterPw-warning">{translate('x_PasswordDesc')}</p>
</div>
);
const help = (
<div>
<h4>Ledger / TREZOR:</h4>
<ul>
<li>
<span>{translate('GEN_Help_1')}</span>
<Link to="/send-transaction"> Ledger or TREZOR or Digital Bitbox</Link>
<span> {translate('GEN_Help_2')}</span>
<span> {translate('GEN_Help_3')}</span>
</li>
</ul>
<h4>Jaxx / Metamask:</h4>
<ul>
<li>
<span>{translate('GEN_Help_1')}</span>
<Link to="/send-transaction"> {translate('x_Mnemonic')}</Link>
<span> {translate('GEN_Help_2')}</span>
</li>
</ul>
<h4>Mist / Geth / Parity:</h4>
<ul>
<li>
<span>{translate('GEN_Help_1')}</span>
<Link to="/send-transaction"> {translate('x_Keystore2')}</Link>
<span> {translate('GEN_Help_2')}</span>
</li>
</ul>
<h4>Guides & FAQ</h4>
<ul>
<li>
<strong>
<a
href={`${knowledgeBaseURL}/getting-started/creating-a-new-wallet-on-myetherwallet`}
target="_blank"
rel="noopener"
>
{translate('GEN_Help_5')}
</a>
</strong>
</li>
<li>
<strong>
<a
href={`${knowledgeBaseURL}/getting-started/getting-started-new`}
target="_blank"
rel="noopener"
>
{translate('GEN_Help_6')}
</a>
</strong>
</li>
</ul>
</div>
);
return <Template content={content} help={help} />;
}
private onClickGenerateFile = () => {
this.props.generateNewWallet(this.state.password);
this.setState({ password: '' });
};
private togglePassword = () => {
this.setState({ isPasswordVisible: !this.state.isPasswordVisible });
};
private onPasswordChange = (e: any) => {
const password = e.target.value;
this.setState({
isPasswordValid: password.length >= MINIMUM_PASSWORD_LENGTH,
password
});
};
}

View File

@ -0,0 +1,76 @@
@import 'common/sass/variables';
@import 'common/sass/mixins';
$step-number-size: 42px;
.FinalSteps {
&-help {
margin-bottom: $space;
}
&-steps {
margin-bottom: $space;
}
&-buttons {
&-btn {
width: 100%;
max-width: 320px;
}
}
}
.StepBox {
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&-title {
margin-bottom: $space-md;
@include ellipsis;
}
&-screen {
position: relative;
width: 100%;
max-width: 340px;
margin: 0 auto $space * 2;
// Keeps box height at a .8 ratio to width
&:after {
content: '';
display: block;
padding-top: 75%;
}
&-img {
position: absolute;
width: 100%;
top: 0;
left: 0;
bottom: 0;
background: #EEE;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(#000, 0.3);
}
&-number {
position: absolute;
bottom: -10px;
right: -10px;
width: $step-number-size;
height: $step-number-size;
line-height: $step-number-size;
font-size: 22px;
color: #FFF;
text-align: center;
background: $ether-navy;
box-shadow: 0 1px 2px rgba(#000, .3);
border-radius: 100%;
}
}
}

View File

@ -0,0 +1,76 @@
import React from 'react';
import { Link } from 'react-router-dom';
import translate from 'translations';
import { WalletType } from '../GenerateWallet';
import SiteImage from 'assets/images/unlock-guide/site.png';
import TabImage from 'assets/images/unlock-guide/tab.png';
import SelectKeystoreImage from 'assets/images/unlock-guide/select-keystore.png';
import ProvideKeystoreImage from 'assets/images/unlock-guide/provide-keystore.png';
import SelectMnemonicImage from 'assets/images/unlock-guide/select-mnemonic.png';
import ProvideMnemonicImage from 'assets/images/unlock-guide/provide-mnemonic.png';
import './FinalSteps.scss';
interface Props {
walletType: WalletType;
}
const FinalSteps: React.SFC<Props> = ({ walletType }) => {
const steps = [
{
name: 'Open MyEtherWallet',
image: SiteImage
},
{
name: 'Go to the account tab',
image: TabImage
}
];
if (walletType === WalletType.Keystore) {
steps.push({
name: 'Select your wallet type',
image: SelectKeystoreImage
});
steps.push({
name: 'Provide file & password',
image: ProvideKeystoreImage
});
} else if (walletType === WalletType.Mnemonic) {
steps.push({
name: 'Select your wallet type',
image: SelectMnemonicImage
});
steps.push({
name: 'Enter your phrase',
image: ProvideMnemonicImage
});
}
return (
<div className="FinalSteps">
<h1 className="FinalSteps-title">{translate('ADD_Label_6')}</h1>
<p className="FinalSteps-help">
All done, youre now ready to access your wallet. Just follow these 4 steps whenever you
want to access your wallet.
</p>
<div className="FinalSteps-steps row">
{steps.map((step, index) => (
<div key={step.name} className="StepBox col-lg-3 col-sm-6 col-xs-12">
<h4 className="StepBox-title">{step.name}</h4>
<div className="StepBox-screen">
<img className="StepBox-screen-img" src={step.image} />
<div className="StepBox-screen-number">{index + 1}</div>
</div>
</div>
))}
</div>
<div className="FinalSteps-buttons">
<Link to="/account" className="FinalSteps-buttons-btn btn btn-primary btn-lg">
Go to Account
</Link>
</div>
</div>
);
};
export default FinalSteps;

View File

@ -0,0 +1,101 @@
import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
import { toChecksumAddress } from 'ethereumjs-util';
import React, { Component } from 'react';
import translate from 'translations';
import { makeBlob } from 'utils/blob';
import './DownloadWallet.scss';
import Template from '../Template';
import { N_FACTOR } from 'config/data';
interface Props {
wallet: IFullWallet;
password: string;
continue(): void;
}
interface State {
hasDownloadedWallet: boolean;
keystore: IV3Wallet | null;
}
export default class DownloadWallet extends Component<Props, State> {
public state: State = {
hasDownloadedWallet: false,
keystore: null
};
public componentWillMount() {
this.setWallet(this.props.wallet, this.props.password);
}
public componentWillUpdate(nextProps: Props) {
if (this.props.wallet !== nextProps.wallet) {
this.setWallet(nextProps.wallet, nextProps.password);
}
}
public render() {
const { hasDownloadedWallet } = this.state;
const filename = this.props.wallet.getV3Filename();
return (
<Template>
<div className="DlWallet">
<h1 className="DlWallet-title">{translate('GEN_Label_2')}</h1>
<a
role="button"
className="DlWallet-download btn btn-primary btn-lg"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby={translate('x_KeystoreDesc')}
download={filename}
href={this.getBlob()}
onClick={this.handleDownloadKeystore}
>
{translate('x_Download')} {translate('x_Keystore2')}
</a>
<div className="DlWallet-warning">
<p>
<strong>Do not lose it!</strong> It cannot be recovered if you lose it.
</p>
<p>
<strong>Do not share it!</strong> Your funds will be stolen if you use this file on a
malicious/phishing site.
</p>
<p>
<strong>Make a backup!</strong> Secure it like the millions of dollars it may one day
be worth.
</p>
</div>
<button
className="DlWallet-continue btn btn-danger"
role="button"
onClick={this.handleContinue}
disabled={!hasDownloadedWallet}
>
I understand. Continue.
</button>
</div>
</Template>
);
}
public getBlob = () =>
(this.state.keystore && makeBlob('text/json;charset=UTF-8', this.state.keystore)) || undefined;
private markDownloaded = () =>
this.state.keystore && this.setState({ hasDownloadedWallet: true });
private handleContinue = () => this.state.hasDownloadedWallet && this.props.continue();
private setWallet(wallet: IFullWallet, password: string) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
keystore.address = toChecksumAddress(keystore.address);
this.setState({ keystore });
}
private handleDownloadKeystore = (e: React.FormEvent<HTMLAnchorElement>) =>
this.state.keystore ? this.markDownloaded() : e.preventDefault();
}

View File

@ -0,0 +1,73 @@
import React, { Component } from 'react';
import translate from 'translations';
import { MINIMUM_PASSWORD_LENGTH } from 'config/data';
import './EnterPassword.scss';
import PasswordInput from './PasswordInput';
import Template from '../Template';
interface Props {
continue(pw: string): void;
}
interface State {
password: string;
isPasswordValid: boolean;
isPasswordVisible: boolean;
}
export default class EnterPassword extends Component<Props, State> {
public state = {
password: '',
isPasswordValid: false,
isPasswordVisible: false
};
public render() {
const { password, isPasswordValid, isPasswordVisible } = this.state;
return (
<Template>
<div className="EnterPw">
<h1 className="EnterPw-title" aria-live="polite">
Generate a {translate('x_Keystore2')}
</h1>
<label className="EnterPw-password">
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
<PasswordInput
password={password}
onPasswordChange={this.onPasswordChange}
isPasswordVisible={isPasswordVisible}
togglePassword={this.togglePassword}
isPasswordValid={isPasswordValid}
/>
</label>
<button
onClick={this.onClickGenerateFile}
disabled={!isPasswordValid}
className="EnterPw-submit btn btn-primary btn-block"
>
{translate('NAV_GenerateWallet')}
</button>
<p className="EnterPw-warning">{translate('x_PasswordDesc')}</p>
</div>
</Template>
);
}
private onClickGenerateFile = () => {
this.props.continue(this.state.password);
};
private togglePassword = () => {
this.setState({ isPasswordVisible: !this.state.isPasswordVisible });
};
private onPasswordChange = (e: any) => {
const password = e.target.value;
this.setState({
isPasswordValid: password.length >= MINIMUM_PASSWORD_LENGTH,
password
});
};
}

View File

@ -0,0 +1,83 @@
import { generate, IFullWallet } from 'ethereumjs-wallet';
import React, { Component } from 'react';
import { WalletType } from '../../GenerateWallet';
import Template from '../Template';
import DownloadWallet from './DownloadWallet';
import EnterPassword from './EnterPassword';
import PaperWallet from './PaperWallet';
import FinalSteps from '../FinalSteps';
export enum Steps {
Password = 'password',
Download = 'download',
Paper = 'paper',
Final = 'final'
}
interface State {
activeStep: Steps;
password: string;
wallet: IFullWallet | null | undefined;
}
export default class GenerateKeystore extends Component<{}, State> {
public state: State = {
activeStep: Steps.Password,
password: '',
wallet: null
};
public render() {
const { activeStep, wallet, password } = this.state;
let content;
switch (activeStep) {
case Steps.Password:
content = <EnterPassword continue={this.generateWalletAndContinue} />;
break;
case Steps.Download:
if (wallet) {
content = (
<DownloadWallet wallet={wallet} password={password} continue={this.continueToPaper} />
);
}
break;
case Steps.Paper:
if (wallet) {
content = <PaperWallet wallet={wallet} continue={this.continueToFinal} />;
}
break;
case Steps.Final:
content = (
<Template>
<FinalSteps walletType={WalletType.Keystore} />
</Template>
);
break;
default:
content = <h1>Uh oh. Not sure how you got here.</h1>;
}
return content;
}
private generateWalletAndContinue = (password: string) => {
this.setState({
password,
activeStep: Steps.Download,
wallet: generate()
});
};
private continueToPaper = () => {
this.setState({ activeStep: Steps.Paper });
};
private continueToFinal = () => {
this.setState({ activeStep: Steps.Final });
};
}

View File

@ -6,7 +6,8 @@
}
&-private {
margin-bottom: $space * 3;
max-width: 700px;
margin: 0 auto $space * 3;
}
&-paper,

View File

@ -0,0 +1,57 @@
import PrintableWallet from 'components/PrintableWallet';
import { IFullWallet } from 'ethereumjs-wallet';
import React from 'react';
import translate from 'translations';
import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss';
import Template from '../Template';
interface Props {
wallet: IFullWallet;
continue(): void;
}
const PaperWallet: React.SFC<Props> = props => (
<Template>
<div className="GenPaper">
{/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<input
className="GenPaper-private form-control"
value={stripHexPrefix(props.wallet.getPrivateKeyString())}
aria-label={translate('x_PrivKey')}
aria-describedby="x_PrivKeyDesc"
type="text"
readOnly={true}
/>
{/* Download Paper Wallet */}
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
<div className="GenPaper-paper">
<PrintableWallet wallet={props.wallet} />
</div>
{/* Warning */}
<div className="GenPaper-warning">
<p>
<strong>Do not lose it!</strong> It cannot be recovered if you lose it.
</p>
<p>
<strong>Do not share it!</strong> Your funds will be stolen if you use this file on a
malicious/phishing site.
</p>
<p>
<strong>Make a backup!</strong> Secure it like the millions of dollars it may one day be
worth.
</p>
</div>
{/* Continue button */}
<button className="GenPaper-continue btn btn-default" onClick={props.continue}>
{translate('NAV_ViewWallet')}
</button>
</div>
</Template>
);
export default PaperWallet;

View File

@ -0,0 +1,2 @@
import Keystore from './Keystore';
export default Keystore;

View File

@ -0,0 +1,41 @@
@import 'common/sass/variables';
.GenerateMnemonic {
position: relative;
text-align: center;
&-title {
margin-bottom: $space-md;
}
&-help {
margin-bottom: $space * 2;
}
&-words {
display: flex;
margin: 0 auto $space;
justify-content: center;
flex-wrap: wrap;
&-column {
max-width: 320px;
margin: 0 $space-md $space-md;
}
}
&-buttons {
&-btn {
margin: 0 5px;
width: 100%;
max-width: 260px;
}
}
&-skip {
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
}
}

View File

@ -0,0 +1,153 @@
import React from 'react';
import { generateMnemonic } from 'bip39';
import translate from 'translations';
import Word from './Word';
import FinalSteps from '../FinalSteps';
import Template from '../Template';
import { WalletType } from '../../GenerateWallet';
import './Mnemonic.scss';
interface State {
words: string[];
confirmValues: string[];
isConfirming: boolean;
isConfirmed: boolean;
}
interface WordTuple {
word: string;
index: number;
}
export default class GenerateMnemonic extends React.Component<{}, State> {
public state: State = {
words: [],
confirmValues: [],
isConfirming: false,
isConfirmed: false
};
public componentDidMount() {
this.regenerateWordArray();
}
public render() {
const { words, isConfirming, isConfirmed } = this.state;
let content;
if (isConfirmed) {
content = <FinalSteps walletType={WalletType.Mnemonic} />;
} else {
const canContinue = this.checkCanContinue();
const firstHalf: WordTuple[] = [];
const lastHalf: WordTuple[] = [];
words.forEach((word, index) => {
if (index < words.length / 2) {
firstHalf.push({ word, index });
} else {
lastHalf.push({ word, index });
}
});
content = (
<div className="GenerateMnemonic">
<h1 className="GenerateMnemonic-title">Generate a {translate('x_Mnemonic')}</h1>
<p className="GenerateMnemonic-help">
{isConfirming
? `
Re-enter your phrase to confirm you copied it correctly. If you
forgot one of your words, just click the button beside the input
to reveal it.
`
: `
Write these words down. Do not copy them to your clipboard, or save
them anywhere online.
`}
</p>
<div className="GenerateMnemonic-words">
{[firstHalf, lastHalf].map((ws, i) => (
<div key={i} className="GenerateMnemonic-words-column">
{ws.map(this.makeWord)}
</div>
))}
</div>
<div className="GenerateMnemonic-buttons">
{!isConfirming && (
<button
className="GenerateMnemonic-buttons-btn btn btn-default"
onClick={this.regenerateWordArray}
>
<i className="fa fa-refresh" /> Regenerate Phrase
</button>
)}
<button
className="GenerateMnemonic-buttons-btn btn btn-primary"
disabled={!canContinue}
onClick={this.goToNextStep}
>
Confirm Phrase
</button>
</div>
<button className="GenerateMnemonic-skip" onClick={this.skip} />
</div>
);
}
return <Template>{content}</Template>;
}
private regenerateWordArray = () => {
this.setState({ words: generateMnemonic().split(' ') });
};
private handleConfirmChange = (index: number, value: string) => {
this.setState((state: State) => {
const confirmValues = [...state.confirmValues];
confirmValues[index] = value;
this.setState({ confirmValues });
});
};
private goToNextStep = () => {
if (!this.checkCanContinue()) {
return;
}
if (this.state.isConfirming) {
this.setState({ isConfirmed: true });
} else {
this.setState({ isConfirming: true });
}
};
private checkCanContinue = () => {
const { isConfirming, words, confirmValues } = this.state;
if (isConfirming) {
return words.reduce((prev, word, index) => {
return word === confirmValues[index] && prev;
}, true);
} else {
return !!words.length;
}
};
private makeWord = (word: WordTuple) => (
<Word
key={`${word.word}${word.index}`}
index={word.index}
word={word.word}
value={this.state.confirmValues[word.index] || ''}
isReadOnly={!this.state.isConfirming}
onChange={this.handleConfirmChange}
/>
);
private skip = () => {
this.setState({ isConfirmed: true });
};
}

View File

@ -0,0 +1,61 @@
@import 'common/sass/variables';
$width: 320px;
$number-width: 40px;
$number-margin: 6px;
@keyframes word-fade {
0% {
color: rgba($text-color, 0);
}
100% {
color: rgba($text-color, 1);
}
}
.MnemonicWord {
display: flex;
width: $width;
margin-bottom: $space-md;
&:last-child {
margin-bottom: 0;
}
&-number {
display: inline-block;
width: $number-width;
margin-right: $number-margin;
text-align: right;
font-size: 26px;
font-weight: 100;
line-height: 40px;
vertical-align: bottom;
}
&-word {
width: $width - $number-width - $number-margin;
&-input {
animation: word-fade 400ms ease 1;
animation-fill-mode: both;
}
&-toggle {
color: $gray-light;
&:hover {
color: $gray;
}
}
}
// Fade-in animation
@for $i from 1 to 12 {
&:nth-child(#{$i}) {
.MnemonicWord-word-input {
animation-delay: $i * 50ms;
}
}
}
}

View File

@ -0,0 +1,70 @@
import React from 'react';
import classnames from 'classnames';
import { translateRaw } from 'translations';
import './Word.scss';
interface Props {
index: number;
word: string;
value: string;
isReadOnly: boolean;
onChange(index: number, value: string): void;
}
interface State {
isShowingWord: boolean;
}
export default class MnemonicWord extends React.Component<Props, State> {
public state = {
isShowingWord: false
};
public render() {
const { index, word, value, isReadOnly } = this.props;
const { isShowingWord } = this.state;
const readOnly = isReadOnly || isShowingWord;
return (
<div className="MnemonicWord">
<span className="MnemonicWord-number">{index + 1}.</span>
<div className="MnemonicWord-word input-group">
<input
className={classnames(
'MnemonicWord-word-input',
'form-control',
word === value && 'is-valid'
)}
value={readOnly ? word : value}
onChange={this.handleChange}
readOnly={readOnly}
/>
{!isReadOnly && (
<span
onClick={this.toggleShow}
aria-label={translateRaw('GEN_Aria_2')}
role="button"
className="MnemonicWord-word-toggle input-group-addon"
>
<i
className={classnames(
'fa',
isShowingWord && 'fa-eye-slash',
!isShowingWord && 'fa-eye'
)}
/>
</span>
)}
</div>
</div>
);
}
private handleChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.props.onChange(this.props.index, ev.currentTarget.value);
};
private toggleShow = () => {
this.setState({ isShowingWord: !this.state.isShowingWord });
};
}

View File

@ -0,0 +1,2 @@
import Mnemonic from './Mnemonic';
export default Mnemonic;

View File

@ -1,95 +0,0 @@
import PrintableWallet from 'components/PrintableWallet';
import { IFullWallet } from 'ethereumjs-wallet';
import { NewTabLink } from 'components/ui';
import React from 'react';
import { Link } from 'react-router-dom';
import translate from 'translations';
import { stripHexPrefix } from 'libs/values';
import './PaperWallet.scss';
import Template from './Template';
import { knowledgeBaseURL } from 'config/data';
const content = (wallet: IFullWallet) => (
<div className="GenPaper">
{/* Private Key */}
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
<input
className="GenPaper-private form-control"
value={stripHexPrefix(wallet.getPrivateKeyString())}
aria-label={translate('x_PrivKey')}
aria-describedby="x_PrivKeyDesc"
type="text"
readOnly={true}
/>
{/* Download Paper Wallet */}
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
<div className="GenPaper-paper">
<PrintableWallet wallet={wallet} />
</div>
{/* Warning */}
<div className="GenPaper-warning">
<p>
<strong>Do not lose it!</strong> It cannot be recovered if you lose it.
</p>
<p>
<strong>Do not share it!</strong> Your funds will be stolen if you use this file on a
malicious/phishing site.
</p>
<p>
<strong>Make a backup!</strong> Secure it like the millions of dollars it may one day be
worth.
</p>
</div>
{/* Continue button */}
<Link className="GenPaper-continue btn btn-default" to="/view-wallet">
{translate('NAV_ViewWallet')}
</Link>
</div>
);
const help = (
<div>
<h4>{translate('GEN_Help_4')}</h4>
<ul>
<li>
<NewTabLink href={`${knowledgeBaseURL}/getting-started/backing-up-your-new-wallet`}>
<strong>{translate('HELP_2a_Title')}</strong>
</NewTabLink>
</li>
<li>
<NewTabLink href={`${knowledgeBaseURL}/security/securing-your-ethereum`}>
<strong>{translate('GEN_Help_15')}</strong>
</NewTabLink>
</li>
<li>
<NewTabLink
href={`${knowledgeBaseURL}/private-keys-passwords/difference-beween-private-key-and-keystore-file`}
>
<strong>{translate('GEN_Help_16')}</strong>
</NewTabLink>
</li>
</ul>
<h4>{translate('GEN_Help_17')}</h4>
<ul>
<li>{translate('GEN_Help_18')}</li>
<li>{translate('GEN_Help_19')}</li>
<li>
<NewTabLink href={`${knowledgeBaseURL}/offline/ethereum-cold-storage-with-myetherwallet`}>
{translate('GEN_Help_20')}
</NewTabLink>
</li>
</ul>
<h4>{translate('x_PrintDesc')}</h4>
</div>
);
const PaperWallet: React.SFC<{
wallet: IFullWallet;
}> = ({ wallet }) => <Template content={content(wallet)} help={help} />;
export default PaperWallet;

View File

@ -1,26 +1,38 @@
@import "common/sass/variables";
@import 'common/sass/variables';
@import 'common/sass/mixins';
.GenerateWallet {
&-column {
&-content {
text-align: center;
position: relative;
text-align: center;
h1 {
margin-top: 0;
}
&-back {
position: absolute;
top: 36px;
left: 30px;
opacity: 0.3;
outline: none;
color: $text-color;
@media (max-width: $screen-sm) {
display: none;
}
&-help {
background-image: url('~assets/images/icon-help-2.svg');
background-size: 8rem;
background-position: 102% -5%;
background-repeat: no-repeat;
.fa {
margin-right: 5px;
}
h4 {
margin: 0 0 $space-sm;
font-weight: 400;
}
&:hover,
&:focus {
opacity: 0.8;
color: $text-color;
}
ul {
list-style: circle;
padding-left: $space;
}
&:active {
opacity: 1;
}
}
}

View File

@ -1,24 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
import './Template.scss';
interface Props {
content: React.ReactElement<any>;
help: React.ReactElement<any>;
children: React.ReactElement<any>;
}
export default class GenerateWalletTemplate extends React.Component<Props, {}> {
public render() {
const { content, help } = this.props;
return (
<div className="GenerateWallet row">
<div className="GenerateWallet-column col-md-9">
<main className="GenerateWallet-column-content Tab-content-pane">{content}</main>
</div>
const GenerateWalletTemplate: React.SFC<Props> = ({ children }) => (
<div className="GenerateWallet Tab-content-pane">
{children}
<Link className="GenerateWallet-back" to="/generate">
<i className="fa fa-arrow-left" /> Back
</Link>
</div>
);
<div className="GenerateWallet-column col-md-3">
<aside className="GenerateWallet-column-help Tab-content-pane">{help}</aside>
</div>
</div>
);
}
}
export default GenerateWalletTemplate;

View File

@ -0,0 +1,37 @@
@import 'common/sass/variables';
.WalletTypes {
&-title,
&-subtitle {
text-align: center;
}
&-title {
margin-bottom: $space;
}
&-subtitle {
max-width: 1040px;
margin: 0 auto;
margin-bottom: $space;
}
}
.WalletType {
margin-bottom: $space * 2;
&-features {
padding-left: 20px;
}
&-select {
@media screen and (min-width: $screen-md) {
padding: 0 10px;
}
&-btn {
padding-left: 0;
padding-right: 0;
}
}
}

View File

@ -0,0 +1,74 @@
import React from 'react';
import translate from 'translations';
import { WalletType } from '../GenerateWallet';
import { NewTabLink } from 'components/ui';
import { Link } from 'react-router-dom';
import './WalletTypes.scss';
const WalletTypes: React.SFC<{}> = () => {
const typeInfo = {
[WalletType.Keystore]: {
name: 'x_Keystore2',
bullets: [
'An encrypted JSON file, protected by a password',
'Back it up on a USB drive',
'Cannot be written, printed, or easily transferred to mobile',
'Compatible with Mist, Parity, Geth',
'Provides a single address for sending and receiving'
]
},
[WalletType.Mnemonic]: {
name: 'x_Mnemonic',
bullets: [
'A 12-word private seed phrase',
'Back it up on paper or USB drive',
'Can be written, printed, and easily typed on mobile, too',
'Compatible with MetaMask, Jaxx, imToken, and more',
'Provides unlimited addresses for sending and receiving'
]
}
};
return (
<div className="WalletTypes Tab-content-pane">
<h1 className="WalletTypes-title">{translate('NAV_GenerateWallet')}</h1>
<p className="WalletTypes-subtitle alert alert-warning">
<strong>Warning</strong>: Managing your own keys can be risky and a single mistake can lead
to irrecoverable loss. If you are new to cryptocurrencies, we strongly recommend using{' '}
<NewTabLink href="https://metamask.io/">MetaMask</NewTabLink>, or purchasing a{' '}
<NewTabLink href="https://www.ledgerwallet.com/r/fa4b?path=/products/">Ledger</NewTabLink>{' '}
or <NewTabLink href="https://trezor.io/?a=myetherwallet.com">TREZOR</NewTabLink> hardware
wallet.{' '}
<NewTabLink href="https://myetherwallet.github.io/knowledge-base/private-keys-passwords/difference-beween-private-key-and-keystore-file.html">
Learn more about different wallet types & staying secure.
</NewTabLink>
</p>
<div className="WalletTypes-types row">
<div className="col-md-1" />
{Object.keys(typeInfo).map(type => (
<div key={type} className="WalletType col-md-5">
<h2 className="WalletType-title">{translate(typeInfo[type].name)}</h2>
<ul className="WalletType-features">
{typeInfo[type].bullets.map(bullet => (
<li key={bullet} className="WalletType-features-feature">
{translate(bullet)}
</li>
))}
</ul>
<div className="WalletType-select">
<Link
className="WalletType-select-btn btn btn-primary btn-block"
to={`/generate/${type}`}
>
Generate a {translate(typeInfo[type].name)}
</Link>
</div>
</div>
))}
</div>
</div>
);
};
export default WalletTypes;

View File

@ -1,94 +1,2 @@
import {
continueToPaper,
generateNewWallet,
resetGenerateWallet,
TContinueToPaper,
TGenerateNewWallet,
TResetGenerateWallet
} from 'actions/generateWallet';
import { IFullWallet } from 'ethereumjs-wallet';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import DownloadWallet from './components/DownloadWallet';
import EnterPassword from './components/EnterPassword';
import PaperWallet from './components/PaperWallet';
import CryptoWarning from './components/CryptoWarning';
import TabSection from 'containers/TabSection';
interface Props {
// Redux state
activeStep: string; // FIXME union actual steps
password: string;
wallet: IFullWallet | null | undefined;
// Actions
generateNewWallet: TGenerateNewWallet;
continueToPaper: TContinueToPaper;
resetGenerateWallet: TResetGenerateWallet;
}
class GenerateWallet extends Component<Props, {}> {
public componentWillUnmount() {
this.props.resetGenerateWallet();
}
public render() {
const { activeStep, wallet, password } = this.props;
let content;
const AnyEnterPassword = EnterPassword as new () => any;
if (window.crypto) {
switch (activeStep) {
case 'password':
content = <AnyEnterPassword generateNewWallet={this.props.generateNewWallet} />;
break;
case 'download':
if (wallet) {
content = (
<DownloadWallet
wallet={wallet}
password={password}
continueToPaper={this.props.continueToPaper}
/>
);
}
break;
case 'paper':
if (wallet) {
content = <PaperWallet wallet={wallet} />;
} else {
content = <h1>Uh oh. Not sure how you got here.</h1>;
}
break;
default:
content = <h1>Uh oh. Not sure how you got here.</h1>;
}
} else {
content = <CryptoWarning />;
}
return (
<TabSection>
<section className="Tab-content">{content}</section>
</TabSection>
);
}
}
function mapStateToProps(state: AppState) {
return {
activeStep: state.generateWallet.activeStep,
password: state.generateWallet.password,
wallet: state.generateWallet.wallet
};
}
export default connect(mapStateToProps, {
generateNewWallet,
continueToPaper,
resetGenerateWallet
})(GenerateWallet);
import GenerateWallet from './GenerateWallet';
export default GenerateWallet;

View File

@ -1,42 +0,0 @@
import { GenerateWalletAction } from 'actions/generateWallet';
import { TypeKeys } from 'actions/generateWallet/constants';
import { IFullWallet } from 'ethereumjs-wallet';
export interface State {
activeStep: string;
wallet?: IFullWallet | null;
password?: string | null;
}
export const INITIAL_STATE: State = {
activeStep: 'password',
wallet: null,
password: null
};
export function generateWallet(state: State = INITIAL_STATE, action: GenerateWalletAction): State {
switch (action.type) {
case TypeKeys.GENERATE_WALLET_GENERATE_WALLET: {
return {
...state,
wallet: action.wallet,
password: action.password,
activeStep: 'download'
};
}
case TypeKeys.GENERATE_WALLET_CONTINUE_TO_PAPER: {
return {
...state,
activeStep: 'paper'
};
}
case TypeKeys.GENERATE_WALLET_RESET: {
return INITIAL_STATE;
}
default:
return state;
}
}

View File

@ -4,7 +4,6 @@ import { config, State as ConfigState } from './config';
import { customTokens, State as CustomTokensState } from './customTokens';
import { deterministicWallets, State as DeterministicWalletsState } from './deterministicWallets';
import { ens, State as EnsState } from './ens';
import { generateWallet, State as GenerateWalletState } from './generateWallet';
import { notifications, State as NotificationsState } from './notifications';
import { rates, State as RatesState } from './rates';
import { State as SwapState, swap } from './swap';
@ -12,7 +11,6 @@ import { State as WalletState, wallet } from './wallet';
import { State as TransactionState, transaction } from './transaction';
export interface AppState {
// Custom reducers
generateWallet: GenerateWalletState;
config: ConfigState;
notifications: NotificationsState;
ens: EnsState;
@ -28,7 +26,6 @@ export interface AppState {
}
export default combineReducers({
generateWallet,
config,
swap,
notifications,

View File

@ -8,6 +8,8 @@
a {
color: #FFF;
font-weight: normal;
text-decoration: underline;
&:hover {
color: #FFF;

View File

@ -1,33 +0,0 @@
import { generateWallet, INITIAL_STATE } from 'reducers/generateWallet';
import * as generateWalletActions from 'actions/generateWallet';
import Wallet from 'ethereumjs-wallet';
describe('generateWallet reducer', () => {
it('should handle GENERATE_WALLET_GENERATE_WALLET', () => {
const { wallet, password, activeStep } = generateWallet(
undefined,
generateWalletActions.generateNewWallet('password')
);
expect(wallet).toBeInstanceOf(Wallet);
expect(password).toEqual('password');
expect(activeStep).toEqual('download');
});
it('should handle GENERATE_WALLET_CONTINUE_TO_PAPER', () => {
expect(
generateWallet(undefined, generateWalletActions.continueToPaper())
).toEqual({
...INITIAL_STATE,
activeStep: 'paper'
});
});
it('should handle GENERATE_WALLET_RESET', () => {
expect(
generateWallet(undefined, generateWalletActions.resetGenerateWallet())
).toEqual({
...INITIAL_STATE
});
});
});

View File

@ -26,4 +26,4 @@
"awesomeTypescriptLoaderOptions": {
"transpileOnly": true
}
}
}