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:
parent
a9af8b6caf
commit
6513acd03d
|
@ -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}>
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
|
@ -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'
|
||||
}
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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)}`;
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, you’re 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;
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
}
|
||||
|
||||
&-private {
|
||||
margin-bottom: $space * 3;
|
||||
max-width: 700px;
|
||||
margin: 0 auto $space * 3;
|
||||
}
|
||||
|
||||
&-paper,
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
import Keystore from './Keystore';
|
||||
export default Keystore;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
};
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Mnemonic from './Mnemonic';
|
||||
export default Mnemonic;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
a {
|
||||
color: #FFF;
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,4 +26,4 @@
|
|||
"awesomeTypescriptLoaderOptions": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue