OSDN Git Service

merge Dashboard into electron
authorZhiting Lin <zlin035@uottawa.ca>
Thu, 8 Nov 2018 10:46:22 +0000 (18:46 +0800)
committerZhiting Lin <zlin035@uottawa.ca>
Thu, 8 Nov 2018 10:46:22 +0000 (18:46 +0800)
55 files changed:
package-lock.json
package.json
src/actions.js
src/features/app/actions.js
src/features/app/components/Container.jsx
src/features/app/components/Register/Register.jsx [deleted file]
src/features/app/components/index.js
src/features/backup/actions.js
src/features/backup/components/Backup.jsx
src/features/backup/components/Backup.scss
src/features/backup/components/index.js
src/features/core/actions.js
src/features/initialization/actions.js [new file with mode: 0644]
src/features/initialization/components/FormIndex.scss [moved from src/features/app/components/Register/Register.scss with 67% similarity]
src/features/initialization/components/Index/Index.jsx [new file with mode: 0644]
src/features/initialization/components/Index/Index.scss [new file with mode: 0644]
src/features/initialization/components/Keystore/Keystore.jsx [new file with mode: 0644]
src/features/initialization/components/Mnemonic/Mnemonic.jsx [new file with mode: 0644]
src/features/initialization/components/MnemonicStepper/MnemonicStepper.jsx [new file with mode: 0644]
src/features/initialization/components/MnemonicStepper/MnemonicStepper.scss [new file with mode: 0644]
src/features/initialization/components/Register/Register.jsx [new file with mode: 0644]
src/features/initialization/components/index.js [new file with mode: 0644]
src/features/initialization/index.js [new file with mode: 0644]
src/features/initialization/reducers.js [new file with mode: 0644]
src/features/initialization/routes.js [new file with mode: 0644]
src/features/mockhsm/actions.js
src/features/mockhsm/components/CheckPassword/CheckPassword.jsx
src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.jsx [new file with mode: 0644]
src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.scss [new file with mode: 0644]
src/features/mockhsm/components/index.js
src/features/mockhsm/reducers.js
src/features/mockhsm/routes.js
src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.jsx [new file with mode: 0644]
src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.scss [new file with mode: 0644]
src/features/shared/components/FileField.jsx [new file with mode: 0644]
src/features/shared/components/FormContainer/FormContainer.scss
src/features/shared/components/Mnemonic/Mnemonic.jsx [new file with mode: 0644]
src/features/shared/components/Mnemonic/Mnemonic.scss [new file with mode: 0644]
src/features/shared/components/RestoreKeystore/RestoreKeystore.jsx [new file with mode: 0644]
src/features/shared/components/RestoreKeystore/RestoreKeystore.scss [new file with mode: 0644]
src/features/shared/components/RestoreMnemonic/RestoreMnemonic.jsx [new file with mode: 0644]
src/features/shared/components/RestoreMnemonic/RestoreMnemonic.scss [new file with mode: 0644]
src/features/shared/components/SingletonField.jsx [new file with mode: 0644]
src/features/shared/components/Stepper/Step.jsx [new file with mode: 0644]
src/features/shared/components/Stepper/StepList.jsx [new file with mode: 0644]
src/features/shared/components/Stepper/stepper.scss [new file with mode: 0644]
src/features/shared/components/TextareaField.jsx [new file with mode: 0644]
src/features/shared/components/index.js
src/features/transactions/components/Show.jsx
src/locales/en/translation.json
src/locales/zh/translation.json
src/reducers.js
src/routes.js
src/sdk/api/backUp.js
static/images/copy.svg [new file with mode: 0755]

index d90e885..9a3b447 100644 (file)
       "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=",
       "dev": true
     },
