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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
| import { Form, Input, InputAddress } from '~/ui'; | import { Form, Input, InputAddress } from '~/ui'; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class AccountDetails extends Component { | export default class AccountDetails extends Component { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     address: PropTypes.string, |     store: PropTypes.object.isRequired | ||||||
|     name: PropTypes.string, |  | ||||||
|     phrase: PropTypes.string |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { address, name } = this.props; |     const { address, name } = this.props.store; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Form> |       <Form> | ||||||
|         <Input |         <Input | ||||||
|           readOnly |  | ||||||
|           allowCopy |           allowCopy | ||||||
|           hint='a descriptive name for the account' |           hint={ | ||||||
|           label='account name' |             <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 } |           value={ name } | ||||||
|         /> |         /> | ||||||
|         <InputAddress |         <InputAddress | ||||||
|           disabled |           disabled | ||||||
|           hint='the network address for the account' |           hint={ | ||||||
|           label='address' |             <FormattedMessage | ||||||
|  |               id='createAccount.accountDetails.address.hint' | ||||||
|  |               defaultMessage='the network address for the account' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           label={ | ||||||
|  |             <FormattedMessage | ||||||
|  |               id='createAccount.accountDetails.address.label' | ||||||
|  |               defaultMessage='address' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|           value={ address } |           value={ address } | ||||||
|         /> |         /> | ||||||
|         { this.renderPhrase() } |         { this.renderPhrase() } | ||||||
| @ -49,7 +70,7 @@ export default class AccountDetails extends Component { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   renderPhrase () { |   renderPhrase () { | ||||||
|     const { phrase } = this.props; |     const { phrase } = this.props.store; | ||||||
| 
 | 
 | ||||||
|     if (!phrase) { |     if (!phrase) { | ||||||
|       return null; |       return null; | ||||||
| @ -57,10 +78,20 @@ export default class AccountDetails extends Component { | |||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Input |       <Input | ||||||
|         readOnly |  | ||||||
|         allowCopy |         allowCopy | ||||||
|         hint='the account recovery phrase' |         hint={ | ||||||
|         label='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)' |           <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 } |         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 | /* You should have received a copy of the GNU General Public License | ||||||
| /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||||
| */ | */ | ||||||
|  | 
 | ||||||
| .address { | .address { | ||||||
|   color: #999; |   color: #999; | ||||||
|   padding-top: 1em; |  | ||||||
|   line-height: 1.618em; |   line-height: 1.618em; | ||||||
|   padding-left: 2em; |   padding-left: 2em; | ||||||
|  |   padding-top: 1em; | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,29 +14,46 @@ | |||||||
| // You should have received a copy of the GNU General Public License
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
| import styles from './accountDetailsGeth.css'; | import styles from './accountDetailsGeth.css'; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class AccountDetailsGeth extends Component { | export default class AccountDetailsGeth extends Component { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     addresses: PropTypes.array |     store: PropTypes.object.isRequired | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { addresses } = this.props; |     const { gethAddresses } = this.props.store; | ||||||
| 
 |  | ||||||
|     const formatted = addresses.map((address, idx) => { |  | ||||||
|       const comma = !idx ? '' : ((idx === addresses.length - 1) ? ' & ' : ', '); |  | ||||||
| 
 |  | ||||||
|       return `${comma}${address}`; |  | ||||||
|     }).join(''); |  | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div> |       <div> | ||||||
|         <div>You have imported { addresses.length } addresses from the Geth keystore:</div> |         <div> | ||||||
|         <div className={ styles.address }>{ formatted }</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> |       </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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | ||||||
| 
 | 
 | ||||||
| import styles from '../createAccount.css'; | import styles from '../createAccount.css'; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class CreationType extends Component { | export default class CreationType extends Component { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     onChange: PropTypes.func.isRequired |     store: PropTypes.object.isRequired | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   componentWillMount () { |  | ||||||
|     this.props.onChange('fromNew'); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|  |     const { createType } = this.props.store; | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className={ styles.spaced }> |       <div className={ styles.spaced }> | ||||||
|         <RadioButtonGroup |         <RadioButtonGroup | ||||||
|           defaultSelected='fromNew' |           defaultSelected={ createType } | ||||||
|           name='creationType' |           name='creationType' | ||||||
|           onChange={ this.onChange } |           onChange={ this.onChange } | ||||||
|         > |         > | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             label='Create new account manually' |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.creationType.fromNew.label' | ||||||
|  |                 defaultMessage='Create new account manually' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             value='fromNew' |             value='fromNew' | ||||||
|           /> |           /> | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             label='Recover account from recovery phrase' |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.creationType.fromPhrase.label' | ||||||
|  |                 defaultMessage='Recover account from recovery phrase' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             value='fromPhrase' |             value='fromPhrase' | ||||||
|           /> |           /> | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             label='Import accounts from Geth keystore' |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.creationType.fromGeth.label' | ||||||
|  |                 defaultMessage='Import accounts from Geth keystore' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             value='fromGeth' |             value='fromGeth' | ||||||
|           /> |           /> | ||||||
|           <RadioButton |           <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' |             value='fromJSON' | ||||||
|           /> |           /> | ||||||
|           <RadioButton |           <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' |             value='fromPresale' | ||||||
|           /> |           /> | ||||||
|           <RadioButton |           <RadioButton | ||||||
|             label='Import raw private key' |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.creationType.fromRaw.label' | ||||||
|  |                 defaultMessage='Import raw private key' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             value='fromRaw' |             value='fromRaw' | ||||||
|           /> |           /> | ||||||
|         </RadioButtonGroup> |         </RadioButtonGroup> | ||||||
| @ -66,6 +97,8 @@ export default class CreationType extends Component { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onChange = (event) => { |   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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| import { IconButton } from 'material-ui'; | import { IconButton } from 'material-ui'; | ||||||
| import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; | ||||||
| import ActionAutorenew from 'material-ui/svg-icons/action/autorenew'; |  | ||||||
| 
 | 
 | ||||||
| import { Form, Input, IdentityIcon } from '~/ui'; | import { Form, Input, IdentityIcon, PasswordStrength } from '~/ui'; | ||||||
| 
 | import { RefreshIcon } from '~/ui/Icons'; | ||||||
| import ERRORS from '../errors'; |  | ||||||
| 
 | 
 | ||||||
| import styles from '../createAccount.css'; | import styles from '../createAccount.css'; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class CreateAccount extends Component { | export default class CreateAccount extends Component { | ||||||
|   static contextTypes = { |   static propTypes = { | ||||||
|     api: PropTypes.object.isRequired, |     newError: PropTypes.func.isRequired, | ||||||
|     store: PropTypes.object.isRequired |     store: PropTypes.object.isRequired | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |  | ||||||
|     onChange: PropTypes.func.isRequired |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   state = { |   state = { | ||||||
|     accountName: '', |  | ||||||
|     accountNameError: ERRORS.noName, |  | ||||||
|     accounts: null, |     accounts: null, | ||||||
|     isValidName: false, |  | ||||||
|     isValidPass: true, |  | ||||||
|     passwordHint: '', |  | ||||||
|     password1: '', |  | ||||||
|     password1Error: null, |  | ||||||
|     password2: '', |  | ||||||
|     password2Error: null, |  | ||||||
|     selectedAddress: '' |     selectedAddress: '' | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentWillMount () { |   componentWillMount () { | ||||||
|     this.createIdentities(); |     return this.createIdentities(); | ||||||
|     this.props.onChange(false, {}); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { accountName, accountNameError, passwordHint, password1, password1Error, password2, password2Error } = this.state; |     const { name, nameError, password, passwordRepeat, passwordRepeatError, passwordHint } = this.props.store; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Form> |       <Form> | ||||||
|         <Input |         <Input | ||||||
|           label='account name' |           error={ nameError } | ||||||
|           hint='a descriptive name for the account' |           hint={ | ||||||
|           error={ accountNameError } |             <FormattedMessage | ||||||
|           value={ accountName } |               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 } |           onChange={ this.onEditAccountName } | ||||||
|  |           value={ name } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='password hint' |           hint={ | ||||||
|           hint='(optional) a hint to help with remembering the password' |             <FormattedMessage | ||||||
|           value={ passwordHint } |               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 } |           onChange={ this.onEditPasswordHint } | ||||||
|  |           value={ passwordHint } | ||||||
|         /> |         /> | ||||||
|         <div className={ styles.passwords }> |         <div className={ styles.passwords }> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password' |               hint={ | ||||||
|               hint='a strong, unique password' |                 <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' |               type='password' | ||||||
|               error={ password1Error } |               value={ password } | ||||||
|               value={ password1 } |  | ||||||
|               onChange={ this.onEditPassword1 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password (repeat)' |               error={ passwordRepeatError } | ||||||
|               hint='verify your password' |               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' |               type='password' | ||||||
|               error={ password2Error } |               value={ passwordRepeat } | ||||||
|               value={ password2 } |  | ||||||
|               onChange={ this.onEditPassword2 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |         <PasswordStrength input={ password } /> | ||||||
|         { this.renderIdentitySelector() } |         { this.renderIdentitySelector() } | ||||||
|         { this.renderIdentities() } |         { this.renderIdentities() } | ||||||
|       </Form> |       </Form> | ||||||
| @ -107,22 +134,24 @@ export default class CreateAccount extends Component { | |||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const buttons = Object.keys(accounts).map((address) => { |     const buttons = Object | ||||||
|       return ( |       .keys(accounts) | ||||||
|         <RadioButton |       .map((address) => { | ||||||
|           className={ styles.button } |         return ( | ||||||
|           key={ address } |           <RadioButton | ||||||
|           value={ address } |             className={ styles.button } | ||||||
|         /> |             key={ address } | ||||||
|       ); |             value={ address } | ||||||
|     }); |           /> | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <RadioButtonGroup |       <RadioButtonGroup | ||||||
|         valueSelected={ selectedAddress } |  | ||||||
|         className={ styles.selector } |         className={ styles.selector } | ||||||
|         name='identitySelector' |         name='identitySelector' | ||||||
|         onChange={ this.onChangeIdentity } |         onChange={ this.onChangeIdentity } | ||||||
|  |         valueSelected={ selectedAddress } | ||||||
|       > |       > | ||||||
|         { buttons } |         { buttons } | ||||||
|       </RadioButtonGroup> |       </RadioButtonGroup> | ||||||
| @ -136,31 +165,29 @@ export default class CreateAccount extends Component { | |||||||
|       return null; |       return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const identities = Object.keys(accounts).map((address) => { |     const identities = Object | ||||||
|       return ( |       .keys(accounts) | ||||||
|         <div |       .map((address) => { | ||||||
|           className={ styles.identity } |         return ( | ||||||
|           key={ address } |           <div | ||||||
|           onTouchTap={ this.onChangeIdentity } |             className={ styles.identity } | ||||||
|         > |             key={ address } | ||||||
|           <IdentityIcon |             onTouchTap={ this.onChangeIdentity } | ||||||
|             address={ address } |           > | ||||||
|             center |             <IdentityIcon | ||||||
|           /> |               address={ address } | ||||||
|         </div> |               center | ||||||
|       ); |             /> | ||||||
|     }); |           </div> | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <div className={ styles.identities }> |       <div className={ styles.identities }> | ||||||
|         { identities } |         { identities } | ||||||
|         <div className={ styles.refresh }> |         <div className={ styles.refresh }> | ||||||
|           <IconButton |           <IconButton onTouchTap={ this.createIdentities }> | ||||||
|             onTouchTap={ this.createIdentities } |             <RefreshIcon color='rgb(0, 151, 167)' /> | ||||||
|           > |  | ||||||
|             <ActionAutorenew |  | ||||||
|               color='rgb(0, 151, 167)' |  | ||||||
|             /> |  | ||||||
|           </IconButton> |           </IconButton> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -168,122 +195,64 @@ export default class CreateAccount extends Component { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   createIdentities = () => { |   createIdentities = () => { | ||||||
|     const { api } = this.context; |     const { store } = this.props; | ||||||
| 
 | 
 | ||||||
|     Promise |     return store | ||||||
|       .all([ |       .createIdentities() | ||||||
|         api.parity.generateSecretPhrase(), |       .then((accounts) => { | ||||||
|         api.parity.generateSecretPhrase(), |         const selectedAddress = Object.keys(accounts)[0]; | ||||||
|         api.parity.generateSecretPhrase(), |         const { phrase } = accounts[selectedAddress]; | ||||||
|         api.parity.generateSecretPhrase(), |  | ||||||
|         api.parity.generateSecretPhrase() |  | ||||||
|       ]) |  | ||||||
|       .then((phrases) => { |  | ||||||
|         return Promise |  | ||||||
|           .all(phrases.map((phrase) => api.parity.phraseToAddress(phrase))) |  | ||||||
|           .then((addresses) => { |  | ||||||
|             const accounts = {}; |  | ||||||
| 
 | 
 | ||||||
|             phrases.forEach((phrase, idx) => { |         store.setAddress(selectedAddress); | ||||||
|               accounts[addresses[idx]] = { |         store.setPhrase(phrase); | ||||||
|                 address: addresses[idx], |  | ||||||
|                 phrase: phrase |  | ||||||
|               }; |  | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             this.setState({ |         this.setState({ | ||||||
|               selectedAddress: addresses[0], |           accounts, | ||||||
|               accounts: accounts |           selectedAddress | ||||||
|             }); |         }); | ||||||
|           }); |  | ||||||
|       }) |       }) | ||||||
|       .catch((error) => { |       .catch((error) => { | ||||||
|         console.error('createIdentities', error); |         this.props.newError(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 |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   onChangeIdentity = (event) => { |   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; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.setState({ |     this.setState({ selectedAddress }, () => { | ||||||
|       selectedAddress: address |       const { phrase } = this.state.accounts[selectedAddress]; | ||||||
|     }, this.updateParent); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   onEditPasswordHint = (event, passwordHint) => { |       store.setAddress(selectedAddress); | ||||||
|     this.setState({ |       store.setPhrase(phrase); | ||||||
|       passwordHint |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditAccountName = (event) => { |   onEditPasswordHint = (event, passwordHint) => { | ||||||
|     const accountName = event.target.value; |     const { store } = this.props; | ||||||
|     let accountNameError = null; |  | ||||||
| 
 | 
 | ||||||
|     if (!accountName || !accountName.trim().length) { |     store.setPasswordHint(passwordHint); | ||||||
|       accountNameError = ERRORS.noName; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       accountName, |  | ||||||
|       accountNameError, |  | ||||||
|       isValidName: !accountNameError |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPassword1 = (event) => { |   onEditAccountName = (event, name) => { | ||||||
|     const password1 = event.target.value; |     const { store } = this.props; | ||||||
|     let password2Error = null; |  | ||||||
| 
 | 
 | ||||||
|     if (password1 !== this.state.password2) { |     store.setName(name); | ||||||
|       password2Error = ERRORS.noMatchPassword; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       password1, |  | ||||||
|       password1Error: null, |  | ||||||
|       password2Error, |  | ||||||
|       isValidPass: !password2Error |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPassword2 = (event) => { |   onEditPassword = (event, password) => { | ||||||
|     const password2 = event.target.value; |     const { store } = this.props; | ||||||
|     let password2Error = null; |  | ||||||
| 
 | 
 | ||||||
|     if (password2 !== this.state.password1) { |     store.setPassword(password); | ||||||
|       password2Error = ERRORS.noMatchPassword; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       password2, |  | ||||||
|       password2Error, |  | ||||||
|       isValidPass: !password2Error |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   newError = (error) => { |   onEditPasswordRepeat = (event, password) => { | ||||||
|     const { store } = this.context; |     const { store } = this.props; | ||||||
| 
 | 
 | ||||||
|     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/>. | /* along with Parity.  If not, see <http://www.gnu.org/licenses/>. | ||||||
| */ | */ | ||||||
| .list { | .list { | ||||||
| } |   input+div>div { | ||||||
| 
 |     top: 13px; | ||||||
| .list input+div>div { |   } | ||||||
|   top: 13px; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .selection { | .selection { | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   margin-bottom: 0.5em; |   margin-bottom: 0.5em; | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .selection .icon { |   .icon { | ||||||
|   display: inline-block; |     display: inline-block; | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
| .selection .detail { |   .detail { | ||||||
|   display: inline-block; |     display: inline-block; | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .detail .address { |     .address { | ||||||
|   color: #aaa; |       color: #aaa; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| .detail .balance { |     .balance { | ||||||
|   font-family: 'Roboto Mono', monospace; |       font-family: 'Roboto Mono', monospace; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,63 +14,68 @@ | |||||||
| // You should have received a copy of the GNU General Public License
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| import { Checkbox } from 'material-ui'; | import { Checkbox } from 'material-ui'; | ||||||
| 
 | 
 | ||||||
| import { IdentityIcon } from '~/ui'; | import { IdentityIcon } from '~/ui'; | ||||||
| 
 | 
 | ||||||
| import styles from './newGeth.css'; | import styles from './newGeth.css'; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class NewGeth extends Component { | export default class NewGeth extends Component { | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|     api: PropTypes.object.isRequired |     api: PropTypes.object.isRequired | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     accounts: PropTypes.object.isRequired, |     store: PropTypes.object.isRequired | ||||||
|     onChange: PropTypes.func.isRequired |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   state = { |  | ||||||
|     available: [] |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   componentDidMount () { |  | ||||||
|     this.loadAvailable(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { available } = this.state; |     const { gethAccountsAvailable, gethAddresses } = this.props.store; | ||||||
| 
 | 
 | ||||||
|     if (!available.length) { |     if (!gethAccountsAvailable.length) { | ||||||
|       return ( |       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 = ( |       const label = ( | ||||||
|         <div className={ styles.selection }> |         <div className={ styles.selection }> | ||||||
|           <div className={ styles.icon }> |           <div className={ styles.icon }> | ||||||
|             <IdentityIcon |             <IdentityIcon | ||||||
|               center inline |  | ||||||
|               address={ account.address } |               address={ account.address } | ||||||
|  |               center | ||||||
|  |               inline | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <div className={ styles.detail }> |           <div className={ styles.detail }> | ||||||
|             <div className={ styles.address }>{ account.address }</div> |             <div className={ styles.address }> | ||||||
|             <div className={ styles.balance }>{ account.balance } ETH</div> |               { account.address } | ||||||
|  |             </div> | ||||||
|  |             <div className={ styles.balance }> | ||||||
|  |               { account.balance } ETH | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       return ( |       return ( | ||||||
|         <Checkbox |         <Checkbox | ||||||
|  |           checked={ gethAddresses.includes(account.address) } | ||||||
|           key={ account.address } |           key={ account.address } | ||||||
|           checked={ account.checked } |  | ||||||
|           label={ label } |           label={ label } | ||||||
|           data-address={ account.address } |           onCheck={ onSelect } | ||||||
|           onCheck={ this.onSelect } |  | ||||||
|         /> |         /> | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
| @ -82,51 +87,9 @@ export default class NewGeth extends Component { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onSelect = (event, checked) => { |   onSelectAddress = (event, address) => { | ||||||
|     const address = event.target.getAttribute('data-address'); |     const { store } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (!address) { |     store.selectGethAccount(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); |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // 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 React, { Component, PropTypes } from 'react'; | ||||||
| import ReactDOM from 'react-dom'; | import ReactDOM from 'react-dom'; | ||||||
| import { FloatingActionButton } from 'material-ui'; | import { FormattedMessage } from 'react-intl'; | ||||||
| import EditorAttachFile from 'material-ui/svg-icons/editor/attach-file'; |  | ||||||
| 
 | 
 | ||||||
| import { Form, Input } from '~/ui'; | import { Form, Input } from '~/ui'; | ||||||
| 
 | import { AttachFileIcon } from '~/ui/Icons'; | ||||||
| import ERRORS from '../errors'; |  | ||||||
| 
 | 
 | ||||||
| import styles from '../createAccount.css'; | import styles from '../createAccount.css'; | ||||||
| 
 | 
 | ||||||
| const FAKEPATH = 'C:\\fakepath\\'; |  | ||||||
| const STYLE_HIDDEN = { display: 'none' }; | const STYLE_HIDDEN = { display: 'none' }; | ||||||
| 
 | 
 | ||||||
|  | @observer | ||||||
| export default class NewImport extends Component { | export default class NewImport extends Component { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     onChange: PropTypes.func.isRequired |     store: PropTypes.object.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, {}); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|  |     const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store; | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Form> |       <Form> | ||||||
|         <Input |         <Input | ||||||
|           label='account name' |           error={ nameError } | ||||||
|           hint='a descriptive name for the account' |           hint={ | ||||||
|           error={ this.state.accountNameError } |             <FormattedMessage | ||||||
|           value={ this.state.accountName } |               id='createAccount.newImport.name.hint' | ||||||
|           onChange={ this.onEditAccountName } |               defaultMessage='a descriptive name for the account' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           label={ | ||||||
|  |             <FormattedMessage | ||||||
|  |               id='createAccount.newImport.name.label' | ||||||
|  |               defaultMessage='account name' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           onChange={ this.onEditName } | ||||||
|  |           value={ name } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='password hint' |           hint={ | ||||||
|           hint='(optional) a hint to help with remembering the password' |             <FormattedMessage | ||||||
|           value={ this.state.passwordHint } |               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 } |           onChange={ this.onEditpasswordHint } | ||||||
|  |           value={ passwordHint } | ||||||
|         /> |         /> | ||||||
|         <div className={ styles.passwords }> |         <div className={ styles.passwords }> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password' |               hint={ | ||||||
|               hint='the password to unlock the wallet' |                 <FormattedMessage | ||||||
|  |                   id='createAccount.newImport.password.hint' | ||||||
|  |                   defaultMessage='the password to unlock the wallet' | ||||||
|  |                 /> | ||||||
|  |               } | ||||||
|  |               label={ | ||||||
|  |                 <FormattedMessage | ||||||
|  |                   id='createAccount.newImport.password.label' | ||||||
|  |                   defaultMessage='password' | ||||||
|  |                 /> | ||||||
|  |               } | ||||||
|               type='password' |               type='password' | ||||||
|               error={ this.state.passwordError } |  | ||||||
|               value={ this.state.password } |  | ||||||
|               onChange={ this.onEditPassword } |               onChange={ this.onEditPassword } | ||||||
|  |               value={ password } | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div> |         <div> | ||||||
|           <Input |           <Input | ||||||
|             disabled |             disabled | ||||||
|             label='wallet file' |             error={ walletFileError } | ||||||
|             hint='the wallet file for import' |             hint={ | ||||||
|             error={ this.state.walletFileError } |               <FormattedMessage | ||||||
|             value={ this.state.walletFile } |                 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 }> |           <div className={ styles.upload }> | ||||||
|             <FloatingActionButton |             <FloatingActionButton | ||||||
|               mini |               mini | ||||||
|               onTouchTap={ this.openFileDialog } |               onTouchTap={ this.openFileDialog } | ||||||
|             > |             > | ||||||
|               <EditorAttachFile /> |               <AttachFileIcon /> | ||||||
|             </FloatingActionButton> |             </FloatingActionButton> | ||||||
|             <input |             <input | ||||||
|               ref='fileUpload' |  | ||||||
|               type='file' |  | ||||||
|               style={ STYLE_HIDDEN } |  | ||||||
|               onChange={ this.onFileChange } |               onChange={ this.onFileChange } | ||||||
|  |               ref='fileUpload' | ||||||
|  |               style={ STYLE_HIDDEN } | ||||||
|  |               type='file' | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| @ -107,73 +130,37 @@ export default class NewImport extends Component { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onFileChange = (event) => { |   onFileChange = (event) => { | ||||||
|     const el = event.target; |     const { store } = this.props; | ||||||
|     const error = ERRORS.noFile; |  | ||||||
| 
 | 
 | ||||||
|     if (el.files.length) { |     if (event.target.files.length) { | ||||||
|       const reader = new FileReader(); |       const reader = new FileReader(); | ||||||
| 
 | 
 | ||||||
|       reader.onload = (event) => { |       reader.onload = (event) => store.setWalletJson(event.target.result); | ||||||
|         this.setState({ |       reader.readAsText(event.target.files[0]); | ||||||
|           walletJson: event.target.result, |  | ||||||
|           walletFileError: null, |  | ||||||
|           isValidFile: true |  | ||||||
|         }, this.updateParent); |  | ||||||
|       }; |  | ||||||
|       reader.readAsText(el.files[0]); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.setState({ |     store.setWalletFile(event.target.value); | ||||||
|       walletFile: el.value.replace(FAKEPATH, ''), |  | ||||||
|       walletFileError: error, |  | ||||||
|       isValidFile: false |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   openFileDialog = () => { |   openFileDialog = () => { | ||||||
|     ReactDOM.findDOMNode(this.refs.fileUpload).click(); |     ReactDOM.findDOMNode(this.refs.fileUpload).click(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   updateParent = () => { |   onEditName = (event, name) => { | ||||||
|     const valid = this.state.isValidName && this.state.isValidPass && this.state.isValidFile; |     const { store } = this.props; | ||||||
| 
 | 
 | ||||||
|     this.props.onChange(valid, { |     store.setName(name); | ||||||
|       name: this.state.accountName, |   } | ||||||
|       passwordHint: this.state.passwordHint, | 
 | ||||||
|       password: this.state.password, |   onEditPassword = (event, password) => { | ||||||
|       phrase: null, |     const { store } = this.props; | ||||||
|       json: this.state.walletJson | 
 | ||||||
|     }); |     store.setPassword(password); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPasswordHint = (event, passwordHint) => { |   onEditPasswordHint = (event, passwordHint) => { | ||||||
|     this.setState({ |     const { store } = this.props; | ||||||
|       passwordHint |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   onEditAccountName = (event) => { |     store.setPasswordHint(passwordHint); | ||||||
|     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); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from '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 styles from '../createAccount.css'; | ||||||
| 
 | 
 | ||||||
| import ERRORS from '../errors'; | @observer | ||||||
| 
 |  | ||||||
| export default class RawKey extends Component { | export default class RawKey extends Component { | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|     api: PropTypes.object.isRequired |     api: PropTypes.object.isRequired | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     onChange: PropTypes.func.isRequired |     store: PropTypes.object.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, {}); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   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 ( |     return ( | ||||||
|       <Form> |       <Form> | ||||||
|         <Input |         <Input | ||||||
|           hint='the raw hex encoded private key' |  | ||||||
|           label='private key' |  | ||||||
|           error={ rawKeyError } |           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 } |           onChange={ this.onEditKey } | ||||||
|  |           value={ rawKey } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='account name' |           error={ nameError } | ||||||
|           hint='a descriptive name for the account' |           hint={ | ||||||
|           error={ accountNameError } |             <FormattedMessage | ||||||
|           value={ accountName } |               id='createAccount.rawKey.name.hint' | ||||||
|           onChange={ this.onEditAccountName } |               defaultMessage='a descriptive name for the account' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           label={ | ||||||
|  |             <FormattedMessage | ||||||
|  |               id='createAccount.rawKey.name.label' | ||||||
|  |               defaultMessage='account name' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           onChange={ this.onEditName } | ||||||
|  |           value={ name } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='password hint' |           hint={ | ||||||
|           hint='(optional) a hint to help with remembering the password' |             <FormattedMessage | ||||||
|           value={ passwordHint } |               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 } |           onChange={ this.onEditPasswordHint } | ||||||
|  |           value={ passwordHint } | ||||||
|         /> |         /> | ||||||
|         <div className={ styles.passwords }> |         <div className={ styles.passwords }> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password' |               hint={ | ||||||
|               hint='a strong, unique password' |                 <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' |               type='password' | ||||||
|               error={ password1Error } |               value={ password } | ||||||
|               value={ password1 } |  | ||||||
|               onChange={ this.onEditPassword1 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password (repeat)' |               error={ passwordRepeatError } | ||||||
|               hint='verify your password' |               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' |               type='password' | ||||||
|               error={ password2Error } |               value={ passwordRepeat } | ||||||
|               value={ password2 } |  | ||||||
|               onChange={ this.onEditPassword2 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |         <PasswordStrength input={ password } /> | ||||||
|       </Form> |       </Form> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   updateParent = () => { |   onEditName = (event, name) => { | ||||||
|     const { isValidName, isValidPass, isValidKey, accountName, passwordHint, password1, rawKey } = this.state; |     const { store } = this.props; | ||||||
|     const isValid = isValidName && isValidPass && isValidKey; |  | ||||||
| 
 | 
 | ||||||
|     this.props.onChange(isValid, { |     store.setName(name); | ||||||
|       name: accountName, |  | ||||||
|       passwordHint, |  | ||||||
|       password: password1, |  | ||||||
|       rawKey |  | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPasswordHint = (event, value) => { |   onEditPasswordHint = (event, passwordHint) => { | ||||||
|     this.setState({ |     const { store } = this.props; | ||||||
|       passwordHint: value | 
 | ||||||
|     }); |     store.setPasswordHint(passwordHint); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditKey = (event) => { |   onEditPassword = (event, password) => { | ||||||
|     const { api } = this.context; |     const { store } = this.props; | ||||||
|     const rawKey = event.target.value; |  | ||||||
|     let rawKeyError = null; |  | ||||||
| 
 | 
 | ||||||
|     if (!rawKey || !rawKey.trim().length) { |     store.setPassword(password); | ||||||
|       rawKeyError = ERRORS.noKey; |  | ||||||
|     } else if (rawKey.substr(0, 2) !== '0x' || rawKey.substr(2).length !== 64 || !api.util.isHex(rawKey)) { |  | ||||||
|       rawKeyError = ERRORS.invalidKey; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       rawKey, |  | ||||||
|       rawKeyError, |  | ||||||
|       isValidKey: !rawKeyError |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditAccountName = (event) => { |   onEditPasswordRepeat = (event, password) => { | ||||||
|     const accountName = event.target.value; |     const { store } = this.props; | ||||||
|     let accountNameError = null; |  | ||||||
| 
 | 
 | ||||||
|     if (!accountName || !accountName.trim().length) { |     store.setPasswordRepeat(password); | ||||||
|       accountNameError = ERRORS.noName; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       accountName, |  | ||||||
|       accountNameError, |  | ||||||
|       isValidName: !accountNameError |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPassword1 = (event) => { |   onEditKey = (event, rawKey) => { | ||||||
|     const password1 = event.target.value; |     const { store } = this.props; | ||||||
|     let password2Error = null; |  | ||||||
| 
 | 
 | ||||||
|     if (password1 !== this.state.password2) { |     store.setRawKey(rawKey); | ||||||
|       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); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| import { Checkbox } from 'material-ui'; | import { Checkbox } from 'material-ui'; | ||||||
| 
 | 
 | ||||||
| import { Form, Input } from '~/ui'; | import { Form, Input, PasswordStrength } from '~/ui'; | ||||||
| 
 | 
 | ||||||
| import styles from '../createAccount.css'; | import styles from '../createAccount.css'; | ||||||
| 
 | 
 | ||||||
| import ERRORS from '../errors'; | @observer | ||||||
| 
 |  | ||||||
| export default class RecoveryPhrase extends Component { | export default class RecoveryPhrase extends Component { | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     onChange: PropTypes.func.isRequired |     store: PropTypes.object.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, {}); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   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 ( |     return ( | ||||||
|       <Form> |       <Form> | ||||||
|         <Input |         <Input | ||||||
|           hint='the account recovery phrase' |           hint={ | ||||||
|           label='account recovery phrase' |             <FormattedMessage | ||||||
|           value={ recoveryPhrase } |               id='createAccount.recoveryPhrase.phrase.hint' | ||||||
|  |               defaultMessage='the account recovery phrase' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           label={ | ||||||
|  |             <FormattedMessage | ||||||
|  |               id='createAccount.recoveryPhrase.phrase.label' | ||||||
|  |               defaultMessage='account recovery phrase' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|           onChange={ this.onEditPhrase } |           onChange={ this.onEditPhrase } | ||||||
|  |           value={ phrase } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='account name' |           error={ nameError } | ||||||
|           hint='a descriptive name for the account' |           hint={ | ||||||
|           error={ accountNameError } |             <FormattedMessage | ||||||
|           value={ accountName } |               id='createAccount.recoveryPhrase.name.hint' | ||||||
|           onChange={ this.onEditAccountName } |               defaultMessage='a descriptive name for the account' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           label={ | ||||||
|  |             <FormattedMessage | ||||||
|  |               id='createAccount.recoveryPhrase.name.label' | ||||||
|  |               defaultMessage='account name' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|  |           onChange={ this.onEditName } | ||||||
|  |           value={ name } | ||||||
|         /> |         /> | ||||||
|         <Input |         <Input | ||||||
|           label='password hint' |           hint={ | ||||||
|           hint='(optional) a hint to help with remembering the password' |             <FormattedMessage | ||||||
|           value={ passwordHint } |               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 } |           onChange={ this.onEditPasswordHint } | ||||||
|  |           value={ passwordHint } | ||||||
|         /> |         /> | ||||||
|         <div className={ styles.passwords }> |         <div className={ styles.passwords }> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password' |               hint={ | ||||||
|               hint='a strong, unique password' |                 <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' |               type='password' | ||||||
|               error={ password1Error } |               value={ password } | ||||||
|               value={ password1 } |  | ||||||
|               onChange={ this.onEditPassword1 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <div className={ styles.password }> |           <div className={ styles.password }> | ||||||
|             <Input |             <Input | ||||||
|               label='password (repeat)' |               error={ passwordRepeatError } | ||||||
|               hint='verify your password' |               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' |               type='password' | ||||||
|               error={ password2Error } |               value={ passwordRepeat } | ||||||
|               value={ password2 } |  | ||||||
|               onChange={ this.onEditPassword2 } |  | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|  |         <PasswordStrength input={ password } /> | ||||||
|         <Checkbox |         <Checkbox | ||||||
|  |           checked={ isWindowsPhrase } | ||||||
|           className={ styles.checkbox } |           className={ styles.checkbox } | ||||||
|           label='Key was created with Parity <1.4.5 on Windows' |           label={ | ||||||
|           checked={ windowsPhrase } |             <FormattedMessage | ||||||
|  |               id='createAccount.recoveryPhrase.windowsKey.label' | ||||||
|  |               defaultMessage='Key was created with Parity <1.4.5 on Windows' | ||||||
|  |             /> | ||||||
|  |           } | ||||||
|           onCheck={ this.onToggleWindowsPhrase } |           onCheck={ this.onToggleWindowsPhrase } | ||||||
|         /> |         /> | ||||||
|       </Form> |       </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) => { |   onToggleWindowsPhrase = (event) => { | ||||||
|     this.setState({ |     const { store } = this.props; | ||||||
|       windowsPhrase: !this.state.windowsPhrase | 
 | ||||||
|     }, this.updateParent); |     store.setWindowsPhrase(!store.isWindowsPhrase); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPhrase = (event) => { |   onEditPhrase = (event, phrase) => { | ||||||
|     const recoveryPhrase = event.target.value |     const { store } = this.props; | ||||||
|       .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 |     store.setPhrase(phrase); | ||||||
|       .split(' ') |  | ||||||
|       .map((part) => part.trim()) |  | ||||||
|       .filter((part) => part.length); |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       recoveryPhrase: phraseParts.join(' '), |  | ||||||
|       recoveryPhraseError: null, |  | ||||||
|       isValidPhrase: true |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditAccountName = (event) => { |   onEditName = (event, name) => { | ||||||
|     const accountName = event.target.value; |     const { store } = this.props; | ||||||
|     let accountNameError = null; |  | ||||||
| 
 | 
 | ||||||
|     if (!accountName || !accountName.trim().length) { |     store.setName(name); | ||||||
|       accountNameError = ERRORS.noName; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       accountName, |  | ||||||
|       accountNameError, |  | ||||||
|       isValidName: !accountNameError |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPassword1 = (event) => { |   onEditPassword = (event, password) => { | ||||||
|     const password1 = event.target.value; |     const { store } = this.props; | ||||||
|     let password2Error = null; |  | ||||||
| 
 | 
 | ||||||
|     if (password1 !== this.state.password2) { |     store.setPassword(password); | ||||||
|       password2Error = ERRORS.noMatchPassword; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.setState({ |  | ||||||
|       password1, |  | ||||||
|       password1Error: null, |  | ||||||
|       password2Error, |  | ||||||
|       isValidPass: !password2Error |  | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onEditPassword2 = (event) => { |   onEditPasswordRepeat = (event, password) => { | ||||||
|     const password2 = event.target.value; |     const { store } = this.props; | ||||||
|     let password2Error = null; |  | ||||||
| 
 | 
 | ||||||
|     if (password2 !== this.state.password1) { |     store.setPasswordRepeat(password); | ||||||
|       password2Error = ERRORS.noMatchPassword; |   } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     this.setState({ |   onEditPasswordHint = (event, passwordHint) => { | ||||||
|       password2, |     const { store } = this.props; | ||||||
|       password2Error, | 
 | ||||||
|       isValidPass: !password2Error |     store.setPasswordHint(passwordHint); | ||||||
|     }, this.updateParent); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
| import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||||
|  | import { connect } from 'react-redux'; | ||||||
|  | import { bindActionCreators } from 'redux'; | ||||||
| 
 | 
 | ||||||
| import ActionDone from 'material-ui/svg-icons/action/done'; | import { createIdentityImg } from '~/api/util/identity'; | ||||||
| import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | import { newError } from '~/redux/actions'; | ||||||
| import ContentClear from 'material-ui/svg-icons/content/clear'; | import { Button, Modal } from '~/ui'; | ||||||
| import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; | import { CancelIcon, CheckIcon, DoneIcon, NextIcon, PrevIcon, PrintIcon } from '~/ui/Icons'; | ||||||
| import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; | import ParityLogo from '~/../assets/images/parity-logo-black-no-text.svg'; | ||||||
| import PrintIcon from 'material-ui/svg-icons/action/print'; |  | ||||||
| 
 |  | ||||||
| import { Button, Modal, Warning } from '~/ui'; |  | ||||||
| 
 | 
 | ||||||
| import AccountDetails from './AccountDetails'; | import AccountDetails from './AccountDetails'; | ||||||
| import AccountDetailsGeth from './AccountDetailsGeth'; | import AccountDetailsGeth from './AccountDetailsGeth'; | ||||||
| @ -34,405 +34,271 @@ import NewGeth from './NewGeth'; | |||||||
| import NewImport from './NewImport'; | import NewImport from './NewImport'; | ||||||
| import RawKey from './RawKey'; | import RawKey from './RawKey'; | ||||||
| import RecoveryPhrase from './RecoveryPhrase'; | import RecoveryPhrase from './RecoveryPhrase'; | ||||||
| 
 | import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store'; | ||||||
| import { createIdentityImg } from '~/api/util/identity'; |  | ||||||
| import print from './print'; | import print from './print'; | ||||||
| import recoveryPage from './recovery-page.ejs'; | import recoveryPage from './recoveryPage.ejs'; | ||||||
| import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg'; |  | ||||||
| 
 | 
 | ||||||
| const TITLES = { | const TITLES = { | ||||||
|   type: 'creation type', |   type: ( | ||||||
|   create: 'create account', |     <FormattedMessage | ||||||
|   info: 'account information', |       id='createAccount.title.createType' | ||||||
|   import: 'import wallet' |       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_NAMES = [TITLES.type, TITLES.create, TITLES.info]; | ||||||
| const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info]; | const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info]; | ||||||
| 
 | 
 | ||||||
| export default class CreateAccount extends Component { | @observer | ||||||
|  | class CreateAccount extends Component { | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|     api: PropTypes.object.isRequired, |     api: PropTypes.object.isRequired | ||||||
|     store: PropTypes.object.isRequired |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     accounts: PropTypes.object.isRequired, |     accounts: PropTypes.object.isRequired, | ||||||
|  |     newError: PropTypes.func.isRequired, | ||||||
|     onClose: PropTypes.func, |     onClose: PropTypes.func, | ||||||
|     onUpdate: PropTypes.func |     onUpdate: PropTypes.func | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   state = { |   store = new Store(this.context.api, this.props.accounts); | ||||||
|     address: null, |  | ||||||
|     name: null, |  | ||||||
|     passwordHint: null, |  | ||||||
|     password: null, |  | ||||||
|     phrase: null, |  | ||||||
|     windowsPhrase: false, |  | ||||||
|     rawKey: null, |  | ||||||
|     json: null, |  | ||||||
|     canCreate: false, |  | ||||||
|     createType: null, |  | ||||||
|     gethAddresses: [], |  | ||||||
|     stage: 0 |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { createType, stage } = this.state; |     const { createType, stage } = this.store; | ||||||
|     const steps = createType === 'fromNew' |  | ||||||
|       ? STAGE_NAMES |  | ||||||
|       : STAGE_IMPORT; |  | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|       <Modal |       <Modal | ||||||
|         visible |         visible | ||||||
|         actions={ this.renderDialogActions() } |         actions={ this.renderDialogActions() } | ||||||
|         current={ stage } |         current={ stage } | ||||||
|         steps={ steps } |         steps={ | ||||||
|  |           createType === 'fromNew' | ||||||
|  |             ? STAGE_NAMES | ||||||
|  |             : STAGE_IMPORT | ||||||
|  |         } | ||||||
|       > |       > | ||||||
|         { this.renderWarning() } |  | ||||||
|         { this.renderPage() } |         { this.renderPage() } | ||||||
|       </Modal> |       </Modal> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   renderPage () { |   renderPage () { | ||||||
|     const { createType, stage } = this.state; |     const { createType, stage } = this.store; | ||||||
|     const { accounts } = this.props; |  | ||||||
| 
 | 
 | ||||||
|     switch (stage) { |     switch (stage) { | ||||||
|       case 0: |       case STAGE_SELECT_TYPE: | ||||||
|         return ( |         return ( | ||||||
|           <CreationType onChange={ this.onChangeType } /> |           <CreationType store={ this.store } /> | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|       case 1: |       case STAGE_CREATE: | ||||||
|         if (createType === 'fromNew') { |         if (createType === 'fromNew') { | ||||||
|           return ( |           return ( | ||||||
|             <NewAccount onChange={ this.onChangeDetails } /> |             <NewAccount | ||||||
|  |               newError={ this.props.newError } | ||||||
|  |               store={ this.store } | ||||||
|  |             /> | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (createType === 'fromGeth') { |         if (createType === 'fromGeth') { | ||||||
|           return ( |           return ( | ||||||
|             <NewGeth |             <NewGeth store={ this.store } /> | ||||||
|               accounts={ accounts } |  | ||||||
|               onChange={ this.onChangeGeth } |  | ||||||
|             /> |  | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (createType === 'fromPhrase') { |         if (createType === 'fromPhrase') { | ||||||
|           return ( |           return ( | ||||||
|             <RecoveryPhrase onChange={ this.onChangeDetails } /> |             <RecoveryPhrase store={ this.store } /> | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (createType === 'fromRaw') { |         if (createType === 'fromRaw') { | ||||||
|           return ( |           return ( | ||||||
|             <RawKey onChange={ this.onChangeDetails } /> |             <RawKey store={ this.store } /> | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return ( |         return ( | ||||||
|           <NewImport onChange={ this.onChangeWallet } /> |           <NewImport store={ this.store } /> | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|       case 2: |       case STAGE_INFO: | ||||||
|         if (createType === 'fromGeth') { |         if (createType === 'fromGeth') { | ||||||
|           return ( |           return ( | ||||||
|             <AccountDetailsGeth addresses={ this.state.gethAddresses } /> |             <AccountDetailsGeth store={ this.store } /> | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return ( |         return ( | ||||||
|           <AccountDetails |           <AccountDetails store={ this.store } /> | ||||||
|             address={ this.state.address } |  | ||||||
|             name={ this.state.name } |  | ||||||
|             phrase={ this.state.phrase } |  | ||||||
|           /> |  | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   renderDialogActions () { |   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) { |     switch (stage) { | ||||||
|       case 0: |       case STAGE_SELECT_TYPE: | ||||||
|         return [ |         return [ | ||||||
|  |           cancelBtn, | ||||||
|           <Button |           <Button | ||||||
|             icon={ <ContentClear /> } |             icon={ <NextIcon /> } | ||||||
|             label='Cancel' |             key='next' | ||||||
|             onClick={ this.onClose } |             label={ | ||||||
|           />, |               <FormattedMessage | ||||||
|           <Button |                 id='createAccount.button.next' | ||||||
|             icon={ <NavigationArrowForward /> } |                 defaultMessage='Next' | ||||||
|             label='Next' |               /> | ||||||
|             onClick={ this.onNext } |             } | ||||||
|  |             onClick={ this.store.nextStage } | ||||||
|           /> |           /> | ||||||
|         ]; |         ]; | ||||||
|       case 1: |  | ||||||
|         const createLabel = createType === 'fromNew' |  | ||||||
|           ? 'Create' |  | ||||||
|           : 'Import'; |  | ||||||
| 
 | 
 | ||||||
|  |       case STAGE_CREATE: | ||||||
|         return [ |         return [ | ||||||
|  |           cancelBtn, | ||||||
|           <Button |           <Button | ||||||
|             icon={ <ContentClear /> } |             icon={ <PrevIcon /> } | ||||||
|             label='Cancel' |             key='back' | ||||||
|             onClick={ this.onClose } |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.button.back' | ||||||
|  |                 defaultMessage='Back' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|  |             onClick={ this.store.prevStage } | ||||||
|           />, |           />, | ||||||
|           <Button |           <Button | ||||||
|             icon={ <NavigationArrowBack /> } |             disabled={ !canCreate || isBusy } | ||||||
|             label='Back' |             icon={ <CheckIcon /> } | ||||||
|             onClick={ this.onPrev } |             key='create' | ||||||
|           />, |             label={ | ||||||
|           <Button |               createType === 'fromNew' | ||||||
|             icon={ <ActionDone /> } |                 ? ( | ||||||
|             label={ createLabel } |                   <FormattedMessage | ||||||
|             disabled={ !this.state.canCreate } |                     id='createAccount.button.create' | ||||||
|  |                     defaultMessage='Create' | ||||||
|  |                   /> | ||||||
|  |                 ) | ||||||
|  |                 : ( | ||||||
|  |                   <FormattedMessage | ||||||
|  |                     id='createAccount.button.import' | ||||||
|  |                     defaultMessage='Import' | ||||||
|  |                   /> | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|             onClick={ this.onCreate } |             onClick={ this.onCreate } | ||||||
|           /> |           /> | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|       case 2: |       case STAGE_INFO: | ||||||
|         return [ |         return [ | ||||||
|           createType === 'fromNew' || createType === 'fromPhrase' ? ( |           ['fromNew', 'fromPhrase'].includes(createType) | ||||||
|             <Button |             ? ( | ||||||
|               icon={ <PrintIcon /> } |               <Button | ||||||
|               label='Print Phrase' |                 icon={ <PrintIcon /> } | ||||||
|               onClick={ this.printPhrase } |                 key='print' | ||||||
|             /> |                 label={ | ||||||
|           ) : null, |                   <FormattedMessage | ||||||
|  |                     id='createAccount.button.print' | ||||||
|  |                     defaultMessage='Print Phrase' | ||||||
|  |                   /> | ||||||
|  |                 } | ||||||
|  |                 onClick={ this.printPhrase } | ||||||
|  |               /> | ||||||
|  |             ) | ||||||
|  |             : null, | ||||||
|           <Button |           <Button | ||||||
|             icon={ <ActionDoneAll /> } |             icon={ <DoneIcon /> } | ||||||
|             label='Close' |             key='close' | ||||||
|  |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='createAccount.button.close' | ||||||
|  |                 defaultMessage='Close' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             onClick={ this.onClose } |             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 = () => { |   onCreate = () => { | ||||||
|     const { createType, windowsPhrase } = this.state; |     this.store.setBusy(true); | ||||||
|     const { api } = this.context; |  | ||||||
| 
 | 
 | ||||||
|     this.setState({ |     return this.store | ||||||
|       canCreate: false |       .createAccount() | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     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 |  | ||||||
|             })); |  | ||||||
|         }) |  | ||||||
|         .then(() => { |  | ||||||
|           this.onNext(); |  | ||||||
|           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(() => { |       .then(() => { | ||||||
|         this.onNext(); |         this.store.setBusy(false); | ||||||
|  |         this.store.nextStage(); | ||||||
|         this.props.onUpdate && this.props.onUpdate(); |         this.props.onUpdate && this.props.onUpdate(); | ||||||
|       }) |       }) | ||||||
|       .catch((error) => { |       .catch((error) => { | ||||||
|         console.error('onCreate', error); |         this.store.setBusy(false); | ||||||
| 
 |         this.props.newError(error); | ||||||
|         this.setState({ |  | ||||||
|           canCreate: true |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         this.newError(error); |  | ||||||
|       }); |       }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onClose = () => { |   onClose = () => { | ||||||
|     this.setState({ |     this.props.onClose && this.props.onClose(); | ||||||
|       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 = () => { |   printPhrase = () => { | ||||||
|     const { address, phrase, name } = this.state; |     const { address, name, phrase } = this.store; | ||||||
|     const identity = createIdentityImg(address); |     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> | <!DOCTYPE html> | ||||||
| 
 |  | ||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <title>Recovery phrase for <%= name %></title> |     <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
 | // You should have received a copy of the GNU General Public License
 | ||||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
|  | import { observer } from 'mobx-react'; | ||||||
| import React, { Component, PropTypes } from 'react'; | import React, { Component, PropTypes } from 'react'; | ||||||
|  | import { FormattedMessage } from 'react-intl'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import ActionDone from 'material-ui/svg-icons/action/done'; | import { bindActionCreators } from 'redux'; | ||||||
| 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 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 { Button, Modal } from '~/ui'; | ||||||
|  | import { CheckIcon, DoneIcon, NextIcon, PrintIcon } from '~/ui/Icons'; | ||||||
| 
 | 
 | ||||||
| import { NewAccount, AccountDetails } from '../CreateAccount'; | 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 Completed from './Completed'; | ||||||
| import TnC from './TnC'; | import TnC from './TnC'; | ||||||
| import Welcome from './Welcome'; | import Welcome from './Welcome'; | ||||||
| 
 | 
 | ||||||
| import { createIdentityImg } from '~/api/util/identity'; | const STAGE_NAMES = [ | ||||||
| import print from '../CreateAccount/print'; |   <FormattedMessage | ||||||
| import recoveryPage from '../CreateAccount/recovery-page.ejs'; |     id='firstRun.title.welcome' | ||||||
| import ParityLogo from '../../../assets/images/parity-logo-black-no-text.svg'; |     defaultMessage='welcome' | ||||||
| 
 |   />, | ||||||
| const STAGE_NAMES = ['welcome', 'terms', 'new account', 'recovery', 'completed']; |   <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 { | class FirstRun extends Component { | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|     api: PropTypes.object.isRequired, |     api: PropTypes.object.isRequired | ||||||
|     store: PropTypes.object.isRequired |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     hasAccounts: PropTypes.bool.isRequired, |     hasAccounts: PropTypes.bool.isRequired, | ||||||
|     visible: PropTypes.bool.isRequired, |     newError: PropTypes.func.isRequired, | ||||||
|     onClose: PropTypes.func.isRequired |     onClose: PropTypes.func.isRequired, | ||||||
|  |     visible: PropTypes.bool.isRequired | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   createStore = new CreateStore(this.context.api, {}, false); | ||||||
|  | 
 | ||||||
|   state = { |   state = { | ||||||
|     stage: 0, |     stage: 0, | ||||||
|     name: '', |  | ||||||
|     address: '', |  | ||||||
|     password: '', |  | ||||||
|     phrase: '', |  | ||||||
|     canCreate: false, |  | ||||||
|     hasAcceptedTnc: false |     hasAcceptedTnc: false | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -79,7 +105,7 @@ class FirstRun extends Component { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   renderStage () { |   renderStage () { | ||||||
|     const { address, name, phrase, stage, hasAcceptedTnc } = this.state; |     const { stage, hasAcceptedTnc } = this.state; | ||||||
| 
 | 
 | ||||||
|     switch (stage) { |     switch (stage) { | ||||||
|       case 0: |       case 0: | ||||||
| @ -96,16 +122,13 @@ class FirstRun extends Component { | |||||||
|       case 2: |       case 2: | ||||||
|         return ( |         return ( | ||||||
|           <NewAccount |           <NewAccount | ||||||
|             onChange={ this.onChangeDetails } |             newError={ this.props.newError } | ||||||
|  |             store={ this.createStore } | ||||||
|           /> |           /> | ||||||
|         ); |         ); | ||||||
|       case 3: |       case 3: | ||||||
|         return ( |         return ( | ||||||
|           <AccountDetails |           <AccountDetails store={ this.createStore } /> | ||||||
|             address={ address } |  | ||||||
|             name={ name } |  | ||||||
|             phrase={ phrase } |  | ||||||
|           /> |  | ||||||
|         ); |         ); | ||||||
|       case 4: |       case 4: | ||||||
|         return ( |         return ( | ||||||
| @ -116,14 +139,16 @@ class FirstRun extends Component { | |||||||
| 
 | 
 | ||||||
|   renderDialogActions () { |   renderDialogActions () { | ||||||
|     const { hasAccounts } = this.props; |     const { hasAccounts } = this.props; | ||||||
|     const { canCreate, stage, hasAcceptedTnc } = this.state; |     const { stage, hasAcceptedTnc } = this.state; | ||||||
|  |     const { canCreate } = this.createStore; | ||||||
| 
 | 
 | ||||||
|     switch (stage) { |     switch (stage) { | ||||||
|       case 0: |       case 0: | ||||||
|         return ( |         return ( | ||||||
|           <Button |           <Button | ||||||
|             icon={ <NavigationArrowForward /> } |             icon={ <NextIcon /> } | ||||||
|             label='Next' |             key='next' | ||||||
|  |             label={ BUTTON_LABEL_NEXT } | ||||||
|             onClick={ this.onNext } |             onClick={ this.onNext } | ||||||
|           /> |           /> | ||||||
|         ); |         ); | ||||||
| @ -132,8 +157,9 @@ class FirstRun extends Component { | |||||||
|         return ( |         return ( | ||||||
|           <Button |           <Button | ||||||
|             disabled={ !hasAcceptedTnc } |             disabled={ !hasAcceptedTnc } | ||||||
|             icon={ <NavigationArrowForward /> } |             icon={ <NextIcon /> } | ||||||
|             label='Next' |             key='next' | ||||||
|  |             label={ BUTTON_LABEL_NEXT } | ||||||
|             onClick={ this.onNext } |             onClick={ this.onNext } | ||||||
|           /> |           /> | ||||||
|         ); |         ); | ||||||
| @ -141,10 +167,15 @@ class FirstRun extends Component { | |||||||
|       case 2: |       case 2: | ||||||
|         const buttons = [ |         const buttons = [ | ||||||
|           <Button |           <Button | ||||||
|             icon={ <ActionDone /> } |  | ||||||
|             label='Create' |  | ||||||
|             key='create' |  | ||||||
|             disabled={ !canCreate } |             disabled={ !canCreate } | ||||||
|  |             icon={ <CheckIcon /> } | ||||||
|  |             key='create' | ||||||
|  |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='firstRun.button.create' | ||||||
|  |                 defaultMessage='Create' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             onClick={ this.onCreate } |             onClick={ this.onCreate } | ||||||
|           /> |           /> | ||||||
|         ]; |         ]; | ||||||
| @ -152,9 +183,14 @@ class FirstRun extends Component { | |||||||
|         if (hasAccounts) { |         if (hasAccounts) { | ||||||
|           buttons.unshift( |           buttons.unshift( | ||||||
|             <Button |             <Button | ||||||
|               icon={ <NavigationArrowForward /> } |               icon={ <NextIcon /> } | ||||||
|               label='Skip' |  | ||||||
|               key='skip' |               key='skip' | ||||||
|  |               label={ | ||||||
|  |                 <FormattedMessage | ||||||
|  |                   id='firstRun.button.skip' | ||||||
|  |                   defaultMessage='Skip' | ||||||
|  |                 /> | ||||||
|  |               } | ||||||
|               onClick={ this.skipAccountCreation } |               onClick={ this.skipAccountCreation } | ||||||
|             /> |             /> | ||||||
|           ); |           ); | ||||||
| @ -165,12 +201,19 @@ class FirstRun extends Component { | |||||||
|         return [ |         return [ | ||||||
|           <Button |           <Button | ||||||
|             icon={ <PrintIcon /> } |             icon={ <PrintIcon /> } | ||||||
|             label='Print Phrase' |             key='print' | ||||||
|  |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='firstRun.button.print' | ||||||
|  |                 defaultMessage='Print Phrase' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             onClick={ this.printPhrase } |             onClick={ this.printPhrase } | ||||||
|           />, |           />, | ||||||
|           <Button |           <Button | ||||||
|             icon={ <NavigationArrowForward /> } |             icon={ <NextIcon /> } | ||||||
|             label='Next' |             key='next' | ||||||
|  |             label={ BUTTON_LABEL_NEXT } | ||||||
|             onClick={ this.onNext } |             onClick={ this.onNext } | ||||||
|           /> |           /> | ||||||
|         ]; |         ]; | ||||||
| @ -178,8 +221,14 @@ class FirstRun extends Component { | |||||||
|       case 4: |       case 4: | ||||||
|         return ( |         return ( | ||||||
|           <Button |           <Button | ||||||
|             icon={ <ActionDoneAll /> } |             icon={ <DoneIcon /> } | ||||||
|             label='Close' |             key='close' | ||||||
|  |             label={ | ||||||
|  |               <FormattedMessage | ||||||
|  |                 id='firstRun.button.close' | ||||||
|  |                 defaultMessage='Close' | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|             onClick={ this.onClose } |             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 = () => { |   onCreate = () => { | ||||||
|     const { api } = this.context; |     this.createStore.setBusy(true); | ||||||
|     const { name, phrase, password } = this.state; |  | ||||||
| 
 | 
 | ||||||
|     this.setState({ |     return this.createStore | ||||||
|       canCreate: false |       .createAccount() | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return api.parity |  | ||||||
|       .newAccountFromPhrase(phrase, password) |  | ||||||
|       .then((address) => api.parity.setAccountName(address, name)) |  | ||||||
|       .then(() => { |       .then(() => { | ||||||
|         this.onNext(); |         this.onNext(); | ||||||
|  |         this.createStore.setBusy(false); | ||||||
|       }) |       }) | ||||||
|       .catch((error) => { |       .catch((error) => { | ||||||
|         console.error('onCreate', error); |         this.createStore.setBusy(false); | ||||||
| 
 |         this.props.newError(error); | ||||||
|         this.setState({ |  | ||||||
|           canCreate: true |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         this.newError(error); |  | ||||||
|       }); |       }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -247,22 +276,35 @@ class FirstRun extends Component { | |||||||
|     this.setState({ stage: this.state.stage + 2 }); |     this.setState({ stage: this.state.stage + 2 }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   newError = (error) => { |  | ||||||
|     const { store } = this.context; |  | ||||||
| 
 |  | ||||||
|     store.dispatch({ type: 'newError', error }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   printPhrase = () => { |   printPhrase = () => { | ||||||
|     const { address, phrase, name } = this.state; |     const { address, phrase, name } = this.createStore; | ||||||
|     const identity = createIdentityImg(address); |     const identity = createIdentityImg(address); | ||||||
| 
 | 
 | ||||||
|     print(recoveryPage({ phrase, name, identity, address, logo: ParityLogo })); |     print(recoveryPage({ | ||||||
|  |       address, | ||||||
|  |       identity, | ||||||
|  |       logo: ParityLogo, | ||||||
|  |       name, | ||||||
|  |       phrase | ||||||
|  |     })); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function mapStateToProps (state) { | 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/>.
 | // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||||
| 
 | 
 | ||||||
| import AddIcon from 'material-ui/svg-icons/content/add'; | 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 CancelIcon from 'material-ui/svg-icons/content/clear'; | ||||||
| import CheckIcon from 'material-ui/svg-icons/navigation/check'; | import CheckIcon from 'material-ui/svg-icons/navigation/check'; | ||||||
| import CloseIcon from 'material-ui/svg-icons/navigation/close'; | 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 MoveIcon from 'material-ui/svg-icons/action/open-with'; | ||||||
| import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; | import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; | ||||||
| import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; | 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 SaveIcon from 'material-ui/svg-icons/content/save'; | ||||||
| import SendIcon from 'material-ui/svg-icons/content/send'; | import SendIcon from 'material-ui/svg-icons/content/send'; | ||||||
| import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; | import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; | ||||||
| @ -40,6 +43,7 @@ import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock'; | |||||||
| 
 | 
 | ||||||
| export { | export { | ||||||
|   AddIcon, |   AddIcon, | ||||||
|  |   AttachFileIcon, | ||||||
|   CancelIcon, |   CancelIcon, | ||||||
|   CheckIcon, |   CheckIcon, | ||||||
|   CloseIcon, |   CloseIcon, | ||||||
| @ -56,6 +60,8 @@ export { | |||||||
|   MoveIcon, |   MoveIcon, | ||||||
|   NextIcon, |   NextIcon, | ||||||
|   PrevIcon, |   PrevIcon, | ||||||
|  |   PrintIcon, | ||||||
|  |   RefreshIcon, | ||||||
|   SaveIcon, |   SaveIcon, | ||||||
|   SendIcon, |   SendIcon, | ||||||
|   SnoozeIcon, |   SnoozeIcon, | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ import { FormattedMessage } from 'react-intl'; | |||||||
| 
 | 
 | ||||||
| import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui'; | import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags, Input } from '~/ui'; | ||||||
| import Certifications from '~/ui/Certifications'; | import Certifications from '~/ui/Certifications'; | ||||||
| import { nullableProptype } from '~/util/proptypes'; | import { arrayOrObjectProptype, nullableProptype } from '~/util/proptypes'; | ||||||
| 
 | 
 | ||||||
| import styles from '../accounts.css'; | import styles from '../accounts.css'; | ||||||
| 
 | 
 | ||||||
| @ -40,7 +40,7 @@ export default class Summary extends Component { | |||||||
|     noLink: PropTypes.bool, |     noLink: PropTypes.bool, | ||||||
|     showCertifications: PropTypes.bool, |     showCertifications: PropTypes.bool, | ||||||
|     handleAddSearchToken: PropTypes.func, |     handleAddSearchToken: PropTypes.func, | ||||||
|     owners: nullableProptype(PropTypes.array) |     owners: nullableProptype(arrayOrObjectProptype()) | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   static defaultProps = { |   static defaultProps = { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user