OSDN Git Service

add the basic initialization hierarchy.
authorZhiting Lin <zlin035@uottawa.ca>
Wed, 24 Oct 2018 06:34:27 +0000 (14:34 +0800)
committerZhiting Lin <zlin035@uottawa.ca>
Wed, 24 Oct 2018 06:34:27 +0000 (14:34 +0800)
12 files changed:
src/actions.js
src/features/app/actions.js
src/features/app/components/Container.jsx
src/features/initialization/actions.js [new file with mode: 0644]
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/Register/Register.jsx [new file with mode: 0644]
src/features/initialization/components/Register/Register.scss [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/routes.js [new file with mode: 0644]
src/routes.js

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..215f88d 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 !== 'initialization') {
+        dispatch(push('/initialization'))
+      }
+    }
+  },
   showConfiguration: () => {
     return (dispatch, getState) => {
       // Need a default here, since locationBeforeTransitions gets cleared
index fb49892..8f93c14 100644 (file)
@@ -1,7 +1,8 @@
 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, Init, Register, Modal } from './'
+import { Initialization } from 'features/initialization/components/index'
 import moment from 'moment'
 import { withI18n } from 'react-i18next'
 
@@ -21,20 +22,20 @@ class Container extends React.Component {
       authOk,
       configKnown,
       configured,
-      location
+      location,
+      accountInit
     } = props
 
     if (!authOk || !configKnown) {
       return
     }
 
-    if (configured) {
-      if (location.pathname === '/' ||
-          location.pathname.indexOf('configuration') >= 0) {
+    if (accountInit|| this.state.noAccountItem) {
+      if (location.pathname === '/' ) {
         this.props.showRoot()
       }
     } else {
-      this.props.showConfiguration()
+      this.props.showInitialization()
     }
   }
 
@@ -87,7 +88,7 @@ class Container extends React.Component {
     } else if (!this.props.configured) {
       layout = <Config>{this.props.children}</Config>
     } 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>
     }
@@ -118,7 +119,7 @@ export default connect(
   (dispatch) => ({
     fetchInfo: options => dispatch(actions.core.fetchCoreInfo(options)),
     showRoot: () => dispatch(actions.app.showRoot),
-    showConfiguration: () => dispatch(actions.app.showConfiguration()),
+    showInitialization: () => dispatch(actions.app.showInitialization()),
     fetchAccountItem: () => dispatch(actions.account.fetchItems())
   })
 )( withI18n() (Container) )
diff --git a/src/features/initialization/actions.js b/src/features/initialization/actions.js
new file mode 100644 (file)
index 0000000..6c07ef3
--- /dev/null
@@ -0,0 +1,45 @@
+import { chainClient } from 'utility/environment'
+import { actions as coreActions } from 'features/core'
+import { fetchTestnetInfo } from 'features/testnet/actions'
+
+const retry = (dispatch, promise, count = 10) => {
+  return dispatch(promise).catch((err) => {
+    var currentTime = new Date().getTime()
+    while (currentTime + 200 >= new Date().getTime()) { /* wait for retry */ }
+
+    if (count >= 1) {
+      retry(dispatch, promise, count - 1)
+    } else {
+      throw(err)
+    }
+  })
+}
+
+let actions = {
+  submitConfiguration: (data) => {
+    const configureWithRetry = (dispatch, config) => {
+      return chainClient().config.configure(config)
+        .then(() => retry(dispatch, coreActions.fetchCoreInfo({throw: true})))
+    }
+
+    return (dispatch) => {
+      if (data.type == 'testnet') {
+        return dispatch(fetchTestnetInfo()).then(testnet =>
+          configureWithRetry(dispatch, testnet))
+      } else {
+        if (data.type == 'new') {
+          data = {
+            isGenerator: true,
+            isSigner: true,
+            quorum: 1,
+          }
+        }
+
+        delete data.type
+        return configureWithRetry(dispatch, data)
+      }
+    }
+  }
+}
+
+export default actions
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..bc64d2b
--- /dev/null
@@ -0,0 +1,24 @@
+import React from 'react'
+import styles from './Index.scss'
+import { Link } from 'react-router'
+
+class Index extends React.Component {
+  constructor(props) {
+    super(props)
+  }
+
+  render() {
+    return (
+      <div className={styles.main}>
+        <Link to='/initialization/register'>
+          Create Wallet
+        </Link>
+        <button>
+          Restore Wallet
+        </button>
+      </div>
+    )
+  }
+}
+
+export default 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..2e7b4ae
--- /dev/null
@@ -0,0 +1,48 @@
+.main {
+  background: $background-inverse-color;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding-top: 50px;
+  display: block;
+  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;
+
+  margin-top: 30px;
+}
+
+.form {
+  background: $background-color;
+  border-radius: $border-radius-standard;
+  width: 500px;
+  padding: 30px;
+}
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..92c8862
--- /dev/null
@@ -0,0 +1,178 @@
+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)))
diff --git a/src/features/initialization/components/Register/Register.scss b/src/features/initialization/components/Register/Register.scss
new file mode 100644 (file)
index 0000000..2e7b4ae
--- /dev/null
@@ -0,0 +1,48 @@
+.main {
+  background: $background-inverse-color;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding-top: 50px;
+  display: block;
+  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;
+
+  margin-top: 30px;
+}
+
+.form {
+  background: $background-color;
+  border-radius: $border-radius-standard;
+  width: 500px;
+  padding: 30px;
+}
diff --git a/src/features/initialization/components/index.js b/src/features/initialization/components/index.js
new file mode 100644 (file)
index 0000000..9674c46
--- /dev/null
@@ -0,0 +1,7 @@
+import Index from './Index/Index'
+import Register from './Register/Register'
+
+export {
+  Index,
+  Register
+}
diff --git a/src/features/initialization/index.js b/src/features/initialization/index.js
new file mode 100644 (file)
index 0000000..b133bca
--- /dev/null
@@ -0,0 +1,7 @@
+import actions from './actions'
+import routes from './routes'
+
+export {
+  actions,
+  routes,
+}
diff --git a/src/features/initialization/routes.js b/src/features/initialization/routes.js
new file mode 100644 (file)
index 0000000..cbb1ea9
--- /dev/null
@@ -0,0 +1,14 @@
+import { RoutingContainer } from 'features/shared/components'
+import { Index, Register } from './components'
+
+export default {
+  path: 'initialization',
+  component: RoutingContainer,
+  indexRoute: { component: Index },
+  childRoutes: [
+    {
+      path: 'register',
+      component: Register
+    }
+  ]
+}
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),