"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",
],
"dependencies": {
"babel-polyfill": "~6.16.0",
+ "bignumber.js": "^7.2.1",
"bootstrap-sass": "~3.3.7",
"btoa": "^1.1.2",
"classnames": "~2.2.5",
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'
balance,
configuration,
core,
+ initialization,
key: mockhsm,
testnet,
transaction,
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
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'
const {
authOk,
configured,
- location
+ location,
+ accountInit
} = props
if (!authOk) {
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) => {
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{
}
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)
} 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>
}
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) )
+++ /dev/null
-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)))
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'
Config,
Login,
Loading,
- Register,
Modal,
Navigation,
SecondaryNavigation,
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'})
}
- },
+ }
}
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'
this.state = {
value: null
}
+
+ this.mnemonicPopup = this.mnemonicPopup.bind(this)
+ this.keystorePopup = this.keystorePopup.bind(this)
}
setValue(event) {
})
}
- 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'>
<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>
<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>
}
}
-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(
justify-content: space-between;
> div {
- width: 45%;
+ width: 30%;
min-height: 100%;
}
}
+
import Backup from './Backup'
export {
}
}
-//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,
updateMiningState,
fetchCoreInfo,
clearSession,
- registerKey,
logIn: (token) => (dispatch) => {
dispatch(setClientToken(token))
return dispatch(fetchCoreInfo({throw: true}))
--- /dev/null
+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;
--- /dev/null
+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))
--- /dev/null
+.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;
+}
+
--- /dev/null
+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))
--- /dev/null
+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))
--- /dev/null
+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))
--- /dev/null
+.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;
+}
--- /dev/null
+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)))
--- /dev/null
+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,
+}
--- /dev/null
+import actions from './actions'
+import reducers from './reducers'
+import routes from './routes'
+
+export {
+ actions,
+ reducers,
+ routes
+}
--- /dev/null
+import { combineReducers } from 'redux'
+
+const mnemonic = (state = [], action) => {
+ if (action.type == 'INIT_ACCOUNT') {
+ return action.data
+ }
+ return state
+}
+
+export default combineReducers({
+ mnemonic
+})
--- /dev/null
+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
+ }
+ ]
+}
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()
})
}
+const createSuccess = ()=> (dispatch) =>{
+ dispatch(create.created())
+ dispatch(push('/keys'))
+}
+
export default {
...create,
...list,
...resetPassword,
checkPassword,
- createExport
+ createExport,
+ createSuccess
}
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) {
--- /dev/null
+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))
--- /dev/null
+.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
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'
export {
List,
New,
+ MnemonicStepper,
Show,
ResetPassword,
- CheckPassword
+ CheckPassword,
}
}
return state
},
+ mnemonic: (state = [], action) => {
+ if (action.type == 'NEW_KEY') {
+ return action.data
+ }
+ return state
+ },
})
-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,
path: ':id/check-password',
component: CheckPassword,
},
+ {
+ path: 'mnemonic',
+ component: MnemonicStepper
+ }
],skipFilter: true, name: 'key' })
\ No newline at end of file
--- /dev/null
+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))
--- /dev/null
+.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;
+}
--- /dev/null
+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
.main {
background: $background-color;
- padding: 0;
display: flex;
flex-direction: row;
padding: 0 $gutter-size;
--- /dev/null
+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)
--- /dev/null
+.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;
+}
--- /dev/null
+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)))
--- /dev/null
+.submitButton{
+ float: left;
+}
--- /dev/null
+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)))
--- /dev/null
+.submitButton{
+ float: left;
+}
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+.marginLeft{
+ margin-left: $gutter-size/2;
+ cursor: pointer;
+}
+
+.floatLeft{
+ float: left;
+}
--- /dev/null
+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
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'
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'
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,
EmptyContent,
ErrorBanner,
FieldLabel,
+ FileField,
Flash,
FormContainer,
FormSection,
JsonField,
KeyConfiguration,
KeyValueTable,
+ Mnemonic,
NotFound,
ObjectSelectorField,
PageContent,
PasswordField,
RawJsonButton,
RelativeTime,
+ RestoreKeystore,
+ RestoreMnemonic,
RoutingContainer,
SearchBar,
Section,
SelectField,
+ SingletonField,
+ Step,
+ StepList,
SubmitIndicator,
TableList,
TextField,
+ TextareaField,
XpubField,
}
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 {
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)
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)
"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",
"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":"页面找不到",
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'
balance,
core,
form,
+ initialization,
key: mockhsm,
routing,
testnet,
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'
balances(store),
configuration,
core,
+ initialization,
transactions(store),
transactionFeeds(store),
unspents(store),
cb
),
+ recovery: (opts = {}, cb) => shared.tryCallback(
+ client.request('/recovery-wallet', opts),
+ cb
+ ),
+
rescan: (cb) => shared.tryCallback(
client.request('/rescan-wallet'),
cb
--- /dev/null
+<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>