+    "bignumber.js": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
+      "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
+    },
     "bin-version": {
       "version": "1.0.4",
       "resolved": "http://registry.npm.taobao.org/bin-version/download/bin-version-1.0.4.tgz",
index 842fbfb..3ae0588 100644 (file)
@@ -88,6 +88,7 @@
   ],
   "dependencies": {
     "babel-polyfill": "~6.16.0",
+    "bignumber.js": "^7.2.1",
     "bootstrap-sass": "~3.3.7",
     "btoa": "^1.1.2",
     "classnames": "~2.2.5",
index 6065186..f137741 100644 (file)
@@ -6,6 +6,7 @@ import { actions as backUp } from 'features/backup'
 import { actions as balance } from 'features/balances'
 import { actions as configuration } from 'features/configuration'
 import { actions as core } from 'features/core'
+import { actions as initialization } from 'features/initialization'
 import { actions as mockhsm } from 'features/mockhsm'
 import { actions as testnet } from 'features/testnet'
 import { actions as transaction } from 'features/transactions'
@@ -22,6 +23,7 @@ const actions = {
   balance,
   configuration,
   core,
+  initialization,
   key: mockhsm,
   testnet,
   transaction,
index 7239c3f..125606e 100644 (file)
@@ -19,6 +19,17 @@ const actions = {
       dispatch({ type: 'CLOSE_DROPDOWN' })
     }
   },
+  showInitialization: () => {
+    return (dispatch, getState) => {
+      // Need a default here, since locationBeforeTransitions gets cleared
+      // during logout.
+      let pathname = (getState().routing.locationBeforeTransitions || {}).pathname
+
+      if (!(pathname.indexOf('initialization') >= 0)) {
+        dispatch(push('/initialization'))
+      }
+    }
+  },
   showConfiguration: () => {
     return (dispatch, getState) => {
       // Need a default here, since locationBeforeTransitions gets cleared
index 94de606..f867e35 100644 (file)
@@ -1,7 +1,7 @@
 import React from 'react'
 import { connect } from 'react-redux'
 import actions from 'actions'
-import { Main, Config, Login, Loading, Register ,Modal } from './'
+import { Main, Config, Login, Loading, Modal } from './'
 import moment from 'moment'
 import { withI18n } from 'react-i18next'
 
@@ -23,7 +23,8 @@ class Container extends React.Component {
     const {
       authOk,
       configured,
-      location
+      location,
+      accountInit
     } = props
 
     if (!authOk) {
@@ -32,13 +33,23 @@ class Container extends React.Component {
 
     if (configured) {
       if (location.pathname === '/' ||
-          location.pathname.indexOf('configuration') >= 0 || location.pathname.includes('index.html')) {
+        location.pathname.indexOf('configuration') >= 0 || location.pathname.includes('index.html')) {
+        this.props.showRoot()
+        }
+      } else {
+        this.props.showInitialization()
+      }
+
+    if (accountInit || !this.state.noAccountItem) {
+      if (location.pathname === '/'|| location.pathname.indexOf('initialization') >= 0) {
         this.props.showRoot()
       }
     } else {
-      this.props.showConfiguration()
+      this.props.showInitialization()
     }
+
   }
+
   componentDidMount() {
     if(window.ipcRenderer){
       window.ipcRenderer.on('redirect', (event, arg) => {
@@ -71,6 +82,12 @@ class Container extends React.Component {
         this.props.updateMiningState(isMining)
       })
     }
+    this.props.fetchKeyItem().then(resp => {
+      if (resp.data.length == 0) {
+        this.setState({noAccountItem: true})
+        this.redirectRoot(this.props)
+      }
+    })
     if(this.props.lng === 'zh'){
       moment.locale('zh-cn')
     }else{
@@ -79,8 +96,7 @@ class Container extends React.Component {
   }
 
   componentWillReceiveProps(nextProps) {
-    if (nextProps.authOk != this.props.authOk ||
-        nextProps.configKnown != this.props.configKnown ||
+    if (nextProps.accountInit != this.props.accountInit ||
         nextProps.configured != this.props.configured ||
         nextProps.location.pathname != this.props.location.pathname) {
       this.redirectRoot(nextProps)
@@ -107,7 +123,7 @@ class Container extends React.Component {
     } else if (!this.props.configKnown) {
       return <Loading>{lang === 'zh'?  '正在连接到Bytom Core...' : 'Connecting to Bytom Core...'}</Loading>
     } else if (!this.props.accountInit && this.state.noAccountItem){
-      layout = <Register>{this.props.children}</Register>
+      layout = <Config>{this.props.children}</Config>
     } else{
       layout = <Main>{this.props.children}</Main>
     }
@@ -142,9 +158,10 @@ export default connect(
     showRoot: () => dispatch(actions.app.showRoot),
     showConfiguration: () => dispatch(actions.app.showConfiguration()),
     uptdateBtmAmountUnit: (param) => dispatch(actions.core.updateBTMAmountUnit(param)),
-    uptdateLang: (param) => dispatch(actions.core.updateLang(param)),
     updateConfiguredStatus: () => dispatch(actions.core.updateConfiguredStatus),
     markFlashDisplayed: (key) => dispatch(actions.app.displayedFlash(key)),
-    fetchAccountItem: () => dispatch(actions.account.fetchItems())
+    fetchAccountItem: () => dispatch(actions.account.fetchItems()),
+    showInitialization: () => dispatch(actions.app.showInitialization()),
+    fetchKeyItem: () => dispatch(actions.key.fetchItems())
   })
 )( withI18n() (Container) )
diff --git a/src/features/app/components/Register/Register.jsx b/src/features/app/components/Register/Register.jsx
deleted file mode 100644 (file)
index 92c8862..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-import React from 'react'
-import {connect} from 'react-redux'
-import {ErrorBanner, TextField, PasswordField} from 'features/shared/components'
-import actions from 'actions'
-import styles from './Register.scss'
-import {reduxForm} from 'redux-form'
-import {chainClient} from 'utility/environment'
-import {withNamespaces} from 'react-i18next'
-
-class Register extends React.Component {
-  constructor(props) {
-    super(props)
-    this.connection = chainClient().connection
-
-    this.submitWithErrors = this.submitWithErrors.bind(this)
-  }
-
-  componentDidMount() {
-    this.setState({
-      init: true
-    })
-  }
-
-  submitWithErrors(data) {
-    return new Promise((resolve, reject) => {
-      this.props.registerKey(data)
-        .catch((err) => reject({_error: err.message}))
-    })
-  }
-
-  setMode(isInit) {
-    this.setState({
-      init: isInit
-    })
-  }
-
-  restore() {
-    const element = document.getElementById('bytom-restore-file-upload-init')
-    element.click()
-  }
-
-  handleFileChange(event) {
-    const files = event.target.files
-    if (files.length <= 0) {
-      this.setState({key: null})
-      return
-    }
-
-    const fileReader = new FileReader()
-    fileReader.onload = fileLoadedEvent => {
-      const backupData = JSON.parse(fileLoadedEvent.target.result)
-      this.connection.request('/restore-wallet', backupData).then(resp => {
-        if (resp.status === 'fail') {
-          this.props.showError(new Error(resp.msg))
-          return
-        }
-        this.props.success()
-      })
-    }
-    fileReader.readAsText(files[0], 'UTF-8')
-
-    const fileElement = document.getElementById('bytom-restore-file-upload-init')
-    fileElement.value = ''
-  }
-
-  render() {
-    const t = this.props.t
-
-    const {
-      fields: {keyAlias, password, repeatPassword, accountAlias},
-      error,
-      handleSubmit,
-      submitting
-    } = this.props
-
-    return (
-      <div className={styles.main}>
-        {
-          this.state && this.state.init &&
-          <div>
-            <h2 className={styles.title}>{t('init.title')}</h2>
-            <div className={styles.formWarpper}>
-              <form className={styles.form} onSubmit={handleSubmit(this.submitWithErrors)}>
-                <TextField
-                  title={t('form.accountAlias')}
-                  placeholder={t('init.accountPlaceholder')}
-                  fieldProps={accountAlias} />
-                <TextField
-                  title={t('form.keyAlias')}
-                  placeholder={t('init.keyPlaceholder')}
-                  fieldProps={keyAlias}/>
-                <PasswordField
-                  title={t('init.keyPassword')}
-                  placeholder={t('init.passwordPlaceholder')}
-                  fieldProps={password} />
-                <PasswordField
-                  title={t('init.repeatKeyPassword')}
-                  placeholder={t('init.repeatPlaceHolder')}
-                  fieldProps={repeatPassword} />
-
-                {error &&
-                <ErrorBanner
-                  title={t('init.errorTitle')}
-                  error={error}/>}
-
-                <button type='submit' className='btn btn-primary' disabled={submitting}>
-                  {t('init.register')}
-                </button>
-                <a className={`${styles.choice} ${(this.state && this.state.init) ? '' : styles.active}`}
-                   href='javascript:;' onClick={this.setMode.bind(this, false)}>
-                  {t('init.restoreWallet')}
-                </a>
-              </form>
-            </div>
-          </div>
-        }
-        {
-          this.state && !this.state.init &&
-          <div>
-            <h2 className={styles.title}>{t('init.restoreWallet')}</h2>
-            <div className={styles.formWarpper}>
-              <form className={styles.form} onSubmit={handleSubmit(this.submitWithErrors)}>
-                <button className='btn btn-primary' onClick={this.restore.bind(this)}>
-                  {t('init.restore')}
-                </button>
-                <a className={`${styles.choice} ${(this.state && this.state.init) ? styles.active : ''}`}
-                   href='javascript:;' onClick={this.setMode.bind(this, true)}>
-                  {t('init.title')}
-                </a>
-
-                <p></p>
-                <p>{t('init.restoreLabel')}</p>
-
-                <input id='bytom-restore-file-upload-init' type='file' style={{'display': 'none'}}
-                       onChange={this.handleFileChange.bind(this)}/>
-              </form>
-            </div>
-          </div>
-        }
-      </div>
-    )
-  }
-}
-
-const validate = (values, props) => {
-  const errors = {}
-  const t = props.t
-
-  if (!values.keyAlias) {
-    errors.keyAlias = t('key.aliasRequired')
-  }
-  if (!values.password) {
-    errors.password = t('key.passwordRequired')
-  } else if (values.password.length < 5) {
-    errors.password = ( t('key.reset.newPWarning') )
-  }
-  if (values.password !== values.repeatPassword) {
-    errors.repeatPassword = ( t('key.reset.repeatPWarning') )
-  }
-  if (!values.accountAlias) {
-    errors.accountAlias = ( t('account.new.aliasWarning') )
-  }
-
-  return errors
-}
-
-export default withNamespaces('translations')( connect(
-  () => ({}),
-  (dispatch) => ({
-    registerKey: (token) => dispatch(actions.core.registerKey(token)),
-    showError: (err) => dispatch({type: 'ERROR', payload: err}),
-    success: () => dispatch({type: 'CREATE_REGISTER_ACCOUNT'})
-  })
-)(reduxForm({
-  form: 'initDefaultPassword',
-  fields: ['keyAlias', 'password', 'repeatPassword', 'accountAlias'],
-  validate
-})(Register)))
index 351b6e8..39dbc68 100644 (file)
@@ -3,7 +3,6 @@ import Main from './Main/Main'
 import Config from './Config/Config'
 import Login from './Login/Login'
 import Loading from './Loading/Loading'
-import Register from  './Register/Register'
 import Modal from './Modal/Modal'
 import Navigation from './Navigation/Navigation'
 import SecondaryNavigation from './SecondaryNavigation/SecondaryNavigation'
@@ -14,7 +13,6 @@ export {
   Config,
   Login,
   Loading,
-  Register,
   Modal,
   Navigation,
   SecondaryNavigation,
index 5848037..9a211d0 100644 (file)
@@ -1,21 +1,32 @@
 import { chainClient } from 'utility/environment'
 
 let actions = {
-  restore: (backupData) => {
+  backup:()=>{
+    return chainClient().backUp.backup()
+      .then(resp => {
+        const date = new Date()
+        const dateStr = date.toLocaleDateString().split(' ')[0]
+        const timestamp = date.getTime()
+        const fileName = ['bytom-wallet-backup-', dateStr, timestamp].join('-')
+
+        let element = document.createElement('a')
+        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(resp.data)))
+        element.setAttribute('download', fileName)
+        element.style.display = 'none'
+        document.body.appendChild(element)
+        element.click()
+
+        document.body.removeChild(element)
+      })
+      .catch(err => { throw {_error: err} })
+  },
+
+  success: ()=>{
     return (dispatch) => {
-      return chainClient().backUp.restore(backupData)
-        .then(resp => {
-          if (resp.status === 'fail') {
-            dispatch({type: 'ERROR', payload: { 'message': resp.msg}})
-          }else {
-            dispatch({type: 'RESTORE_SUCCESS'})
-          }
-        })
-        .catch(err => {
-          dispatch({type: 'ERROR', payload: err})
-        })
+      dispatch( { type: 'HIDE_MODAL' })
+      dispatch({type: 'RESTORE_SUCCESS'})
     }
-  },
+  }
 
 }
 
index f0c79bc..457c08e 100644 (file)
@@ -1,9 +1,7 @@
 import React from 'react'
-import componentClassNames from 'utility/componentClassNames'
-import {PageContent, PageTitle} from 'features/shared/components'
+import { connect } from 'react-redux'
+import { RestoreKeystore, RestoreMnemonic, PageContent, PageTitle } from 'features/shared/components'
 import styles from './Backup.scss'
-import {connect} from 'react-redux'
-import {chainClient} from 'utility/environment'
 import actions from 'actions'
 import {withNamespaces} from 'react-i18next'
 
@@ -13,6 +11,9 @@ class Backup extends React.Component {
     this.state = {
       value: null
     }
+
+    this.mnemonicPopup = this.mnemonicPopup.bind(this)
+    this.keystorePopup = this.keystorePopup.bind(this)
   }
 
   setValue(event) {
@@ -21,61 +22,35 @@ class Backup extends React.Component {
     })
   }
 
-  backup() {
-    chainClient().backUp.backup()
-      .then(resp => {
-        const date = new Date()
-        const dateStr = date.toLocaleDateString().split(' ')[0]
-        const timestamp = date.getTime()
-        const fileName = ['bytom-wallet-backup-', dateStr, timestamp].join('-')
-
-        let element = document.createElement('a')
-        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(resp.data)))
-        element.setAttribute('download', fileName)
-        element.style.display = 'none'
-        document.body.appendChild(element)
-        element.click()
-
-        document.body.removeChild(element)
-      })
-      .catch(err => { throw {_error: err} })
-  }
-
-  handleFileChange(event) {
-    const files = event.target.files
-    if (files.length <= 0) {
-      this.setState({key: null})
-      return
-    }
-
-    const fileReader = new FileReader()
-    fileReader.onload = fileLoadedEvent => {
-      const backupData = JSON.parse(fileLoadedEvent.target.result)
-      this.props.restoreFile(backupData)
-    }
-    fileReader.readAsText(files[0], 'UTF-8')
-
-    const fileElement = document.getElementById('bytom-restore-file-upload')
-    fileElement.value = ''
+  mnemonicPopup(e) {
+    e.preventDefault()
+    this.props.showModal(
+      <RestoreMnemonic success={this.props.success}/>
+    )
   }
 
-  restore() {
-    const element = document.getElementById('bytom-restore-file-upload')
-    element.click()
+  keystorePopup(e){
+    e.preventDefault()
+    this.props.showModal(
+      <RestoreKeystore success={this.props.success}/>
+    )
   }
 
   render() {
-    const t = this.props.t
+    const {
+      t,
+    } = this.props
 
-    const newButton = <button className={`btn btn-primary btn-lg ${styles.submit}`} onClick={this.backup.bind(this)}>
+    const newButton = <button className={`btn btn-primary btn-lg ${styles.submit}`} onClick={() => this.props.backup()}>
       {t('backup.download')}
     </button>
-    const restoreButton = <button className={`btn btn-primary btn-lg ${styles.submit}`} onClick={this.restore.bind(this)}>
+    const restoreKeystoreButton = <button className={`btn btn-primary btn-lg ${styles.submit}`} onClick={this.keystorePopup}>
       {t('backup.selectFile')}
     </button>
-    // const rescanButton = <button className={`btn btn-primary btn-lg ${styles.submit}`}  onClick={() => this.props.rescan()}>
-    //   {lang === 'zh' ? '重新扫描' : 'Rescan'}
-    // </button>
+
+    const restoreMnemonicButton = <button className={`btn btn-primary btn-lg ${styles.submit}`} onClick={this.mnemonicPopup}>
+      {t('backup.restore')}
+    </button>
 
     return (
       <div className='flex-container'>
@@ -104,11 +79,26 @@ class Backup extends React.Component {
                   <input className={styles.choice_radio_button}
                          type='radio'
                          name='type'
-                         value='restore' />
+                         value='restoreKeystore' />
+                  <div className={`${styles.choice} ${styles.restore}`}>
+                    <span className={styles.choice_title}>{t('backup.restoreKeystore')}</span>
+                    <p>
+                      {t('backup.restoreKeystoreDescription')}
+                    </p>
+                  </div>
+                </label>
+              </div>
+
+              <div className={styles.choice_wrapper}>
+                <label>
+                  <input className={styles.choice_radio_button}
+                         type='radio'
+                         name='type'
+                         value='restoreMnemonic' />
                   <div className={`${styles.choice} ${styles.restore}`}>
-                    <span className={styles.choice_title}>{t('backup.restore')}</span>
+                    <span className={styles.choice_title}>{t('backup.restoreMnemonic')}</span>
                     <p>
-                      {t('backup.restoreDescription')}
+                      {t('backup.restoreMnemonicDescription')}
                     </p>
                   </div>
                 </label>
@@ -125,13 +115,17 @@ class Backup extends React.Component {
 
               <div>
                 {
-                  this.state.value === 'restore'
+                  this.state.value === 'restoreKeystore'
                   &&
-                    <span className={styles.submitWrapper}>{restoreButton}</span>
+                    <span className={styles.submitWrapper}>{restoreKeystoreButton}</span>
+                }
+              </div>
+
+              <div>
+                {
+                  this.state.value === 'restoreMnemonic'
+                  &&  <span className={styles.submitWrapper}>{restoreMnemonicButton}</span>
                 }
-                <input id='bytom-restore-file-upload' type='file'
-                       style={{'display': 'none', 'alignItems': 'center', 'fontSize': '12px'}}
-                       onChange={this.handleFileChange.bind(this)}/>
               </div>
             </div>
           </div>
@@ -142,15 +136,19 @@ class Backup extends React.Component {
   }
 }
 
-const mapStateToProps = (state) => ({
-  core: state.core,
-  navAdvancedState: state.app.navAdvancedState,
-})
+const mapStateToProps = () => ({})
 
 const mapDispatchToProps = (dispatch) => ({
   backup: () => dispatch(actions.backUp.backup()),
-  rescan: () => dispatch(actions.backUp.rescan()),
-  restoreFile: (backUpFile) => dispatch(actions.backUp.restore(backUpFile)),
+  success: () => dispatch(actions.backUp.success()),
+  showModal: (body) => dispatch(actions.app.showModal(
+    body,
+    actions.app.hideModal,
+    null,
+    {
+      noCloseBtn: true
+    }
+  ))
 })
 
 export default connect(
index aeafa5d..2309129 100644 (file)
@@ -13,7 +13,7 @@ code {
   justify-content: space-between;
 
   > div {
-    width: 45%;
+    width: 30%;
     min-height: 100%;
   }
 }
index e7156bc..ff4f0b2 100644 (file)
@@ -40,50 +40,6 @@ const fetchCoreInfo = (options = {}) => {
   }
 }
 
-//todo: change the function later
-const registerKey = (data) => {
-  return (dispatch) => {
-    if (typeof data.keyAlias == 'string')  data.keyAlias = data.keyAlias.trim()
-
-    const keyData = {
-      'alias': data.keyAlias,
-      'password': data.password
-    }
-    return chainClient().mockHsm.keys.create(keyData)
-      .then((resp) => {
-        if (resp.status === 'fail') {
-          throw resp
-        }
-
-        if (typeof data.accountAlias == 'string')  data.accountAlias = data.accountAlias.trim()
-        const accountData = {
-          'root_xpubs':[resp.data.xpub],
-          'quorum':1,
-          'alias': data.accountAlias}
-
-        chainClient().accounts.create(accountData)
-          .then((resp) => {
-            if (resp.status === 'fail') {
-              throw resp
-            }
-
-            if(resp.status === 'success') {
-              dispatch({type: 'CREATE_REGISTER_ACCOUNT', resp})
-            }
-          })
-          .catch((err) => {
-            if (!err.status) {
-              throw err
-            }
-          })
-      })
-      .catch((err) => {
-        if (!err.status) {
-          throw err
-        }
-      })
-  }
-}
 
 let actions = {
   setClientToken,
@@ -93,7 +49,6 @@ let actions = {
   updateMiningState,
   fetchCoreInfo,
   clearSession,
-  registerKey,
   logIn: (token) => (dispatch) => {
     dispatch(setClientToken(token))
     return dispatch(fetchCoreInfo({throw: true}))
diff --git a/src/features/initialization/actions.js b/src/features/initialization/actions.js
new file mode 100644 (file)
index 0000000..803ba41
--- /dev/null
@@ -0,0 +1,128 @@
+import { chainClient } from 'utility/environment'
+import {push} from 'react-router-redux'
+
+const registerKey = (data) => {
+  return (dispatch) => {
+    if (typeof data.keyAlias == 'string')  data.keyAlias = data.keyAlias.trim()
+
+    const keyData = {
+      'alias': data.keyAlias,
+      'password': data.password
+    }
+
+    return chainClient().mockHsm.keys.create(keyData)
+      .then((resp) => {
+        if (resp.status === 'fail') {
+          throw resp
+        }
+
+        if (typeof data.accountAlias == 'string')  data.accountAlias = data.accountAlias.trim()
+        const accountData = {
+          'root_xpubs':[resp.data.xpub],
+          'quorum':1,
+          'alias': data.accountAlias}
+
+        dispatch({type: 'INIT_ACCOUNT', data: resp.data.mnemonic})
+
+        chainClient().accounts.create(accountData)
+          .then((resp) => {
+            if (resp.status === 'fail') {
+              throw resp
+            }
+
+            if(resp.status === 'success') {
+              dispatch(push('/initialization/mnemonic'))
+            }
+          })
+          .catch((err) => {
+            throw err
+          })
+      })
+      .catch((err) => {
+        throw err
+      })
+  }
+}
+
+const restoreKeystore = (data, success) => {
+  return (dispatch) => {
+    const file = data.file
+
+    return new Promise(function(resolve, reject){
+      const fileReader = new FileReader()
+
+      fileReader.onload = function(e) {
+        const result = JSON.parse(e.target.result)
+        return chainClient().backUp.restore(result)
+          .then(resp => {
+            if (resp.status === 'fail') {
+              throw resp
+            }
+            resolve()
+            dispatch(success)
+          })
+          .catch((err) => {
+            reject(err) })
+      }
+
+      fileReader.readAsText(file, 'UTF-8')
+      fileReader.onerror = function(error) { reject(error) }
+    })
+  }
+}
+
+const restoreMnemonic = (data, success) => {
+  return (dispatch) => {
+    if (typeof data.keyAlias == 'string')  data.keyAlias = data.keyAlias.trim()
+    if (typeof data.mnemonic == 'string') data.mnemonic = data.mnemonic.trim()
+
+    const keyData = {
+      'alias': data.keyAlias,
+      'password': data.password,
+      'mnemonic': data.mnemonic
+    }
+
+    return chainClient().mockHsm.keys.create(keyData)
+      .then((resp) => {
+        if (resp.status === 'fail') {
+          throw resp
+        }else{
+          return chainClient().backUp.recovery({
+            xpub: resp.data.xpub
+          })
+            .then((resp) => {
+              if (resp.status === 'fail') {
+                throw resp
+              }
+
+              dispatch(success)
+            })
+            .catch((err) => {
+              throw err
+            })
+        }
+      })
+      .catch((err) => {
+        throw err
+      })
+  }
+}
+
+const initSucceeded = () => (dispatch) => {
+  dispatch({type: 'CREATE_REGISTER_ACCOUNT'})
+  dispatch(push({
+    pathname: '/transactions',
+    state: {
+      preserveFlash: true
+    }
+  }))
+}
+
+let actions = {
+  initSucceeded,
+  registerKey,
+  restoreKeystore,
+  restoreMnemonic
+}
+
+export default actions
   overflow: auto;
 }
 
-.choice {
-  margin-left: 10px;
-}
-
-.image {
-  width: 150px;
-  position: absolute;
-  top: calc(50px);
-  left: calc(50% - 75px);
-}
-
 .title{
   text-align: center;
   color: white;
 }
 
-.switch {
-  display: flex;
-  justify-content: space-around;
-
-  margin-top: 10px;
-}
-
 .formWarpper {
   display: flex;
   justify-content: space-around;
diff --git a/src/features/initialization/components/Index/Index.jsx b/src/features/initialization/components/Index/Index.jsx
new file mode 100644 (file)
index 0000000..f3416b8
--- /dev/null
@@ -0,0 +1,117 @@
+import React from 'react'
+import styles from './Index.scss'
+import { Link } from 'react-router'
+import {withNamespaces} from 'react-i18next'
+import {connect} from 'react-redux'
+
+class Index extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = {
+      value: null
+    }
+  }
+
+
+  setValue(event) {
+    this.setState({
+      value:event.target.value
+    })
+  }
+
+  render() {
+    const coreData = this.props.coreData
+    const t = this.props.t
+    if(!coreData){
+      return <ul></ul>
+    }
+
+    const networkID = coreData.networkId
+    const createButton = <Link className={`btn btn-primary ${styles.submit}`} to='/initialization/register'>{t('init.create')}</Link>
+    const restoreKeystore = <Link className={`btn btn-primary ${styles.submit}`}to='/initialization/restoreKeystore'>{t('init.restoreWallet')}</Link>
+    const restoreMnemonic = <Link className={`btn btn-primary ${styles.submit}`} to='/initialization/restoreMnemonic'>{t('init.restoreWallet')}</Link>
+
+    return (
+      <div onChange={e => this.setValue(e)}>
+        <h2 className={styles.title}>{t('init.welcome',{network:networkID})}</h2>
+
+        <div className={styles.choices}>
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                     type='radio'
+                     name='type'
+                     value='backup'/>
+              <div className={`${styles.choice} ${styles.backup} `}>
+                <span className={styles.choice_title}>{t('init.create')}</span>
+                <p>
+                  {t('init.createDescription')}
+                </p>
+              </div>
+            </label>
+          </div>
+
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                     type='radio'
+                     name='type'
+                     value='restoreKeystore' />
+              <div className={`${styles.choice} ${styles.restore}`}>
+                <span className={styles.choice_title}>{t('backup.restoreKeystore')}</span>
+                <p>
+                  {t('backup.restoreKeystoreDescription')}
+                </p>
+              </div>
+            </label>
+          </div>
+
+          <div className={styles.choice_wrapper}>
+            <label>
+              <input className={styles.choice_radio_button}
+                     type='radio'
+                     name='type'
+                     value='restoreMnemonic' />
+              <div className={`${styles.choice} ${styles.restore}`}>
+                <span className={styles.choice_title}>{t('backup.restoreMnemonic')}</span>
+                <p>
+                  {t('backup.restoreMnemonicDescription')}
+                </p>
+              </div>
+            </label>
+          </div>
+        </div>
+
+        <div className={styles.choices}>
+          <div>
+            {
+              this.state.value === 'backup'
+              &&<span className={styles.submitWrapper}>{createButton}</span>
+            }
+          </div>
+
+          <div>
+            {
+              this.state.value === 'restoreKeystore'
+              &&
+              <span className={styles.submitWrapper}>{restoreKeystore}</span>
+            }
+          </div>
+
+          <div>
+            {
+              this.state.value === 'restoreMnemonic'
+              &&  <span className={styles.submitWrapper}>{restoreMnemonic}</span>
+            }
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default withNamespaces('translations') (connect(
+  (state) => ({
+    coreData:state.core.coreData
+  }),
+)(Index))
diff --git a/src/features/initialization/components/Index/Index.scss b/src/features/initialization/components/Index/Index.scss
new file mode 100644 (file)
index 0000000..68a8c55
--- /dev/null
@@ -0,0 +1,155 @@
+.main {
+  background: $background-inverse-color;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding-top: 50px;
+  display: block;
+  overflow: auto;
+}
+
+.title {
+  font-size: $font-size-h2;
+  font-weight: normal;
+  margin-top: $gutter-size;
+  margin-bottom: $gutter-size;
+  text-align: center;
+  text-transform: uppercase;
+}
+
+.choices {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+
+  > div {
+    width: 30%;
+    min-height: 100%;
+  }
+}
+
+.choice_wrapper {
+  display: flex;
+  min-height: 100%;
+
+  > label {
+    display: flex;
+    min-height:100%;
+    font-weight: normal;
+  }
+}
+
+.choice_radio_button {
+  position: absolute;
+  visibility: hidden;
+}
+
+.choice {
+  border: 1px solid $table-border-color;
+  border-radius: $border-radius-base;
+  cursor: pointer;
+  min-height: 100%;
+  padding: 20px $grid-gutter-width;
+  padding-top: 110px;
+  text-align: center;
+
+  background-repeat: no-repeat;
+  background-position: center 25px;
+  background-size: 90px 90px;
+
+  &:hover {
+    background-color: $background-emphasis-color;
+  }
+
+  &.disabled {
+    cursor: default;
+    background-color: $background-emphasis-color;
+    opacity: 0.75;
+
+    .choice_title {
+      color: $text-light-color;
+    }
+  }
+
+  p {
+    line-height: 1.4;
+  }
+
+  svg {
+    display: block;
+    margin: 0 auto;
+    width: 80px;
+    height: 80px;
+  }
+
+  .choice_title{
+    display: block;
+    font-size: $font-size-section-title;
+    margin: 12px 0;
+    color: $text-strong-color;
+    font-weight: 600;
+  }
+}
+
+.backup {
+  background-image: url('images/backup/backup.svg')
+}
+
+.restore {
+  background-image: url('images/backup/restore.svg')
+}
+
+input[type=radio]:checked ~ .choice {
+  strong {
+    color: $text-danger;
+  }
+
+  &:hover {
+    background-color: $background-color;
+  }
+
+  &.backup {
+    background-image: url('images/backup/backup-active.svg')
+  }
+
+  &.restore {
+    background-image: url('images/backup/restore-active.svg')
+  }
+
+  .choice_title {
+    color: $brand-primary;
+  }
+
+  svg {
+    polygon {
+      stroke: $brand-primary;
+      fill: transparentize($brand-primary, 0.85);
+    }
+
+    rect, path {
+      fill: $brand-primary
+    }
+  }
+}
+
+.submitWrapper {
+  display: block;
+  margin-top: 30px;
+}
+
+.submit {
+  display: block;
+  width: 100%;
+  margin-top: $gutter-size;
+  padding:5px 0px;
+}
+
+.infoLink {
+  position: relative;
+  left: 4px;
+  top: 1px;
+  color:$text-light-color;
+}
+
diff --git a/src/features/initialization/components/Keystore/Keystore.jsx b/src/features/initialization/components/Keystore/Keystore.jsx
new file mode 100644 (file)
index 0000000..09e2e79
--- /dev/null
@@ -0,0 +1,43 @@
+import React from 'react'
+import {RestoreKeystore} from 'features/shared/components'
+import {withNamespaces} from 'react-i18next'
+import { Link } from 'react-router'
+import {connect} from 'react-redux'
+import actions from 'actions'
+import styles from '../FormIndex.scss'
+
+class Keystore extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    const t = this.props.t
+
+    return (
+      <div className={styles.main}>
+        <div>
+          <h2 className={styles.title}>{t('backup.restoreKeystore')}</h2>
+          <div className={styles.formWarpper}>
+            <div className={styles.form}>
+              <RestoreKeystore success={this.props.success}/>
+              <Link
+                className='btn btn-link'
+                to='/initialization/'>
+                {t('commonWords.cancel')}
+              </Link>
+            </div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+
+export default withNamespaces('translations') (connect(
+  () => ({}),
+  (dispatch) => ({
+    success: () => dispatch(actions.initialization.initSucceeded()),
+  })
+)(Keystore))
diff --git a/src/features/initialization/components/Mnemonic/Mnemonic.jsx b/src/features/initialization/components/Mnemonic/Mnemonic.jsx
new file mode 100644 (file)
index 0000000..4e67b63
--- /dev/null
@@ -0,0 +1,43 @@
+import React from 'react'
+import {RestoreMnemonic} from 'features/shared/components'
+import {withNamespaces} from 'react-i18next'
+import { Link } from 'react-router'
+import {connect} from 'react-redux'
+import actions from 'actions'
+import styles from '../FormIndex.scss'
+
+class Mnemonic extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    const t = this.props.t
+    return (
+      <div className={styles.main}>
+        <div>
+          <h2 className={styles.title}>{t('backup.restoreMnemonic')}</h2>
+          <div className={styles.formWarpper}>
+            <div className={styles.form}>
+              <RestoreMnemonic success={this.props.success}/>
+              <Link
+                className='btn btn-link'
+                to='/initialization/'>
+                {t('commonWords.cancel')}
+              </Link>
+            </div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+
+
+export default withNamespaces('translations') (connect(
+  () => ({}),
+  (dispatch) => ({
+    success: () => dispatch(actions.initialization.initSucceeded()),
+  })
+)(Mnemonic))
diff --git a/src/features/initialization/components/MnemonicStepper/MnemonicStepper.jsx b/src/features/initialization/components/MnemonicStepper/MnemonicStepper.jsx
new file mode 100644 (file)
index 0000000..c649501
--- /dev/null
@@ -0,0 +1,63 @@
+import React from 'react'
+import {connect} from 'react-redux'
+import actions from 'actions'
+import { Step, StepList, ConfirmMnemonic, Mnemonic } from 'features/shared/components'
+import styles from './MnemonicStepper.scss'
+import {withNamespaces} from 'react-i18next'
+
+class MnemonicStepper extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    const t = this.props.t
+    return (
+      <div className={styles.main}>
+        <div>
+          <h2 className={styles.title}>{t('mnemonic.backup')}</h2>
+          <div className={styles.formWarpper}>
+            <div className={styles.form}>
+              <StepList>
+                <Step
+                  nextL={t('mnemonic.continue')}
+                >
+                  <Mnemonic
+                    mnemonic={this.props.mnemonic}
+                  />
+                  <button
+                    className={'btn btn-link'}
+                    onClick={() => this.props.succeeded()}
+                  >
+                    {t('mnemonic.skip')}
+                  </button>
+                </Step>
+                <Step>
+                  <ConfirmMnemonic
+                    mnemonic={this.props.mnemonic}
+                    succeeded={this.props.succeeded}
+                  />
+                </Step>
+              </StepList>
+            </div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  const mnemonic = (state.initialization || {}).mnemonic || []
+  if (mnemonic) return {mnemonic}
+  return {}
+}
+
+const mapDispatchToProps = ( dispatch ) => ({
+  succeeded: () => dispatch(actions.initialization.initSucceeded()),
+})
+
+export default withNamespaces('translations') (connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(MnemonicStepper))
diff --git a/src/features/initialization/components/MnemonicStepper/MnemonicStepper.scss b/src/features/initialization/components/MnemonicStepper/MnemonicStepper.scss
new file mode 100644 (file)
index 0000000..4b52af6
--- /dev/null
@@ -0,0 +1,31 @@
+.main {
+  background: $background-inverse-color;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding-top: 50px;
+  display: block;
+  overflow: auto;
+}
+
+.title{
+  text-align: center;
+  color: white;
+}
+
+.formWarpper {
+  display: flex;
+  justify-content: space-around;
+
+  margin-top: 30px;
+}
+
+.form {
+  background: $background-color;
+  border-radius: $border-radius-standard;
+  width: 850px;
+  padding: 30px;
+  padding-bottom: $gutter-size*1.25;
+}
diff --git a/src/features/initialization/components/Register/Register.jsx b/src/features/initialization/components/Register/Register.jsx
new file mode 100644 (file)
index 0000000..0719339
--- /dev/null
@@ -0,0 +1,108 @@
+import React from 'react'
+import {connect} from 'react-redux'
+import {ErrorBanner, TextField, PasswordField} from 'features/shared/components'
+import actions from 'actions'
+import { Link } from 'react-router'
+import styles from '../FormIndex.scss'
+import {reduxForm} from 'redux-form'
+import {withNamespaces} from 'react-i18next'
+
+class Register extends React.Component {
+  constructor(props) {
+    super(props)
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.registerKey(data)
+        .catch((err) => reject({_error: err.message}))
+    })
+  }
+
+  render() {
+    const t = this.props.t
+
+    const {
+      fields: {keyAlias, password, repeatPassword, accountAlias},
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+    return (
+      <div className={styles.main}>
+        <div>
+          <h2 className={styles.title}>{t('init.title')}</h2>
+          <div className={styles.formWarpper}>
+            <form className={styles.form} onSubmit={handleSubmit(this.submitWithErrors)}>
+              <TextField
+                title={t('form.accountAlias')}
+                placeholder={t('init.accountPlaceholder')}
+                fieldProps={accountAlias} />
+              <TextField
+                title={t('form.keyAlias')}
+                placeholder={t('init.keyPlaceholder')}
+                fieldProps={keyAlias}/>
+              <PasswordField
+                title={t('init.keyPassword')}
+                placeholder={t('init.passwordPlaceholder')}
+                fieldProps={password} />
+              <PasswordField
+                title={t('init.repeatKeyPassword')}
+                placeholder={t('init.repeatPlaceHolder')}
+                fieldProps={repeatPassword} />
+
+              {error &&
+              <ErrorBanner
+                title={t('init.errorTitle')}
+                error={error}/>}
+
+              <button type='submit' className='btn btn-primary' disabled={submitting}>
+                {t('init.register')}
+              </button>
+              <Link
+                className='btn btn-link'
+                to='/initialization/'>
+                {t('commonWords.cancel')}
+              </Link>
+            </form>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+const validate = (values, props) => {
+  const errors = {}
+  const t = props.t
+
+  if (!values.keyAlias) {
+    errors.keyAlias = t('key.aliasRequired')
+  }
+  if (!values.password) {
+    errors.password = t('key.passwordRequired')
+  } else if (values.password.length < 5) {
+    errors.password = ( t('key.reset.newPWarning') )
+  }
+  if (values.password !== values.repeatPassword) {
+    errors.repeatPassword = ( t('key.reset.repeatPWarning') )
+  }
+  if (!values.accountAlias) {
+    errors.accountAlias = ( t('account.new.aliasWarning') )
+  }
+
+  return errors
+}
+
+export default withNamespaces('translations')( connect(
+  () => ({}),
+  (dispatch) => ({
+    registerKey: (token) => dispatch(actions.initialization.registerKey(token))
+  })
+)(reduxForm({
+  form: 'initDefaultPassword',
+  fields: ['keyAlias', 'password', 'repeatPassword', 'accountAlias'],
+  validate
+})(Register)))
diff --git a/src/features/initialization/components/index.js b/src/features/initialization/components/index.js
new file mode 100644 (file)
index 0000000..a5f7dba
--- /dev/null
@@ -0,0 +1,13 @@
+import Index from './Index/Index'
+import Register from './Register/Register'
+import MnemonicStepper from './MnemonicStepper/MnemonicStepper'
+import Mnemonic from './Mnemonic/Mnemonic'
+import Keystore from './Keystore/Keystore'
+
+export {
+  Index,
+  Register,
+  Mnemonic,
+  Keystore,
+  MnemonicStepper,
+}
diff --git a/src/features/initialization/index.js b/src/features/initialization/index.js
new file mode 100644 (file)
index 0000000..f4fbe4d
--- /dev/null
@@ -0,0 +1,9 @@
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+  actions,
+  reducers,
+  routes
+}
diff --git a/src/features/initialization/reducers.js b/src/features/initialization/reducers.js
new file mode 100644 (file)
index 0000000..e39ceb2
--- /dev/null
@@ -0,0 +1,12 @@
+import { combineReducers } from 'redux'
+
+const mnemonic = (state = [], action) => {
+  if (action.type == 'INIT_ACCOUNT') {
+    return action.data
+  }
+  return state
+}
+
+export default combineReducers({
+  mnemonic
+})
diff --git a/src/features/initialization/routes.js b/src/features/initialization/routes.js
new file mode 100644 (file)
index 0000000..cedcb24
--- /dev/null
@@ -0,0 +1,30 @@
+import { RoutingContainer } from 'features/shared/components'
+import { Index, Register, Restore, Mnemonic, Keystore, MnemonicStepper } from './components'
+
+export default {
+  path: 'initialization',
+  component: RoutingContainer,
+  indexRoute: { component: Index },
+  childRoutes: [
+    {
+      path: 'register',
+      component: Register
+    },
+    {
+      path: 'mnemonic',
+      component: MnemonicStepper
+    },
+    {
+      path: 'restore',
+      component: Restore
+    },
+    {
+      path: 'restoreMnemonic',
+      component: Mnemonic
+    },
+    {
+      path: 'restoreKeystore',
+      component: Keystore
+    }
+  ]
+}
index 0d887d9..4a2fd83 100644 (file)
@@ -14,6 +14,20 @@ const create = baseCreateActions(type, {
   clientApi,
 })
 
+create.submitForm = (data) => function (dispatch) {
+  if (typeof data.alias == 'string')  data.alias = data.alias.trim()
+
+  return clientApi().create(data)
+    .then((resp) => {
+      if (resp.status === 'fail') {
+        throw resp
+      }
+
+      dispatch({type: 'NEW_KEY', data: resp.data.mnemonic})
+      dispatch( push('/keys/mnemonic') )
+    })
+}
+
 const resetPassword = {
   submitResetForm: (params) => {
     let promise = Promise.resolve()
@@ -66,10 +80,16 @@ const createExport =  (arg, fileName) => (dispatch) => {
   })
 }
 
+const createSuccess = ()=> (dispatch) =>{
+  dispatch(create.created())
+  dispatch(push('/keys'))
+}
+
 export default {
   ...create,
   ...list,
   ...resetPassword,
   checkPassword,
-  createExport
+  createExport,
+  createSuccess
 }
index 1b84b8d..f51fd3e 100644 (file)
@@ -1,7 +1,7 @@
 import React, {Component} from 'react'
 import { reduxForm } from 'redux-form'
 
-import { FormContainer, FormSection, PasswordField} from 'features/shared/components'
+import {NotFound, FormContainer, FormSection, PasswordField} from 'features/shared/components'
 
 class CheckPassword extends Component {
   constructor(props) {
diff --git a/src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.jsx b/src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.jsx
new file mode 100644 (file)
index 0000000..4cfb0a4
--- /dev/null
@@ -0,0 +1,68 @@
+import React from 'react'
+import {connect} from 'react-redux'
+import actions from 'actions'
+import { NotFound, Step, StepList, ConfirmMnemonic, Mnemonic, PageContent, PageTitle } from 'features/shared/components'
+import componentClassNames from 'utility/componentClassNames'
+import styles from './MnemonicStepper.scss'
+import {withNamespaces} from 'react-i18next'
+
+class MnemonicStepper extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    if (this.props.mnemonic.length === 0) {
+      return <NotFound />
+    }
+    const t = this.props.t
+
+    return (
+      <div className={componentClassNames(this, 'flex-container')}>
+        <PageTitle title={t('mnemonic.backup')} />
+
+          <div className={`${styles.main} flex-container`}>
+            <div className={styles.content}>
+              <StepList>
+                <Step
+                nextL={t('mnemonic.continue')}
+                >
+                  <Mnemonic
+                    mnemonic={this.props.mnemonic}
+                  />
+                  <button className={`btn btn-default ${styles.marginLeft}`}
+                          onClick={() => this.props.succeeded()}
+                  >
+                    {t('mnemonic.skip')}
+                  </button>
+                </Step>
+
+                <Step>
+                  <ConfirmMnemonic
+                    mnemonic={this.props.mnemonic}
+                    succeeded={this.props.succeeded}
+                  />
+
+                </Step>
+              </StepList>
+            </div>
+          </div>
+      </div>
+    )
+  }
+}
+
+const mapStateToProps = (state) => {
+  const mnemonic = (state.key || {}).mnemonic || []
+  if (mnemonic) return {mnemonic}
+  return {}
+}
+
+const mapDispatchToProps = ( dispatch ) => ({
+  succeeded: () => dispatch(actions.key.createSuccess()),
+})
+
+export default withNamespaces('translations') (connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(MnemonicStepper))
diff --git a/src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.scss b/src/features/mockhsm/components/MnemonicStepper/MnemonicStepper.scss
new file mode 100644 (file)
index 0000000..3af3fe9
--- /dev/null
@@ -0,0 +1,20 @@
+.main {
+  background: $background-color;
+  display: flex;
+  flex-direction: row;
+  padding: 0 $gutter-size;
+  margin-top: $title-height;
+}
+
+.content {
+  min-width: 400px;
+  max-width: 900px;
+  width: 100%;
+  margin: 0 auto;
+  padding: $gutter-size;
+}
+
+
+.marginLeft{
+  margin-left: $gutter-size/2;
+}
\ No newline at end of file
index 72b69e0..5f81eda 100644 (file)
@@ -1,5 +1,6 @@
 import List from './List'
 import New from './New'
+import MnemonicStepper from './MnemonicStepper/MnemonicStepper'
 import Show from './Show'
 import ResetPassword from './ResetPassword/ResetPassword'
 import CheckPassword from './CheckPassword/CheckPassword'
@@ -7,7 +8,8 @@ import CheckPassword from './CheckPassword/CheckPassword'
 export {
   List,
   New,
+  MnemonicStepper,
   Show,
   ResetPassword,
-  CheckPassword
+  CheckPassword,
 }
index 92563db..72628f1 100644 (file)
@@ -17,4 +17,10 @@ export default combineReducers({
     }
     return state
   },
+  mnemonic: (state = [], action) => {
+    if (action.type == 'NEW_KEY') {
+      return action.data
+    }
+    return state
+  },
 })
index 0fab932..810059c 100644 (file)
@@ -1,4 +1,4 @@
-import { List, New, Show, ResetPassword, CheckPassword } from './components'
+import { List, New, Show, ResetPassword, CheckPassword, MnemonicStepper } from './components'
 import { makeRoutes } from 'features/shared'
 
 export default (store) => makeRoutes(store, 'key', List, New, Show,
@@ -12,4 +12,8 @@ export default (store) => makeRoutes(store, 'key', List, New, Show,
         path: ':id/check-password',
         component: CheckPassword,
       },
+      {
+        path: 'mnemonic',
+        component: MnemonicStepper
+      }
     ],skipFilter: true, name: 'key' })
\ No newline at end of file
diff --git a/src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.jsx b/src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.jsx
new file mode 100644 (file)
index 0000000..d2eb5ca
--- /dev/null
@@ -0,0 +1,119 @@
+import React from 'react'
+import { ErrorBanner, SingletonField} from 'features/shared/components'
+import {reduxForm} from 'redux-form'
+import style from './ConfirmMnemonic.scss'
+import {withNamespaces} from 'react-i18next'
+
+class ConfirmMnemonic extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state = this.getInitialState()
+
+    this.submitWithValidation = this.submitWithValidation.bind(this)
+  }
+
+  getInitialState() {
+    let seedWords = []
+    let randomThreshold = 0.3
+    let splitMnemonic = this.props.mnemonic.split(' ')
+    for (let i = 0; i < splitMnemonic.length; i++) {
+      let hideWord = Math.random()
+      seedWords.push({
+        word: hideWord > randomThreshold ? splitMnemonic[i] : '',
+        show: hideWord > randomThreshold,
+        index: i,
+      })
+      if(hideWord<= randomThreshold){
+        this.props.fields.words.addField({
+          seedIndex: i
+        })
+      }
+    }
+    return {
+      seedWords: seedWords,
+      splitMnemoic: splitMnemonic,
+    }
+  }
+
+  submitWithValidation(data) {
+    for(let word of data.words){
+      if(word.value.trim() !== this.state.splitMnemoic[word.seedIndex]){
+        return new Promise((_, reject) => reject({
+          _error: 'please match the word'
+        }))
+      }
+    }
+
+    return new Promise((resolve, reject) => {
+      this.props.succeeded()
+        .catch((err) => reject({type: err}))
+    })
+  }
+
+  render() {
+    const {
+      fields: {words},
+      error,
+      handleSubmit,
+      submitting,
+      t
+    } = this.props
+
+    const { seedWords } = this.state
+    let counter = 0
+
+    return (
+      <form  onSubmit={handleSubmit(this.submitWithValidation)}>
+        <h4>{t('mnemonic.confirmTitle')}</h4>
+        <p>{t('mnemonic.confirmMessage')}</p>
+        <div className={style.seedArea}>
+          {seedWords.map((seedWord) => {
+            return ( seedWord.show ?
+              <div key={seedWord.index} className={`${style.seed} ${style.seedWord}`}>{seedWord.word}</div> :
+              (words[counter]? <SingletonField
+                className={style.seedWord}
+                key={seedWord.index}
+                fieldProps={ words[counter++].value }
+              /> : null)
+            )
+          })}
+        </div>
+
+        {error&& <ErrorBanner error={error} />}
+
+        <button
+          className={`btn btn-primary ${style.submit}`}
+          type='submit'
+          disabled={submitting}>
+          {t('mnemonic.confirm')}
+        </button>
+
+      </form>
+    )
+  }
+}
+
+const validate = (values, props) => {
+  const errors = {words: {}}
+  const splitMnemonic = props.mnemonic.split(' ')
+
+  // Actions
+  let numError
+  values.words.forEach((word, index) => {
+    const seedIndex = values.words[index].seedIndex
+    numError = values.words[index].value !== splitMnemonic[seedIndex]
+    if (numError) {
+      errors.words[index] = {...errors.words[index], value:' '}
+    }
+  })
+  return errors
+}
+
+export default  withNamespaces('translations') (reduxForm({
+  form: 'MnemonicInit',
+  fields: [
+    'words[].value',
+    'words[].seedIndex'
+  ],
+  validate
+})(ConfirmMnemonic))
diff --git a/src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.scss b/src/features/shared/components/ConfirmMnemonic/ConfirmMnemonic.scss
new file mode 100644 (file)
index 0000000..ee773ba
--- /dev/null
@@ -0,0 +1,25 @@
+.submit{
+  float: left;
+}
+
+.seed{
+  user-select: none;
+  background-color: #F8F8F8;
+  border-radius: 3px;
+  border: 1px solid $border-color;
+  padding: 6px 0px;
+  color: $text-strong-color;
+}
+
+.seedWord{
+  width: 121px;
+  height: 36px;
+  margin: 5px 10px 5px 0;
+  text-align: center;
+}
+
+.seedArea{
+  display: flex;
+  flex-wrap: wrap;
+  margin-bottom: $gutter-size/2;
+}
diff --git a/src/features/shared/components/FileField.jsx b/src/features/shared/components/FileField.jsx
new file mode 100644 (file)
index 0000000..4e342d3
--- /dev/null
@@ -0,0 +1,30 @@
+import React from 'react'
+import { FieldLabel } from 'features/shared/components'
+
+class FileField extends React.Component {
+  constructor(props) {
+    super(props)
+    this.onChange = this.onChange.bind(this)
+  }
+
+  onChange(e) {
+    const { fieldProps: { onChange } } = this.props
+    onChange(e.target.files[0])
+  }
+
+  render() {
+
+    return(
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <input
+          type='file'
+          onChange={this.onChange}
+        />
+        {this.props.hint && <span className='help-block'>{this.props.hint}</span>}
+      </div>
+    )
+  }
+}
+
+export default FileField
\ No newline at end of file
index 5f0bad6..9e4c536 100644 (file)
@@ -1,6 +1,5 @@
 .main {
   background: $background-color;
-  padding: 0;
   display: flex;
   flex-direction: row;
   padding: 0 $gutter-size;
diff --git a/src/features/shared/components/Mnemonic/Mnemonic.jsx b/src/features/shared/components/Mnemonic/Mnemonic.jsx
new file mode 100644 (file)
index 0000000..06dbecf
--- /dev/null
@@ -0,0 +1,42 @@
+import React from 'react'
+import { copyToClipboard } from 'utility/clipboard'
+import styles from './Mnemonic.scss'
+import {withNamespaces} from 'react-i18next'
+
+class Mnemonic extends React.Component {
+  constructor(props) {
+    super(props)
+    this.state={
+      mnemonicArray : this.props.mnemonic.split(' ')
+    }
+  }
+
+  render() {
+    const t = this.props.t
+    const {mnemonicArray} = this.state
+    return (
+     <div>
+       <div className={styles.flexContainer}>
+         <h4>{t('init.mnemonic')}</h4>
+         <button
+           className='btn btn-link'
+           onClick={() => copyToClipboard(this.props.mnemonic)}
+         >
+           <img className={styles.copy} src={require('images/copy.svg')}/>
+         </button>
+       </div>
+       <p>{t('mnemonic.backupMessage')}</p>
+       <div className={`${styles.flexContainer} ${styles.seedArea}`}>
+
+         {
+           mnemonicArray.map((seedWord) =>
+             <div className={styles.seed}>{seedWord}</div>)
+         }
+       </div>
+
+     </div>
+    )
+  }
+}
+
+export default withNamespaces('translations') (Mnemonic)
diff --git a/src/features/shared/components/Mnemonic/Mnemonic.scss b/src/features/shared/components/Mnemonic/Mnemonic.scss
new file mode 100644 (file)
index 0000000..7da82a8
--- /dev/null
@@ -0,0 +1,27 @@
+.seed{
+  user-select: none;
+  width: 121px;
+  height: 36px;
+  background-color: #F8F8F8;
+  border: 1px solid $border-color;
+  border-radius: 3px;
+  color: $text-strong-color;
+  margin: 5px 10px 5px 0;
+  padding: 6px 0px;
+  float: left;
+  text-align: center;
+}
+
+.copy{
+  width: 18px;
+  margin-bottom: 3px;
+}
+
+.flexContainer{
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.seedArea{
+  margin-bottom : $gutter-size/2;
+}
diff --git a/src/features/shared/components/RestoreKeystore/RestoreKeystore.jsx b/src/features/shared/components/RestoreKeystore/RestoreKeystore.jsx
new file mode 100644 (file)
index 0000000..5ee53d2
--- /dev/null
@@ -0,0 +1,71 @@
+import React from 'react'
+import {connect} from 'react-redux'
+import {ErrorBanner, FileField} from 'features/shared/components'
+import actions from 'actions'
+import {reduxForm} from 'redux-form'
+import {withNamespaces} from 'react-i18next'
+import style from './RestoreKeystore.scss'
+
+class RestoreKeystore extends React.Component {
+  constructor(props) {
+    super(props)
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.restoreKeystore(data, this.props.success)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const t = this.props.t
+
+    const {
+      fields: {file},
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+
+    return (
+      <div >
+        <div>
+          <h4 >{t('init.restoreWallet')}</h4>
+          <div>
+            <form onSubmit={handleSubmit(this.submitWithErrors)}>
+              <p>{t('init.restoreLabel')}</p>
+
+              <FileField
+                fieldProps={file}
+              />
+
+              {error &&
+              <ErrorBanner
+                title={t('init.errorTitle')}
+                error={error}/>}
+
+              <button type='submit' className={`btn btn-primary ${style.submitButton}`} disabled={submitting}>
+                {t('init.restore')}
+              </button>
+
+            </form>
+          </div>
+        </div>
+      </div>
+    )
+  }
+}
+
+
+export default withNamespaces('translations')( connect(
+  () => ({}),
+  (dispatch) => ({
+    restoreKeystore: (token, success) => dispatch(actions.initialization.restoreKeystore(token, success)),
+  })
+)(reduxForm({
+  form: 'restoreKeystore',
+  fields: ['file'],
+})(RestoreKeystore)))
diff --git a/src/features/shared/components/RestoreKeystore/RestoreKeystore.scss b/src/features/shared/components/RestoreKeystore/RestoreKeystore.scss
new file mode 100644 (file)
index 0000000..2a8c16f
--- /dev/null
@@ -0,0 +1,3 @@
+.submitButton{
+  float: left;
+}
diff --git a/src/features/shared/components/RestoreMnemonic/RestoreMnemonic.jsx b/src/features/shared/components/RestoreMnemonic/RestoreMnemonic.jsx
new file mode 100644 (file)
index 0000000..40ec7dd
--- /dev/null
@@ -0,0 +1,109 @@
+import React from 'react'
+import {connect} from 'react-redux'
+import {ErrorBanner, PasswordField, TextField, TextareaField} from 'features/shared/components'
+import actions from 'actions'
+import {reduxForm} from 'redux-form'
+import {withNamespaces} from 'react-i18next'
+import style from './RestoreMnemonic.scss'
+
+class RestoreMnemonic extends React.Component {
+  constructor(props) {
+    super(props)
+    this.submitWithErrors = this.submitWithErrors.bind(this)
+  }
+
+  submitWithErrors(data) {
+    return new Promise((resolve, reject) => {
+      this.props.restoreMnemonic(data, this.props.success)
+        .catch((err) => reject({_error: err}))
+    })
+  }
+
+  render() {
+    const t = this.props.t
+
+    const {
+      fields: {mnemonic, keyAlias, password, confirmPassword},
+      error,
+      handleSubmit,
+      submitting
+    } = this.props
+
+
+    return (
+        <div>
+          <h4>{t('init.restoreWallet')}</h4>
+          <div>
+            <form onSubmit={handleSubmit(this.submitWithErrors)}>
+
+              <TextareaField
+                title={t('init.mnemonic')}
+                fieldProps={mnemonic}
+              />
+              <TextField
+                title={t('form.keyAlias')}
+                placeholder={t('init.keyPlaceholder')}
+                fieldProps={keyAlias}/>
+              <PasswordField
+                title={t('init.keyPassword')}
+                placeholder={t('init.passwordPlaceholder')}
+                fieldProps={password} />
+              <PasswordField
+                title={t('init.repeatKeyPassword')}
+                placeholder={t('init.repeatPlaceHolder')}
+                fieldProps={confirmPassword} />
+
+              {error &&
+              <ErrorBanner
+                title={t('init.errorTitle')}
+                error={error}/>}
+
+              <button type='submit' className={`btn btn-primary ${style.submitButton}`} disabled={submitting}>
+                {t('init.restore')}
+              </button>
+
+            </form>
+          </div>
+        </div>
+    )
+  }
+}
+
+const validate = (values, props) => {
+  const errors = {}
+  const t = props.t
+
+  if (!values.mnemonic) {
+    errors.mnemonic = t('init.mnemonicRequire')
+  }
+  if (!values.keyAlias) {
+    errors.keyAlias = t('key.aliasRequired')
+  }
+  if (!values.password) {
+    errors.password = t('key.passwordRequired')
+  } else if (values.password.length < 5) {
+    errors.password = ( t('key.reset.newPWarning') )
+  }
+  if (values.password !== values.confirmPassword) {
+    errors.confirmPassword = ( t('key.reset.repeatPWarning') )
+  }
+
+  return errors
+}
+
+
+export default withNamespaces('translations')( connect(
+  () => ({}),
+  (dispatch) => ({
+    restoreMnemonic: (token, success) => dispatch(actions.initialization.restoreMnemonic(token, success)),
+  })
+)(reduxForm({
+  form: 'restoreMnemonic',
+  fields: [
+    'mnemonic',
+    'keyAlias',
+    'password',
+    'confirmPassword',
+  ],
+  validate
+})(RestoreMnemonic)))
diff --git a/src/features/shared/components/RestoreMnemonic/RestoreMnemonic.scss b/src/features/shared/components/RestoreMnemonic/RestoreMnemonic.scss
new file mode 100644 (file)
index 0000000..2a8c16f
--- /dev/null
@@ -0,0 +1,3 @@
+.submitButton{
+  float: left;
+}
diff --git a/src/features/shared/components/SingletonField.jsx b/src/features/shared/components/SingletonField.jsx
new file mode 100644 (file)
index 0000000..695ecac
--- /dev/null
@@ -0,0 +1,34 @@
+import React from 'react'
+import pick from 'lodash/pick'
+import disableAutocomplete from 'utility/disableAutocomplete'
+
+const TEXT_FIELD_PROPS = [
+  'value',
+  'onBlur',
+  'onChange',
+  'onFocus',
+  'name'
+]
+
+class SingletonField extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    const fieldProps = pick(this.props.fieldProps, TEXT_FIELD_PROPS)
+    const {touched, error} = this.props.fieldProps
+
+    return(
+      <div className={` ${this.props.className} ${touched && error &&'has-error'}`}>
+        <input
+          className='form-control'
+          autoFocus={!!this.props.autoFocus}
+          {...disableAutocomplete}
+          {...fieldProps} />
+      </div>
+    )
+  }
+}
+
+export default SingletonField
diff --git a/src/features/shared/components/Stepper/Step.jsx b/src/features/shared/components/Stepper/Step.jsx
new file mode 100644 (file)
index 0000000..d1c543f
--- /dev/null
@@ -0,0 +1,71 @@
+import React from 'react'
+import styles from './stepper.scss'
+import {withNamespaces} from 'react-i18next'
+
+class Step extends React.Component {
+
+  render() {
+    const {
+      isActive,
+      displayPrevious,
+      displayNext,
+      component,
+      children,
+      t,
+      nextL,
+      prevL
+    } = this.props
+
+    if(isActive === false) return null
+
+    return (
+      <div>
+          {component ? React.createElement(component) : children}
+          <Next
+            label={nextL || t('commonWords.continue')}
+            isActive={displayNext}
+            goToNextStep={() => this.props.goToNextStep()}
+          />
+          <Previous
+            label={prevL || t('commonWords.cancel')}
+            isActive={displayPrevious}
+            goToPreviousStep={() => this.props.goToPreviousStep()}
+          />
+      </div>
+    )
+  }
+}
+
+class Next extends React.Component {
+
+  render() {
+    const { isActive, label } = this.props
+    if (isActive === false) return null
+
+    return (
+      <button
+        className={`btn btn-primary ${styles.floatLeft}`}
+        onClick={() => this.props.goToNextStep()}>
+        {label}
+      </button>
+    )
+  }
+}
+
+class Previous extends React.Component {
+
+  render() {
+    const { isActive, label } = this.props
+    if (isActive === false) return null
+
+    return (
+      <a
+        className={`btn btn-link ${styles.marginLeft}`}
+        onClick={() => this.props.goToPreviousStep()}>
+        {label}
+      </a>
+    )
+  }
+}
+
+export default withNamespaces('translations') (Step)
\ No newline at end of file
diff --git a/src/features/shared/components/Stepper/StepList.jsx b/src/features/shared/components/Stepper/StepList.jsx
new file mode 100644 (file)
index 0000000..0d5c282
--- /dev/null
@@ -0,0 +1,41 @@
+import React from 'react'
+
+class StepList extends React.Component {
+  constructor(props) {
+    super(props)
+
+    this.state = {
+      currentStep: 0,
+      totalSteps: this.props.children.length - 1,
+    }
+  }
+
+  goToPreviousStep () {
+    this.setState({ currentStep: this.state.currentStep - 1 })
+  }
+
+  goToNextStep() {
+    this.setState({ currentStep: this.state.currentStep + 1 })
+  }
+
+  render() {
+    const children = React.Children.map(this.props.children, (child, index) => {
+      const { currentStep, totalSteps } = this.state
+
+      return React.cloneElement(child, {
+        isActive: index === currentStep,
+        displayPrevious: currentStep > 0,
+        displayNext: currentStep < totalSteps,
+        displaySubmit: currentStep === totalSteps,
+        goToPreviousStep: () => this.goToPreviousStep(),
+        goToNextStep: () => this.goToNextStep(),
+      })
+    })
+
+    return (
+      <div>{children} </div>
+    )
+  }
+}
+
+export default StepList
\ No newline at end of file
diff --git a/src/features/shared/components/Stepper/stepper.scss b/src/features/shared/components/Stepper/stepper.scss
new file mode 100644 (file)
index 0000000..e52d5d1
--- /dev/null
@@ -0,0 +1,8 @@
+.marginLeft{
+  margin-left: $gutter-size/2;
+  cursor: pointer;
+}
+
+.floatLeft{
+  float: left;
+}
diff --git a/src/features/shared/components/TextareaField.jsx b/src/features/shared/components/TextareaField.jsx
new file mode 100644 (file)
index 0000000..fa4cb59
--- /dev/null
@@ -0,0 +1,40 @@
+import React from 'react'
+import pick from 'lodash/pick'
+import { FieldLabel } from 'features/shared/components'
+import disableAutocomplete from 'utility/disableAutocomplete'
+
+const TEXT_FIELD_PROPS = [
+  'value',
+  'onBlur',
+  'onChange',
+  'onFocus',
+  'name'
+]
+
+class TextareaField extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    const fieldProps = pick(this.props.fieldProps, TEXT_FIELD_PROPS)
+    const {touched, error} = this.props.fieldProps
+
+    return(
+      <div className='form-group'>
+        {this.props.title && <FieldLabel>{this.props.title}</FieldLabel>}
+        <textarea className='form-control'
+               title={fieldProps.value||''}
+          placeholder={this.props.placeholder}
+          autoFocus={!!this.props.autoFocus}
+          {...disableAutocomplete}
+          {...fieldProps} />
+
+        {touched && error && <span className='text-danger'><strong>{error}</strong></span>}
+        {this.props.hint && <span className='help-block'>{this.props.hint}</span>}
+      </div>
+    )
+  }
+}
+
+export default TextareaField
index 35b2a54..3bff6cf 100644 (file)
@@ -7,10 +7,12 @@ import BaseList from './BaseList/BaseList'
 import BaseNew from './BaseNew'
 import BaseShow from './BaseShow'
 import CopyableBlock from './CopyableBlock/CopyableBlock'
+import ConfirmMnemonic from './ConfirmMnemonic/ConfirmMnemonic'
 import ConsoleSection from './ConsoleSection/ConsoleSection'
 import EmptyContent from './EmptyContent/EmptyContent'
 import ErrorBanner from './ErrorBanner/ErrorBanner'
 import FieldLabel from './FieldLabel/FieldLabel'
+import FileField from './FileField'
 import Flash from './Flash/Flash'
 import FormContainer from './FormContainer/FormContainer'
 import FormSection from './FormSection/FormSection'
@@ -19,6 +21,7 @@ import HiddenField from './HiddenField'
 import JsonField from './JsonField/JsonField'
 import KeyConfiguration from './KeyConfiguration'
 import KeyValueTable from './KeyValueTable/KeyValueTable'
+import Mnemonic from './Mnemonic/Mnemonic'
 import NotFound from './NotFound'
 import ObjectSelectorField from './ObjectSelectorField/ObjectSelectorField'
 import PageContent from './PageContent/PageContent'
@@ -27,19 +30,26 @@ import Pagination from './Pagination/Pagination'
 import PasswordField from './PasswordField/PasswordField'
 import RawJsonButton from './RawJsonButton'
 import RelativeTime from './RelativeTime'
+import RestoreKeystore from './RestoreKeystore/RestoreKeystore'
+import RestoreMnemonic from './RestoreMnemonic/RestoreMnemonic'
 import RoutingContainer from './RoutingContainer'
 import SearchBar from './SearchBar/SearchBar'
 import Section from './Section/Section'
 import SelectField from './SelectField'
+import SingletonField from './SingletonField'
+import Step from './Stepper/Step'
+import StepList from './Stepper/StepList'
 import SubmitIndicator from './SubmitIndicator/SubmitIndicator'
 import TableList from './TableList/TableList'
 import TextField from './TextField'
+import TextareaField from './TextareaField'
 import XpubField from './XpubField/XpubField'
 
 export {
   AmountUnitField,
   AmountInputMask,
   CheckboxField,
+  ConfirmMnemonic,
   ConsoleSection,
   Autocomplete,
   BaseUpdate,
@@ -50,6 +60,7 @@ export {
   EmptyContent,
   ErrorBanner,
   FieldLabel,
+  FileField,
   Flash,
   FormContainer,
   FormSection,
@@ -58,6 +69,7 @@ export {
   JsonField,
   KeyConfiguration,
   KeyValueTable,
+  Mnemonic,
   NotFound,
   ObjectSelectorField,
   PageContent,
@@ -66,12 +78,18 @@ export {
   PasswordField,
   RawJsonButton,
   RelativeTime,
+  RestoreKeystore,
+  RestoreMnemonic,
   RoutingContainer,
   SearchBar,
   Section,
   SelectField,
+  SingletonField,
+  Step,
+  StepList,
   SubmitIndicator,
   TableList,
   TextField,
+  TextareaField,
   XpubField,
 }
index b386c7b..28ec7cd 100644 (file)
@@ -13,6 +13,8 @@ import { Summary } from './'
 import { buildTxInputDisplay, buildTxOutputDisplay } from 'utility/buildInOutDisplay'
 import { btmID } from 'utility/environment'
 import moment from 'moment/moment'
+import BigNumber from 'bignumber.js'
+
 
 class Show extends BaseShow {
 
@@ -26,7 +28,7 @@ class Show extends BaseShow {
       const confirmation = this.props.highestBlock - item.blockHeight + 1
       const btmInput = item.inputs.reduce((sum, input) => {
         if (input.type === 'spend' && input.assetId === btmID) {
-          sum += input.amount
+          sum = BigNumber(input.amount).plus(sum)
         }
         return sum
       }, 0)
@@ -35,12 +37,12 @@ class Show extends BaseShow {
 
       const btmOutput = item.outputs.reduce((sum, output) => {
         if ((output.type === 'control' || output.type === 'retire')&& output.assetId === btmID) {
-          sum += output.amount
+          sum = BigNumber(output.amount).plus(sum)
         }
         return sum
       }, 0)
 
-      const gasAmount = btmInput > 0 ? btmInput - btmOutput : 0
+      const gasAmount = btmInput > 0 ? btmInput.minus(btmOutput) : 0
 
       const gas = normalizeGlobalBTMAmount(btmID, gasAmount, btmAmountUnit)
 
index 2f594de..1707fdf 100644 (file)
@@ -24,7 +24,9 @@
     "page":"Page",
     "close":"Close",
     "errorCode":"Error Code:",
-    "requestId":"Request ID:"
+    "requestId":"Request ID:",
+    "cancel": "Cancel",
+    "continue":"Continue"
   },
   "crumbName":{
     "transaction": "transactions",
     "keyPlaceholder":"Please enter the key alias...",
     "passwordPlaceholder":"Please enter the key password...",
     "repeatPlaceHolder":"Please repeat the key password...",
-    "errorTitle":"Error in initialization"
+    "errorTitle":"Error in initialization",
+    "mnemonic":"mnemonic",
+    "mnemonicRequire":"Mnemonic is Required",
+    "create": "Create Wallet",
+    "createDescription":"This option will create a default account, key password and give you a option to keep you mnemonic.  Warning: key seed words will display only once, it's important to remember your password.",
+    "welcome":"Welcome to Bytom __network__"
   },
   "backup":{
     "title":"Backup and Restore",
     "backup":"Back Up",
     "backupDescription":"This option will back up all data stored in this core, including blockchain data, accounts, assets and balances.",
-    "restore":"Restore",
-    "restoreDescription":"This option will restore the wallet data form a file. You might need to rescan your wallet, if you balance is not up to date.",
+    "restoreKeystore":"Restore from Keystore File",
+    "restoreKeystoreDescription":"This option will restore the wallet data form a file. You might need to rescan your wallet, if you balance is not up to date.",
+    "restoreMnemonic":"Restore by Mnemonic",
+    "restoreMnemonicDescription":"This option will restore the key related data by mnemonic. You might need to rescan your wallet, if you balance is not up to date.",
     "download":"Download Backup",
-    "selectFile":"Select Restore File"
+    "selectFile":"Select Restore File",
+    "restore":"Restore"
+  },
+  "mnemonic":{
+    "backup":"Backup Seed",
+    "backupMessage":"Mnemonic used to restore the key related information. Please write down the following seed and save it in a secure location.",
+    "continue":"I have written down the seed",
+    "skip": "Skip the backup",
+    "confirmTitle":"Enter your wallet recover phrase",
+    "confirm": "Confirm Seed",
+    "confirmMessage":"Confirm that you backed up your Mnemonic by filling the missing words."
   },
   "notFound":{
     "title":"Not Found",
index a7e7edf..ac1176d 100644 (file)
@@ -24,7 +24,9 @@
     "page":"页面",
     "close":"关闭",
     "errorCode":"错误代码:",
-    "requestId":"请求ID:"
+    "requestId":"请求ID:",
+    "cancel": "返回",
+    "continue":"继续"
   },
   "crumbName":{
     "transaction": "交易",
     "keyPlaceholder":"请输入密钥别名...",
     "passwordPlaceholder":"请输入密钥密码...",
     "repeatPlaceHolder":"请重复输入密钥密码...",
-    "errorTitle":"初始化错误"
+    "errorTitle":"初始化错误",
+    "mnemonic":"助记词",
+    "mnemonicRequire":"助记词是必须项",
+    "create": "新建钱包",
+    "createDescription":"这个选项将生成默认的挖矿账户和默认密钥。 助记词将会在此生成,您可以选择保留助记词或者跳过。 注意: 助记词只生成一次, 请将密码,助记词妥善保存",
+    "welcome":"欢迎来到__network__"
   },
   "backup":{
     "title":"备份与恢复",
     "backup":"备份",
     "backupDescription":"这个选项备份所有本地数据,包括账户,资产和余额。 请妥善保管你的备份文件。",
-    "restore":"恢复",
-    "restoreDescription":"这个选项将从文件中恢复钱包数据。 如果你的钱包余额显示不正确,请点击扫描钱包的按钮。",
+    "restoreKeystore":"从Keystore文件恢复",
+    "restoreKeystoreDescription":"这个选项将从文件中恢复钱包数据。 如果你的钱包余额显示不正确,请点击扫描钱包的按钮。",
+    "restoreMnemonic":"通过助记词恢复",
+    "restoreMnemonicDescription":"这个选项将通过助记词恢复密钥相关的数据。 如果你的钱包余额显示不正确,请点击扫描钱包的按钮。",
     "download":"下载备份",
-    "selectFile":"选择备份文件"
+    "selectFile":"选择备份文件",
+    "restore":"恢复"
+  },
+  "mnemonic":{
+    "backup":"备份助记词",
+    "backupMessage":"助记词用于恢复密钥相关的信息。 请将它准确的抄写到纸上,并存放在的安全的地方。",
+    "continue":"我已抄写助记词",
+    "skip": "跳过备份",
+    "confirmTitle":"请重复输入钱包助记词",
+    "confirm": "验证助记词",
+    "confirmMessage":"填写缺失的文字以验证你已经抄写下助记词。"
   },
   "notFound":{
     "title":"页面找不到",
index fdb5f1a..4474dcc 100644 (file)
@@ -7,6 +7,7 @@ import { reducers as app } from 'features/app'
 import { reducers as asset } from 'features/assets'
 import { reducers as balance } from 'features/balances'
 import { reducers as core } from 'features/core'
+import { reducers as initialization } from 'features/initialization'
 import { reducers as mockhsm } from 'features/mockhsm'
 import { reducers as testnet } from 'features/testnet'
 import { reducers as transaction } from 'features/transactions'
@@ -51,6 +52,7 @@ const makeRootReducer = () => (state, action) => {
     balance,
     core,
     form,
+    initialization,
     key: mockhsm,
     routing,
     testnet,
index 5d95211..6eb9fba 100644 (file)
@@ -6,6 +6,7 @@ import { routes as assets } from 'features/assets'
 import { routes as balances } from 'features/balances'
 import { routes as configuration } from 'features/configuration'
 import { routes as core } from 'features/core'
+import { routes as initialization } from 'features/initialization'
 import { routes as transactions } from 'features/transactions'
 import { routes as transactionFeeds } from 'features/transactionFeeds'
 import { routes as unspents } from 'features/unspents'
@@ -22,6 +23,7 @@ const makeRoutes = (store) => ({
     balances(store),
     configuration,
     core,
+    initialization,
     transactions(store),
     transactionFeeds(store),
     unspents(store),
index ab44aab..16a60ee 100644 (file)
@@ -12,6 +12,11 @@ const backUp = (client) => {
       cb
     ),
 
+    recovery: (opts = {}, cb) => shared.tryCallback(
+      client.request('/recovery-wallet', opts),
+      cb
+    ),
+
     rescan: (cb) => shared.tryCallback(
       client.request('/rescan-wallet'),
       cb
diff --git a/static/images/copy.svg b/static/images/copy.svg
new file mode 100755 (executable)
index 0000000..d090ef0
--- /dev/null
@@ -0,0 +1,23 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 18 18">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: none;
+      }
+
+      .cls-2 {
+        clip-path: url(#clip-path);
+      }
+
+      .cls-3 {
+        fill: #20252d;
+      }
+    </style>
+    <clipPath id="clip-path">
+      <rect id="Rectangle_3774" data-name="Rectangle 3774" class="cls-1" width="18" height="18" transform="translate(61 117)"/>
+    </clipPath>
+  </defs>
+  <g id="copy" class="cls-2" transform="translate(-61 -117)">
+    <path id="Subtraction_47" data-name="Subtraction 47" class="cls-3" d="M-4797.6,2096H-4808v-10.4h1.3v9.1h9.1v1.3Zm2.6-2.6h-10.4V2083h6.5l3.9,3.965v6.434Zm-9.1-9.1v7.8h7.8v-3.9h-3.9v-3.9Zm5.051-.043v2.916h2.723Z" transform="translate(4871.5 -1963.5)"/>
+  </g>
+</svg>