AccountCreate updates (#3988)
* Add esjify for mocha + ejs * First pass through, intl + basic smoketests * Create store * Update for renames * Pass store around * createType into store * Move stage into store * Update labels * Define stages * address into store * Add @observer * Retrieve name from store * Store phrase in store * isWindowsPhrase into store * gethAddresses to store * Store manages geth addresses * passwordHint into store * Fix build * rawKey into store * import json files * name set direct from component * No parent change callbacks * canCreate from store * createAccounts into store * expand create tests * Windows phrase testcases * Properly bind newError * FirstRun use of new CreateAccount * Add fix & test for selectedAddress match * Call into store from props * onChangeIdentity fix & test * Phrase set fix & test * RecoveryPhrase tested manually (issues addressed via tests) * Hex import manual test (& tests added for errors) * New eslint update fixes * grumble: set default type from store (with test) * grumble: pass copy of accounts (observable injection) * grumble: Summary owners can be array or array-like
This commit is contained in:
		
							parent
							
								
									153f2ca2f2
								
							
						
					
					
						commit
						06433033d9
					
				| @ -14,33 +14,54 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import { Form, Input, InputAddress } from '~/ui'; | ||||
| 
 | ||||
| @observer | ||||
| export default class AccountDetails extends Component { | ||||
|   static propTypes = { | ||||
|     address: PropTypes.string, | ||||
|     name: PropTypes.string, | ||||
|     phrase: PropTypes.string | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { address, name } = this.props; | ||||
|     const { address, name } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         <Input | ||||
|           readOnly | ||||
|           allowCopy | ||||
|           hint='a descriptive name for the account' | ||||
|           label='account name' | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.accountDetails.name.hint' | ||||
|               defaultMessage='a descriptive name for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.accountDetails.name.label' | ||||
|               defaultMessage='account name' | ||||
|             /> | ||||
|           } | ||||
|           readOnly | ||||
|           value={ name } | ||||
|         /> | ||||
|         <InputAddress | ||||
|           disabled | ||||
|           hint='the network address for the account' | ||||
|           label='address' | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.accountDetails.address.hint' | ||||
|               defaultMessage='the network address for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.accountDetails.address.label' | ||||
|               defaultMessage='address' | ||||
|             /> | ||||
|           } | ||||
|           value={ address } | ||||
|         /> | ||||
|         { this.renderPhrase() } | ||||
| @ -49,7 +70,7 @@ export default class AccountDetails extends Component { | ||||
|   } | ||||
| 
 | ||||
|   renderPhrase () { | ||||
|     const { phrase } = this.props; | ||||
|     const { phrase } = this.props.store; | ||||
| 
 | ||||
|     if (!phrase) { | ||||
|       return null; | ||||
| @ -57,10 +78,20 @@ export default class AccountDetails extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <Input | ||||
|         readOnly | ||||
|         allowCopy | ||||
|         hint='the account recovery phrase' | ||||
|         label='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)' | ||||
|         hint={ | ||||
|           <FormattedMessage | ||||
|             id='createAccount.accountDetails.phrase.hint' | ||||
|             defaultMessage='the account recovery phrase' | ||||
|           /> | ||||
|         } | ||||
|         label={ | ||||
|           <FormattedMessage | ||||
|             id='createAccount.accountDetails.phrase.label' | ||||
|             defaultMessage='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)' | ||||
|           /> | ||||
|         } | ||||
|         readOnly | ||||
|         value={ phrase } | ||||
|       /> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,42 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import AccountDetails from './'; | ||||
| 
 | ||||
| let component; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <AccountDetails | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/AccountDetails', () => { | ||||
|   it('renders with defaults', () => { | ||||
|     expect(render()).to.be.ok; | ||||
|   }); | ||||
| }); | ||||
| @ -14,9 +14,10 @@ | ||||
| /* You should have received a copy of the GNU General Public License | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| .address { | ||||
|   color: #999; | ||||
|   padding-top: 1em; | ||||
|   line-height: 1.618em; | ||||
|   padding-left: 2em; | ||||
|   padding-top: 1em; | ||||
| } | ||||
|  | ||||
| @ -14,29 +14,46 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import styles from './accountDetailsGeth.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class AccountDetailsGeth extends Component { | ||||
|   static propTypes = { | ||||
|     addresses: PropTypes.array | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { addresses } = this.props; | ||||
| 
 | ||||
|     const formatted = addresses.map((address, idx) => { | ||||
|       const comma = !idx ? '' : ((idx === addresses.length - 1) ? ' & ' : ', '); | ||||
| 
 | ||||
|       return `${comma}${address}`; | ||||
|     }).join(''); | ||||
|     const { gethAddresses } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         <div>You have imported { addresses.length } addresses from the Geth keystore:</div> | ||||
|         <div className={ styles.address }>{ formatted }</div> | ||||
|         <div> | ||||
|           <FormattedMessage | ||||
|             id='createAccount.accountDetailsGeth.imported' | ||||
|             defaultMessage='You have imported {number} addresses from the Geth keystore:' | ||||
|             values={ { | ||||
|               number: gethAddresses.length | ||||
|             } } | ||||
|           /> | ||||
|         </div> | ||||
|         <div className={ styles.address }> | ||||
|           { this.formatAddresses(gethAddresses) } | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   formatAddresses (addresses) { | ||||
|     return addresses.map((address, index) => { | ||||
|       const comma = !index | ||||
|         ? '' | ||||
|         : ((index === addresses.length - 1) ? ' & ' : ', '); | ||||
| 
 | ||||
|       return `${comma}${address}`; | ||||
|     }).join(''); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,60 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import AccountDetailsGeth from './'; | ||||
| 
 | ||||
| let component; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <AccountDetailsGeth | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/AccountDetailsGeth', () => { | ||||
|   it('renders with defaults', () => { | ||||
|     expect(render()).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('utility', () => { | ||||
|     describe('formatAddresses', () => { | ||||
|       let instance; | ||||
| 
 | ||||
|       beforeEach(() => { | ||||
|         instance = component.instance(); | ||||
|       }); | ||||
| 
 | ||||
|       it('renders a single item', () => { | ||||
|         expect(instance.formatAddresses(['one'])).to.equal('one'); | ||||
|       }); | ||||
| 
 | ||||
|       it('renders multiple items', () => { | ||||
|         expect(instance.formatAddresses(['one', 'two', 'three'])).to.equal('one, two & three'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,50 +14,81 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | ||||
| 
 | ||||
| import styles from '../createAccount.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class CreationType extends Component { | ||||
|   static propTypes = { | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.onChange('fromNew'); | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { createType } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={ styles.spaced }> | ||||
|         <RadioButtonGroup | ||||
|           defaultSelected='fromNew' | ||||
|           defaultSelected={ createType } | ||||
|           name='creationType' | ||||
|           onChange={ this.onChange } | ||||
|         > | ||||
|           <RadioButton | ||||
|             label='Create new account manually' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromNew.label' | ||||
|                 defaultMessage='Create new account manually' | ||||
|               /> | ||||
|             } | ||||
|             value='fromNew' | ||||
|           /> | ||||
|           <RadioButton | ||||
|             label='Recover account from recovery phrase' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromPhrase.label' | ||||
|                 defaultMessage='Recover account from recovery phrase' | ||||
|               /> | ||||
|             } | ||||
|             value='fromPhrase' | ||||
|           /> | ||||
|           <RadioButton | ||||
|             label='Import accounts from Geth keystore' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromGeth.label' | ||||
|                 defaultMessage='Import accounts from Geth keystore' | ||||
|               /> | ||||
|             } | ||||
|             value='fromGeth' | ||||
|           /> | ||||
|           <RadioButton | ||||
|             label='Import account from a backup JSON file' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromJSON.label' | ||||
|                 defaultMessage='Import account from a backup JSON file' | ||||
|               /> | ||||
|             } | ||||
|             value='fromJSON' | ||||
|           /> | ||||
|           <RadioButton | ||||
|             label='Import account from an Ethereum pre-sale wallet' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromPresale.label' | ||||
|                 defaultMessage='Import account from an Ethereum pre-sale wallet' | ||||
|               /> | ||||
|             } | ||||
|             value='fromPresale' | ||||
|           /> | ||||
|           <RadioButton | ||||
|             label='Import raw private key' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.creationType.fromRaw.label' | ||||
|                 defaultMessage='Import raw private key' | ||||
|               /> | ||||
|             } | ||||
|             value='fromRaw' | ||||
|           /> | ||||
|         </RadioButtonGroup> | ||||
| @ -66,6 +97,8 @@ export default class CreationType extends Component { | ||||
|   } | ||||
| 
 | ||||
|   onChange = (event) => { | ||||
|     this.props.onChange(event.target.value); | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setCreateType(event.target.value); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,76 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import CreationType from './'; | ||||
| 
 | ||||
| let component; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <CreationType | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/CreationType', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(component).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('selector', () => { | ||||
|     const SELECT_TYPE = 'fromRaw'; | ||||
|     let selector; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       store.setCreateType(SELECT_TYPE); | ||||
|       selector = component.find('RadioButtonGroup'); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders the selector', () => { | ||||
|       expect(selector.get(0)).to.be.ok; | ||||
|     }); | ||||
| 
 | ||||
|     it('passes the store type to defaultSelected', () => { | ||||
|       expect(selector.props().defaultSelected).to.equal(SELECT_TYPE); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('events', () => { | ||||
|     describe('onChange', () => { | ||||
|       beforeEach(() => { | ||||
|         component.instance().onChange({ target: { value: 'testing' } }); | ||||
|       }); | ||||
| 
 | ||||
|       it('changes the store createType', () => { | ||||
|         expect(store.createType).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,86 +14,113 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { IconButton } from 'material-ui'; | ||||
| import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | ||||
| import ActionAutorenew from 'material-ui/svg-icons/action/autorenew'; | ||||
| 
 | ||||
| import { Form, Input, IdentityIcon } from '~/ui'; | ||||
| 
 | ||||
| import ERRORS from '../errors'; | ||||
| import { Form, Input, IdentityIcon, PasswordStrength } from '~/ui'; | ||||
| import { RefreshIcon } from '~/ui/Icons'; | ||||
| 
 | ||||
| import styles from '../createAccount.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class CreateAccount extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
|   static propTypes = { | ||||
|     newError: PropTypes.func.isRequired, | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     accountName: '', | ||||
|     accountNameError: ERRORS.noName, | ||||
|     accounts: null, | ||||
|     isValidName: false, | ||||
|     isValidPass: true, | ||||
|     passwordHint: '', | ||||
|     password1: '', | ||||
|     password1Error: null, | ||||
|     password2: '', | ||||
|     password2Error: null, | ||||
|     selectedAddress: '' | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.createIdentities(); | ||||
|     this.props.onChange(false, {}); | ||||
|     return this.createIdentities(); | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error } = this.state; | ||||
|     const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         <Input | ||||
|           label='account name' | ||||
|           hint='a descriptive name for the account' | ||||
|           error={ accountNameError } | ||||
|           value={ accountName } | ||||
|           error={ nameError } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newAccount.name.hint' | ||||
|               defaultMessage='a descriptive name for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newAccount.name.label' | ||||
|               defaultMessage='account name' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditAccountName } | ||||
|           value={ name } | ||||
|         /> | ||||
|         <Input | ||||
|           label='password hint' | ||||
|           hint='(optional) a hint to help with remembering the password' | ||||
|           value={ passwordHint } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newAccount.hint.hint' | ||||
|               defaultMessage='(optional) a hint to help with remembering the password' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newAccount.hint.label' | ||||
|               defaultMessage='password hint' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditPasswordHint } | ||||
|           value={ passwordHint } | ||||
|         /> | ||||
|         <div className={ styles.passwords }> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password' | ||||
|               hint='a strong, unique password' | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newAccount.password.hint' | ||||
|                   defaultMessage='a strong, unique password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newAccount.password.label' | ||||
|                   defaultMessage='password' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPassword } | ||||
|               type='password' | ||||
|               error={ password1Error } | ||||
|               value={ password1 } | ||||
|               onChange={ this.onEditPassword1 } | ||||
|               value={ password } | ||||
|             /> | ||||
|           </div> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password (repeat)' | ||||
|               hint='verify your password' | ||||
|               error={ passwordRepeatError } | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newAccount.password2.hint' | ||||
|                   defaultMessage='verify your password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newAccount.password2.label' | ||||
|                   defaultMessage='password (repeat)' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPasswordRepeat } | ||||
|               type='password' | ||||
|               error={ password2Error } | ||||
|               value={ password2 } | ||||
|               onChange={ this.onEditPassword2 } | ||||
|               value={ passwordRepeat } | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <PasswordStrength input={ password } /> | ||||
|         { this.renderIdentitySelector() } | ||||
|         { this.renderIdentities() } | ||||
|       </Form> | ||||
| @ -107,7 +134,9 @@ export default class CreateAccount extends Component { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const buttons = Object.keys(accounts).map((address) => { | ||||
|     const buttons = Object | ||||
|       .keys(accounts) | ||||
|       .map((address) => { | ||||
|         return ( | ||||
|           <RadioButton | ||||
|             className={ styles.button } | ||||
| @ -119,10 +148,10 @@ export default class CreateAccount extends Component { | ||||
| 
 | ||||
|     return ( | ||||
|       <RadioButtonGroup | ||||
|         valueSelected={ selectedAddress } | ||||
|         className={ styles.selector } | ||||
|         name='identitySelector' | ||||
|         onChange={ this.onChangeIdentity } | ||||
|         valueSelected={ selectedAddress } | ||||
|       > | ||||
|         { buttons } | ||||
|       </RadioButtonGroup> | ||||
| @ -136,7 +165,9 @@ export default class CreateAccount extends Component { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     const identities = Object.keys(accounts).map((address) => { | ||||
|     const identities = Object | ||||
|       .keys(accounts) | ||||
|       .map((address) => { | ||||
|         return ( | ||||
|           <div | ||||
|             className={ styles.identity } | ||||
| @ -155,12 +186,8 @@ export default class CreateAccount extends Component { | ||||
|       <div className={ styles.identities }> | ||||
|         { identities } | ||||
|         <div className={ styles.refresh }> | ||||
|           <IconButton | ||||
|             onTouchTap={ this.createIdentities } | ||||
|           > | ||||
|             <ActionAutorenew | ||||
|               color='rgb(0, 151, 167)' | ||||
|             /> | ||||
|           <IconButton onTouchTap={ this.createIdentities }> | ||||
|             <RefreshIcon color='rgb(0, 151, 167)' /> | ||||
|           </IconButton> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -168,122 +195,64 @@ export default class CreateAccount extends Component { | ||||
|   } | ||||
| 
 | ||||
|   createIdentities = () => { | ||||
|     const { api } = this.context; | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     Promise | ||||
|       .all([ | ||||
|         api.parity.generateSecretPhrase(), | ||||
|         api.parity.generateSecretPhrase(), | ||||
|         api.parity.generateSecretPhrase(), | ||||
|         api.parity.generateSecretPhrase(), | ||||
|         api.parity.generateSecretPhrase() | ||||
|       ]) | ||||
|       .then((phrases) => { | ||||
|         return Promise | ||||
|           .all(phrases.map((phrase) => api.parity.phraseToAddress(phrase))) | ||||
|           .then((addresses) => { | ||||
|             const accounts = {}; | ||||
|     return store | ||||
|       .createIdentities() | ||||
|       .then((accounts) => { | ||||
|         const selectedAddress = Object.keys(accounts)[0]; | ||||
|         const { phrase } = accounts[selectedAddress]; | ||||
| 
 | ||||
|             phrases.forEach((phrase, idx) => { | ||||
|               accounts[addresses[idx]] = { | ||||
|                 address: addresses[idx], | ||||
|                 phrase: phrase | ||||
|               }; | ||||
|             }); | ||||
|         store.setAddress(selectedAddress); | ||||
|         store.setPhrase(phrase); | ||||
| 
 | ||||
|         this.setState({ | ||||
|               selectedAddress: addresses[0], | ||||
|               accounts: accounts | ||||
|             }); | ||||
|           accounts, | ||||
|           selectedAddress | ||||
|         }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createIdentities', error); | ||||
|         setTimeout(this.createIdentities, 1000); | ||||
|         this.newError(error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   updateParent = () => { | ||||
|     const { isValidName, isValidPass, accounts, accountName, passwordHint, password1, selectedAddress } = this.state; | ||||
|     const isValid = isValidName && isValidPass; | ||||
| 
 | ||||
|     this.props.onChange(isValid, { | ||||
|       address: selectedAddress, | ||||
|       name: accountName, | ||||
|       passwordHint, | ||||
|       password: password1, | ||||
|       phrase: accounts[selectedAddress].phrase | ||||
|         this.props.newError(error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeIdentity = (event) => { | ||||
|     const address = event.target.value || event.target.getAttribute('value'); | ||||
|     const { store } = this.props; | ||||
|     const selectedAddress = event.target.value || event.target.getAttribute('value'); | ||||
| 
 | ||||
|     if (!address) { | ||||
|     if (!selectedAddress) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       selectedAddress: address | ||||
|     }, this.updateParent); | ||||
|   } | ||||
|     this.setState({ selectedAddress }, () => { | ||||
|       const { phrase } = this.state.accounts[selectedAddress]; | ||||
| 
 | ||||
|   onEditPasswordHint = (event, passwordHint) => { | ||||
|     this.setState({ | ||||
|       passwordHint | ||||
|       store.setAddress(selectedAddress); | ||||
|       store.setPhrase(phrase); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onEditAccountName = (event) => { | ||||
|     const accountName = event.target.value; | ||||
|     let accountNameError = null; | ||||
|   onEditPasswordHint = (event, passwordHint) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (!accountName || !accountName.trim().length) { | ||||
|       accountNameError = ERRORS.noName; | ||||
|     store.setPasswordHint(passwordHint); | ||||
|   } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       accountName, | ||||
|       accountNameError, | ||||
|       isValidName: !accountNameError | ||||
|     }, this.updateParent); | ||||
|   onEditAccountName = (event, name) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setName(name); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword1 = (event) => { | ||||
|     const password1 = event.target.value; | ||||
|     let password2Error = null; | ||||
|   onEditPassword = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (password1 !== this.state.password2) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     store.setPassword(password); | ||||
|   } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password1, | ||||
|       password1Error: null, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|   } | ||||
|   onEditPasswordRepeat = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|   onEditPassword2 = (event) => { | ||||
|     const password2 = event.target.value; | ||||
|     let password2Error = null; | ||||
| 
 | ||||
|     if (password2 !== this.state.password1) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password2, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|   } | ||||
| 
 | ||||
|   newError = (error) => { | ||||
|     const { store } = this.context; | ||||
| 
 | ||||
|     store.dispatch({ type: 'newError', error }); | ||||
|     store.setPasswordRepeat(password); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										161
									
								
								js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import { createApi, createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import NewAccount from './'; | ||||
| 
 | ||||
| let api; | ||||
| let component; | ||||
| let instance; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   api = createApi(); | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <NewAccount | ||||
|       store={ store } | ||||
|     />, | ||||
|     { | ||||
|       context: { api } | ||||
|     } | ||||
|   ); | ||||
|   instance = component.instance(); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/NewAccount', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(component).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('lifecycle', () => { | ||||
|     describe('componentWillMount', () => { | ||||
|       beforeEach(() => { | ||||
|         return instance.componentWillMount(); | ||||
|       }); | ||||
| 
 | ||||
|       it('creates initial accounts', () => { | ||||
|         expect(Object.keys(instance.state.accounts).length).to.equal(5); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the initial selected value', () => { | ||||
|         expect(instance.state.selectedAddress).to.equal(Object.keys(instance.state.accounts)[0]); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('event handlers', () => { | ||||
|     describe('onChangeIdentity', () => { | ||||
|       let address; | ||||
| 
 | ||||
|       beforeEach(() => { | ||||
|         address = Object.keys(instance.state.accounts)[3]; | ||||
| 
 | ||||
|         sinon.spy(store, 'setAddress'); | ||||
|         sinon.spy(store, 'setPhrase'); | ||||
|         instance.onChangeIdentity({ target: { value: address } }); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setAddress.restore(); | ||||
|         store.setPhrase.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the state with the new value', () => { | ||||
|         expect(instance.state.selectedAddress).to.equal(address); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the new address on the store', () => { | ||||
|         expect(store.setAddress).to.have.been.calledWith(address); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the new phrase on the store', () => { | ||||
|         expect(store.setPhrase).to.have.been.calledWith(instance.state.accounts[address].phrase); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPassword', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPassword'); | ||||
|         instance.onEditPassword(null, 'test'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPassword.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPassword).to.have.been.calledWith('test'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordRepeat', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordRepeat'); | ||||
|         instance.onEditPasswordRepeat(null, 'test'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordRepeat.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordRepeat).to.have.been.calledWith('test'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordHint', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordHint'); | ||||
|         instance.onEditPasswordHint(null, 'test'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordHint.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordHint).to.have.been.calledWith('test'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditAccountName', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setName'); | ||||
|         instance.onEditAccountName(null, 'test'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setName.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setName).to.have.been.calledWith('test'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -15,29 +15,28 @@ | ||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| .list { | ||||
| } | ||||
| 
 | ||||
| .list input+div>div { | ||||
|   input+div>div { | ||||
|     top: 13px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .selection { | ||||
|   display: inline-block; | ||||
|   margin-bottom: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .selection .icon { | ||||
|   .icon { | ||||
|     display: inline-block; | ||||
|   } | ||||
| 
 | ||||
| .selection .detail { | ||||
|   .detail { | ||||
|     display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .detail .address { | ||||
|     .address { | ||||
|       color: #aaa; | ||||
|     } | ||||
| 
 | ||||
| .detail .balance { | ||||
|     .balance { | ||||
|       font-family: 'Roboto Mono', monospace; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -14,63 +14,68 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { Checkbox } from 'material-ui'; | ||||
| 
 | ||||
| import { IdentityIcon } from '~/ui'; | ||||
| 
 | ||||
| import styles from './newGeth.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class NewGeth extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     accounts: PropTypes.object.isRequired, | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     available: [] | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.loadAvailable(); | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { available } = this.state; | ||||
|     const { gethAccountsAvailable, gethAddresses } = this.props.store; | ||||
| 
 | ||||
|     if (!available.length) { | ||||
|     if (!gethAccountsAvailable.length) { | ||||
|       return ( | ||||
|         <div className={ styles.list }>There are currently no importable keys available from the Geth keystore, which are not already available on your Parity instance</div> | ||||
|         <div className={ styles.list }> | ||||
|           <FormattedMessage | ||||
|             id='createAccount.newGeth.noKeys' | ||||
|             defaultMessage='There are currently no importable keys available from the Geth keystore, which are not already available on your Parity instance' | ||||
|           /> | ||||
|         </div> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const checkboxes = available.map((account) => { | ||||
|     const checkboxes = gethAccountsAvailable.map((account) => { | ||||
|       const onSelect = (event) => this.onSelectAddress(event, account.address); | ||||
| 
 | ||||
|       const label = ( | ||||
|         <div className={ styles.selection }> | ||||
|           <div className={ styles.icon }> | ||||
|             <IdentityIcon | ||||
|               center inline | ||||
|               address={ account.address } | ||||
|               center | ||||
|               inline | ||||
|             /> | ||||
|           </div> | ||||
|           <div className={ styles.detail }> | ||||
|             <div className={ styles.address }>{ account.address }</div> | ||||
|             <div className={ styles.balance }>{ account.balance } ETH</div> | ||||
|             <div className={ styles.address }> | ||||
|               { account.address } | ||||
|             </div> | ||||
|             <div className={ styles.balance }> | ||||
|               { account.balance } ETH | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       ); | ||||
| 
 | ||||
|       return ( | ||||
|         <Checkbox | ||||
|           checked={ gethAddresses.includes(account.address) } | ||||
|           key={ account.address } | ||||
|           checked={ account.checked } | ||||
|           label={ label } | ||||
|           data-address={ account.address } | ||||
|           onCheck={ this.onSelect } | ||||
|           onCheck={ onSelect } | ||||
|         /> | ||||
|       ); | ||||
|     }); | ||||
| @ -82,51 +87,9 @@ export default class NewGeth extends Component { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   onSelect = (event, checked) => { | ||||
|     const address = event.target.getAttribute('data-address'); | ||||
|   onSelectAddress = (event, address) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (!address) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const { available } = this.state; | ||||
|     const account = available.find((_account) => _account.address === address); | ||||
| 
 | ||||
|     account.checked = checked; | ||||
|     const selected = available.filter((_account) => _account.checked); | ||||
| 
 | ||||
|     this.setState({ | ||||
|       available | ||||
|     }); | ||||
| 
 | ||||
|     this.props.onChange(selected.length, selected.map((account) => account.address)); | ||||
|   } | ||||
| 
 | ||||
|   loadAvailable = () => { | ||||
|     const { api } = this.context; | ||||
|     const { accounts } = this.props; | ||||
| 
 | ||||
|     api.parity | ||||
|       .listGethAccounts() | ||||
|       .then((_addresses) => { | ||||
|         const addresses = (addresses || []).filter((address) => !accounts[address]); | ||||
| 
 | ||||
|         return Promise | ||||
|           .all(addresses.map((address) => api.eth.getBalance(address))) | ||||
|           .then((balances) => { | ||||
|             this.setState({ | ||||
|               available: addresses.map((address, idx) => { | ||||
|                 return { | ||||
|                   address, | ||||
|                   balance: api.util.fromWei(balances[idx]).toFormat(5), | ||||
|                   checked: false | ||||
|                 }; | ||||
|               }) | ||||
|             }); | ||||
|           }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('loadAvailable', error); | ||||
|       }); | ||||
|     store.selectGethAccount(address); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										66
									
								
								js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								js/src/modals/CreateAccount/NewGeth/newGeth.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import NewGeth from './'; | ||||
| 
 | ||||
| let component; | ||||
| let instance; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <NewGeth | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
|   instance = component.instance(); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/NewGeth', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(render()).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('events', () => { | ||||
|     describe('onSelectAddress', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'selectGethAccount'); | ||||
|         instance.onSelectAddress(null, 'testAddress'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.selectGethAccount.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.selectGethAccount).to.have.been.calledWith('testAddress'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,91 +14,114 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { FloatingActionButton } from 'material-ui'; | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import { FloatingActionButton } from 'material-ui'; | ||||
| import EditorAttachFile from 'material-ui/svg-icons/editor/attach-file'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import { Form, Input } from '~/ui'; | ||||
| 
 | ||||
| import ERRORS from '../errors'; | ||||
| import { AttachFileIcon } from '~/ui/Icons'; | ||||
| 
 | ||||
| import styles from '../createAccount.css'; | ||||
| 
 | ||||
| const FAKEPATH = 'C:\\fakepath\\'; | ||||
| const STYLE_HIDDEN = { display: 'none' }; | ||||
| 
 | ||||
| @observer | ||||
| export default class NewImport extends Component { | ||||
|   static propTypes = { | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     accountName: '', | ||||
|     accountNameError: ERRORS.noName, | ||||
|     isValidFile: false, | ||||
|     isValidPass: true, | ||||
|     isValidName: false, | ||||
|     password: '', | ||||
|     passwordError: null, | ||||
|     passwordHint: '', | ||||
|     walletFile: '', | ||||
|     walletFileError: ERRORS.noFile, | ||||
|     walletJson: '' | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.onChange(false, {}); | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         <Input | ||||
|           label='account name' | ||||
|           hint='a descriptive name for the account' | ||||
|           error={ this.state.accountNameError } | ||||
|           value={ this.state.accountName } | ||||
|           onChange={ this.onEditAccountName } | ||||
|           error={ nameError } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newImport.name.hint' | ||||
|               defaultMessage='a descriptive name for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newImport.name.label' | ||||
|               defaultMessage='account name' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditName } | ||||
|           value={ name } | ||||
|         /> | ||||
|         <Input | ||||
|           label='password hint' | ||||
|           hint='(optional) a hint to help with remembering the password' | ||||
|           value={ this.state.passwordHint } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newImport.hint.hint' | ||||
|               defaultMessage='(optional) a hint to help with remembering the password' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.newImport.hint.label' | ||||
|               defaultMessage='password hint' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditpasswordHint } | ||||
|           value={ passwordHint } | ||||
|         /> | ||||
|         <div className={ styles.passwords }> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password' | ||||
|               hint='the password to unlock the wallet' | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newImport.password.hint' | ||||
|                   defaultMessage='the password to unlock the wallet' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.newImport.password.label' | ||||
|                   defaultMessage='password' | ||||
|                 /> | ||||
|               } | ||||
|               type='password' | ||||
|               error={ this.state.passwordError } | ||||
|               value={ this.state.password } | ||||
|               onChange={ this.onEditPassword } | ||||
|               value={ password } | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div> | ||||
|           <Input | ||||
|             disabled | ||||
|             label='wallet file' | ||||
|             hint='the wallet file for import' | ||||
|             error={ this.state.walletFileError } | ||||
|             value={ this.state.walletFile } | ||||
|             error={ walletFileError } | ||||
|             hint={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.newImport.file.hint' | ||||
|                 defaultMessage='the wallet file for import' | ||||
|               /> | ||||
|             } | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.newImport.file.label' | ||||
|                 defaultMessage='wallet file' | ||||
|               /> | ||||
|             } | ||||
|             value={ walletFile } | ||||
|           /> | ||||
|           <div className={ styles.upload }> | ||||
|             <FloatingActionButton | ||||
|               mini | ||||
|               onTouchTap={ this.openFileDialog } | ||||
|             > | ||||
|               <EditorAttachFile /> | ||||
|               <AttachFileIcon /> | ||||
|             </FloatingActionButton> | ||||
|             <input | ||||
|               ref='fileUpload' | ||||
|               type='file' | ||||
|               style={ STYLE_HIDDEN } | ||||
|               onChange={ this.onFileChange } | ||||
|               ref='fileUpload' | ||||
|               style={ STYLE_HIDDEN } | ||||
|               type='file' | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
| @ -107,73 +130,37 @@ export default class NewImport extends Component { | ||||
|   } | ||||
| 
 | ||||
|   onFileChange = (event) => { | ||||
|     const el = event.target; | ||||
|     const error = ERRORS.noFile; | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (el.files.length) { | ||||
|     if (event.target.files.length) { | ||||
|       const reader = new FileReader(); | ||||
| 
 | ||||
|       reader.onload = (event) => { | ||||
|         this.setState({ | ||||
|           walletJson: event.target.result, | ||||
|           walletFileError: null, | ||||
|           isValidFile: true | ||||
|         }, this.updateParent); | ||||
|       }; | ||||
|       reader.readAsText(el.files[0]); | ||||
|       reader.onload = (event) => store.setWalletJson(event.target.result); | ||||
|       reader.readAsText(event.target.files[0]); | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       walletFile: el.value.replace(FAKEPATH, ''), | ||||
|       walletFileError: error, | ||||
|       isValidFile: false | ||||
|     }, this.updateParent); | ||||
|     store.setWalletFile(event.target.value); | ||||
|   } | ||||
| 
 | ||||
|   openFileDialog = () => { | ||||
|     ReactDOM.findDOMNode(this.refs.fileUpload).click(); | ||||
|   } | ||||
| 
 | ||||
|   updateParent = () => { | ||||
|     const valid = this.state.isValidName && this.state.isValidPass && this.state.isValidFile; | ||||
|   onEditName = (event, name) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     this.props.onChange(valid, { | ||||
|       name: this.state.accountName, | ||||
|       passwordHint: this.state.passwordHint, | ||||
|       password: this.state.password, | ||||
|       phrase: null, | ||||
|       json: this.state.walletJson | ||||
|     }); | ||||
|     store.setName(name); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setPassword(password); | ||||
|   } | ||||
| 
 | ||||
|   onEditPasswordHint = (event, passwordHint) => { | ||||
|     this.setState({ | ||||
|       passwordHint | ||||
|     }); | ||||
|   } | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|   onEditAccountName = (event) => { | ||||
|     const accountName = event.target.value; | ||||
|     let accountNameError = null; | ||||
| 
 | ||||
|     if (!accountName || !accountName.trim().length) { | ||||
|       accountNameError = ERRORS.noName; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       accountName, | ||||
|       accountNameError, | ||||
|       isValidName: !accountNameError | ||||
|     }, this.updateParent); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword = (event) => { | ||||
|     const password = event.target.value; | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password, | ||||
|       passwordError: null, | ||||
|       isValidPass: true | ||||
|     }, this.updateParent); | ||||
|     store.setPasswordHint(passwordHint); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										96
									
								
								js/src/modals/CreateAccount/NewImport/newImport.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								js/src/modals/CreateAccount/NewImport/newImport.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import NewImport from './'; | ||||
| 
 | ||||
| let component; | ||||
| let instance; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <NewImport | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
|   instance = component.instance(); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/NewImport', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(render()).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('events', () => { | ||||
|     describe('onEditName', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setName'); | ||||
|         instance.onEditName(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setName.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setName).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPassword', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPassword'); | ||||
|         instance.onEditPassword(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPassword.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPassword).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordHint', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordHint'); | ||||
|         instance.onEditPasswordHint(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordHint.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordHint).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,172 +14,152 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import { Form, Input } from '~/ui'; | ||||
| import { Form, Input, PasswordStrength } from '~/ui'; | ||||
| 
 | ||||
| import styles from '../createAccount.css'; | ||||
| 
 | ||||
| import ERRORS from '../errors'; | ||||
| 
 | ||||
| @observer | ||||
| export default class RawKey extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     accountName: '', | ||||
|     accountNameError: ERRORS.noName, | ||||
|     isValidKey: false, | ||||
|     isValidName: false, | ||||
|     isValidPass: true, | ||||
|     passwordHint: '', | ||||
|     password1: '', | ||||
|     password1Error: null, | ||||
|     password2: '', | ||||
|     password2Error: null, | ||||
|     rawKey: '', | ||||
|     rawKeyError: ERRORS.noKey | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.onChange(false, {}); | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, rawKey, rawKeyError } = this.state; | ||||
|     const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, rawKey, rawKeyError } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         <Input | ||||
|           hint='the raw hex encoded private key' | ||||
|           label='private key' | ||||
|           error={ rawKeyError } | ||||
|           value={ rawKey } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.private.hint' | ||||
|               defaultMessage='the raw hex encoded private key' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.private.label' | ||||
|               defaultMessage='private key' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditKey } | ||||
|           value={ rawKey } | ||||
|         /> | ||||
|         <Input | ||||
|           label='account name' | ||||
|           hint='a descriptive name for the account' | ||||
|           error={ accountNameError } | ||||
|           value={ accountName } | ||||
|           onChange={ this.onEditAccountName } | ||||
|           error={ nameError } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.name.hint' | ||||
|               defaultMessage='a descriptive name for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.name.label' | ||||
|               defaultMessage='account name' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditName } | ||||
|           value={ name } | ||||
|         /> | ||||
|         <Input | ||||
|           label='password hint' | ||||
|           hint='(optional) a hint to help with remembering the password' | ||||
|           value={ passwordHint } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.hint.hint' | ||||
|               defaultMessage='(optional) a hint to help with remembering the password' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.rawKey.hint.label' | ||||
|               defaultMessage='password hint' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditPasswordHint } | ||||
|           value={ passwordHint } | ||||
|         /> | ||||
|         <div className={ styles.passwords }> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password' | ||||
|               hint='a strong, unique password' | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.rawKey.password.hint' | ||||
|                   defaultMessage='a strong, unique password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.rawKey.password.label' | ||||
|                   defaultMessage='password' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPassword } | ||||
|               type='password' | ||||
|               error={ password1Error } | ||||
|               value={ password1 } | ||||
|               onChange={ this.onEditPassword1 } | ||||
|               value={ password } | ||||
|             /> | ||||
|           </div> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password (repeat)' | ||||
|               hint='verify your password' | ||||
|               error={ passwordRepeatError } | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.rawKey.password2.hint' | ||||
|                   defaultMessage='verify your password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.rawKey.password2.label' | ||||
|                   defaultMessage='password (repeat)' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPasswordRepeat } | ||||
|               type='password' | ||||
|               error={ password2Error } | ||||
|               value={ password2 } | ||||
|               onChange={ this.onEditPassword2 } | ||||
|               value={ passwordRepeat } | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <PasswordStrength input={ password } /> | ||||
|       </Form> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   updateParent = () => { | ||||
|     const { isValidName, isValidPass, isValidKey, accountName, passwordHint, password1, rawKey } = this.state; | ||||
|     const isValid = isValidName && isValidPass && isValidKey; | ||||
|   onEditName = (event, name) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     this.props.onChange(isValid, { | ||||
|       name: accountName, | ||||
|       passwordHint, | ||||
|       password: password1, | ||||
|       rawKey | ||||
|     }); | ||||
|     store.setName(name); | ||||
|   } | ||||
| 
 | ||||
|   onEditPasswordHint = (event, value) => { | ||||
|     this.setState({ | ||||
|       passwordHint: value | ||||
|     }); | ||||
|   onEditPasswordHint = (event, passwordHint) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setPasswordHint(passwordHint); | ||||
|   } | ||||
| 
 | ||||
|   onEditKey = (event) => { | ||||
|     const { api } = this.context; | ||||
|     const rawKey = event.target.value; | ||||
|     let rawKeyError = null; | ||||
|   onEditPassword = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (!rawKey || !rawKey.trim().length) { | ||||
|       rawKeyError = ERRORS.noKey; | ||||
|     } else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !api.util.isHex(rawKey)) { | ||||
|       rawKeyError = ERRORS.invalidKey; | ||||
|     store.setPassword(password); | ||||
|   } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       rawKey, | ||||
|       rawKeyError, | ||||
|       isValidKey: !rawKeyError | ||||
|     }, this.updateParent); | ||||
|   onEditPasswordRepeat = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setPasswordRepeat(password); | ||||
|   } | ||||
| 
 | ||||
|   onEditAccountName = (event) => { | ||||
|     const accountName = event.target.value; | ||||
|     let accountNameError = null; | ||||
|   onEditKey = (event, rawKey) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (!accountName || !accountName.trim().length) { | ||||
|       accountNameError = ERRORS.noName; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       accountName, | ||||
|       accountNameError, | ||||
|       isValidName: !accountNameError | ||||
|     }, this.updateParent); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword1 = (event) => { | ||||
|     const password1 = event.target.value; | ||||
|     let password2Error = null; | ||||
| 
 | ||||
|     if (password1 !== this.state.password2) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password1, | ||||
|       password1Error: null, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword2 = (event) => { | ||||
|     const password2 = event.target.value; | ||||
|     let password2Error = null; | ||||
| 
 | ||||
|     if (password2 !== this.state.password1) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password2, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|     store.setRawKey(rawKey); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										126
									
								
								js/src/modals/CreateAccount/RawKey/rawKey.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								js/src/modals/CreateAccount/RawKey/rawKey.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import RawKey from './'; | ||||
| 
 | ||||
| let component; | ||||
| let instance; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <RawKey | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
|   instance = component.instance(); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/RawKey', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(component).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('events', () => { | ||||
|     describe('onEditName', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setName'); | ||||
|         instance.onEditName(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setName.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setName).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditKey', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setRawKey'); | ||||
|         instance.onEditKey(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setRawKey.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setRawKey).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPassword', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPassword'); | ||||
|         instance.onEditPassword(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPassword.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPassword).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordRepeat', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordRepeat'); | ||||
|         instance.onEditPasswordRepeat(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordRepeat.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordRepeat).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordHint', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordHint'); | ||||
|         instance.onEditPasswordHint(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordHint.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordHint).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,183 +14,165 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { Checkbox } from 'material-ui'; | ||||
| 
 | ||||
| import { Form, Input } from '~/ui'; | ||||
| import { Form, Input, PasswordStrength } from '~/ui'; | ||||
| 
 | ||||
| import styles from '../createAccount.css'; | ||||
| 
 | ||||
| import ERRORS from '../errors'; | ||||
| 
 | ||||
| @observer | ||||
| export default class RecoveryPhrase extends Component { | ||||
|   static propTypes = { | ||||
|     onChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     accountName: '', | ||||
|     accountNameError: ERRORS.noName, | ||||
|     isValidPass: true, | ||||
|     isValidName: false, | ||||
|     isValidPhrase: true, | ||||
|     passwordHint: '', | ||||
|     password1: '', | ||||
|     password1Error: null, | ||||
|     password2: '', | ||||
|     password2Error: null, | ||||
|     recoveryPhrase: '', | ||||
|     recoveryPhraseError: null, | ||||
|     windowsPhrase: false | ||||
|   } | ||||
| 
 | ||||
|   componentWillMount () { | ||||
|     this.props.onChange(false, {}); | ||||
|     store: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error, recoveryPhrase, windowsPhrase } = this.state; | ||||
|     const { isWindowsPhrase, name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint, phrase } = this.props.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         <Input | ||||
|           hint='the account recovery phrase' | ||||
|           label='account recovery phrase' | ||||
|           value={ recoveryPhrase } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.phrase.hint' | ||||
|               defaultMessage='the account recovery phrase' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.phrase.label' | ||||
|               defaultMessage='account recovery phrase' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditPhrase } | ||||
|           value={ phrase } | ||||
|         /> | ||||
|         <Input | ||||
|           label='account name' | ||||
|           hint='a descriptive name for the account' | ||||
|           error={ accountNameError } | ||||
|           value={ accountName } | ||||
|           onChange={ this.onEditAccountName } | ||||
|           error={ nameError } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.name.hint' | ||||
|               defaultMessage='a descriptive name for the account' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.name.label' | ||||
|               defaultMessage='account name' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditName } | ||||
|           value={ name } | ||||
|         /> | ||||
|         <Input | ||||
|           label='password hint' | ||||
|           hint='(optional) a hint to help with remembering the password' | ||||
|           value={ passwordHint } | ||||
|           hint={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.hint.hint' | ||||
|               defaultMessage='(optional) a hint to help with remembering the password' | ||||
|             /> | ||||
|           } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.hint.label' | ||||
|               defaultMessage='password hint' | ||||
|             /> | ||||
|           } | ||||
|           onChange={ this.onEditPasswordHint } | ||||
|           value={ passwordHint } | ||||
|         /> | ||||
|         <div className={ styles.passwords }> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password' | ||||
|               hint='a strong, unique password' | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.recoveryPhrase.password.hint' | ||||
|                   defaultMessage='a strong, unique password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.recoveryPhrase.password.label' | ||||
|                   defaultMessage='password' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPassword } | ||||
|               type='password' | ||||
|               error={ password1Error } | ||||
|               value={ password1 } | ||||
|               onChange={ this.onEditPassword1 } | ||||
|               value={ password } | ||||
|             /> | ||||
|           </div> | ||||
|           <div className={ styles.password }> | ||||
|             <Input | ||||
|               label='password (repeat)' | ||||
|               hint='verify your password' | ||||
|               error={ passwordRepeatError } | ||||
|               hint={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.recoveryPhrase.password2.hint' | ||||
|                   defaultMessage='verify your password' | ||||
|                 /> | ||||
|               } | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='createAccount.recoveryPhrase.password2.label' | ||||
|                   defaultMessage='password (repeat)' | ||||
|                 /> | ||||
|               } | ||||
|               onChange={ this.onEditPasswordRepeat } | ||||
|               type='password' | ||||
|               error={ password2Error } | ||||
|               value={ password2 } | ||||
|               onChange={ this.onEditPassword2 } | ||||
|               value={ passwordRepeat } | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <PasswordStrength input={ password } /> | ||||
|         <Checkbox | ||||
|           checked={ isWindowsPhrase } | ||||
|           className={ styles.checkbox } | ||||
|           label='Key was created with Parity <1.4.5 on Windows' | ||||
|           checked={ windowsPhrase } | ||||
|           label={ | ||||
|             <FormattedMessage | ||||
|               id='createAccount.recoveryPhrase.windowsKey.label' | ||||
|               defaultMessage='Key was created with Parity <1.4.5 on Windows' | ||||
|             /> | ||||
|           } | ||||
|           onCheck={ this.onToggleWindowsPhrase } | ||||
|         /> | ||||
|       </Form> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   updateParent = () => { | ||||
|     const { accountName, isValidName, isValidPass, isValidPhrase, password1, passwordHint, recoveryPhrase, windowsPhrase } = this.state; | ||||
|     const isValid = isValidName && isValidPass && isValidPhrase; | ||||
| 
 | ||||
|     this.props.onChange(isValid, { | ||||
|       name: accountName, | ||||
|       password: password1, | ||||
|       passwordHint, | ||||
|       phrase: recoveryPhrase, | ||||
|       windowsPhrase | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onEditPasswordHint = (event, value) => { | ||||
|     this.setState({ | ||||
|       passwordHint: value | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onToggleWindowsPhrase = (event) => { | ||||
|     this.setState({ | ||||
|       windowsPhrase: !this.state.windowsPhrase | ||||
|     }, this.updateParent); | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setWindowsPhrase(!store.isWindowsPhrase); | ||||
|   } | ||||
| 
 | ||||
|   onEditPhrase = (event) => { | ||||
|     const recoveryPhrase = event.target.value | ||||
|       .toLowerCase() // wordlists are lowercase
 | ||||
|       .trim() // remove whitespace at both ends
 | ||||
|       .replace(/\s/g, ' ') // replace any whitespace with single space
 | ||||
|       .replace(/ +/g, ' '); // replace multiple spaces with a single space
 | ||||
|   onEditPhrase = (event, phrase) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     const phraseParts = recoveryPhrase | ||||
|       .split(' ') | ||||
|       .map((part) => part.trim()) | ||||
|       .filter((part) => part.length); | ||||
| 
 | ||||
|     this.setState({ | ||||
|       recoveryPhrase: phraseParts.join(' '), | ||||
|       recoveryPhraseError: null, | ||||
|       isValidPhrase: true | ||||
|     }, this.updateParent); | ||||
|     store.setPhrase(phrase); | ||||
|   } | ||||
| 
 | ||||
|   onEditAccountName = (event) => { | ||||
|     const accountName = event.target.value; | ||||
|     let accountNameError = null; | ||||
|   onEditName = (event, name) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (!accountName || !accountName.trim().length) { | ||||
|       accountNameError = ERRORS.noName; | ||||
|     store.setName(name); | ||||
|   } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       accountName, | ||||
|       accountNameError, | ||||
|       isValidName: !accountNameError | ||||
|     }, this.updateParent); | ||||
|   onEditPassword = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     store.setPassword(password); | ||||
|   } | ||||
| 
 | ||||
|   onEditPassword1 = (event) => { | ||||
|     const password1 = event.target.value; | ||||
|     let password2Error = null; | ||||
|   onEditPasswordRepeat = (event, password) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|     if (password1 !== this.state.password2) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     store.setPasswordRepeat(password); | ||||
|   } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password1, | ||||
|       password1Error: null, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|   } | ||||
|   onEditPasswordHint = (event, passwordHint) => { | ||||
|     const { store } = this.props; | ||||
| 
 | ||||
|   onEditPassword2 = (event) => { | ||||
|     const password2 = event.target.value; | ||||
|     let password2Error = null; | ||||
| 
 | ||||
|     if (password2 !== this.state.password1) { | ||||
|       password2Error = ERRORS.noMatchPassword; | ||||
|     } | ||||
| 
 | ||||
|     this.setState({ | ||||
|       password2, | ||||
|       password2Error, | ||||
|       isValidPass: !password2Error | ||||
|     }, this.updateParent); | ||||
|     store.setPasswordHint(passwordHint); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,141 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import { createStore } from '../createAccount.test.js'; | ||||
| 
 | ||||
| import RecoveryPhrase from './'; | ||||
| 
 | ||||
| let component; | ||||
| let instance; | ||||
| let store; | ||||
| 
 | ||||
| function render () { | ||||
|   store = createStore(); | ||||
|   component = shallow( | ||||
|     <RecoveryPhrase | ||||
|       store={ store } | ||||
|     /> | ||||
|   ); | ||||
|   instance = component.instance(); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/RecoveryPhrase', () => { | ||||
|   beforeEach(() => { | ||||
|     render(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders with defaults', () => { | ||||
|     expect(component).to.be.ok; | ||||
|   }); | ||||
| 
 | ||||
|   describe('event handlers', () => { | ||||
|     describe('onEditName', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setName'); | ||||
|         instance.onEditName(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setName.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setName).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPhrase', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPhrase'); | ||||
|         instance.onEditPhrase(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPhrase.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPhrase).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPassword', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPassword'); | ||||
|         instance.onEditPassword(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPassword.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPassword).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordRepeat', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordRepeat'); | ||||
|         instance.onEditPasswordRepeat(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordRepeat.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordRepeat).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onEditPasswordHint', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setPasswordHint'); | ||||
|         instance.onEditPasswordHint(null, 'testValue'); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setPasswordHint.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setPasswordHint).to.have.been.calledWith('testValue'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('onToggleWindowsPhrase', () => { | ||||
|       beforeEach(() => { | ||||
|         sinon.spy(store, 'setWindowsPhrase'); | ||||
|         instance.onToggleWindowsPhrase(); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         store.setWindowsPhrase.restore(); | ||||
|       }); | ||||
| 
 | ||||
|       it('calls into the store', () => { | ||||
|         expect(store.setWindowsPhrase).to.have.been.calledWith(true); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,17 +14,17 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| 
 | ||||
| import ActionDone from 'material-ui/svg-icons/action/done'; | ||||
| import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | ||||
| import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; | ||||
| import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; | ||||
| import PrintIcon from 'material-ui/svg-icons/action/print'; | ||||
| 
 | ||||
| import { Button, Modal, Warning } from '~/ui'; | ||||
| import { createIdentityImg } from '~/api/util/identity'; | ||||
| import { newError } from '~/redux/actions'; | ||||
| import { Button, Modal } from '~/ui'; | ||||
| import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons'; | ||||
| import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg'; | ||||
| 
 | ||||
| import AccountDetails from './AccountDetails'; | ||||
| import AccountDetailsGeth from './AccountDetailsGeth'; | ||||
| @ -34,405 +34,271 @@ import NewGeth from './NewGeth'; | ||||
| import NewImport from './NewImport'; | ||||
| import RawKey from './RawKey'; | ||||
| import RecoveryPhrase from './RecoveryPhrase'; | ||||
| 
 | ||||
| import { createIdentityImg } from '~/api/util/identity'; | ||||
| import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store'; | ||||
| import print from './print'; | ||||
| import recoveryPage from './recovery-page.ejs'; | ||||
| import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg'; | ||||
| import recoveryPage from './recoveryPage.ejs'; | ||||
| 
 | ||||
| const TITLES = { | ||||
|   type: 'creation type', | ||||
|   create: 'create account', | ||||
|   info: 'account information', | ||||
|   import: 'import wallet' | ||||
|   type: ( | ||||
|     <FormattedMessage | ||||
|       id='createAccount.title.createType' | ||||
|       defaultMessage='creation type' | ||||
|     /> | ||||
|   ), | ||||
|   create: ( | ||||
|     <FormattedMessage | ||||
|       id='createAccount.title.createAccount' | ||||
|       defaultMessage='create account' | ||||
|     /> | ||||
|   ), | ||||
|   info: ( | ||||
|     <FormattedMessage | ||||
|       id='createAccount.title.accountInfo' | ||||
|       defaultMessage='account information' | ||||
|     /> | ||||
|   ), | ||||
|   import: ( | ||||
|     <FormattedMessage | ||||
|       id='createAccount.title.importWallet' | ||||
|       defaultMessage='import wallet' | ||||
|     /> | ||||
|   ) | ||||
| }; | ||||
| const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info]; | ||||
| const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info]; | ||||
| 
 | ||||
| export default class CreateAccount extends Component { | ||||
| @observer | ||||
| class CreateAccount extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
|     store: PropTypes.object.isRequired | ||||
|     api: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     accounts: PropTypes.object.isRequired, | ||||
|     newError: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func, | ||||
|     onUpdate: PropTypes.func | ||||
|   } | ||||
| 
 | ||||
|   state = { | ||||
|     address: null, | ||||
|     name: null, | ||||
|     passwordHint: null, | ||||
|     password: null, | ||||
|     phrase: null, | ||||
|     windowsPhrase: false, | ||||
|     rawKey: null, | ||||
|     json: null, | ||||
|     canCreate: false, | ||||
|     createType: null, | ||||
|     gethAddresses: [], | ||||
|     stage: 0 | ||||
|   } | ||||
|   store = new Store(this.context.api, this.props.accounts); | ||||
| 
 | ||||
|   render () { | ||||
|     const { createType, stage } = this.state; | ||||
|     const steps = createType === 'fromNew' | ||||
|       ? STAGE_NAMES | ||||
|       : STAGE_IMPORT; | ||||
|     const { createType, stage } = this.store; | ||||
| 
 | ||||
|     return ( | ||||
|       <Modal | ||||
|         visible | ||||
|         actions={ this.renderDialogActions() } | ||||
|         current={ stage } | ||||
|         steps={ steps } | ||||
|         steps={ | ||||
|           createType === 'fromNew' | ||||
|             ? STAGE_NAMES | ||||
|             : STAGE_IMPORT | ||||
|         } | ||||
|       > | ||||
|         { this.renderWarning() } | ||||
|         { this.renderPage() } | ||||
|       </Modal> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   renderPage () { | ||||
|     const { createType, stage } = this.state; | ||||
|     const { accounts } = this.props; | ||||
|     const { createType, stage } = this.store; | ||||
| 
 | ||||
|     switch (stage) { | ||||
|       case 0: | ||||
|       case STAGE_SELECT_TYPE: | ||||
|         return ( | ||||
|           <CreationType onChange={ this.onChangeType } /> | ||||
|           <CreationType store={ this.store } /> | ||||
|         ); | ||||
| 
 | ||||
|       case 1: | ||||
|       case STAGE_CREATE: | ||||
|         if (createType === 'fromNew') { | ||||
|           return ( | ||||
|             <NewAccount onChange={ this.onChangeDetails } /> | ||||
|             <NewAccount | ||||
|               newError={ this.props.newError } | ||||
|               store={ this.store } | ||||
|             /> | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         if (createType === 'fromGeth') { | ||||
|           return ( | ||||
|             <NewGeth | ||||
|               accounts={ accounts } | ||||
|               onChange={ this.onChangeGeth } | ||||
|             /> | ||||
|             <NewGeth store={ this.store } /> | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         if (createType === 'fromPhrase') { | ||||
|           return ( | ||||
|             <RecoveryPhrase onChange={ this.onChangeDetails } /> | ||||
|             <RecoveryPhrase store={ this.store } /> | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         if (createType === 'fromRaw') { | ||||
|           return ( | ||||
|             <RawKey onChange={ this.onChangeDetails } /> | ||||
|             <RawKey store={ this.store } /> | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|           <NewImport onChange={ this.onChangeWallet } /> | ||||
|           <NewImport store={ this.store } /> | ||||
|         ); | ||||
| 
 | ||||
|       case 2: | ||||
|       case STAGE_INFO: | ||||
|         if (createType === 'fromGeth') { | ||||
|           return ( | ||||
|             <AccountDetailsGeth addresses={ this.state.gethAddresses } /> | ||||
|             <AccountDetailsGeth store={ this.store } /> | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|           <AccountDetails | ||||
|             address={ this.state.address } | ||||
|             name={ this.state.name } | ||||
|             phrase={ this.state.phrase } | ||||
|           /> | ||||
|           <AccountDetails store={ this.store } /> | ||||
|         ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   renderDialogActions () { | ||||
|     const { createType, stage } = this.state; | ||||
|     const { createType, canCreate, isBusy, stage } = this.store; | ||||
| 
 | ||||
|     const cancelBtn = ( | ||||
|       <Button | ||||
|         icon={ <CancelIcon /> } | ||||
|         key='cancel' | ||||
|         label={ | ||||
|           <FormattedMessage | ||||
|             id='createAccount.button.cancel' | ||||
|             defaultMessage='Cancel' | ||||
|           /> | ||||
|         } | ||||
|         onClick={ this.onClose } | ||||
|       /> | ||||
|     ); | ||||
| 
 | ||||
|     switch (stage) { | ||||
|       case 0: | ||||
|       case STAGE_SELECT_TYPE: | ||||
|         return [ | ||||
|           cancelBtn, | ||||
|           <Button | ||||
|             icon={ <ContentClear /> } | ||||
|             label='Cancel' | ||||
|             onClick={ this.onClose } | ||||
|           />, | ||||
|           <Button | ||||
|             icon={ <NavigationArrowForward /> } | ||||
|             label='Next' | ||||
|             onClick={ this.onNext } | ||||
|             icon={ <NextIcon /> } | ||||
|             key='next' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.button.next' | ||||
|                 defaultMessage='Next' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.store.nextStage } | ||||
|           /> | ||||
|         ]; | ||||
|       case 1: | ||||
|         const createLabel = createType === 'fromNew' | ||||
|           ? 'Create' | ||||
|           : 'Import'; | ||||
| 
 | ||||
|       case STAGE_CREATE: | ||||
|         return [ | ||||
|           cancelBtn, | ||||
|           <Button | ||||
|             icon={ <ContentClear /> } | ||||
|             label='Cancel' | ||||
|             onClick={ this.onClose } | ||||
|             icon={ <PrevIcon /> } | ||||
|             key='back' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.button.back' | ||||
|                 defaultMessage='Back' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.store.prevStage } | ||||
|           />, | ||||
|           <Button | ||||
|             icon={ <NavigationArrowBack /> } | ||||
|             label='Back' | ||||
|             onClick={ this.onPrev } | ||||
|           />, | ||||
|           <Button | ||||
|             icon={ <ActionDone /> } | ||||
|             label={ createLabel } | ||||
|             disabled={ !this.state.canCreate } | ||||
|             disabled={ !canCreate || isBusy } | ||||
|             icon={ <CheckIcon /> } | ||||
|             key='create' | ||||
|             label={ | ||||
|               createType === 'fromNew' | ||||
|                 ? ( | ||||
|                   <FormattedMessage | ||||
|                     id='createAccount.button.create' | ||||
|                     defaultMessage='Create' | ||||
|                   /> | ||||
|                 ) | ||||
|                 : ( | ||||
|                   <FormattedMessage | ||||
|                     id='createAccount.button.import' | ||||
|                     defaultMessage='Import' | ||||
|                   /> | ||||
|                 ) | ||||
|             } | ||||
|             onClick={ this.onCreate } | ||||
|           /> | ||||
|         ]; | ||||
| 
 | ||||
|       case 2: | ||||
|       case STAGE_INFO: | ||||
|         return [ | ||||
|           createType === 'fromNew' || createType === 'fromPhrase' ? ( | ||||
|           ['fromNew', 'fromPhrase'].includes(createType) | ||||
|             ? ( | ||||
|               <Button | ||||
|                 icon={ <PrintIcon /> } | ||||
|               label='Print Phrase' | ||||
|                 key='print' | ||||
|                 label={ | ||||
|                   <FormattedMessage | ||||
|                     id='createAccount.button.print' | ||||
|                     defaultMessage='Print Phrase' | ||||
|                   /> | ||||
|                 } | ||||
|                 onClick={ this.printPhrase } | ||||
|               /> | ||||
|           ) : null, | ||||
|             ) | ||||
|             : null, | ||||
|           <Button | ||||
|             icon={ <ActionDoneAll /> } | ||||
|             label='Close' | ||||
|             icon={ <DoneIcon /> } | ||||
|             key='close' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='createAccount.button.close' | ||||
|                 defaultMessage='Close' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.onClose } | ||||
|           /> | ||||
|         ]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   renderWarning () { | ||||
|     const { createType, stage } = this.state; | ||||
| 
 | ||||
|     if (stage !== 1 || ['fromJSON', 'fromPresale'].includes(createType)) { | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Warning | ||||
|         warning={ | ||||
|           <FormattedMessage | ||||
|             id='createAccount.warning.insecurePassword' | ||||
|             defaultMessage='It is recommended that a strong password be used to secure your accounts. Empty and trivial passwords are a security risk.' | ||||
|           /> | ||||
|         } | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   onNext = () => { | ||||
|     this.setState({ | ||||
|       stage: this.state.stage + 1 | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onPrev = () => { | ||||
|     this.setState({ | ||||
|       stage: this.state.stage - 1 | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onCreate = () => { | ||||
|     const { createType, windowsPhrase } = this.state; | ||||
|     const { api } = this.context; | ||||
|     this.store.setBusy(true); | ||||
| 
 | ||||
|     this.setState({ | ||||
|       canCreate: false | ||||
|     }); | ||||
| 
 | ||||
|     if (createType === 'fromNew' || createType === 'fromPhrase') { | ||||
|       let phrase = this.state.phrase; | ||||
| 
 | ||||
|       if (createType === 'fromPhrase' && windowsPhrase) { | ||||
|         phrase = phrase | ||||
|           .split(' ') // get the words
 | ||||
|           .map((word) => word === 'misjudged' ? word : `${word}\r`) // add \r after each (except last in dict)
 | ||||
|           .join(' '); // re-create string
 | ||||
|       } | ||||
| 
 | ||||
|       return api.parity | ||||
|         .newAccountFromPhrase(phrase, this.state.password) | ||||
|         .then((address) => { | ||||
|           this.setState({ address }); | ||||
|           return api.parity | ||||
|             .setAccountName(address, this.state.name) | ||||
|             .then(() => api.parity.setAccountMeta(address, { | ||||
|               timestamp: Date.now(), | ||||
|               passwordHint: this.state.passwordHint | ||||
|             })); | ||||
|         }) | ||||
|     return this.store | ||||
|       .createAccount() | ||||
|       .then(() => { | ||||
|           this.onNext(); | ||||
|         this.store.setBusy(false); | ||||
|         this.store.nextStage(); | ||||
|         this.props.onUpdate && this.props.onUpdate(); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|           console.error('onCreate', error); | ||||
| 
 | ||||
|           this.setState({ | ||||
|             canCreate: true | ||||
|           }); | ||||
| 
 | ||||
|           this.newError(error); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     if (createType === 'fromRaw') { | ||||
|       return api.parity | ||||
|         .newAccountFromSecret(this.state.rawKey, this.state.password) | ||||
|         .then((address) => { | ||||
|           this.setState({ address }); | ||||
|           return api.parity | ||||
|             .setAccountName(address, this.state.name) | ||||
|             .then(() => api.parity.setAccountMeta(address, { | ||||
|               timestamp: Date.now(), | ||||
|               passwordHint: this.state.passwordHint | ||||
|             })); | ||||
|         }) | ||||
|         .then(() => { | ||||
|           this.onNext(); | ||||
|           this.props.onUpdate && this.props.onUpdate(); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('onCreate', error); | ||||
| 
 | ||||
|           this.setState({ | ||||
|             canCreate: true | ||||
|           }); | ||||
| 
 | ||||
|           this.newError(error); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     if (createType === 'fromGeth') { | ||||
|       return api.parity | ||||
|         .importGethAccounts(this.state.gethAddresses) | ||||
|         .then((result) => { | ||||
|           console.log('result', result); | ||||
| 
 | ||||
|           return Promise.all(this.state.gethAddresses.map((address) => { | ||||
|             return api.parity.setAccountName(address, 'Geth Import'); | ||||
|           })); | ||||
|         }) | ||||
|         .then(() => { | ||||
|           this.onNext(); | ||||
|           this.props.onUpdate && this.props.onUpdate(); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('onCreate', error); | ||||
| 
 | ||||
|           this.setState({ | ||||
|             canCreate: true | ||||
|           }); | ||||
| 
 | ||||
|           this.newError(error); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     return api.parity | ||||
|       .newAccountFromWallet(this.state.json, this.state.password) | ||||
|       .then((address) => { | ||||
|         this.setState({ | ||||
|           address: address | ||||
|         }); | ||||
| 
 | ||||
|         return api.parity | ||||
|           .setAccountName(address, this.state.name) | ||||
|           .then(() => api.parity.setAccountMeta(address, { | ||||
|             timestamp: Date.now(), | ||||
|             passwordHint: this.state.passwordHint | ||||
|           })); | ||||
|       }) | ||||
|       .then(() => { | ||||
|         this.onNext(); | ||||
|         this.props.onUpdate && this.props.onUpdate(); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('onCreate', error); | ||||
| 
 | ||||
|         this.setState({ | ||||
|           canCreate: true | ||||
|         }); | ||||
| 
 | ||||
|         this.newError(error); | ||||
|         this.store.setBusy(false); | ||||
|         this.props.newError(error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   onClose = () => { | ||||
|     this.setState({ | ||||
|       stage: 0, | ||||
|       canCreate: false | ||||
|     }, () => { | ||||
|     this.props.onClose && this.props.onClose(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeType = (value) => { | ||||
|     this.setState({ | ||||
|       createType: value | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeDetails = (canCreate, { name, passwordHint, address, password, phrase, rawKey, windowsPhrase }) => { | ||||
|     const nextState = { | ||||
|       canCreate, | ||||
|       name, | ||||
|       passwordHint, | ||||
|       address, | ||||
|       password, | ||||
|       phrase, | ||||
|       windowsPhrase: windowsPhrase || false, | ||||
|       rawKey | ||||
|     }; | ||||
| 
 | ||||
|     this.setState(nextState); | ||||
|   } | ||||
| 
 | ||||
|   onChangeRaw = (canCreate, rawKey) => { | ||||
|     this.setState({ | ||||
|       canCreate, | ||||
|       rawKey | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeGeth = (canCreate, gethAddresses) => { | ||||
|     this.setState({ | ||||
|       canCreate, | ||||
|       gethAddresses | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeWallet = (canCreate, { name, passwordHint, password, json }) => { | ||||
|     this.setState({ | ||||
|       canCreate, | ||||
|       name, | ||||
|       passwordHint, | ||||
|       password, | ||||
|       json | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   newError = (error) => { | ||||
|     const { store } = this.context; | ||||
| 
 | ||||
|     store.dispatch({ type: 'newError', error }); | ||||
|   } | ||||
| 
 | ||||
|   printPhrase = () => { | ||||
|     const { address, phrase, name } = this.state; | ||||
|     const { address, name, phrase } = this.store; | ||||
|     const identity = createIdentityImg(address); | ||||
| 
 | ||||
|     print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo })); | ||||
|     print(recoveryPage({ | ||||
|       address, | ||||
|       identity, | ||||
|       logo: ParityLogo, | ||||
|       name, | ||||
|       phrase | ||||
|     })); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function mapDispatchToProps (dispatch) { | ||||
|   return bindActionCreators({ | ||||
|     newError | ||||
|   }, dispatch); | ||||
| } | ||||
| 
 | ||||
| export default connect( | ||||
|   null, | ||||
|   mapDispatchToProps | ||||
| )(CreateAccount); | ||||
|  | ||||
							
								
								
									
										51
									
								
								js/src/modals/CreateAccount/createAccount.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								js/src/modals/CreateAccount/createAccount.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import { ACCOUNTS, createApi, createRedux } from './createAccount.test.js'; | ||||
| 
 | ||||
| import CreateAccount from './'; | ||||
| 
 | ||||
| let api; | ||||
| let component; | ||||
| 
 | ||||
| function render () { | ||||
|   api = createApi(); | ||||
|   component = shallow( | ||||
|     <CreateAccount | ||||
|       accounts={ ACCOUNTS } | ||||
|     />, | ||||
|     { | ||||
|       context: { | ||||
|         store: createRedux() | ||||
|       } | ||||
|     } | ||||
|   ).find('CreateAccount').shallow({ | ||||
|     context: { api } | ||||
|   }); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount', () => { | ||||
|   describe('rendering', () => { | ||||
|     it('renders with defaults', () => { | ||||
|       expect(render()).to.be.ok; | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										71
									
								
								js/src/modals/CreateAccount/createAccount.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								js/src/modals/CreateAccount/createAccount.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import BigNumber from 'bignumber.js'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import Store from './store'; | ||||
| 
 | ||||
| const ADDRESS = '0x00000123456789abcdef123456789abcdef123456789abcdef'; | ||||
| const ACCOUNTS = { [ADDRESS]: {} }; | ||||
| const GETH_ADDRESSES = [ | ||||
|   '0x123456789abcdef123456789abcdef123456789abcdef00000', | ||||
|   '0x00000123456789abcdef123456789abcdef123456789abcdef' | ||||
| ]; | ||||
| 
 | ||||
| let counter = 1; | ||||
| 
 | ||||
| function createApi () { | ||||
|   return { | ||||
|     eth: { | ||||
|       getBalance: sinon.stub().resolves(new BigNumber(1)) | ||||
|     }, | ||||
|     parity: { | ||||
|       generateSecretPhrase: sinon.stub().resolves('some account phrase'), | ||||
|       importGethAccounts: sinon.stub().resolves(), | ||||
|       listGethAccounts: sinon.stub().resolves(GETH_ADDRESSES), | ||||
|       newAccountFromPhrase: sinon.stub().resolves(ADDRESS), | ||||
|       newAccountFromSecret: sinon.stub().resolves(ADDRESS), | ||||
|       newAccountFromWallet: sinon.stub().resolves(ADDRESS), | ||||
|       phraseToAddress: () => Promise.resolve(`${++counter}`), | ||||
|       setAccountMeta: sinon.stub().resolves(), | ||||
|       setAccountName: sinon.stub().resolves() | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function createRedux () { | ||||
|   return { | ||||
|     dispatch: sinon.stub(), | ||||
|     subscribe: sinon.stub(), | ||||
|     getState: () => { | ||||
|       return {}; | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function createStore () { | ||||
|   return new Store(createApi(), ACCOUNTS); | ||||
| } | ||||
| 
 | ||||
| export { | ||||
|   ACCOUNTS, | ||||
|   ADDRESS, | ||||
|   GETH_ADDRESSES, | ||||
|   createApi, | ||||
|   createRedux, | ||||
|   createStore | ||||
| }; | ||||
| @ -1,5 +1,4 @@ | ||||
| <!DOCTYPE html> | ||||
| 
 | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <title>Recovery phrase for <%= name %></title> | ||||
							
								
								
									
										379
									
								
								js/src/modals/CreateAccount/store.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								js/src/modals/CreateAccount/store.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,379 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { action, computed, observable, transaction } from 'mobx'; | ||||
| 
 | ||||
| import apiutil from '~/api/util'; | ||||
| 
 | ||||
| import ERRORS from './errors'; | ||||
| 
 | ||||
| const FAKEPATH = 'C:\\fakepath\\'; | ||||
| const STAGE_SELECT_TYPE = 0; | ||||
| const STAGE_CREATE = 1; | ||||
| const STAGE_INFO = 2; | ||||
| 
 | ||||
| export default class Store { | ||||
|   @observable accounts = null; | ||||
|   @observable address = null; | ||||
|   @observable createType = 'fromNew'; | ||||
|   @observable description = ''; | ||||
|   @observable gethAccountsAvailable = []; | ||||
|   @observable gethAddresses = []; | ||||
|   @observable isBusy = false; | ||||
|   @observable isWindowsPhrase = false; | ||||
|   @observable name = ''; | ||||
|   @observable nameError = ERRORS.noName; | ||||
|   @observable password = ''; | ||||
|   @observable passwordHint = ''; | ||||
|   @observable passwordRepeat = ''; | ||||
|   @observable phrase = ''; | ||||
|   @observable rawKey = ''; | ||||
|   @observable rawKeyError = ERRORS.nokey; | ||||
|   @observable stage = STAGE_SELECT_TYPE; | ||||
|   @observable walletFile = ''; | ||||
|   @observable walletFileError = ERRORS.noFile; | ||||
|   @observable walletJson = ''; | ||||
| 
 | ||||
|   constructor (api, accounts, loadGeth = true) { | ||||
|     this._api = api; | ||||
|     this.accounts = Object.assign({}, accounts); | ||||
| 
 | ||||
|     if (loadGeth) { | ||||
|       this.loadAvailableGethAccounts(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @computed get canCreate () { | ||||
|     switch (this.createType) { | ||||
|       case 'fromGeth': | ||||
|         return this.gethAddresses.length !== 0; | ||||
| 
 | ||||
|       case 'fromJSON': | ||||
|       case 'fromPresale': | ||||
|         return !(this.nameError || this.walletFileError); | ||||
| 
 | ||||
|       case 'fromNew': | ||||
|         return !(this.nameError || this.passwordRepeatError); | ||||
| 
 | ||||
|       case 'fromPhrase': | ||||
|         return !(this.nameError || this.passwordRepeatError); | ||||
| 
 | ||||
|       case 'fromRaw': | ||||
|         return !(this.nameError || this.passwordRepeatError || this.rawKeyError); | ||||
| 
 | ||||
|       default: | ||||
|         return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @computed get passwordRepeatError () { | ||||
|     return this.password === this.passwordRepeat | ||||
|       ? null | ||||
|       : ERRORS.noMatchPassword; | ||||
|   } | ||||
| 
 | ||||
|   @action clearErrors = () => { | ||||
|     transaction(() => { | ||||
|       this.password = ''; | ||||
|       this.passwordRepeat = ''; | ||||
|       this.nameError = null; | ||||
|       this.rawKeyError = null; | ||||
|       this.walletFileError = null; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action selectGethAccount = (address) => { | ||||
|     if (this.gethAddresses.includes(address)) { | ||||
|       this.gethAddresses = this.gethAddresses.filter((_address) => _address !== address); | ||||
|     } else { | ||||
|       this.gethAddresses = [address].concat(this.gethAddresses.peek()); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @action setAddress = (address) => { | ||||
|     this.address = address; | ||||
|   } | ||||
| 
 | ||||
|   @action setBusy = (isBusy) => { | ||||
|     this.isBusy = isBusy; | ||||
|   } | ||||
| 
 | ||||
|   @action setCreateType = (createType) => { | ||||
|     this.clearErrors(); | ||||
|     this.createType = createType; | ||||
|   } | ||||
| 
 | ||||
|   @action setDescription = (description) => { | ||||
|     this.description = description; | ||||
|   } | ||||
| 
 | ||||
|   @action setGethAccountsAvailable = (gethAccountsAvailable) => { | ||||
|     this.gethAccountsAvailable = [].concat(gethAccountsAvailable); | ||||
|   } | ||||
| 
 | ||||
|   @action setWindowsPhrase = (isWindowsPhrase = false) => { | ||||
|     this.isWindowsPhrase = isWindowsPhrase; | ||||
|   } | ||||
| 
 | ||||
|   @action setName = (name) => { | ||||
|     let nameError = null; | ||||
| 
 | ||||
|     if (!name || !name.trim().length) { | ||||
|       nameError = ERRORS.noName; | ||||
|     } | ||||
| 
 | ||||
|     transaction(() => { | ||||
|       this.name = name; | ||||
|       this.nameError = nameError; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setPassword = (password) => { | ||||
|     this.password = password; | ||||
|   } | ||||
| 
 | ||||
|   @action setPasswordHint = (passwordHint) => { | ||||
|     this.passwordHint = passwordHint; | ||||
|   } | ||||
| 
 | ||||
|   @action setPasswordRepeat = (passwordRepeat) => { | ||||
|     this.passwordRepeat = passwordRepeat; | ||||
|   } | ||||
| 
 | ||||
|   @action setPhrase = (phrase) => { | ||||
|     const recoveryPhrase = phrase | ||||
|       .toLowerCase() // wordlists are lowercase
 | ||||
|       .trim() // remove whitespace at both ends
 | ||||
|       .replace(/\s/g, ' ') // replace any whitespace with single space
 | ||||
|       .replace(/ +/g, ' '); // replace multiple spaces with a single space
 | ||||
| 
 | ||||
|     const phraseParts = recoveryPhrase | ||||
|       .split(' ') | ||||
|       .map((part) => part.trim()) | ||||
|       .filter((part) => part.length); | ||||
| 
 | ||||
|     this.phrase = phraseParts.join(' '); | ||||
|   } | ||||
| 
 | ||||
|   @action setRawKey = (rawKey) => { | ||||
|     let rawKeyError = null; | ||||
| 
 | ||||
|     if (!rawKey || !rawKey.trim().length) { | ||||
|       rawKeyError = ERRORS.noKey; | ||||
|     } else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !apiutil.isHex(rawKey)) { | ||||
|       rawKeyError = ERRORS.invalidKey; | ||||
|     } | ||||
| 
 | ||||
|     transaction(() => { | ||||
|       this.rawKey = rawKey; | ||||
|       this.rawKeyError = rawKeyError; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setStage = (stage) => { | ||||
|     this.stage = stage; | ||||
|   } | ||||
| 
 | ||||
|   @action setWalletFile = (walletFile) => { | ||||
|     transaction(() => { | ||||
|       this.walletFile = walletFile.replace(FAKEPATH, ''); | ||||
|       this.walletFileError = ERRORS.noFile; | ||||
|       this.walletJson = null; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setWalletJson = (walletJson) => { | ||||
|     transaction(() => { | ||||
|       this.walletFileError = null; | ||||
|       this.walletJson = walletJson; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action nextStage = () => { | ||||
|     this.stage++; | ||||
|   } | ||||
| 
 | ||||
|   @action prevStage = () => { | ||||
|     this.stage--; | ||||
|   } | ||||
| 
 | ||||
|   createAccount = () => { | ||||
|     switch (this.createType) { | ||||
|       case 'fromGeth': | ||||
|         return this.createAccountFromGeth(); | ||||
| 
 | ||||
|       case 'fromJSON': | ||||
|       case 'fromPresale': | ||||
|         return this.createAccountFromWallet(); | ||||
| 
 | ||||
|       case 'fromNew': | ||||
|       case 'fromPhrase': | ||||
|         return this.createAccountFromPhrase(); | ||||
| 
 | ||||
|       case 'fromRaw': | ||||
|         return this.createAccountFromRaw(); | ||||
| 
 | ||||
|       default: | ||||
|         throw new Error(`Cannot create account for ${this.createType}`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   createAccountFromGeth = (timestamp = Date.now()) => { | ||||
|     return this._api.parity | ||||
|       .importGethAccounts(this.gethAddresses.peek()) | ||||
|       .then(() => { | ||||
|         return Promise.all(this.gethAddresses.map((address) => { | ||||
|           return this._api.parity.setAccountName(address, 'Geth Import'); | ||||
|         })); | ||||
|       }) | ||||
|       .then(() => { | ||||
|         return Promise.all(this.gethAddresses.map((address) => { | ||||
|           return this._api.parity.setAccountMeta(address, { | ||||
|             timestamp | ||||
|           }); | ||||
|         })); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createAccount', error); | ||||
|         throw error; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   createAccountFromPhrase = (timestamp = Date.now()) => { | ||||
|     let formattedPhrase = this.phrase; | ||||
| 
 | ||||
|     if (this.isWindowsPhrase && this.createType === 'fromPhrase') { | ||||
|       formattedPhrase = this.phrase | ||||
|         .split(' ') // get the words
 | ||||
|         .map((word) => word === 'misjudged' ? word : `${word}\r`) // add \r after each (except last in dict)
 | ||||
|         .join(' '); // re-create string
 | ||||
|     } | ||||
| 
 | ||||
|     return this._api.parity | ||||
|       .newAccountFromPhrase(formattedPhrase, this.password) | ||||
|       .then((address) => { | ||||
|         this.setAddress(address); | ||||
| 
 | ||||
|         return this._api.parity | ||||
|           .setAccountName(address, this.name) | ||||
|           .then(() => this._api.parity.setAccountMeta(address, { | ||||
|             passwordHint: this.passwordHint, | ||||
|             timestamp | ||||
|           })); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createAccount', error); | ||||
|         throw error; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   createAccountFromRaw = (timestamp = Date.now()) => { | ||||
|     return this._api.parity | ||||
|       .newAccountFromSecret(this.rawKey, this.password) | ||||
|       .then((address) => { | ||||
|         this.setAddress(address); | ||||
| 
 | ||||
|         return this._api.parity | ||||
|           .setAccountName(address, this.name) | ||||
|           .then(() => this._api.parity.setAccountMeta(address, { | ||||
|             passwordHint: this.passwordHint, | ||||
|             timestamp | ||||
|           })); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createAccount', error); | ||||
|         throw error; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   createAccountFromWallet = (timestamp = Date.now()) => { | ||||
|     return this._api.parity | ||||
|       .newAccountFromWallet(this.walletJson, this.password) | ||||
|       .then((address) => { | ||||
|         this.setAddress(address); | ||||
| 
 | ||||
|         return this._api.parity | ||||
|           .setAccountName(address, this.name) | ||||
|           .then(() => this._api.parity.setAccountMeta(address, { | ||||
|             passwordHint: this.passwordHint, | ||||
|             timestamp | ||||
|           })); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createAccount', error); | ||||
|         throw error; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   createIdentities = () => { | ||||
|     return Promise | ||||
|       .all([ | ||||
|         this._api.parity.generateSecretPhrase(), | ||||
|         this._api.parity.generateSecretPhrase(), | ||||
|         this._api.parity.generateSecretPhrase(), | ||||
|         this._api.parity.generateSecretPhrase(), | ||||
|         this._api.parity.generateSecretPhrase() | ||||
|       ]) | ||||
|       .then((phrases) => { | ||||
|         return Promise | ||||
|           .all(phrases.map((phrase) => this._api.parity.phraseToAddress(phrase))) | ||||
|           .then((addresses) => { | ||||
|             return phrases.reduce((accounts, phrase, index) => { | ||||
|               const address = addresses[index]; | ||||
| 
 | ||||
|               accounts[address] = { | ||||
|                 address, | ||||
|                 phrase | ||||
|               }; | ||||
| 
 | ||||
|               return accounts; | ||||
|             }, {}); | ||||
|           }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('createIdentities', error); | ||||
|         throw error; | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
|   loadAvailableGethAccounts () { | ||||
|     return this._api.parity | ||||
|       .listGethAccounts() | ||||
|       .then((_addresses) => { | ||||
|         const addresses = (_addresses || []).filter((address) => !this.accounts[address]); | ||||
| 
 | ||||
|         return Promise | ||||
|           .all(addresses.map((address) => this._api.eth.getBalance(address))) | ||||
|           .then((balances) => { | ||||
|             this.setGethAccountsAvailable(addresses.map((address, index) => { | ||||
|               return { | ||||
|                 address, | ||||
|                 balance: apiutil.fromWei(balances[index]).toFormat(5) | ||||
|               }; | ||||
|             })); | ||||
|           }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.warn('loadAvailableGethAccounts', error); | ||||
|       }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export { | ||||
|   STAGE_CREATE, | ||||
|   STAGE_INFO, | ||||
|   STAGE_SELECT_TYPE | ||||
| }; | ||||
							
								
								
									
										624
									
								
								js/src/modals/CreateAccount/store.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										624
									
								
								js/src/modals/CreateAccount/store.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,624 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import Store from './store'; | ||||
| 
 | ||||
| import { ACCOUNTS, ADDRESS, GETH_ADDRESSES, createApi } from './createAccount.test.js'; | ||||
| 
 | ||||
| let api; | ||||
| let store; | ||||
| 
 | ||||
| function createStore (loadGeth) { | ||||
|   api = createApi(); | ||||
|   store = new Store(api, ACCOUNTS, loadGeth); | ||||
| 
 | ||||
|   return store; | ||||
| } | ||||
| 
 | ||||
| describe('modals/CreateAccount/Store', () => { | ||||
|   beforeEach(() => { | ||||
|     createStore(); | ||||
|   }); | ||||
| 
 | ||||
|   describe('constructor', () => { | ||||
|     it('captures the accounts passed', () => { | ||||
|       expect(store.accounts).to.deep.equal(ACCOUNTS); | ||||
|     }); | ||||
| 
 | ||||
|     it('starts as non-busy', () => { | ||||
|       expect(store.isBusy).to.be.false; | ||||
|     }); | ||||
| 
 | ||||
|     it('sets the initial createType to fromNew', () => { | ||||
|       expect(store.createType).to.equal('fromNew'); | ||||
|     }); | ||||
| 
 | ||||
|     it('sets the initial stage to create', () => { | ||||
|       expect(store.stage).to.equal(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('loads the geth accounts', () => { | ||||
|       expect(store.gethAccountsAvailable.map((account) => account.address)).to.deep.equal([GETH_ADDRESSES[0]]); | ||||
|     }); | ||||
| 
 | ||||
|     it('does not load geth accounts when loadGeth === false', () => { | ||||
|       createStore(false); | ||||
|       expect(store.gethAccountsAvailable.peek()).to.deep.equal([]); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('@action', () => { | ||||
|     describe('clearErrors', () => { | ||||
|       it('clears all errors', () => { | ||||
|         store.clearErrors(); | ||||
| 
 | ||||
|         expect(store.nameError).to.be.null; | ||||
|         expect(store.passwordRepeatError).to.be.null; | ||||
|         expect(store.rawKeyError).to.be.null; | ||||
|         expect(store.walletFileError).to.be.null; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('selectGethAccount', () => { | ||||
|       it('selects and deselects and address', () => { | ||||
|         expect(store.gethAddresses.peek()).to.deep.equal([]); | ||||
|         store.selectGethAccount(GETH_ADDRESSES[0]); | ||||
|         expect(store.gethAddresses.peek()).to.deep.equal([GETH_ADDRESSES[0]]); | ||||
|         store.selectGethAccount(GETH_ADDRESSES[0]); | ||||
|         expect(store.gethAddresses.peek()).to.deep.equal([]); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setAddress', () => { | ||||
|       const ADDR = '0x1234567890123456789012345678901234567890'; | ||||
| 
 | ||||
|       it('sets the address', () => { | ||||
|         store.setAddress(ADDR); | ||||
|         expect(store.address).to.equal(ADDR); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setBusy', () => { | ||||
|       it('sets the busy flag', () => { | ||||
|         store.setBusy(true); | ||||
|         expect(store.isBusy).to.be.true; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setCreateType', () => { | ||||
|       it('allows changing the type', () => { | ||||
|         store.setCreateType('testing'); | ||||
|         expect(store.createType).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setDescription', () => { | ||||
|       it('allows setting the description', () => { | ||||
|         store.setDescription('testing'); | ||||
|         expect(store.description).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setName', () => { | ||||
|       it('allows setting the name', () => { | ||||
|         store.setName('testing'); | ||||
|         expect(store.name).to.equal('testing'); | ||||
|         expect(store.nameError).to.be.null; | ||||
|       }); | ||||
| 
 | ||||
|       it('sets errors on invalid names', () => { | ||||
|         store.setName(''); | ||||
|         expect(store.nameError).not.to.be.null; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setPassword', () => { | ||||
|       it('allows setting the password', () => { | ||||
|         store.setPassword('testing'); | ||||
|         expect(store.password).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setPasswordHint', () => { | ||||
|       it('allows setting the passwordHint', () => { | ||||
|         store.setPasswordHint('testing'); | ||||
|         expect(store.passwordHint).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setPasswordRepeat', () => { | ||||
|       it('allows setting the passwordRepeat', () => { | ||||
|         store.setPasswordRepeat('testing'); | ||||
|         expect(store.passwordRepeat).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setPhrase', () => { | ||||
|       it('allows setting the phrase', () => { | ||||
|         store.setPhrase('testing'); | ||||
|         expect(store.phrase).to.equal('testing'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setRawKey', () => { | ||||
|       it('sets error when empty key', () => { | ||||
|         store.setRawKey(null); | ||||
|         expect(store.rawKeyError).not.to.be.null; | ||||
|       }); | ||||
| 
 | ||||
|       it('sets error when non-hex value', () => { | ||||
|         store.setRawKey('0000000000000000000000000000000000000000000000000000000000000000'); | ||||
|         expect(store.rawKeyError).not.to.be.null; | ||||
|       }); | ||||
| 
 | ||||
|       it('sets error when non-valid length value', () => { | ||||
|         store.setRawKey('0x0'); | ||||
|         expect(store.rawKeyError).not.to.be.null; | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the key when checks pass', () => { | ||||
|         const KEY = '0x1000000000000000000000000000000000000000000000000000000000000000'; | ||||
| 
 | ||||
|         store.setRawKey(KEY); | ||||
|         expect(store.rawKey).to.equal(KEY); | ||||
|         expect(store.rawKeyError).to.be.null; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setStage', () => { | ||||
|       it('changes to the provided stage', () => { | ||||
|         store.setStage(2); | ||||
|         expect(store.stage).to.equal(2); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setWalletFile', () => { | ||||
|       it('sets the filepath', () => { | ||||
|         store.setWalletFile('testing'); | ||||
|         expect(store.walletFile).to.equal('testing'); | ||||
|       }); | ||||
| 
 | ||||
|       it('cleans up the fakepath', () => { | ||||
|         store.setWalletFile('C:\\fakepath\\testing'); | ||||
|         expect(store.walletFile).to.equal('testing'); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the error', () => { | ||||
|         store.setWalletFile('testing'); | ||||
|         expect(store.walletFileError).not.to.be.null; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setWalletJson', () => { | ||||
|       it('sets the json', () => { | ||||
|         store.setWalletJson('testing'); | ||||
|         expect(store.walletJson).to.equal('testing'); | ||||
|       }); | ||||
| 
 | ||||
|       it('clears previous file errors', () => { | ||||
|         store.setWalletFile('testing'); | ||||
|         store.setWalletJson('testing'); | ||||
|         expect(store.walletFileError).to.be.null; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('setWindowsPhrase', () => { | ||||
|       it('allows setting the windows toggle', () => { | ||||
|         store.setWindowsPhrase(true); | ||||
|         expect(store.isWindowsPhrase).to.be.true; | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('nextStage/prevStage', () => { | ||||
|       it('changes to next/prev', () => { | ||||
|         expect(store.stage).to.equal(0); | ||||
|         store.nextStage(); | ||||
|         expect(store.stage).to.equal(1); | ||||
|         store.prevStage(); | ||||
|         expect(store.stage).to.equal(0); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('@computed', () => { | ||||
|     describe('canCreate', () => { | ||||
|       beforeEach(() => { | ||||
|         store.clearErrors(); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === fromGeth', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromGeth'); | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on none selected', () => { | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns true when selected', () => { | ||||
|           store.selectGethAccount(GETH_ADDRESSES[0]); | ||||
|           expect(store.canCreate).to.be.true; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === fromJSON/fromPresale', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromJSON'); | ||||
|         }); | ||||
| 
 | ||||
|         it('returns true on no errors', () => { | ||||
|           expect(store.canCreate).to.be.true; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on nameError', () => { | ||||
|           store.setName(''); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on walletFileError', () => { | ||||
|           store.setWalletFile('testing'); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === fromNew', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromNew'); | ||||
|         }); | ||||
| 
 | ||||
|         it('returns true on no errors', () => { | ||||
|           expect(store.canCreate).to.be.true; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on nameError', () => { | ||||
|           store.setName(''); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on passwordRepeatError', () => { | ||||
|           store.setPassword('testing'); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === fromPhrase', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromPhrase'); | ||||
|         }); | ||||
| 
 | ||||
|         it('returns true on no errors', () => { | ||||
|           expect(store.canCreate).to.be.true; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on nameError', () => { | ||||
|           store.setName(''); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on passwordRepeatError', () => { | ||||
|           store.setPassword('testing'); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === fromRaw', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromRaw'); | ||||
|         }); | ||||
| 
 | ||||
|         it('returns true on no errors', () => { | ||||
|           expect(store.canCreate).to.be.true; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on nameError', () => { | ||||
|           store.setName(''); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on passwordRepeatError', () => { | ||||
|           store.setPassword('testing'); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
| 
 | ||||
|         it('returns false on rawKeyError', () => { | ||||
|           store.setRawKey('testing'); | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createType === anythingElse', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('anythingElse'); | ||||
|         }); | ||||
| 
 | ||||
|         it('always returns false', () => { | ||||
|           expect(store.canCreate).to.be.false; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('passwordRepeatError', () => { | ||||
|       it('is clear when passwords match', () => { | ||||
|         store.setPassword('testing'); | ||||
|         store.setPasswordRepeat('testing'); | ||||
|         expect(store.passwordRepeatError).to.be.null; | ||||
|       }); | ||||
| 
 | ||||
|       it('has error when passwords does not match', () => { | ||||
|         store.setPassword('testing'); | ||||
|         store.setPasswordRepeat('testing2'); | ||||
|         expect(store.passwordRepeatError).not.to.be.null; | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('operations', () => { | ||||
|     describe('createAccount', () => { | ||||
|       let createAccountFromGethSpy; | ||||
|       let createAccountFromWalletSpy; | ||||
|       let createAccountFromPhraseSpy; | ||||
|       let createAccountFromRawSpy; | ||||
| 
 | ||||
|       beforeEach(() => { | ||||
|         createAccountFromGethSpy = sinon.spy(store, 'createAccountFromGeth'); | ||||
|         createAccountFromWalletSpy = sinon.spy(store, 'createAccountFromWallet'); | ||||
|         createAccountFromPhraseSpy = sinon.spy(store, 'createAccountFromPhrase'); | ||||
|         createAccountFromRawSpy = sinon.spy(store, 'createAccountFromRaw'); | ||||
|       }); | ||||
| 
 | ||||
|       it('throws error on invalid createType', () => { | ||||
|         store.setCreateType('testing'); | ||||
|         expect(() => store.createAccount()).to.throw; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromGeth on createType === fromGeth', () => { | ||||
|         store.setCreateType('fromGeth'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromGethSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromWallet on createType === fromJSON', () => { | ||||
|         store.setCreateType('fromJSON'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromWalletSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromPhrase on createType === fromNew', () => { | ||||
|         store.setCreateType('fromNew'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromPhraseSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromPhrase on createType === fromPhrase', () => { | ||||
|         store.setCreateType('fromPhrase'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromPhraseSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromWallet on createType === fromPresale', () => { | ||||
|         store.setCreateType('fromPresale'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromWalletSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       it('calls createAccountFromRaw on createType === fromRaw', () => { | ||||
|         store.setCreateType('fromRaw'); | ||||
|         store.createAccount(); | ||||
|         expect(createAccountFromRawSpy).to.have.been.called; | ||||
|       }); | ||||
| 
 | ||||
|       describe('createAccountFromGeth', () => { | ||||
|         beforeEach(() => { | ||||
|           store.selectGethAccount(GETH_ADDRESSES[0]); | ||||
|         }); | ||||
| 
 | ||||
|         it('calls parity.importGethAccounts', () => { | ||||
|           return store.createAccountFromGeth().then(() => { | ||||
|             expect(store._api.parity.importGethAccounts).to.have.been.calledWith([GETH_ADDRESSES[0]]); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account name', () => { | ||||
|           return store.createAccountFromGeth().then(() => { | ||||
|             expect(store._api.parity.setAccountName).to.have.been.calledWith(GETH_ADDRESSES[0], 'Geth Import'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account meta', () => { | ||||
|           return store.createAccountFromGeth(-1).then(() => { | ||||
|             expect(store._api.parity.setAccountMeta).to.have.been.calledWith(GETH_ADDRESSES[0], { | ||||
|               timestamp: -1 | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createAccountFromPhrase', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setCreateType('fromPhrase'); | ||||
|           store.setName('some name'); | ||||
|           store.setPassword('P@55worD'); | ||||
|           store.setPasswordHint('some hint'); | ||||
|           store.setPhrase('some phrase'); | ||||
|         }); | ||||
| 
 | ||||
|         it('calls parity.newAccountFromWallet', () => { | ||||
|           return store.createAccountFromPhrase().then(() => { | ||||
|             expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('some phrase', 'P@55worD'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the address', () => { | ||||
|           return store.createAccountFromPhrase().then(() => { | ||||
|             expect(store.address).to.equal(ADDRESS); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account name', () => { | ||||
|           return store.createAccountFromPhrase().then(() => { | ||||
|             expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account meta', () => { | ||||
|           return store.createAccountFromPhrase(-1).then(() => { | ||||
|             expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, { | ||||
|               passwordHint: 'some hint', | ||||
|               timestamp: -1 | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('adjusts phrases for Windows', () => { | ||||
|           store.setWindowsPhrase(true); | ||||
|           return store.createAccountFromPhrase().then(() => { | ||||
|             expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('some\r phrase\r', 'P@55worD'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('adjusts phrases for Windows (except last word)', () => { | ||||
|           store.setWindowsPhrase(true); | ||||
|           store.setPhrase('misjudged phrase'); | ||||
|           return store.createAccountFromPhrase().then(() => { | ||||
|             expect(store._api.parity.newAccountFromPhrase).to.have.been.calledWith('misjudged phrase\r', 'P@55worD'); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createAccountFromRaw', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setName('some name'); | ||||
|           store.setPassword('P@55worD'); | ||||
|           store.setPasswordHint('some hint'); | ||||
|           store.setRawKey('rawKey'); | ||||
|         }); | ||||
| 
 | ||||
|         it('calls parity.newAccountFromSecret', () => { | ||||
|           return store.createAccountFromRaw().then(() => { | ||||
|             expect(store._api.parity.newAccountFromSecret).to.have.been.calledWith('rawKey', 'P@55worD'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the address', () => { | ||||
|           return store.createAccountFromRaw().then(() => { | ||||
|             expect(store.address).to.equal(ADDRESS); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account name', () => { | ||||
|           return store.createAccountFromRaw().then(() => { | ||||
|             expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account meta', () => { | ||||
|           return store.createAccountFromRaw(-1).then(() => { | ||||
|             expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, { | ||||
|               passwordHint: 'some hint', | ||||
|               timestamp: -1 | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       describe('createAccountFromWallet', () => { | ||||
|         beforeEach(() => { | ||||
|           store.setName('some name'); | ||||
|           store.setPassword('P@55worD'); | ||||
|           store.setPasswordHint('some hint'); | ||||
|           store.setWalletJson('json'); | ||||
|         }); | ||||
| 
 | ||||
|         it('calls parity.newAccountFromWallet', () => { | ||||
|           return store.createAccountFromWallet().then(() => { | ||||
|             expect(store._api.parity.newAccountFromWallet).to.have.been.calledWith('json', 'P@55worD'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the address', () => { | ||||
|           return store.createAccountFromWallet().then(() => { | ||||
|             expect(store.address).to.equal(ADDRESS); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account name', () => { | ||||
|           return store.createAccountFromWallet().then(() => { | ||||
|             expect(store._api.parity.setAccountName).to.have.been.calledWith(ADDRESS, 'some name'); | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|         it('sets the account meta', () => { | ||||
|           return store.createAccountFromWallet(-1).then(() => { | ||||
|             expect(store._api.parity.setAccountMeta).to.have.been.calledWith(ADDRESS, { | ||||
|               passwordHint: 'some hint', | ||||
|               timestamp: -1 | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('createIdentities', () => { | ||||
|       it('creates calls parity.generateSecretPhrase', () => { | ||||
|         return store.createIdentities().then(() => { | ||||
|           expect(store._api.parity.generateSecretPhrase).to.have.been.called; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('returns a map of 5 accounts', () => { | ||||
|         return store.createIdentities().then((accounts) => { | ||||
|           expect(Object.keys(accounts).length).to.equal(5); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('creates accounts with an address & phrase', () => { | ||||
|         return store.createIdentities().then((accounts) => { | ||||
|           Object.keys(accounts).forEach((address) => { | ||||
|             const account = accounts[address]; | ||||
| 
 | ||||
|             expect(account.address).to.equal(address); | ||||
|             expect(account.phrase).to.be.ok; | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('loadAvailableGethAccounts', () => { | ||||
|       it('retrieves the list from parity.listGethAccounts', () => { | ||||
|         return store.loadAvailableGethAccounts().then(() => { | ||||
|           expect(store._api.parity.listGethAccounts).to.have.been.called; | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('sets the available addresses with balances', () => { | ||||
|         return store.loadAvailableGethAccounts().then(() => { | ||||
|           expect(store.gethAccountsAvailable[0]).to.deep.equal({ | ||||
|             address: GETH_ADDRESSES[0], | ||||
|             balance: '0.00000' | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('filters accounts already available', () => { | ||||
|         return store.loadAvailableGethAccounts().then(() => { | ||||
|           expect(store.gethAccountsAvailable.length).to.equal(1); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @ -14,47 +14,73 @@ | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { observer } from 'mobx-react'; | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { connect } from 'react-redux'; | ||||
| import ActionDone from 'material-ui/svg-icons/action/done'; | ||||
| import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; | ||||
| import PrintIcon from 'material-ui/svg-icons/action/print'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| 
 | ||||
| import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg'; | ||||
| import { createIdentityImg } from '~/api/util/identity'; | ||||
| import { newError } from '~/redux/actions'; | ||||
| import { Button, Modal } from '~/ui'; | ||||
| import { CheckIcon, DoneIcon, NextIcon, PrintIcon } from '~/ui/Icons'; | ||||
| 
 | ||||
| import { NewAccount, AccountDetails } from '../CreateAccount'; | ||||
| import print from '../CreateAccount/print'; | ||||
| import recoveryPage from '../CreateAccount/recoveryPage.ejs'; | ||||
| import CreateStore from '../CreateAccount/store'; | ||||
| 
 | ||||
| import Completed from './Completed'; | ||||
| import TnC from './TnC'; | ||||
| import Welcome from './Welcome'; | ||||
| 
 | ||||
| import { createIdentityImg } from '~/api/util/identity'; | ||||
| import print from '../CreateAccount/print'; | ||||
| import recoveryPage from '../CreateAccount/recovery-page.ejs'; | ||||
| import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg'; | ||||
| 
 | ||||
| const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed']; | ||||
| const STAGE_NAMES = [ | ||||
|   <FormattedMessage | ||||
|     id='firstRun.title.welcome' | ||||
|     defaultMessage='welcome' | ||||
|   />, | ||||
|   <FormattedMessage | ||||
|     id='firstRun.title.terms' | ||||
|     defaultMessage='terms' | ||||
|   />, | ||||
|   <FormattedMessage | ||||
|     id='firstRun.title.newAccount' | ||||
|     defaultMessage='new account' | ||||
|   />, | ||||
|   <FormattedMessage | ||||
|     id='firstRun.title.recovery' | ||||
|     defaultMessage='recovery' | ||||
|   />, | ||||
|   <FormattedMessage | ||||
|     id='firstRun.title.completed' | ||||
|     defaultMessage='completed' | ||||
|   /> | ||||
| ]; | ||||
| const BUTTON_LABEL_NEXT = ( | ||||
|   <FormattedMessage | ||||
|     id='firstRun.button.next' | ||||
|     defaultMessage='Next' | ||||
|   /> | ||||
| ); | ||||
| 
 | ||||
| @observer | ||||
| class FirstRun extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
|     store: PropTypes.object.isRequired | ||||
|     api: PropTypes.object.isRequired | ||||
|   } | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     hasAccounts: PropTypes.bool.isRequired, | ||||
|     visible: PropTypes.bool.isRequired, | ||||
|     onClose: PropTypes.func.isRequired | ||||
|     newError: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     visible: PropTypes.bool.isRequired | ||||
|   } | ||||
| 
 | ||||
|   createStore = new CreateStore(this.context.api, {}, false); | ||||
| 
 | ||||
|   state = { | ||||
|     stage: 0, | ||||
|     name: '', | ||||
|     address: '', | ||||
|     password: '', | ||||
|     phrase: '', | ||||
|     canCreate: false, | ||||
|     hasAcceptedTnc: false | ||||
|   } | ||||
| 
 | ||||
| @ -79,7 +105,7 @@ class FirstRun extends Component { | ||||
|   } | ||||
| 
 | ||||
|   renderStage () { | ||||
|     const { address, name, phrase, stage, hasAcceptedTnc } = this.state; | ||||
|     const { stage, hasAcceptedTnc } = this.state; | ||||
| 
 | ||||
|     switch (stage) { | ||||
|       case 0: | ||||
| @ -96,16 +122,13 @@ class FirstRun extends Component { | ||||
|       case 2: | ||||
|         return ( | ||||
|           <NewAccount | ||||
|             onChange={ this.onChangeDetails } | ||||
|             newError={ this.props.newError } | ||||
|             store={ this.createStore } | ||||
|           /> | ||||
|         ); | ||||
|       case 3: | ||||
|         return ( | ||||
|           <AccountDetails | ||||
|             address={ address } | ||||
|             name={ name } | ||||
|             phrase={ phrase } | ||||
|           /> | ||||
|           <AccountDetails store={ this.createStore } /> | ||||
|         ); | ||||
|       case 4: | ||||
|         return ( | ||||
| @ -116,14 +139,16 @@ class FirstRun extends Component { | ||||
| 
 | ||||
|   renderDialogActions () { | ||||
|     const { hasAccounts } = this.props; | ||||
|     const { canCreate, stage, hasAcceptedTnc } = this.state; | ||||
|     const { stage, hasAcceptedTnc } = this.state; | ||||
|     const { canCreate } = this.createStore; | ||||
| 
 | ||||
|     switch (stage) { | ||||
|       case 0: | ||||
|         return ( | ||||
|           <Button | ||||
|             icon={ <NavigationArrowForward /> } | ||||
|             label='Next' | ||||
|             icon={ <NextIcon /> } | ||||
|             key='next' | ||||
|             label={ BUTTON_LABEL_NEXT } | ||||
|             onClick={ this.onNext } | ||||
|           /> | ||||
|         ); | ||||
| @ -132,8 +157,9 @@ class FirstRun extends Component { | ||||
|         return ( | ||||
|           <Button | ||||
|             disabled={ !hasAcceptedTnc } | ||||
|             icon={ <NavigationArrowForward /> } | ||||
|             label='Next' | ||||
|             icon={ <NextIcon /> } | ||||
|             key='next' | ||||
|             label={ BUTTON_LABEL_NEXT } | ||||
|             onClick={ this.onNext } | ||||
|           /> | ||||
|         ); | ||||
| @ -141,10 +167,15 @@ class FirstRun extends Component { | ||||
|       case 2: | ||||
|         const buttons = [ | ||||
|           <Button | ||||
|             icon={ <ActionDone /> } | ||||
|             label='Create' | ||||
|             key='create' | ||||
|             disabled={ !canCreate } | ||||
|             icon={ <CheckIcon /> } | ||||
|             key='create' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='firstRun.button.create' | ||||
|                 defaultMessage='Create' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.onCreate } | ||||
|           /> | ||||
|         ]; | ||||
| @ -152,9 +183,14 @@ class FirstRun extends Component { | ||||
|         if (hasAccounts) { | ||||
|           buttons.unshift( | ||||
|             <Button | ||||
|               icon={ <NavigationArrowForward /> } | ||||
|               label='Skip' | ||||
|               icon={ <NextIcon /> } | ||||
|               key='skip' | ||||
|               label={ | ||||
|                 <FormattedMessage | ||||
|                   id='firstRun.button.skip' | ||||
|                   defaultMessage='Skip' | ||||
|                 /> | ||||
|               } | ||||
|               onClick={ this.skipAccountCreation } | ||||
|             /> | ||||
|           ); | ||||
| @ -165,12 +201,19 @@ class FirstRun extends Component { | ||||
|         return [ | ||||
|           <Button | ||||
|             icon={ <PrintIcon /> } | ||||
|             label='Print Phrase' | ||||
|             key='print' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='firstRun.button.print' | ||||
|                 defaultMessage='Print Phrase' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.printPhrase } | ||||
|           />, | ||||
|           <Button | ||||
|             icon={ <NavigationArrowForward /> } | ||||
|             label='Next' | ||||
|             icon={ <NextIcon /> } | ||||
|             key='next' | ||||
|             label={ BUTTON_LABEL_NEXT } | ||||
|             onClick={ this.onNext } | ||||
|           /> | ||||
|         ]; | ||||
| @ -178,8 +221,14 @@ class FirstRun extends Component { | ||||
|       case 4: | ||||
|         return ( | ||||
|           <Button | ||||
|             icon={ <ActionDoneAll /> } | ||||
|             label='Close' | ||||
|             icon={ <DoneIcon /> } | ||||
|             key='close' | ||||
|             label={ | ||||
|               <FormattedMessage | ||||
|                 id='firstRun.button.close' | ||||
|                 defaultMessage='Close' | ||||
|               /> | ||||
|             } | ||||
|             onClick={ this.onClose } | ||||
|           /> | ||||
|         ); | ||||
| @ -208,38 +257,18 @@ class FirstRun extends Component { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onChangeDetails = (valid, { name, address, password, phrase }) => { | ||||
|     this.setState({ | ||||
|       canCreate: valid, | ||||
|       name: name, | ||||
|       address: address, | ||||
|       password: password, | ||||
|       phrase: phrase | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   onCreate = () => { | ||||
|     const { api } = this.context; | ||||
|     const { name, phrase, password } = this.state; | ||||
|     this.createStore.setBusy(true); | ||||
| 
 | ||||
|     this.setState({ | ||||
|       canCreate: false | ||||
|     }); | ||||
| 
 | ||||
|     return api.parity | ||||
|       .newAccountFromPhrase(phrase, password) | ||||
|       .then((address) => api.parity.setAccountName(address, name)) | ||||
|     return this.createStore | ||||
|       .createAccount() | ||||
|       .then(() => { | ||||
|         this.onNext(); | ||||
|         this.createStore.setBusy(false); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error('onCreate', error); | ||||
| 
 | ||||
|         this.setState({ | ||||
|           canCreate: true | ||||
|         }); | ||||
| 
 | ||||
|         this.newError(error); | ||||
|         this.createStore.setBusy(false); | ||||
|         this.props.newError(error); | ||||
|       }); | ||||
|   } | ||||
| 
 | ||||
| @ -247,22 +276,35 @@ class FirstRun extends Component { | ||||
|     this.setState({ stage: this.state.stage + 2 }); | ||||
|   } | ||||
| 
 | ||||
|   newError = (error) => { | ||||
|     const { store } = this.context; | ||||
| 
 | ||||
|     store.dispatch({ type: 'newError', error }); | ||||
|   } | ||||
| 
 | ||||
|   printPhrase = () => { | ||||
|     const { address, phrase, name } = this.state; | ||||
|     const { address, phrase, name } = this.createStore; | ||||
|     const identity = createIdentityImg(address); | ||||
| 
 | ||||
|     print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo })); | ||||
|     print(recoveryPage({ | ||||
|       address, | ||||
|       identity, | ||||
|       logo: ParityLogo, | ||||
|       name, | ||||
|       phrase | ||||
|     })); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function mapStateToProps (state) { | ||||
|   return { hasAccounts: state.personal.hasAccounts }; | ||||
|   const { hasAccounts } = state.personal; | ||||
| 
 | ||||
|   return { | ||||
|     hasAccounts | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export default connect(mapStateToProps, null)(FirstRun); | ||||
| function mapDispatchToProps (dispatch) { | ||||
|   return bindActionCreators({ | ||||
|     newError | ||||
|   }, dispatch); | ||||
| } | ||||
| 
 | ||||
| export default connect( | ||||
|   mapStateToProps, | ||||
|   mapDispatchToProps | ||||
| )(FirstRun); | ||||
|  | ||||
							
								
								
									
										69
									
								
								js/src/modals/FirstRun/firstRun.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								js/src/modals/FirstRun/firstRun.spec.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| // Copyright 2015, 2016 Parity Technologies (UK) Ltd.
 | ||||
| // This file is part of Parity.
 | ||||
| 
 | ||||
| // Parity is free software: you can redistribute it and/or modify
 | ||||
| // it under the terms of the GNU General Public License as published by
 | ||||
| // the Free Software Foundation, either version 3 of the License, or
 | ||||
| // (at your option) any later version.
 | ||||
| 
 | ||||
| // Parity is distributed in the hope that it will be useful,
 | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||||
| // GNU General Public License for more details.
 | ||||
| 
 | ||||
| // You should have received a copy of the GNU General Public License
 | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import { shallow } from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import FirstRun from './'; | ||||
| 
 | ||||
| let component; | ||||
| let onClose; | ||||
| 
 | ||||
| function createApi () { | ||||
|   return {}; | ||||
| } | ||||
| 
 | ||||
| function createRedux () { | ||||
|   return { | ||||
|     dispatch: sinon.stub(), | ||||
|     subscribe: sinon.stub(), | ||||
|     getState: () => { | ||||
|       return { | ||||
|         personal: { | ||||
|           hasAccounts: false | ||||
|         } | ||||
|       }; | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function render (props = { visible: true }) { | ||||
|   onClose = sinon.stub(); | ||||
|   component = shallow( | ||||
|     <FirstRun | ||||
|       { ...props } | ||||
|       onClose={ onClose } | ||||
|     />, | ||||
|     { | ||||
|       context: { | ||||
|         store: createRedux() | ||||
|       } | ||||
|     } | ||||
|   ).find('FirstRun').shallow({ | ||||
|     context: { | ||||
|       api: createApi() | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   return component; | ||||
| } | ||||
| 
 | ||||
| describe('modals/FirstRun', () => { | ||||
|   it('renders defaults', () => { | ||||
|     expect(render()).to.be.ok; | ||||
|   }); | ||||
| }); | ||||
| @ -15,6 +15,7 @@ | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| import AddIcon from 'material-ui/svg-icons/content/add'; | ||||
| import AttachFileIcon from 'material-ui/svg-icons/editor/attach-file'; | ||||
| import CancelIcon from 'material-ui/svg-icons/content/clear'; | ||||
| import CheckIcon from 'material-ui/svg-icons/navigation/check'; | ||||
| import CloseIcon from 'material-ui/svg-icons/navigation/close'; | ||||
| @ -31,6 +32,8 @@ import LockedIcon from 'material-ui/svg-icons/action/lock'; | ||||
| import MoveIcon from 'material-ui/svg-icons/action/open-with'; | ||||
| import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; | ||||
| import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; | ||||
| import PrintIcon from 'material-ui/svg-icons/action/print'; | ||||
| import RefreshIcon from 'material-ui/svg-icons/action/autorenew'; | ||||
| import SaveIcon from 'material-ui/svg-icons/content/save'; | ||||
| import SendIcon from 'material-ui/svg-icons/content/send'; | ||||
| import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; | ||||
| @ -40,6 +43,7 @@ import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock'; | ||||
| 
 | ||||
| export { | ||||
|   AddIcon, | ||||
|   AttachFileIcon, | ||||
|   CancelIcon, | ||||
|   CheckIcon, | ||||
|   CloseIcon, | ||||
| @ -56,6 +60,8 @@ export { | ||||
|   MoveIcon, | ||||
|   NextIcon, | ||||
|   PrevIcon, | ||||
|   PrintIcon, | ||||
|   RefreshIcon, | ||||
|   SaveIcon, | ||||
|   SendIcon, | ||||
|   SnoozeIcon, | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { FormattedMessage } from 'react-intl'; | ||||
| 
 | ||||
| import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui'; | ||||
| import Certifications from '~/ui/Certifications'; | ||||
| import { nullableProptype } from '~/util/proptypes'; | ||||
| import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; | ||||
| 
 | ||||
| import styles from '../accounts.css'; | ||||
| 
 | ||||
| @ -40,7 +40,7 @@ export default class Summary extends Component { | ||||
|     noLink: PropTypes.bool, | ||||
|     showCertifications: PropTypes.bool, | ||||
|     handleAddSearchToken: PropTypes.func, | ||||
|     owners: nullableProptype(PropTypes.array) | ||||
|     owners: nullableProptype(arrayOrObjectProptype()) | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user