diff --git a/package.json b/package.json index 5a133364e..1c8bf63da 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "uniqid": "^4.1.1", "uport-connect": "^0.6.0", "web3": "^0.20.1", + "web3-eth-accounts": "^1.0.0-beta.34", "web3-provider-engine": "^14.0.3", "webstomp-client": "^1.2.0" }, diff --git a/packages/login-ui/components/AccountSelector/AccountSelector.js b/packages/login-ui/components/AccountSelector/AccountSelector.js index 1df306904..3fdbbfe6d 100644 --- a/packages/login-ui/components/AccountSelector/AccountSelector.js +++ b/packages/login-ui/components/AccountSelector/AccountSelector.js @@ -3,86 +3,109 @@ * Licensed under the AGPL Version 3 license. */ -import networkService from '@chronobank/login/network/NetworkService' -import { addError } from '@chronobank/login/redux/network/actions' -import { CircularProgress, MenuItem, SelectField } from 'material-ui' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' +import { MuiThemeProvider } from 'material-ui' import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import styles from '../../components/stylesLoginPage' +import React, { PureComponent } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' +import { navigateToSelectImportMethod, onWalletSelect } from '@chronobank/login/redux/network/actions' +import { + WalletEntryModel, +} from 'models/persistAccount' + +import arrow from 'assets/img/icons/prev-white.svg' import './AccountSelector.scss' -import { Button } from '../../settings' -const mapStateToProps = (state) => ({ - accounts: state.get('network').accounts, - selectedAccount: state.get('network').selectedAccount, - isLoading: state.get('network').isLoading, -}) +function mapDispatchToProps (dispatch) { + return { + navigateToSelectImportMethod: () => dispatch(navigateToSelectImportMethod()), + onWalletSelect: (wallet) => dispatch(onWalletSelect(wallet)), + } +} -const mapDispatchToProps = (dispatch) => ({ - loadAccounts: () => networkService.loadAccounts(), - selectAccount: (value) => networkService.selectAccount(value), - addError: (error) => dispatch(addError(error)), -}) +function mapStateToProps (state) { + return { + walletsList: state.get('persistAccount').walletsList.map( + (wallet) => new WalletEntryModel({...wallet}) + ), + } +} @connect(mapStateToProps, mapDispatchToProps) -class AccountSelector extends PureComponent { +export default class SelectWalletPage extends PureComponent { static propTypes = { - onSelectAccount: PropTypes.func, - loadAccounts: PropTypes.func, - selectAccount: PropTypes.func, - addError: PropTypes.func, - accounts: PropTypes.array, - selectedAccount: PropTypes.string, - isLoading: PropTypes.bool, + onWalletSelect: PropTypes.func, + walletsList: PropTypes.arrayOf( + PropTypes.instanceOf(WalletEntryModel) + ), + navigateToSelectImportMethod: PropTypes.func, } - componentWillMount () { - this.props.loadAccounts().then(() => { - }).catch((e) => { - this.props.selectAccount(null) - this.props.addError(e.message) - }) + static defaultProps = { + onWalletSelect: () => {}, + walletsList: [], } - handleSelect = () => { - this.props.onSelectAccount(this.props.selectedAccount) - } + renderWalletsList (){ + const { onWalletSelect, walletsList } = this.props + + if (!walletsList || !walletsList.length){ + return ( +
+ Sorry, there are no accounts to display +
+ ) + } - handleChange = (event, index, value) => { - this.props.selectAccount(value) + return ( +
+ { + walletsList ? walletsList.map((w, i) => ( + onWalletSelect(w)} + /> + )) : null + } +
+ ) } render () { - const { accounts, selectedAccount, isLoading } = this.props return ( -
- } - value={selectedAccount} - onChange={this.handleChange} - fullWidth - {...styles.selectField} - > - {accounts && accounts.map((a) => )} - -
-
- + or
+ Create New Account +
+
+
- + ) } } - -export default AccountSelector diff --git a/packages/login-ui/components/AccountSelector/AccountSelector.scss b/packages/login-ui/components/AccountSelector/AccountSelector.scss index 79f684d13..168f3cf01 100644 --- a/packages/login-ui/components/AccountSelector/AccountSelector.scss +++ b/packages/login-ui/components/AccountSelector/AccountSelector.scss @@ -6,11 +6,88 @@ @import "~styles/partials/variables"; @import "~styles/partials/mixins"; +.wrapper { + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.content { + margin: 0 auto; + max-width: 380px; +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.wallets-list { + border-top: 1px solid $color-purpule; + margin-bottom: 45px; + color: #fff; +} + +.empty-list { + border-top: 1px solid $color-purpule; + border-bottom: 1px solid $color-purpule; + padding: 18px 0; + margin-bottom: 30px; + text-align: center; + color: #fff; + width: 380px; + + @include xs-only { + width: 100%; + } +} + .actions { - margin-top: 40px; - display: flex; - justify-content: flex-end; + text-align: center; + margin-bottom: 45px; + line-height: 30px; + color: #fff; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } } -.action { -} \ No newline at end of file +.button { + text-align: center; + margin-bottom: 22px; + + button { + padding: 0 30px; + } +} + +.actionIcon { + transform: matrix(-1,0,0,-1,0,0); +} + +.description { + font-weight: 400; + color: $additionalData-color-1; + font-size: 16px; + line-height: 22px; + margin-bottom: 40px; + text-align: center; +} diff --git a/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.js b/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.js new file mode 100644 index 000000000..c8f759da8 --- /dev/null +++ b/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.js @@ -0,0 +1,158 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import networkService from '@chronobank/login/network/NetworkService' +import web3Provider from '@chronobank/login/network/Web3Provider' +import web3Utils from '@chronobank/login/network/Web3Utils' +import { clearErrors, DUCK_NETWORK } from '@chronobank/login/redux/network/actions' +import { + getNetworksWithProviders, + getNetworkWithProviderNames, + getProviderById, +} from '@chronobank/login/network/settings' +import { Popover } from 'material-ui' +import PropTypes from 'prop-types' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { Button } from 'components' +import classnames from 'classnames' + +import { Translate } from 'react-redux-i18n' +import Web3 from 'web3' +import './CommonNetworkSelector.scss' + +const mapStateToProps = (state) => { + const network = state.get(DUCK_NETWORK) + return { + providersList: getNetworksWithProviders(network.isLocal), + networkProviderName: getNetworkWithProviderNames(network.selectedProviderId, network.selectedNetworkId, network.isLocal), + isLocal: network.isLocal, + selectedNetworkId: network.selectedNetworkId, + selectedProvider: getProviderById(network.selectedProviderId), + networks: network.networks, + isLoading: network.isLoading, + } +} + +const mapDispatchToProps = (dispatch) => ({ + selectProviderWithNetwork: (networkId, providerId) => { + networkService.selectProvider(providerId) + networkService.selectNetwork(networkId) + }, + selectNetwork: (network) => networkService.selectNetwork(network), + clearErrors: () => dispatch(clearErrors()), + getProviderURL: () => networkService.getProviderURL(), +}) + +@connect(mapStateToProps, mapDispatchToProps) +export default class CommonNetworkSelector extends PureComponent { + static propTypes = { + clearErrors: PropTypes.func, + selectNetwork: PropTypes.func, + selectProviderWithNetwork: PropTypes.func, + getProviderURL: PropTypes.func, + selectedNetworkId: PropTypes.number, + networks: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.number, + protocol: PropTypes.string, + name: PropTypes.string, + scanner: PropTypes.arrayOf(PropTypes.string), + bitcoin: PropTypes.string, + nem: PropTypes.string, + })), + onSelect: PropTypes.func, + isLoading: PropTypes.bool, + } + + constructor (props) { + super (props) + + this.state = { + open: false, + } + } + + componentDidMount(){ + networkService.autoSelect() + } + + handleClick = (data) => { + this.props.clearErrors() + this.props.selectProviderWithNetwork(data.network.id, data.provider.id) + this.resolveNetwork() + } + + resolveNetwork = () => { + const web3 = new Web3() + web3Provider.reinit(web3, web3Utils.createStatusEngine(this.props.getProviderURL())) + web3Provider.resolve() + } + + handleClickButton = (event) => { + // This prevents ghost click. + event.preventDefault() + + this.setState({ + open: true, + anchorEl: event.currentTarget, + }) + } + + getFullNetworkName(item){ + return `${item.provider.name} - ${item.network.name}` + } + + handleRequestClose = () => { + this.setState({ + open: false, + }) + } + + renderMenuItem(item, i){ + const { selectedNetworkId, selectedProvider } = this.props + const checked = item.provider.id === selectedProvider.id && item.network.id === selectedNetworkId + + return ( +
  • this.handleClick(item)} + key={i} + > + {this.getFullNetworkName(item)} +
  • + ) + } + + render () { + const { selectedNetworkId, selectedProvider, networks, isLoading, networkProviderName, providersList } = this.props + + return ( +
    + + + + + +
    + ) + } +} + diff --git a/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.scss b/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.scss new file mode 100644 index 000000000..7fc678f27 --- /dev/null +++ b/packages/login-ui/components/CommonNetworkSelector/CommonNetworkSelector.scss @@ -0,0 +1,48 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ +@import "~styles/partials/mixins"; +@import "~styles/partials/variables"; + +.providersList { + border-radius: 20px; + border-top: 3px solid #E2A864; + overflow: hidden; + background: #fff; + margin-top: 10px; + + @include xs-only { + width: 100%; + top: 10px; + } +} +.providerItem { + padding: 15px 20px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + cursor: pointer; + + &:hover, &.providerItemActive { + background: #F2F2F2; + } +} + +.root { + +} + +.langButton { + button { + color: #5DB3ED; + border:1px solid #5DB3ED; + background: transparent; + font-size: 14px; + text-transform: none; + + &:hover { + color:#fff; + border-color: transparent; + } + } +} diff --git a/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.js b/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.js new file mode 100644 index 000000000..4e9330cc3 --- /dev/null +++ b/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.js @@ -0,0 +1,183 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { connect } from 'react-redux' +import { push } from 'react-router-redux' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { Button } from 'components' +import { + initConfirmMnemonicPage, + navigateToConfirmMnemonicPage, + onSubmitConfirmMnemonic, + onSubmitConfirmMnemonicSuccess, + onSubmitConfirmMnemonicFail, +} from '@chronobank/login/redux/network/actions' + +import './ConfirmMnemonic.scss' + +export const FORM_CONFIRM_MNEMONIC = 'ConfirmMnemonicForm' + +function mapStateToProps (state) { + + return { + mnemonic: state.get('network').newAccountMnemonic, + } +} + +function mapDispatchToProps (dispatch) { + return { + navigateToConfirmPage: () => dispatch(navigateToConfirmMnemonicPage()), + initConfirmMnemonicPage: () => dispatch(initConfirmMnemonicPage()), + onSubmit: (values) => { + const confirmMnemonic = values.get('mnemonic') + + dispatch(onSubmitConfirmMnemonic(confirmMnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitConfirmMnemonicSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitConfirmMnemonicFail(errors, dispatch, submitErrors)), + } +} + +class ConfirmMnemonicPage extends Component { + static propTypes = { + mnemonic: PropTypes.string, + initConfirmMnemonicPage: PropTypes.func, + } + + static defaultProps = { + mnemonic: '', + } + + constructor (props){ + super(props) + + const wordsArray = props.mnemonic ? + props.mnemonic.split(' ').map((word, index) => { + return { index, word } + }) : [] + + this.state = { + confirmPhrase: [], + currentWordsArray: wordsArray.sort((a,b) => a.word < b.word), + } + } + + componentDidMount(){ + this.props.initConfirmMnemonicPage() + } + + getCurrentMnemonic (){ + return this.state.confirmPhrase.map((item) => item.word).join(' ') + } + + getWordsButtons (){ + return this.state.currentWordsArray.map((item, index) => { + const wordSelected = this.state.confirmPhrase.includes(item) + + return ( + + )} + ) + } + + onClickWord (word, e){ + const { dispatch, change } = this.props + + if (!this.state.confirmPhrase.includes(word)) { + this.setState( + { confirmPhrase: this.state.confirmPhrase.concat(word) }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + } + + clearMnemonic (){ + const { dispatch, change } = this.props + + this.setState( + { confirmPhrase: [] }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + + clearLastWord (){ + const { dispatch, change } = this.props + + this.setState( + { confirmPhrase: this.state.confirmPhrase.slice(0, -1) }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + + render () { + const { handleSubmit, error } = this.props + console.log('confirm mnemonic page', this.props) + + return ( + +
    +
    +
    Confirm back-up phrase
    + +

    Click on back-up phrase words in the correct order.

    + +
    +
    { this.getCurrentMnemonic() }
    + + +
    + +
    + {error} +
    + +
    + { this.getWordsButtons() } +
    + +
    +
    Start Over
    +
    Undo
    +
    + +
    + + + Back +
    + +
    +
    +
    +
    +
    +
    + + + ) + } +} + +const form = reduxForm({ form: FORM_CONFIRM_MNEMONIC })(ConfirmMnemonicPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.scss b/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.scss new file mode 100644 index 000000000..50acb912b --- /dev/null +++ b/packages/login-ui/components/ConfirmMnemonic/ConfirmMnemonic.scss @@ -0,0 +1,216 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + + +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; +} + +.description { + margin-bottom: 30px; + color: $additionalData-color-1; + +} + +.passPhraseWrapper { + position: relative; + margin-bottom: 20px; + background: $background-color-1; + min-height: 100px; + border-radius: 2px; + padding: 20px 45px 20px 30px; +} + +.passPhrase { + font-size: 16px; + font-weight: 700; + line-height: 24px; + color: $button-color-2; + text-align: left; + min-height: 46px; + width: 100%; + + @include md-only { + min-height: 70px; + color: $synced-color; + } +} + +.wordsBlock { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.word { + width: 23%; + margin-bottom: 10px; + + button { + white-space: nowrap; + width: 100%; + cursor: pointer; + user-select: none; + color: #FFFFFF; + background-color: #614DBA; + text-transform: lowercase; + line-height: 62px; + border-radius: 2px; + font-size: 16px; + font-weight: 700; + padding: 12px 0; + } + + &:hover { + background-color: $button-color-2; + color: $background-color-1; + } + + @include md-only { + width: 31%; + } +} + +.submitButton { + background: #00A0D2; + font-size: 14px; + font-weight: 500; + color: #fff; + border-radius: 21px; + cursor: pointer; + line-height: 40px; + height: 40px; + min-width: 150px; + border: none; + margin-bottom: 40px; + padding: 0; + box-shadow: none; + + &:disabled { + cursor: not-allowed; + } + + @include md-only { + min-width: 180px; + } + + &:hover { + background: #0088C3; + } +} + +.progressBlock { + width: 50px; + margin: 0 auto 20px; + display: flex; + justify-content: space-between; +} + +.progressPoint { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progressPointInactive { + background: $color-blue-4; + } +} + +.clearMnemonic { + width: 23px; + height: 23px; + display: inline-block; + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: auto; + padding: 11px; + cursor: pointer; + box-sizing: content-box; + + img { + width: 100%; + } +} + +.controlsBlock { + display: flex; + justify-content: space-between; + margin-bottom: 35px; + + .control { + width: 49%; + line-height: 42px; + font-size: 16px; + border: 1px solid $color-blue-4; + border-radius: 2px; + color: $color-blue-4; + cursor: pointer; + font-weight: 700; + + &:hover { + background: $color-hover; + color: $background-color-1; + border: 0; + } + } + +} + +.form { + text-align: center; + width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + width: auto; + padding: 0 20px; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.error { + color: red; + margin-bottom: 20px; + display: none; + text-align: center; +} + +.visible { + display: block; +} diff --git a/packages/login-ui/components/CreateAccount/CreateAccount.js b/packages/login-ui/components/CreateAccount/CreateAccount.js new file mode 100644 index 000000000..61b496946 --- /dev/null +++ b/packages/login-ui/components/CreateAccount/CreateAccount.js @@ -0,0 +1,176 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' + +import { Button } from 'components' +import { + onSubmitCreateAccountPage, + onSubmitCreateAccountPageSuccess, + onSubmitCreateAccountPageFail, +} from '@chronobank/login/redux/network/actions' +import AutomaticProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/AutomaticProviderSelector' +import ManualProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/ManualProviderSelector' + +import validate from './validate' + +import styles from 'layouts/Splash/styles' +import fieldStyles from './styles' +import './CreateAccount.scss' + +const STRATEGY_MANUAL = 'manual' +const STRATEGY_AUTOMATIC = 'automatic' + +const nextStrategy = { + [STRATEGY_AUTOMATIC]: STRATEGY_MANUAL, + [STRATEGY_MANUAL]: STRATEGY_AUTOMATIC, +} + +export const FORM_CREATE_ACCOUNT = 'CreateAccountForm' + +function mapStateToProps (state) { + + return { + isImportMode: state.get('network').importAccountMode, + } +} + +function mapDispatchToProps (dispatch) { + return { + onSubmit: async (values) => { + const walletName = values.get('walletName') + const password = values.get('password') + + await dispatch(onSubmitCreateAccountPage(walletName, password)) + }, + onSubmitSuccess: () => dispatch(onSubmitCreateAccountPageSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitCreateAccountPageFail(errors, dispatch, submitErrors)), + } +} + +class CreateAccountPage extends PureComponent { + static propTypes = { + isImportMode: PropTypes.bool, + } + + constructor(){ + super() + + this.state = { + isShowProvider: true, + strategy: STRATEGY_AUTOMATIC, + } + } + + handleToggleProvider = (isShowProvider) => this.setState({ isShowProvider }) + + handleSelectorSwitch = (currentStrategy) => this.setState({ strategy: nextStrategy[currentStrategy] }) + + renderProviderSelector () { + switch (this.state.strategy) { + case STRATEGY_MANUAL: + return this.renderManualProviderSelector() + case STRATEGY_AUTOMATIC: + return this.renderAutomaticProviderSelector() + default: + return null + } + } + + renderAutomaticProviderSelector () { + return ( + + ) + } + + renderManualProviderSelector () { + return ( + + ) + } + + render () { + const { handleSubmit, pristine, valid, initialValues, isImportMode } = this.props + + return ( + +
    +
    + Create New Account +
    + +
    + Created wallet will be encrypted using given password and stored in your + browser's local storage. +
    + +
    + { this.renderProviderSelector() } +
    + +
    + + + +
    + +
    + + or
    + Use an existing account +
    + +
    + +
    + ) + } +} + +const form = reduxForm({ form: FORM_CREATE_ACCOUNT, validate })(CreateAccountPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) + diff --git a/packages/login-ui/components/CreateAccount/CreateAccount.scss b/packages/login-ui/components/CreateAccount/CreateAccount.scss new file mode 100644 index 000000000..3048ae140 --- /dev/null +++ b/packages/login-ui/components/CreateAccount/CreateAccount.scss @@ -0,0 +1,197 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ +@import "~styles/partials/mixins"; +@import "~styles/partials/variables"; + +$copyRightPadding: 200px; + +.root { + background-color: #242045; + position: relative; + display: flex; + min-height: 100vh; + flex-direction: column; + font-family: $font-proxima; + + @include md-only { + min-height: 100vh; + padding: 0; + } + + &:after { + // hack for preloading material icons and avoid FUOC on render + content: ''; + font-family: 'Material Icons'; + } +} + +.form { + flex: 1; + justify-content: center; + flex-direction: column; + display: flex; + margin: 0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.content { + display: flex; + flex: 1; + justify-content: center; + flex-direction: column; + margin: 0 auto; + + @include xs-only { + padding: 80px 20px 0; + } +} + +.footer { + display: flex; + background-color: rgba(255, 255, 255, 0.1); + bottom: 0; + left: 0; + right: 0; + position: absolute; + padding: 17px 80px; + + @include md-only { + padding: 20px; + flex-direction: column-reverse; + } +} + +.header-container { + display: flex; + justify-content: center; + height: 340px; + overflow: hidden; + margin-bottom: 20px; + + @include md-only { + display: none; + } +} + +.header-picture { + display: flex; + position: absolute; + height: 320px; + overflow: hidden; + flex: 1; +} + +.header-picture-crop { + display: flex; + position: absolute; + height: 400px; + top: -115px; + flex: 1; +} + +.header-logos { + z-index: 100; + display: flex; + position: relative; + flex-direction: column; + align-items: center; + top: 90px; + width: 300px; + height: 200px; +} + +.chrono-wallet-logo-bright { + display: flex; + width: 114px; + height: 117px; + margin-bottom: 40px; +} + +.chrono-wallet-text-bright { + display: flex; + height: 59px; + width: 282px; +} + +.copyright { + max-width: $copyRightPadding; + font-size: 12px; + font-weight: 300; + color: white; +} + +.create-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 10px; + text-align: center; +} + +.create-title-description { + font-weight: 400; + color: $color-description; + font-size: 16px; + line-height: 22px; + margin-bottom: 10px; + text-align: center; + max-width: 520px; +} + +.links { + padding-right: $copyRightPadding; // compensation for centering + text-align: center; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + + @include md-only { + padding: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } +} + +.actions { + color: white; + text-align: center; + line-height: 30px; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.fields-block { + margin: 0 auto 50px; + max-width: 300px; +} + +.button { + margin-bottom: 25px; +} + +.selector { + color: #fff; +} diff --git a/packages/login-ui/components/CreateAccount/styles.js b/packages/login-ui/components/CreateAccount/styles.js new file mode 100644 index 000000000..265d41576 --- /dev/null +++ b/packages/login-ui/components/CreateAccount/styles.js @@ -0,0 +1,8 @@ +export default { + textField: { + underlineStyle: { + borderColor: '#A3A3CC', + bottom: 0, + }, + }, +} diff --git a/packages/login-ui/components/CreateAccount/validate.js b/packages/login-ui/components/CreateAccount/validate.js new file mode 100644 index 000000000..183b6eb86 --- /dev/null +++ b/packages/login-ui/components/CreateAccount/validate.js @@ -0,0 +1,32 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import ErrorList from 'platform/ErrorList' +import * as validator from 'models/validator' + +const validateEqualPasswords = (password, confirmPassword) => password === confirmPassword ? null : 'Wrong password' + +export default (values) => { + const walletName = values.get('walletName') + + let walletNameErrors = new ErrorList() + walletNameErrors.add(validator.required(walletName)) + + const password = values.get('password') + + let passwordErrors = new ErrorList() + passwordErrors.add(validator.required(password)) + + const confirmPassword = values.get('confirmPassword') + let confirmPasswordErrors = new ErrorList() + confirmPasswordErrors.add(validator.required(confirmPassword)) + confirmPasswordErrors.add(validateEqualPasswords(password, confirmPassword)) + + return { + walletName: walletNameErrors.getErrors(), + password: passwordErrors.getErrors(), + confirmPassword: confirmPasswordErrors.getErrors(), + } +} diff --git a/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.js b/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.js index f9344cac1..ac64bd434 100644 --- a/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.js +++ b/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.js @@ -3,80 +3,117 @@ * Licensed under the AGPL Version 3 license. */ -import mnemonicProvider from '@chronobank/login/network/mnemonicProvider' -import { Checkbox, MuiThemeProvider } from 'material-ui' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' -import { Translate } from 'react-redux-i18n' -import theme from 'styles/themes/default' -import BackButton from '../../components/BackButton/BackButton' -import styles from '../../components/stylesLoginPage' -import Warning from '../../components/Warning/Warning' +import { push } from 'react-router-redux' +import { connect } from 'react-redux' +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' +import { initMnemonicPage, navigateToConfirmMnemonicPage } from '@chronobank/login/redux/network/actions' + +import PrintIcon from 'assets/img/icons/print-white.svg' + import './GenerateMnemonic.scss' -import { Button } from '../../settings' -class GenerateMnemonic extends PureComponent { - static propTypes = { - onBack: PropTypes.func, +function mapStateToProps (state, ownProps) { + + return { + mnemonic: state.get('network').newAccountMnemonic, } +} - constructor () { - super() - this.state = { - isConfirmed: false, - mnemonicKey: mnemonicProvider.generateMnemonic(), - } +function mapDispatchToProps (dispatch, ownProps) { + return { + initMnemonicPage: () => dispatch(initMnemonicPage()), + navigateToConfirmPage: () => dispatch(navigateToConfirmMnemonicPage()), } +} - componentWillMount () { - this.setState({ mnemonicKey: mnemonicProvider.generateMnemonic() }) +@connect(mapStateToProps, mapDispatchToProps) +export default class MnemonicPage extends Component { + static propTypes = { + mnemonic: PropTypes.string, + initMnemonicPage: PropTypes.func, + navigateToConfirmPage: PropTypes.func, } - componentWillUnmount () { - this.setState({ mnemonicKey: '' }) + static defaultProps = { + mnemonic: '', } - handleCheckClick = (target, value) => { - this.setState({ isConfirmed: value }) + componentDidMount(){ + this.props.initMnemonicPage() } - render () { - const { isConfirmed, mnemonicKey } = this.state + navigateToConfirmPage(){ + this.props.navigateToConfirmPage() + } + render () { return ( -
    - this.props.onBack()} - to='loginWithMnemonic' - /> - -
    -
    -
    -
    {mnemonicKey}
    + +
    +
    +
    Write down back-up phrase
    + +

    + You can use this phrase to login and access your wallet, + even if you forgot your password. You may also print the key + which will be provided with a QR code. Use this QR code to + scan on phone on ChronoWallet recover page. +

    + +
    +
    { this.props.mnemonic }
    +
    +
    {}}> + +
    +
    +
    + +
    +
    Important! Read the security guidelines
    + +
      +
    1. +

      + Don't share your back-up phrase (mnemonic key) with someone you don't trust. +  Double check services you're giving your mnemonic to and don't share your phrase with anyone. +

      +
    2. + +
    3. +

      + Don't loose your back-up phrase (mnemonic key). +  We do not store this information and Your account will be lost + together with all your funds and history. +

      +
    4. +
    -
    - +
    -
    - } - checked={isConfirmed} - {...styles.checkbox} - /> -
    +
    + +
    +
    +
    +
    - -
    +
    + ) } } - -export default GenerateMnemonic diff --git a/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.scss b/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.scss index 839abf173..c7d7994be 100644 --- a/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.scss +++ b/packages/login-ui/components/GenerateMnemonic/GenerateMnemonic.scss @@ -4,45 +4,164 @@ */ @import "~styles/partials/variables"; +@import "~styles/partials/mixins"; -.root { - background-color: white; - border-radius: 2px; - color: $color-primary-0; - padding: 16px; - margin-top: 16px; - box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.24), 0 0 8px 0 rgba(0, 0, 0, 0.12); +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + padding: 0 20px; + } } -.keyBox { - background-color: $color-primary-light; - padding: 7px 8px 11px; +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; } -.keyLabel { - opacity: 0.6; - font-size: 12px; +.description { + margin-bottom: 30px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; + + @include md-only { + margin-bottom: 20px; + } } -.keyValue { - font-size: 18px; - font-weight: 600; - margin-top: 7px; +.passPhraseWrapper { + position: relative; + margin-bottom: 20px; + background: $background-color-1; + min-height: 100px; + border-radius: 2px; + padding: 30px 60px 20px 30px; + + @include md-only { + margin-bottom: 0; + } } -.message { - font-size: 14px; - margin-top: 16px; +.passPhrase { + font-size: 16px; + font-weight: 700; + line-height: 24px; + color: $button-color-2; + text-align: left; + min-height: 46px; + width: 100%; + + @include md-only { + min-height: 55px; + } +} + + +.progressBlock { + width: 50px; + margin: 0 auto 20px; + display: flex; + justify-content: space-between; +} + +.progressPoint { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progressPointInactive { + background: $color-blue-4; + } } .actions { - margin-top: 32px; + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.infoBlock { + text-align: left; + padding: 32px 30px 10px; + border-top: 5px solid #FF6D6B; + background: $background-color-1; + border-radius: 3px; + margin-bottom: 40px; + + @include md-only { + border-radius: 0; + } +} + +.infoBlockList { + font-weight: bold; + font-size: 16px; + margin-left: 15px; +} + +.listItemContent { + font-weight: 400; + color: #A3A3CC; + line-height: 22px; + margin-bottom: 20px; + + b { + color: #fff; + font-weight: bold; + } +} + +.infoBlockHeader { + color: #FF6D6B; + font-weight: 700; + font-size: 20px; + margin-bottom: 20px; +} + +.printButtonWrapper { + position: absolute; + right: 0; + width: 45px; + top: 0; + bottom: 0; display: flex; + justify-content: flex-start; align-items: center; - justify-content: flex-end; } -.actionConfirm { - margin-right: 40px; - flex-grow: 1; -} \ No newline at end of file +.printButton { + width: 24px; + opacity: 0.25; + cursor: pointer; + + &:hover { + opacity: 1; + } +} + diff --git a/packages/login-ui/components/GenerateWallet/GenerateWallet.js b/packages/login-ui/components/GenerateWallet/GenerateWallet.js index e97f864fb..b7407454b 100644 --- a/packages/login-ui/components/GenerateWallet/GenerateWallet.js +++ b/packages/login-ui/components/GenerateWallet/GenerateWallet.js @@ -3,131 +3,81 @@ * Licensed under the AGPL Version 3 license. */ -import walletGenerator from '@chronobank/login/network/walletGenerator' -import { addError, clearErrors } from '@chronobank/login/redux/network/actions' -import download from 'js-file-download' -import { Checkbox, MuiThemeProvider, TextField } from 'material-ui' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import BackButton from '../../components/BackButton/BackButton' -import styles from '../../components/stylesLoginPage' -import Warning from '../../components/Warning/Warning' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { Button } from 'components' +import { + downloadWallet, +} from 'redux/persistAccount/actions' +import { + navigateToLoginPage, +} from '@chronobank/login/redux/network/actions' + +import Wallet from 'assets/img/icons/wallet-white.svg' + import './GenerateWallet.scss' -import { Button } from '../../settings' -const initialState = { - password: '', - isWarningSuppressed: false, - walletJSON: null, - isDownloaded: false, +function mapDispatchToProps (dispatch) { + return { + downloadWallet: () => dispatch(downloadWallet()), + navigateToLoginPage: () => dispatch(navigateToLoginPage()), + } } -const mapDispatchToProps = (dispatch) => ({ - addError: (error) => dispatch(addError(error)), - clearErrors: () => dispatch(clearErrors()), -}) - @connect(null, mapDispatchToProps) -class GenerateWallet extends PureComponent { +export default class MnemonicPage extends Component { static propTypes = { - onBack: PropTypes.func.isRequired, - addError: PropTypes.func, - clearErrors: PropTypes.func, - } - - constructor (props, context, updater) { - super(props, context, updater) - - // TODO replace with async arrow when class properties will work correctly - this.handleGenerateWalletClick = this.handleGenerateWalletClick.bind(this) - - this.state = { - ...initialState, - } - } - - handlePasswordChange = (target, value) => { - this.setState({ password: value }) - } - - handleWarningCheck = (target, value) => { - this.setState({ isWarningSuppressed: value }) - } - - async handleGenerateWalletClick () { - this.props.clearErrors() - try { - if (!this.state.walletJSON) { - // create new instance - const walletJSON = await walletGenerator.getWallet(this.state.password) - this.setState({ - walletJSON, - password: '', - }) - } - - const wallet = this.state.walletJSON - download(JSON.stringify(wallet), `${wallet.id}.dat`) - this.setState({ - isDownloaded: true, - }) - } catch (e) { - this.props.addError(e.message) - } + downloadWallet: PropTypes.func, + navigateToLoginPage: PropTypes.func, } render () { - const { password, isWarningSuppressed, isDownloaded } = this.state - const isPasswordValid = password.length > 8 + const { downloadWallet, navigateToLoginPage } = this.props return ( -
    - - -
    - {!isDownloaded ? ( -
    -
    - } - onChange={this.handlePasswordChange} - type='password' - value={password} - errorText={!isPasswordValid && } - fullWidth - /> - -
    - ) : ( -
    - )} + +
    +
    +
    Download a Wallet File
    + +

    + You can use this wallet file in password recovery option to + make your account available in another browser, for example. + The file is protected by the same password as your created before. +

    +
    - {!isDownloaded && ( -
    - } - onCheck={this.handleWarningCheck} - checked={isWarningSuppressed} - {...styles.checkbox} - /> -
    - )} + + +
    + +
    +
    +
    +
    - -
    +
    + ) } } - -export default GenerateWallet diff --git a/packages/login-ui/components/GenerateWallet/GenerateWallet.scss b/packages/login-ui/components/GenerateWallet/GenerateWallet.scss index d072c92e4..ed40e91e3 100644 --- a/packages/login-ui/components/GenerateWallet/GenerateWallet.scss +++ b/packages/login-ui/components/GenerateWallet/GenerateWallet.scss @@ -6,39 +6,104 @@ @import "~styles/partials/variables"; @import "~styles/partials/mixins"; -.root { - background-color: white; - border-radius: 2px; - color: $color-primary-0; - padding: 16px; - margin-top: 16px; - box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.24), 0 0 8px 0 rgba(0, 0, 0, 0.12); +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + padding: 0 20px; + } } -.hint { - font-size: 18px; - margin-top: 5px; +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; } -.actions { - display: flex; - align-items: center; - margin-top: 32px; - justify-content: flex-end; +.description { + margin-bottom: 50px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; @include md-only { - flex-wrap: wrap; + margin-bottom: 20px; } } -.actions { - margin-top: 32px; +.progress-block { + width: 50px; + margin: 0 auto 20px; display: flex; - align-items: center; - justify-content: flex-end; + justify-content: space-between; +} + +.progress-point { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progress-point-inactive { + background: $color-blue-4; + } +} + +.row { + margin: 0 auto 50px; } -.actionConfirm { - margin-right: 40px; - flex-grow: 1; -} \ No newline at end of file +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.wallet-img { + width: 48px; + height: 48px; + display: block; + margin: 0 auto; +} + +.button { + text-align: center; + margin-bottom: 50px; + + button { + font-size: 14px; + text-transform: none; + width: 110px; + height: 100px; + padding: 0; + border-radius: 3px; + } + + img { + width: 50px; + margin-bottom: 5px; + } +} diff --git a/packages/login-ui/components/LoginForm/LoginForm.js b/packages/login-ui/components/LoginForm/LoginForm.js index c150f63ec..a0a2945bd 100644 --- a/packages/login-ui/components/LoginForm/LoginForm.js +++ b/packages/login-ui/components/LoginForm/LoginForm.js @@ -3,24 +3,26 @@ * Licensed under the AGPL Version 3 license. */ -import networkService from '@chronobank/login/network/NetworkService' -import { clearErrors, DUCK_NETWORK } from '@chronobank/login/redux/network/actions' -import { MD5 } from 'crypto-js' -import LocaleDropDown from 'layouts/partials/LocaleDropDown/LocaleDropDown' -import { MuiThemeProvider } from 'material-ui' -import { yellow800 } from 'material-ui/styles/colors' -import WarningIcon from 'material-ui/svg-icons/alert/warning' import PropTypes from 'prop-types' -import React, { Component } from 'react' +import { MuiThemeProvider, CircularProgress } from 'material-ui' +import React, { PureComponent } from 'react' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' import { connect } from 'react-redux' import { Translate } from 'react-redux-i18n' -import { login } from 'redux/session/actions' -import inverted from 'styles/themes/inversed' -import LoginWithOptions from '../LoginWithOptions/LoginWithOptions' -import AutomaticProviderSelector from '../ProviderSelectorSwitcher/AutomaticProviderSelector' -import ManualProviderSelector from '../ProviderSelectorSwitcher/ManualProviderSelector' - -import './LoginPage.scss' +import { UserRow, Button } from 'components' +import { + onSubmitLoginForm, + onSubmitLoginFormFail, + initLoginPage, + navigateToSelectWallet, +} from '@chronobank/login/redux/network/actions' +import AutomaticProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/AutomaticProviderSelector' +import ManualProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/ManualProviderSelector' + +import styles from 'layouts/Splash/styles' +import './LoginForm.scss' const STRATEGY_MANUAL = 'manual' const STRATEGY_AUTOMATIC = 'automatic' @@ -30,76 +32,53 @@ const nextStrategy = { [STRATEGY_MANUAL]: STRATEGY_AUTOMATIC, } -const mapStateToProps = (state) => { - const network = state.get(DUCK_NETWORK) +export const FORM_LOGIN_PAGE = 'FormLoginPage' + +function mapStateToProps (state, ownProps) { + return { - errors: network.errors, - selectedAccount: network.selectedAccount, - selectedProviderId: network.selectedProviderId, - selectedNetworkId: network.selectedNetworkId, - isLoading: network.isLoading, + selectedWallet: state.get('persistAccount').selectedWallet, + isLoginSubmitting: state.get('network').isLoginSubmitting, } } -const mapDispatchToProps = (dispatch) => ({ - checkNetwork: () => networkService.checkNetwork(), - createNetworkSession: (account, provider, network) => networkService.createNetworkSession(account, provider, network), - login: (account) => dispatch(login(account)), - clearErrors: () => dispatch(clearErrors()), -}) +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + const password = values.get('password') + + await dispatch(onSubmitLoginForm(password)) + }, + onSubmitFail: () => dispatch(onSubmitLoginFormFail()), + initLoginPage: () => dispatch(initLoginPage()), + navigateToSelectWallet: () => dispatch(navigateToSelectWallet()), + } +} -@connect(mapStateToProps, mapDispatchToProps) -class LoginForm extends Component { +class LoginPage extends PureComponent { static propTypes = { - clearErrors: PropTypes.func, - checkNetwork: PropTypes.func, - createNetworkSession: PropTypes.func, - login: PropTypes.func, - selectedAccount: PropTypes.string, - selectedProviderId: PropTypes.number, - selectedNetworkId: PropTypes.number, - errors: PropTypes.arrayOf(PropTypes.string), + initLoginPage: PropTypes.func, + navigateToSelectWallet: PropTypes.func, + isLoginSubmitting: PropTypes.bool, } - constructor (props, context, updater) { - super(props, context, updater) + constructor(props){ + super(props) - // TODO replace with async arrow when class properties will work correctly - this.handleLogin = this.handleLogin.bind(this) this.state = { isShowProvider: true, strategy: STRATEGY_AUTOMATIC, } } - async handleLogin () { - this.props.clearErrors() - const isPassed = await this.props.checkNetwork( - this.props.selectedAccount, - this.props.selectedProviderId, - this.props.selectedNetworkId, - ) - if (isPassed) { - this.props.createNetworkSession( - this.props.selectedAccount, - this.props.selectedProviderId, - this.props.selectedNetworkId, - ) - this.props.login(this.props.selectedAccount) - } + componentWillMount(){ + this.props.initLoginPage() } handleToggleProvider = (isShowProvider) => this.setState({ isShowProvider }) handleSelectorSwitch = (currentStrategy) => this.setState({ strategy: nextStrategy[currentStrategy] }) - renderError = (error) => ( -
    -
    -
    {error}
    -
    - ) - renderProviderSelector () { switch (this.state.strategy) { case STRATEGY_MANUAL: @@ -131,34 +110,60 @@ class LoginForm extends Component { } render () { - const { - errors, - } = this.props + const { handleSubmit, pristine, valid, initialValues, isImportMode, onSubmit, selectedWallet, + navigateToSelectWallet, isLoginSubmitting } = this.props return ( - -
    -
    -
    - {this.renderProviderSelector()} - - {errors && ( -
    - {errors.map(this.renderError)} + +
    + +
    Log In
    + +
    + { this.renderProviderSelector() } +
    + +
    + + +
    +
    - )} -
      -
    • - -
    • -
    -
    + +
    +
    +
    + + ) } } -export default LoginForm +const form = reduxForm({ form: FORM_LOGIN_PAGE })(LoginPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/LoginForm/LoginForm.scss b/packages/login-ui/components/LoginForm/LoginForm.scss new file mode 100644 index 000000000..8514b0650 --- /dev/null +++ b/packages/login-ui/components/LoginForm/LoginForm.scss @@ -0,0 +1,96 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.selector { + color: white; +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/packages/login-ui/components/LoginForm/LoginPage.scss b/packages/login-ui/components/LoginForm/LoginPage.scss deleted file mode 100644 index 258a65c56..000000000 --- a/packages/login-ui/components/LoginForm/LoginPage.scss +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright 2017–2018, LaborX PTY - * Licensed under the AGPL Version 3 license. - */ - -@import "~styles/partials/variables"; -@import "~styles/partials/mixins"; - -.form { - color: white; - font-weight: 400; - margin: 0 20px; -} - -.title { - font-size: 34px; - margin-bottom: 10px; - font-weight: 500; -} - -.subtitle { - font-size: 18px; - opacity: 0.6; - margin-bottom: 25px; - font-weight: 300; -} - -.errors { - margin-top: 5px; -} - -.error { - display: flex; - align-items: center; - margin-top: 20px; -} - -.errorIcon { - max-width: 30px; - flex-grow: 0; - margin-right: 20px; -} - -.errorText { - flex-grow: 1; - font-weight: 300; - line-height: 20px; -} - -ul.actions { - position: absolute; - top: 30px; - right: 80px; - display: flex; - justify-content: flex-end; - flex: 0 0 auto; - padding: 0; - list-style: none; - font-size: 18px; - line-height: 48px; - - @include md-only { - top: 10px; - right: 10px; - flex: 1 1 auto; - } - - li { - - flex: 0 0 auto; - - a { - padding: 10px 20px; - text-decoration: none; - color: $color-white; - - &:hover { - text-decoration: underline; - } - } - } -} - -input:-webkit-autofill, -input:-webkit-autofill:hover, -input:-webkit-autofill:focus, -input:-webkit-autofill:active { - -webkit-animation: autofill 0s forwards; - animation: autofill 0s forwards; -} - -@keyframes autofill { - 100% { - background: transparent; - color: $color-primary-3; - } -} - -@-webkit-keyframes autofill { - 100% { - background: transparent; - color: $color-primary-3; - } -} - -input[type="password"] { - box-shadow: none; -} diff --git a/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.js b/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.js index 3dc4f403e..831e1f88d 100644 --- a/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.js +++ b/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.js @@ -3,118 +3,73 @@ * Licensed under the AGPL Version 3 license. */ -import mnemonicProvider from '@chronobank/login/network/mnemonicProvider' -import { CircularProgress, TextField } from 'material-ui' -import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' import React, { PureComponent } from 'react' import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import BackButton from '../../components/BackButton/BackButton' -import styles from '../../components/stylesLoginPage' -import { Button } from '../../settings' +import { reduxForm, Field } from 'redux-form/immutable' +import { Link } from 'react-router' +import { TextField } from 'redux-form-material-ui' +import styles from 'layouts/Splash/styles' +import { Button } from 'components' +import { + onSubmitMnemonicLoginForm, + onSubmitMnemonicLoginFormSuccess, + onSubmitMnemonicLoginFormFail, +} from '@chronobank/login/redux/network/actions' import './LoginWithMnemonic.scss' -const mapStateToProps = (state) => ({ - isLoading: state.get('network').isLoading, -}) +export const FORM_MNEMONIC_LOGIN_PAGE = 'FormMnemonicLoginPage' -@connect(mapStateToProps, null) -class LoginWithMnemonic extends PureComponent { - static propTypes = { - onLogin: PropTypes.func.isRequired, - onBack: PropTypes.func.isRequired, - onGenerate: PropTypes.func.isRequired, - isLoading: PropTypes.bool, - } - - constructor (props) { - super(props) - this.state = { - mnemonicKey: '', - isValidated: false, - } - } - - componentWillMount () { - // use it for tests - // address: 0x13f219bbb158a49b3e09505fccc333916f11bacb - // this.setState({ - // mnemonicKey: 'leave plate clog interest recall distance actor gun flash cupboard ritual hold', - // isValidated: true - // }) - this.setState({ mnemonicKey: '' }) +function mapDispatchToProps (dispatch) { + return { + onSubmit: (values) => { + const confirmMnemonic = values.get('mnemonic') + dispatch(onSubmitMnemonicLoginForm(confirmMnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitMnemonicLoginFormSuccess()), + onSubmitFail: () => dispatch(onSubmitMnemonicLoginFormFail()), } +} - componentWillUnmount () { - this.setState({ mnemonicKey: '' }) - } +class LoginWithMnemonic extends PureComponent { + render () { + const { handleSubmit } = this.props - handleMnemonicBlur = () => { - this.setState({ mnemonicKey: this.mnemonicKey.getValue().trim() }) - } + return ( + +
    - handleMnemonicChange = () => { - const mnemonicKey = this.mnemonicKey.getValue() - const isValidated = mnemonicProvider.validateMnemonic(mnemonicKey.trim()) - this.setState({ mnemonicKey, isValidated }) - } +
    Mnemonic form
    - render () { - const { isLoading } = this.props - const { mnemonicKey, isValidated } = this.state +
    + +
    - return ( -
    - this.props.onBack()} - to='options' - /> -
    this.mnemonicKey.focus()}> - { - this.mnemonicKey = input - }} - floatingLabelText={} - value={mnemonicKey} - onChange={this.handleMnemonicChange} - onBlur={this.handleMnemonicBlur} - errorText={(isValidated || mnemonicKey === '') ? '' : } - multiLine - fullWidth - disabled={isLoading} - {...styles.textField} - /> -
    -
    -
    +
    + or  + back
    -
    -
    -
    -
    + + + ) } } -export default LoginWithMnemonic +const form = reduxForm({ form: FORM_MNEMONIC_LOGIN_PAGE })(LoginWithMnemonic) +export default connect(null, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.scss b/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.scss index 903e4a153..6d524ee18 100644 --- a/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.scss +++ b/packages/login-ui/components/LoginWithMnemonic/LoginWithMnemonic.scss @@ -4,36 +4,91 @@ */ @import "~styles/partials/variables"; -@import '../LoginPageShared'; +@import "~styles/partials/mixins"; -$gutter: 16px; +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; -.root { - margin-top: 24px; + &:hover { + text-decoration: underline; + } + } + } } -.generateIcon { - @extend %buttonIcon; - display: inline-block; - width: 16px; - height: 16px; +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } } -.actions { - display: flex; - margin: 25px (-$gutter/2) 0; +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; } -.action { - margin: 0 ($gutter/2); - width: calc(50% - 16px); - flex-grow: 1; - display: flex; - justify-content: flex-end; +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; } -.whiteButton { - button { - color: $color-white; +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; } -} \ No newline at end of file + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/packages/login-ui/components/LoginWithOptions/LoginWithOptions.js b/packages/login-ui/components/LoginWithOptions/LoginWithOptions.js index ac4c29968..b83ffd613 100644 --- a/packages/login-ui/components/LoginWithOptions/LoginWithOptions.js +++ b/packages/login-ui/components/LoginWithOptions/LoginWithOptions.js @@ -3,402 +3,134 @@ * Licensed under the AGPL Version 3 license. */ -import ledgerProvider from '@chronobank/login/network/LedgerProvider' -import mnemonicProvider from '@chronobank/login/network/mnemonicProvider' -import networkService from '@chronobank/login/network/NetworkService' -import privateKeyProvider from '@chronobank/login/network/privateKeyProvider' -import { LOCAL_PRIVATE_KEYS } from '@chronobank/login/network/settings' -import trezorProvider from '@chronobank/login/network/TrezorProvider' -import walletProvider from '@chronobank/login/network/walletProvider' -import { loginLedger } from '@chronobank/login/redux/ledger/actions' -import { addError, clearErrors, loading, NETWORK_SET_TEST_MNEMONIC, NETWORK_SET_TEST_WALLET_FILE } from '@chronobank/login/redux/network/actions' -import { loginTrezor } from '@chronobank/login/redux/trezor/actions' -import pascalCase from 'pascal-case' import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' import React, { PureComponent } from 'react' +import { Link } from 'react-router' import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import GenerateMnemonic from '../../components/GenerateMnemonic/GenerateMnemonic' -import GenerateWallet from '../../components/GenerateWallet/GenerateWallet' -import LoginLocal from '../../components/LoginLocal/LoginLocal' -import LoginMetamask from '../../components/LoginMetamask/LoginMetamask' -import LoginUPort from '../../components/LoginUPort/LoginUPort' -import LoginLedger from '../../components/LoginWithLedger/LoginWithLedger' -import LoginWithMnemonic from '../../components/LoginWithMnemonic/LoginWithMnemonic' -import LoginWithPrivateKey from '../../components/LoginWithPrivateKey/LoginWithPrivateKey' -import LoginTrezor from '../../components/LoginWithTrezor/LoginWithTrezor' -import LoginWithWallet from '../../components/LoginWithWallet/LoginWithWallet' - +import { Button } from 'components' + +import { + navigateToMnemonicImportMethod, + navigateToPrivateKeyImportMethod, + navigateToCreateAccount, + initImportMethodsPage, + navigateToCreateAccountWithoutImport, +} from '@chronobank/login/redux/network/actions' + +import Trezor from 'assets/img/icons/trezor-white.svg' +import Ledger from 'assets/img/icons/ledger-nano-white.svg' +import Plugin from 'assets/img/icons/plugin-white.svg' +import Mnemonic from 'assets/img/icons/mnemonic-white.svg' +import Key from 'assets/img/icons/key-white.svg' +import Wallet from 'assets/img/icons/wallet-white.svg' +import Uport from 'assets/img/icons/uport.svg' + +// import styles from 'layouts/Splash/styles' import './LoginWithOptions.scss' -export const STEP_SELECT_OPTION = 'step/SELECT_OPTION' -export const STEP_GENERATE_WALLET = 'step/GENERATE_WALLET' -export const STEP_GENERATE_MNEMONIC = 'step/GENERATE_MNEMONIC' -export const STEP_LOGIN_WITH_MNEMONIC = 'step/LOGIN_WITH_MNEMONIC' - -const STEP_LOGIN_WITH_WALLET = 'step/LOGIN_WITH_WALLET' -const STEP_LOGIN_WITH_PRIVATE_KEY = 'step/LOGIN_WITH_PRIVATE_KEY' -const STEP_LOGIN_WITH_LEDGER = 'step/LOGIN_WITH_LEDGER' -const STEP_LOGIN_WITH_TREZOR = 'step/LOGIN_WITH_TREZOR' -const STEP_LOGIN_WITH_METAMASK = 'step/LOGIN_WITH_METAMASK' -const STEP_LOGIN_WITH_UPORT = 'step/LOGIN_WITH_UPORT' -const STEP_LOGIN_LOCAL = 'step/LOGIN_LOCAL' - -const T = () => true - -const loginOptions = [ - { - nextStep: STEP_LOGIN_WITH_MNEMONIC, - title: 'LoginWithOptions.mnemonicKey', - showOption: T, - }, - { - nextStep: STEP_LOGIN_WITH_WALLET, - title: 'LoginWithOptions.walletFile', - showOption: T, - }, - { - nextStep: STEP_LOGIN_WITH_PRIVATE_KEY, - title: 'LoginWithOptions.privateKey', - showOption: T, - }, - { - nextStep: STEP_LOGIN_WITH_LEDGER, - title: 'LoginWithOptions.ledgerNano', - showOption: T, - }, - { - nextStep: STEP_LOGIN_WITH_TREZOR, - title: 'LoginWithOptions.trezor', - showOption: T, - }, - { - nextStep: STEP_LOGIN_WITH_METAMASK, - title: 'LoginWithOptions.metamask', - showOption: ({ isMetamask }) => isMetamask, - }, - { - nextStep: STEP_LOGIN_WITH_UPORT, - title: 'LoginWithOptions.uport', - showOption: T, - }, - { - nextStep: STEP_LOGIN_LOCAL, - title: 'LoginWithOptions.local', - showOption: ({ isLocal }) => isLocal, - }, -] - -class LoginOption extends PureComponent { - static propTypes = { - option: PropTypes.shape({ - title: PropTypes.string.isRequired, - nextStep: PropTypes.string.isRequired, - }).isRequired, - onClick: PropTypes.func, - } - - handleClick = () => this.props.onClick(this.props.option.nextStep) - - render () { - return ( -
    -
    -
    arrow_forward
    -
    - ) +function mapDispatchToProps (dispatch) { + return { + navigateToMnemonicImportMethod: () => dispatch(navigateToMnemonicImportMethod()), + navigateToPrivateKeyImportMethod: () => dispatch(navigateToPrivateKeyImportMethod()), + navigateToCreateAccount: () => dispatch(navigateToCreateAccount()), + navigateToCreateAccountWithoutImport: () => dispatch(navigateToCreateAccountWithoutImport()), + initImportMethodsPage: () => dispatch(initImportMethodsPage()), } } -const mapStateToProps = (state) => ({ - wallets: state.get('multisigWallet'), - selectedNetworkId: state.get('network').selectedNetworkId, - accounts: state.get('network').accounts, - isLocal: state.get('network').isLocal, - isMetamask: state.get('network').isMetamask, -}) - -const mapDispatchToProps = (dispatch) => { - return ({ - addError: (error) => dispatch(addError(error)), - clearErrors: () => dispatch(clearErrors()), - loading: () => dispatch(loading()), - loginLedger: () => loginLedger(), - loginTrezor: () => loginTrezor(), - setIsMnemonic: () => dispatch({ type: NETWORK_SET_TEST_MNEMONIC }), - setIsWalletFile: () => dispatch({ type: NETWORK_SET_TEST_WALLET_FILE }), - }) -} - -@connect(mapStateToProps, mapDispatchToProps) -class LoginWithOptions extends PureComponent { +@connect(null, mapDispatchToProps) +export default class ImportMethodsPage extends PureComponent { static propTypes = { - isLocal: PropTypes.bool, - isMetamask: PropTypes.bool, - accounts: PropTypes.arrayOf(PropTypes.string), - onLogin: PropTypes.func, - addError: PropTypes.func, - clearErrors: PropTypes.func, - onToggleProvider: PropTypes.func, - selectedNetworkId: PropTypes.number, - loading: PropTypes.func, - loginLedger: PropTypes.func, - loginTrezor: PropTypes.func, - setIsMnemonic: PropTypes.func, - setIsWalletFile: PropTypes.func, - } - - constructor (props, context, updater) { - super(props, context, updater) - this.state = { - step: STEP_SELECT_OPTION, - } - } - - handleMnemonicLogin = (mnemonicKey) => { - this.props.loading() - this.props.clearErrors() - const provider = mnemonicProvider.getMnemonicProvider(mnemonicKey, networkService.getProviderSettings()) - networkService.selectAccount(provider.ethereum.getAddress()) - this.props.setIsMnemonic() - this.setupAndLogin(provider) - } - - handlePrivateKeyLogin = (privateKey) => { - this.props.loading() - this.props.clearErrors() - try { - const provider = privateKeyProvider.getPrivateKeyProvider(privateKey, networkService.getProviderSettings(), this.props.wallets) - networkService.selectAccount(provider.ethereum.getAddress()) - this.setupAndLogin(provider) - } catch (e) { - this.props.addError(e.message) - } - } - - handleLoginLocal = (account) => { - this.props.clearErrors() - try { - const index = Math.max(this.props.accounts.indexOf(account), 0) - const provider = privateKeyProvider.getPrivateKeyProvider(LOCAL_PRIVATE_KEYS[index], networkService.getProviderSettings(), this.props.wallets) - this.setupAndLogin(provider) - } catch (e) { - this.props.addError(e.message) - } + navigateToMnemonicImportMethod: PropTypes.func, + navigateToPrivateKeyImportMethod: PropTypes.func, + initImportMethodsPage: PropTypes.func, + navigateToCreateAccountWithoutImport: PropTypes.func, } - handleLedgerLogin = () => { - this.props.loading() - this.props.clearErrors() - try { - ledgerProvider.setupAndStart(networkService.getProviderURL()) - const provider = ledgerProvider.getNetworkProvider(networkService.getProviderSettings()) - this.setupAndLogin(provider) - } catch (e) { - this.props.addError(e.message) - } + componentWillMount(){ + this.props.initImportMethodsPage() } - handleTrezorLogin = () => { - this.props.loading() - this.props.clearErrors() - try { - trezorProvider.setupAndStart(networkService.getProviderURL()) - const provider = trezorProvider.getNetworkProvider(networkService.getProviderSettings()) - this.setupAndLogin(provider) - } catch (e) { - this.props.addError(e.message) - } - } - - handleWalletUpload = (wallet, password) => { - this.props.loading() - this.props.clearErrors() - try { - const provider = walletProvider.getProvider(wallet, password, networkService.getProviderSettings()) - networkService.selectAccount(provider.ethereum.getAddress()) - this.props.setIsWalletFile() - this.setupAndLogin(provider) - } catch (e) { - this.props.addError(e.message) - } - } - - handleChangeOption = (step) => { - this.props.clearErrors() - this.setStep(step) - } - - handleSelectStepSelectOption = () => this.handleChangeOption(STEP_SELECT_OPTION) + handleMnemonicLogin = () => this.props.navigateToMnemonicImportMethod() - handleSelectStepGenerateWallet = () => this.handleChangeOption(STEP_GENERATE_WALLET) + handlePrivateKeyLogin = () => this.props.navigateToPrivateKeyImportMethod() - handleSelectStepGenerateMnemonic = () => this.handleChangeOption(STEP_GENERATE_MNEMONIC) + handleWalletFileLogin = () => {} - handleSelectStepLoginWithMnemonic = () => this.handleChangeOption(STEP_LOGIN_WITH_MNEMONIC) - - handleSelectStepLoginWithWallet = () => this.handleChangeOption(STEP_LOGIN_WITH_WALLET) - - handleToggleProvider (step) { - this.props.onToggleProvider(step !== STEP_GENERATE_WALLET && step !== STEP_GENERATE_MNEMONIC) - } - - async setupAndLogin (provider) { - try { - await networkService.setup(provider) - this.props.onLogin() - } catch (e) { - // eslint-disable-next-line - console.error('login error', e.message) - } - } - - setStep (step) { - this.setState({ step }) - this.handleToggleProvider(step) - } - - checkOption = (option) => { - return option.showOption(this.props) - } - - renderOption = (option) => ( - - ) - - renderOptions () { - return loginOptions - .filter(this.checkOption) - .map(this.renderOption) - } - - renderStep (step) { - const renderer = `render${pascalCase(step)}` - return this[renderer] ? this[renderer]() : null - } - - renderStepSelectOption () { - const { selectedNetworkId } = this.props - - if (!selectedNetworkId) { - return null - } - - return ( -
    -
    {}
    -
    {this.renderOptions()}
    -
    - ) - } - - renderStepLoginWithMnemonic () { - return ( - - ) - } - - renderStepLoginWithUport () { - return ( - - ) - } - - renderStepLoginWithWallet () { - return ( - - ) - } - - renderStepLoginWithPrivateKey () { - return ( - - ) - } - - renderStepGenerateWallet () { - return ( - - ) - } - - renderStepGenerateMnemonic () { - return ( - - ) - } - - renderStepLoginWithLedger () { - return ( - - ) - } - - renderStepLoginWithTrezor () { - return ( - - ) - } - - renderStepLoginWithMetamask () { - return ( - - ) - } - - renderStepLoginLocal () { - return ( - - ) - } + handleCreateAccount = () => this.props.navigateToCreateAccountWithoutImport() render () { - const { step } = this.state - return ( -
    - {this.renderStep(step)} -
    + +
    + +
    Add an Existing Account
    + +
    + + + + + + + + + + + + + +
    + +
    + or
    + Create New Account +
    + +
    +
    ) } } - -export default LoginWithOptions diff --git a/packages/login-ui/components/LoginWithOptions/LoginWithOptions.scss b/packages/login-ui/components/LoginWithOptions/LoginWithOptions.scss index 873e81447..d18a725c9 100644 --- a/packages/login-ui/components/LoginWithOptions/LoginWithOptions.scss +++ b/packages/login-ui/components/LoginWithOptions/LoginWithOptions.scss @@ -6,37 +6,119 @@ @import "~styles/partials/variables"; @import "~styles/partials/mixins"; -.optionBox { +ul.actions { + position: absolute; + top: 30px; + right: 80px; display: flex; - cursor: pointer; - transition: all 0.2s; - padding: 16px 8px 16px 16px; - border-bottom: 1px solid rgba($color-white, 0.1); + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; - &:last-child { - border-bottom: 0; - border-radius: 0 0 $border-radius $border-radius; + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; } - &:first-child { - border-radius: $border-radius $border-radius 0 0; - } + li { - &:hover { - background-color: rgba($color-white, 0.2); + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } } } -.optionTitle { - margin-bottom: 16px; - margin-top: 24px; - opacity: 0.6; +.page { + max-width: 350px; + margin:0 auto; + +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.methods { + margin-bottom: 15px; + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} + +.button { + width: 31%; + margin-bottom: 10px; + + &.button-uport { + display: none; + + @include xs-only { + display: inline-block; + } + } + + &.button-trezor, + &.button-ledger, + &.button-plugin { + @include xs-only { + display: none; + } + } + + button { + width: 100%; + height: 100px; + padding: 0; + text-transform: none; + border-radius: 3px; + } + + img { + width: 50px; + margin-bottom: 5px; + } } -.optionName { - flex-grow: 1; +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; } -.arrow { - opacity: 0.6; +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + cursor: pointer; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } } diff --git a/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.js b/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.js index 0f3253b84..a46af14ea 100644 --- a/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.js +++ b/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.js @@ -3,86 +3,73 @@ * Licensed under the AGPL Version 3 license. */ -import privateKeyProvider from '@chronobank/login/network/privateKeyProvider' -import { CircularProgress, TextField } from 'material-ui' -import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' import React, { PureComponent } from 'react' import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import BackButton from '../../components/BackButton/BackButton' -import styles from '../../components/stylesLoginPage' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import styles from 'layouts/Splash/styles' +import { Button } from 'components' +import { + onSubmitPrivateKeyLoginForm, + onSubmitPrivateKeyLoginFormSuccess, + onSubmitPrivateKeyLoginFormFail, +} from '@chronobank/login/redux/network/actions' import './LoginWithPrivateKey.scss' -import { Button } from '../../settings' -const mapStateToProps = (state) => ({ - isLoading: state.get('network').isLoading, -}) +export const FORM_PRIVATE_KEY_LOGIN_PAGE = 'FormPrivateKeyLoginPage' -@connect(mapStateToProps, null) -class LoginWithPrivateKey extends PureComponent { - static propTypes = { - isLoading: PropTypes.bool, - onBack: PropTypes.func.isRequired, - onLogin: PropTypes.func.isRequired, - } - - constructor () { - super() - this.state = { - privateKey: '', - isValidated: false, - } - } - - handlePrivateKeyChange = () => { - const privateKey = this.privateKey.getValue() - const isValidated = privateKeyProvider.validatePrivateKey(privateKey.trim()) - this.setState({ privateKey, isValidated }) +function mapDispatchToProps (dispatch) { + return { + onSubmit: (values) => { + const privateKey = values.get('pk') + dispatch(onSubmitPrivateKeyLoginForm(privateKey)) + }, + onSubmitSuccess: () => dispatch(onSubmitPrivateKeyLoginFormSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitPrivateKeyLoginFormFail(errors, dispatch, submitErrors)), } +} +class MnemonicLoginPage extends PureComponent { render () { - const { isValidated, privateKey } = this.state - const { isLoading } = this.props + const { handleSubmit } = this.props + return ( -
    -
    - this.props.onBack()} - to='options' - /> -
    - { - this.privateKey = input - }} - floatingLabelText={} - value={privateKey} - onChange={this.handlePrivateKeyChange} - errorText={(isValidated || privateKey === '') ? '' : } - multiLine - fullWidth - spellCheck={false} - {...styles.textField} - /> + +
    -
    -
    -
    -
    + +
    + + or  + back +
    + +
    +
    ) } } -export default LoginWithPrivateKey +const form = reduxForm({ form: FORM_PRIVATE_KEY_LOGIN_PAGE })(MnemonicLoginPage) +export default connect(null, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.scss b/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.scss index 67de2be20..6d524ee18 100644 --- a/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.scss +++ b/packages/login-ui/components/LoginWithPrivateKey/LoginWithPrivateKey.scss @@ -3,18 +3,92 @@ * Licensed under the AGPL Version 3 license. */ -.actions { +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; display: flex; - margin-top: 20px; justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } } -.action { - width: 50%; - display: flex; - justify-content: flex-end; +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; } -.back { - margin-top: 24px; -} \ No newline at end of file +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/packages/login-ui/components/LoginWithWallet/LoginWithWallet.js b/packages/login-ui/components/LoginWithWallet/LoginWithWallet.js index 997d2a574..feee9c663 100644 --- a/packages/login-ui/components/LoginWithWallet/LoginWithWallet.js +++ b/packages/login-ui/components/LoginWithWallet/LoginWithWallet.js @@ -3,192 +3,80 @@ * Licensed under the AGPL Version 3 license. */ -import { clearErrors, loading } from '@chronobank/login/redux/network/actions' -import { CircularProgress, FlatButton, TextField } from 'material-ui' import PropTypes from 'prop-types' -import React, { PureComponent } from 'react' -import { connect } from 'react-redux' -import { Translate } from 'react-redux-i18n' -import BackButton from '../../components/BackButton/BackButton' -import styles from '../../components/stylesLoginPage' -import './LoginWithWallet.scss' -import { Button } from '../../settings' - -const mapStateToProps = (state) => ({ - isLoading: state.get('network').isLoading, -}) +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' + +import FileIcon from 'assets/img/icons/file-white.svg' +import DeleteIcon from 'assets/img/icons/delete-white.svg' +import SpinnerGif from 'assets/img/spinningwheel.gif' +import WarningIcon from 'assets/img/icons/warning.svg' +import CheckIcon from 'assets/img/icons/check-green.svg' -const mapDispatchToProps = (dispatch) => ({ - clearErrors: () => dispatch(clearErrors()), - loading: (isLoading) => dispatch(loading(isLoading)), -}) +import './LoginWithWallet.scss' -@connect(mapStateToProps, mapDispatchToProps) -class LoginWithWallet extends PureComponent { +export default class LoginWithWallet extends Component { static propTypes = { - isLoading: PropTypes.bool, - onBack: PropTypes.func.isRequired, - onGenerate: PropTypes.func.isRequired, - onLogin: PropTypes.func.isRequired, - clearErrors: PropTypes.func, - loading: PropTypes.func, - } - - constructor () { - super() - this.state = { - password: '', - wallet: null, - isUploaded: false, - isUploading: false, - fileName: '', - } - } - - handleFileUploaded = (e) => { - this.props.clearErrors() - this.setState({ - isUploading: false, - isUploaded: true, - wallet: e.target.result, - }) - } - - handleUploadFile = (e) => { - const file = e.target.files[ 0 ] - if (!file) { - return - } - this.setState({ - isUploading: true, - fileName: file.name, - }) - const reader = new FileReader() - reader.onload = this.handleFileUploaded - reader.readAsText(file) - this.props.clearErrors() - } - - handlePasswordChange = (target, value) => { - this.setState({ password: value }) - this.props.clearErrors() - } - - handleEnterPassword = () => { - this.props.clearErrors() - this.forceUpdate() - this.props.onLogin(this.state.wallet, this.state.password) + mnemonic: PropTypes.string, } - handleRemoveWallet = () => { - this.setState({ - wallet: null, - isUploaded: false, - isUploading: false, - fileName: '', - }) - this.walletFileUploadInput.value = '' + static defaultProps = { + mnemonic: '', } render () { - const { isLoading } = this.props - const { - password, isUploading, isUploaded, fileName, - } = this.state - return ( -
    -
    - -
    -
    - - {!isUploaded && !isUploading && ( -
    this.walletFileUploadInput.click()} - > -
    {}
    + +
    +
    +
    Upload a Wallet File
    + +

    + Upload a wallet file to add the login information to your browser. + We provide the file on New Account Creation. +

    + +
    + + + + + + +
    - )} - {isUploading && ( -
    - - {} +
    + + or +
    + Back
    - )} - - {isUploaded && ( -
    -
    attachment
    -
    {fileName}
    -
    delete -
    -
    - )} - - this.walletFileUploadInput = input} - type='file' - styleName='hide' - /> -
    - - } - type='password' - value={password} - onChange={this.handlePasswordChange} - required - fullWidth - {...styles.textField} - /> - - {isLoading && ( -
    - {} -
    - )} -
    -
    -
    -
    -
    -
    + ) } } - -export default LoginWithWallet diff --git a/packages/login-ui/components/LoginWithWallet/LoginWithWallet.scss b/packages/login-ui/components/LoginWithWallet/LoginWithWallet.scss index 106d35416..6eea2ae81 100644 --- a/packages/login-ui/components/LoginWithWallet/LoginWithWallet.scss +++ b/packages/login-ui/components/LoginWithWallet/LoginWithWallet.scss @@ -4,108 +4,105 @@ */ @import "~styles/partials/variables"; -@import '../LoginPageShared'; +@import "~styles/partials/mixins"; -$gutter: 16px; - -// TODO @dkchv: will be moved to mui lib -%mui-button-secondary { - border-radius: $border-radius; - background-color: $color-white; -} - -%mui-button-secondary-content { - padding: 10px; - display: flex; - justify-content: center; - color: $color-blue; - text-transform: uppercase; - font-weight: 500; - font-size: 14px; -} - -.root { - margin-top: 24px; -} +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; -.buttonIcon { - @extend %buttonIcon; - font-size: 16px; - color: $color-primary-1; + @include md-only { + padding: 0 20px; + } } -.hide { - display: none; +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; } -.back { - margin-bottom: 16px; -} +.description { + margin-bottom: 50px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; -.upload { - @extend %mui-button-secondary; - cursor: pointer; + @include md-only { + margin-bottom: 20px; + } } -.uploadContent { - @extend %mui-button-secondary-content; +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; &:hover { - background-color: rgba($color-blue, 0.4); + color: #FFB54E; } -} -.progress { - @extend %mui-button-secondary; - @extend %mui-button-secondary-content; + @include md-only { + margin: 0 0 10px; + } } -.progressText { - margin-left: 10px; +.submit { + margin-bottom: 25px; } -.uploaded { - @extend %mui-button-secondary; - @extend %mui-button-secondary-content; - align-items: center; +.row { + margin-bottom: 50px; } -.walletIcon { - flex-grow: 0; - margin-right: 5px; -} +.button { + button { + display: flex; + align-items: center; + padding: 0 20px; + line-height: 45px; + min-width: 280px; + margin: 0 auto 20px; + text-transform: none; + + &:disabled { + background: $background-color-1; + } + } -.walletName { - flex-grow: 1; -} + &.button-warning button { + background: $color-red-2; + } + .button-text { + flex: 1; + text-align: left; + } -.walletRemove { - flex-grow: 0; - margin-left: 10px; - cursor: pointer; -} + .before-img { + width: 24px; + display: inline-block; + margin-right: 5px; + } -.tip { - margin-top: 10px; - text-align: center; - color: white; - font-weight: 300; + .after-img { + width: 24px; + display: inline-block; + } } .actions { - display: flex; - margin: 24px (-$gutter / 2) 0; + text-align: center; + margin-bottom: 45px; + color: $additionalData-color-1; + line-height: 30px; } -.action { - margin: 0 ($gutter / 2); - width: 50%; - display: flex; - justify-content: flex-end; -} +.submit { + margin-bottom: 25px; -.whiteButton { - button { - color: $color-white; + button:disabled { + background: $background-color-1; } -} \ No newline at end of file +} diff --git a/packages/login-ui/components/NetworkSelector/NetworkSelector.js b/packages/login-ui/components/NetworkSelector/NetworkSelector.js index fc2263b27..0f32607a9 100644 --- a/packages/login-ui/components/NetworkSelector/NetworkSelector.js +++ b/packages/login-ui/components/NetworkSelector/NetworkSelector.js @@ -57,6 +57,7 @@ export default class NetworkSelector extends PureComponent { resolveNetwork = () => { const web3 = new Web3() + console.log('resolve network', this.props.getProviderURL()) web3Provider.reinit(web3, web3Utils.createStatusEngine(this.props.getProviderURL())) web3Provider.resolve() } diff --git a/packages/login-ui/components/RecoverAccount/RecoverAccount.js b/packages/login-ui/components/RecoverAccount/RecoverAccount.js new file mode 100644 index 000000000..a346cb752 --- /dev/null +++ b/packages/login-ui/components/RecoverAccount/RecoverAccount.js @@ -0,0 +1,128 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import { + WalletEntryModel, +} from 'models/persistAccount' +import { + onSubmitRecoverAccountForm, + onSubmitRecoverAccountFormSuccess, + onSubmitRecoverAccountFormFail, + initRecoverAccountPage, +} from '@chronobank/login/redux/network/actions' + +import { Button, UserRow } from 'components' + +import styles from 'layouts/Splash/styles' +import './RecoverAccount.scss' + +export const FORM_RECOVER_ACCOUNT = 'RecoverAccountPage' + +function mapStateToProps (state, ownProps) { + const selectedWallet = state.get('persistAccount').selectedWallet + return { + selectedWallet: new WalletEntryModel({...selectedWallet}), + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + let words = [], mnemonic = '' + + for (let i = 1; i <= 12; i++) { + const word = values.get(`word-${i}`) + word && words.push(word) + } + + mnemonic = words.join(' ') + + await dispatch(onSubmitRecoverAccountForm(mnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitRecoverAccountFormSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitRecoverAccountFormFail(errors, dispatch, submitErrors)), + initRecoverAccountPage: () => dispatch(initRecoverAccountPage()), + } +} + +class RecoverAccountPage extends PureComponent { + static propTypes = { + selectedWallet: PropTypes.instanceOf(WalletEntryModel), + initRecoverAccountPage: PropTypes.func, + } + + componentWillMount(){ + this.props.initRecoverAccountPage() + } + + get getSelectedWalletName(){ + const { selectedWallet } = this.props + return selectedWallet && selectedWallet.name || '' + } + + render () { + const { handleSubmit, selectedWallet } = this.props + + const wordsArray = new Array(12).fill() + + return ( + +
    +
    + Enter mnemonic to reset password +
    + +
    + +
    + +
    + { + wordsArray.map((item, i) => ( + + )) + } +
    + +
    + + or
    + Back +
    + +
    + +
    + ) + } +} + +const form = reduxForm({ form: FORM_RECOVER_ACCOUNT })(RecoverAccountPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/RecoverAccount/RecoverAccount.scss b/packages/login-ui/components/RecoverAccount/RecoverAccount.scss new file mode 100644 index 000000000..ceaa1309e --- /dev/null +++ b/packages/login-ui/components/RecoverAccount/RecoverAccount.scss @@ -0,0 +1,77 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.actions { + text-align: center; + margin-bottom: 45px; + line-height: 30px; + color: #fff; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.button { + text-align: center; + margin-bottom: 22px; + + button { + padding: 0 30px; + } +} + +.actionIcon { + transform: matrix(-1,0,0,-1,0,0); +} + +.user-row { + border-top: 1px solid $color-purpule; + margin-bottom: 10px; + color: #fff; +} + +.fields-block { + margin-bottom: 50px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.field { + width: 23% !important; +} diff --git a/packages/login-ui/components/ResetPassword/ResetPassword.js b/packages/login-ui/components/ResetPassword/ResetPassword.js new file mode 100644 index 000000000..8c0d513ab --- /dev/null +++ b/packages/login-ui/components/ResetPassword/ResetPassword.js @@ -0,0 +1,116 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { + WalletEntryModel, +} from 'models/persistAccount' +import { + onSubmitResetAccountPasswordForm, + onSubmitResetAccountPasswordSuccess, + onSubmitResetAccountPasswordFail, + initResetPasswordPage, +} from '@chronobank/login/redux/network/actions' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import { UserRow, Button } from 'components' + +import styles from 'layouts/Splash/styles' +import validate from './validate' +import './ResetPassword.scss' + +export const FORM_RESET_PASSWORD = 'ResetPasswordPage' + +function mapStateToProps (state, ownProps) { + const selectedWallet = state.get('persistAccount').selectedWallet + return { + selectedWallet: new WalletEntryModel({...selectedWallet}), + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + const password = values.get('password') + + await dispatch(onSubmitResetAccountPasswordForm(password)) + }, + onSubmitSuccess: () => dispatch(onSubmitResetAccountPasswordSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitResetAccountPasswordFail(errors, dispatch, submitErrors)), + initResetPasswordPage: () => dispatch(initResetPasswordPage()), + } +} + +class ResetPasswordPage extends PureComponent { + static propTypes = { + selectedWallet: PropTypes.instanceOf(WalletEntryModel), + initResetPasswordPage: PropTypes.func, + } + + componentWillMount(){ + this.props.initResetPasswordPage() + } + + get getSelectedWalletName(){ + const { selectedWallet } = this.props + return selectedWallet && selectedWallet.name || '' + } + render () { + const { handleSubmit, selectedWallet } = this.props + + return ( + +
    + +
    Reset password
    + +
    + {}} + /> +
    + +
    + + +
    + +
    + +
    + +
    +
    + ) + } +} + +const form = reduxForm({ form: FORM_RESET_PASSWORD, validate })(ResetPasswordPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/packages/login-ui/components/ResetPassword/ResetPassword.scss b/packages/login-ui/components/ResetPassword/ResetPassword.scss new file mode 100644 index 000000000..d1ce6b9bf --- /dev/null +++ b/packages/login-ui/components/ResetPassword/ResetPassword.scss @@ -0,0 +1,92 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/packages/login-ui/components/ResetPassword/validate.js b/packages/login-ui/components/ResetPassword/validate.js new file mode 100644 index 000000000..a23dc2614 --- /dev/null +++ b/packages/login-ui/components/ResetPassword/validate.js @@ -0,0 +1,27 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import ErrorList from 'platform/ErrorList' +import * as validator from 'models/validator' + +const validateEqualPasswords = (password, confirmPassword) => password === confirmPassword ? null : 'Wrong password' + +export default (values) => { + + const password = values.get('password') + + let passwordErrors = new ErrorList() + passwordErrors.add(validator.required(password)) + + const confirmPassword = values.get('confirmPassword') + let confirmPasswordErrors = new ErrorList() + confirmPasswordErrors.add(validator.required(confirmPassword)) + confirmPasswordErrors.add(validateEqualPasswords(password, confirmPassword)) + + return { + password: passwordErrors.getErrors(), + confirmPassword: confirmPasswordErrors.getErrors(), + } +} diff --git a/packages/login-ui/components/SelectWallet/SelectWallet.js b/packages/login-ui/components/SelectWallet/SelectWallet.js new file mode 100644 index 000000000..0929db635 --- /dev/null +++ b/packages/login-ui/components/SelectWallet/SelectWallet.js @@ -0,0 +1,111 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' +import { connect } from 'react-redux' +import React, { PureComponent } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' +import { navigateToSelectImportMethod, onWalletSelect } from '@chronobank/login/redux/network/actions' +import { + WalletEntryModel, +} from 'models/persistAccount' + +import arrow from 'assets/img/icons/prev-white.svg' +import './SelectWallet.scss' + +function mapDispatchToProps (dispatch) { + return { + navigateToSelectImportMethod: () => dispatch(navigateToSelectImportMethod()), + onWalletSelect: (wallet) => dispatch(onWalletSelect(wallet)), + } +} + +function mapStateToProps (state) { + return { + walletsList: state.get('persistAccount').walletsList.map( + (wallet) => new WalletEntryModel({...wallet}) + ), + } +} + +@connect(mapStateToProps, mapDispatchToProps) +export default class SelectWalletPage extends PureComponent { + static propTypes = { + onWalletSelect: PropTypes.func, + walletsList: PropTypes.arrayOf( + PropTypes.instanceOf(WalletEntryModel) + ), + navigateToSelectImportMethod: PropTypes.func, + } + + static defaultProps = { + onWalletSelect: () => {}, + walletsList: [], + } + + renderWalletsList (){ + const { onWalletSelect, walletsList } = this.props + + if (!walletsList || !walletsList.length){ + return ( +
    + Sorry, there are no accounts to display +
    + ) + } + + return ( +
    + { + walletsList ? walletsList.map((w, i) => ( + onWalletSelect(w)} + /> + )) : null + } +
    + ) + } + + render () { + return ( + +
    + +
    My Accounts
    + +
    + Browse account stored on your device. +
    + If you have created an account before you may use Add an Existing Account option below. +
    + +
    + { this.renderWalletsList() } + +
    + + or
    + Create New Account +
    +
    + +
    +
    + ) + } +} diff --git a/packages/login-ui/components/SelectWallet/SelectWallet.scss b/packages/login-ui/components/SelectWallet/SelectWallet.scss new file mode 100644 index 000000000..168f3cf01 --- /dev/null +++ b/packages/login-ui/components/SelectWallet/SelectWallet.scss @@ -0,0 +1,93 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.wrapper { + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.content { + margin: 0 auto; + max-width: 380px; +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.wallets-list { + border-top: 1px solid $color-purpule; + margin-bottom: 45px; + color: #fff; +} + +.empty-list { + border-top: 1px solid $color-purpule; + border-bottom: 1px solid $color-purpule; + padding: 18px 0; + margin-bottom: 30px; + text-align: center; + color: #fff; + width: 380px; + + @include xs-only { + width: 100%; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + line-height: 30px; + color: #fff; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.button { + text-align: center; + margin-bottom: 22px; + + button { + padding: 0 30px; + } +} + +.actionIcon { + transform: matrix(-1,0,0,-1,0,0); +} + +.description { + font-weight: 400; + color: $additionalData-color-1; + font-size: 16px; + line-height: 22px; + margin-bottom: 40px; + text-align: center; +} diff --git a/packages/login-ui/components/index.js b/packages/login-ui/components/index.js new file mode 100644 index 000000000..fcf046928 --- /dev/null +++ b/packages/login-ui/components/index.js @@ -0,0 +1,11 @@ +export { default as GenerateMnemonic } from './GenerateMnemonic/GenerateMnemonic' +export { default as LoginWithMnemonic } from './LoginWithMnemonic/LoginWithMnemonic' +export { default as LoginWithPrivateKey } from './LoginWithPrivateKey/LoginWithPrivateKey' +export { default as LoginForm } from './LoginForm/LoginForm' +export { default as LoginWithOptions } from './LoginWithOptions/LoginWithOptions' +export { default as AccountSelector } from './AccountSelector/AccountSelector' +export { default as RecoverAccount } from './RecoverAccount/RecoverAccount' +export { default as ResetPassword } from './ResetPassword/ResetPassword' +export { default as CreateAccount } from './CreateAccount/CreateAccount' +export { default as ConfirmMnemonic } from './ConfirmMnemonic/ConfirmMnemonic' +export { default as CommonNetworkSelector } from './CommonNetworkSelector/CommonNetworkSelector' diff --git a/packages/login/network/HDWalletProvider.js b/packages/login/network/HDWalletProvider.js index a8cadf0c3..c67192379 100644 --- a/packages/login/network/HDWalletProvider.js +++ b/packages/login/network/HDWalletProvider.js @@ -18,6 +18,7 @@ ProviderEngine.prototype.changeProvider = function (source, idx) { export default class HDWalletProvider { constructor (wallet, provider_url, address_index = 0, num_addresses = 0) { + console.log('HDWALLET', provider_url) this.hdwallet = hdkey.fromMasterSeed(wallet.getPrivateKey()) this.wallet_hdpath = WALLET_HD_PATH this.wallets = [wallet] diff --git a/packages/login/network/NetworkService.js b/packages/login/network/NetworkService.js index b810833d3..075e5e761 100644 --- a/packages/login/network/NetworkService.js +++ b/packages/login/network/NetworkService.js @@ -98,6 +98,7 @@ class NetworkService extends EventEmitter { } const web3 = new Web3() + console.log('check local session', providerURL) web3Provider.reinit(web3, new web3.providers.HttpProvider(providerURL || TESTRPC_URL)) const accounts = await web3Provider.getAccounts() @@ -256,6 +257,7 @@ class NetworkService extends EventEmitter { async checkTestRPC (providerUrl) { const web3 = new Web3() + console.log('checkTestRPC', providerUrl) web3.setProvider(new web3.providers.HttpProvider(providerUrl || TESTRPC_URL)) const web3Provider = new Web3Provider(web3) diff --git a/packages/login/network/settings.js b/packages/login/network/settings.js index 72d348c8b..3f5314969 100644 --- a/packages/login/network/settings.js +++ b/packages/login/network/settings.js @@ -268,6 +268,30 @@ export const getNetworksByProvider = (providerId, withLocal = false) => { } } +export const getNetworksWithProviders = (withLocal = false) => { + let networks = [] + + Object.keys(providerMap) + .filter((key) => !providerMap[key].disabled) + .forEach((key) => { + const provider = providerMap[key] + + const networksProvider = getNetworksByProvider(provider && provider.id, withLocal) + .map((network) => ({ + provider: provider, + network, + })) + + networks = networks.concat(networksProvider) + }) + + return networks +} + +export const getNetworkWithProviderNames = (providerId, networkId, withLocal = false) => { + return `${getProviderById(providerId).name} - ${getNetworkById(networkId, providerId, withLocal).name}` +} + export const getProviderById = (providerId) => { return providerMap[Object.keys(providerMap).find((key) => providerMap[key].id === providerId)] || {} } diff --git a/packages/login/redux/network/actions.js b/packages/login/redux/network/actions.js index 85e60f5eb..084f6619e 100644 --- a/packages/login/redux/network/actions.js +++ b/packages/login/redux/network/actions.js @@ -3,9 +3,36 @@ * Licensed under the AGPL Version 3 license. */ +import { + createAccount, + resetPasswordAccount, + validateMnemonicForAccount, + validateAccountName, + decryptAccount, + accountAdd, + accountSelect, +} from 'redux/persistAccount/actions' +import { + FORM_CONFIRM_MNEMONIC, + FORM_MNEMONIC_LOGIN_PAGE, + FORM_PRIVATE_KEY_LOGIN_PAGE, + FORM_LOGIN_PAGE, + FORM_CREATE_ACCOUNT, + FORM_RECOVER_ACCOUNT, + FORM_RESET_PASSWORD, +} from 'pages' +import Web3 from 'web3' +import bip39 from 'bip39' +import Accounts from 'web3-eth-accounts' +import { login } from 'redux/session/actions' +import { stopSubmit, SubmissionError } from 'redux-form' +import { push } from 'react-router-redux' +import mnemonicProvider from '@chronobank/login/network/mnemonicProvider' import { ethereumProvider } from '../../network/EthereumProvider' import { btcProvider, ltcProvider, btgProvider } from '../../network/BitcoinProvider' import { nemProvider } from '../../network/NemProvider' +import networkService from '../../network/NetworkService' +import privateKeyProvider from '../../network/privateKeyProvider' export const DUCK_NETWORK = 'network' @@ -20,6 +47,25 @@ export const NETWORK_SET_TEST_RPC = 'network/SET_TEST_RPC' export const NETWORK_SET_TEST_METAMASK = 'network/SET_TEST_METAMASK' export const NETWORK_SET_NETWORK = 'network/SET_NETWORK' export const NETWORK_SET_PROVIDER = 'network/SET_PROVIDER' +export const NETWORK_SET_NEW_ACCOUNT_CREDENTIALS = 'network/SET_NEW_ACCOUNT_CREDENTIALS' +export const NETWORK_SET_NEW_MNEMONIC = 'network/SET_NEW_MNEMONIC' +export const NETWORK_RESET_NEW_MNEMONIC = 'network/RESET_NEW_MNEMONIC' + +export const NETWORK_SET_IMPORT_MNEMONIC = 'network/SET_IMPORT_MNEMONIC' +export const NETWORK_SET_IMPORT_PRIVATE_KEY = 'network/SET_NEW_PRIVATE_KEY' +export const NETWORK_RESET_IMPORT_PRIVATE_KEY = 'network/RESET_NEW_PRIVATE_KEY' +export const NETWORK_SET_IMPORT_ACCOUNT_MODE = 'network/SET_IMPORT_ACCOUNT_MODE' +export const NETWORK_RESET_IMPORT_ACCOUNT_MODE = 'network/RESET_IMPORT_ACCOUNT_MODE' +export const NETWORK_SET_LOGIN_SUBMITTING = 'network/SET_LOGIN_SUBMITTING' +export const NETWORK_RESET_LOGIN_SUBMITTING = 'network/RESET_LOGIN_SUBMITTING' +export const NETWORK_SET_ACCOUNT_RECOVERY_MODE = 'network/SET_ACCOUNT_RECOVERY_MODE' +export const NETWORK_RESET_ACCOUNT_RECOVERY_MODE = 'network/RESET_ACCOUNT_RECOVERY_MODE' + +export const WALLETS_ADD = 'network/WALLETS_ADD' +export const WALLETS_SELECT = 'network/WALLETS_SELECT' +export const WALLETS_LOAD = 'network/WALLETS_LOAD' +export const WALLETS_UPDATE_LIST = 'network/WALLETS_UPDATE_LIST' +export const WALLETS_REMOVE = 'network/WALLETS_REMOVE' export const loading = (isLoading = true) => (dispatch) => { dispatch({ type: NETWORK_LOADING, isLoading }) @@ -33,6 +79,387 @@ export const clearErrors = () => (dispatch) => { dispatch({ type: NETWORK_CLEAR_ERRORS }) } +export const navigateToCreateAccountWithoutImport = () => (dispatch) => { + dispatch(navigateToCreateAccount()) + dispatch({ type: NETWORK_RESET_IMPORT_ACCOUNT_MODE }) +} + +export const initConfirmMnemonicPage = () => (dispatch, getState) => { + const state = getState() + + const { newAccountMnemonic } = state.get('network') + + if (!newAccountMnemonic){ + dispatch(navigateToCreateAccount()) + } + +} + +export const initMnemonicPage = () => (dispatch, getState) => { + const state = getState() + + const { newAccountName, newAccountPassword } = state.get('network') + + const emptyAccountCredentials = !newAccountName || !newAccountPassword + + if (emptyAccountCredentials){ + dispatch(navigateToCreateAccount()) + } +} + +export const initLoginPage = () => (dispatch, getState) => { + const state = getState() + + const { selectedWallet } = state.get('persistAccount') + + dispatch({ type: NETWORK_RESET_LOGIN_SUBMITTING }) + + if (!selectedWallet){ + dispatch(navigateToSelectWallet()) + } + +} + +export const resetNewMnemonic = () => (dispatch) => { + dispatch({ type: NETWORK_RESET_NEW_MNEMONIC }) +} +export const generateNewMnemonic = () => (dispatch) => { + const mnemonic = mnemonicProvider.generateMnemonic() + + dispatch({ type: NETWORK_SET_NEW_MNEMONIC, mnemonic }) +} + +export const resetImportAccountMode = () => (dispatch) => { + dispatch({ type: NETWORK_RESET_IMPORT_ACCOUNT_MODE }) +} + +export const onSubmitCreateAccountPage = (walletName, walletPassword) => async (dispatch, getState) => { + const state = getState() + + const { importAccountMode, newAccountMnemonic, newAccountPrivateKey } = state.get('network') + + const validateName = dispatch(validateAccountName(walletName)) + + if (!validateName){ + throw new SubmissionError({ walletName: 'Wrong wallet name' }) + } + + dispatch({ type: NETWORK_SET_NEW_ACCOUNT_CREDENTIALS, walletName, walletPassword }) + + if (importAccountMode){ + let wallet = await dispatch(createAccount({ + name: walletName, + password: walletPassword, + mnemonic: newAccountMnemonic, + privateKey: newAccountPrivateKey, + numberOfAccounts: 0, + })) + + dispatch(accountAdd(wallet)) + + dispatch(accountSelect(wallet)) + + dispatch(resetImportAccountMode()) + + dispatch(navigateToLoginPage()) + + return + } + + dispatch(generateNewMnemonic()) + + dispatch(navigateToGenerateMnemonicPage()) + +} + +export const onSubmitCreateAccountPageSuccess = () => (dispatch) => { + +} + +export const onSubmitCreateAccountPageFail = (errors, dispatch, submitErrors) => (dispatch) => { + dispatch(stopSubmit(FORM_CREATE_ACCOUNT, submitErrors && submitErrors.errors)) +} + +export const initImportMethodsPage = () => (dispatch) => { + dispatch({ type: NETWORK_SET_IMPORT_ACCOUNT_MODE }) +} + +export const onSubmitConfirmMnemonic = (confirmMnemonic) => (dispatch, getState) => { + const state = getState() + + const { newAccountMnemonic } = state.get('network') + + if (confirmMnemonic !== newAccountMnemonic){ + throw new SubmissionError({ _error: 'Please enter correct mnemonic phrase' }) + } + +} + +export const onSubmitConfirmMnemonicSuccess = () => async (dispatch, getState) => { + const state = getState() + + const { newAccountMnemonic, newAccountName, newAccountPassword } = state.get('network') + + let wallet = await dispatch(createAccount({ + name: newAccountName, + password: newAccountPassword, + mnemonic: newAccountMnemonic, + numberOfAccounts: 0, + })) + + dispatch(accountAdd(wallet)) + + dispatch(accountSelect(wallet)) + + dispatch(navigateToDownloadWalletPage()) +} + +export const onSubmitConfirmMnemonicFail = (errors, dispatch, submitErrors) => (dispatch) => { + dispatch(stopSubmit(FORM_CONFIRM_MNEMONIC, submitErrors && submitErrors.errors)) +} + +export const navigateToConfirmMnemonicPage = () => (dispatch) => { + dispatch(push('/login/confirm-mnemonic')) +} + +export const navigateToDownloadWalletPage = () => (dispatch) => { + dispatch(push('/login/download-wallet')) +} + +export const navigateToCreateAccount = () => (dispatch) => { + dispatch(push('/login/create-account')) +} + +export const navigateToSelectImportMethod = () => (dispatch) => { + dispatch(push('/login/import-methods')) +} + +export const navigateToMnemonicImportMethod = () => (dispatch) => { + dispatch(push('/login/mnemonic-login')) +} + +export const navigateToPrivateKeyImportMethod = () => (dispatch) => { + dispatch(push('/login/private-key-login')) +} + +export const navigateToSelectWallet = () => (dispatch) => { + dispatch(push('/login/select-account')) +} + +export const navigateToLoginPage = () => (dispatch) => { + dispatch(push('/login')) +} + +export const navigateToResetPasswordPage = () => (dispatch) => { + dispatch(push('/login/reset-password')) +} + +export const navigateToRecoverAccountPage = () => (dispatch) => { + dispatch(push('/login/recover-account')) +} + +export const navigateToGenerateMnemonicPage = () => (dispatch) => { + dispatch(push('/login/mnemonic')) +} + +export const onSubmitMnemonicLoginForm = (mnemonic) => async (dispatch) => { + if (!bip39.validateMnemonic(mnemonic)){ + throw new Error('Invalid mnemonic') + } + + dispatch({ type: NETWORK_SET_NEW_MNEMONIC, mnemonic }) + +} + +export const onSubmitMnemonicLoginFormSuccess = () => (dispatch) => { + dispatch(navigateToCreateAccount()) +} + +export const onSubmitMnemonicLoginFormFail = () => (dispatch) => { + dispatch(stopSubmit(FORM_MNEMONIC_LOGIN_PAGE, { key: 'Wrong mnemonic' })) + +} + +export const onSubmitPrivateKeyLoginForm = (privateKey) => (dispatch) => { + let pk = privateKey || '' + + if (pk.slice(0, 2) === '0x'){ + pk = pk.slice(2) + } + + dispatch({ type: NETWORK_SET_IMPORT_PRIVATE_KEY, privateKey: pk }) +} + +export const onSubmitPrivateKeyLoginFormSuccess = () => (dispatch) => { + dispatch(navigateToCreateAccount()) +} + +export const onSubmitPrivateKeyLoginFormFail = () => (dispatch) => { + dispatch(stopSubmit(FORM_PRIVATE_KEY_LOGIN_PAGE, { pk: 'Wrong private key' })) + +} + +export const onSubmitLoginForm = (password) => async (dispatch, getState) => { + const state = getState() + + dispatch({ type: NETWORK_SET_LOGIN_SUBMITTING }) + + const { selectedWallet } = state.get('persistAccount') + let wallet = await dispatch(decryptAccount(selectedWallet, password)) + + let privateKey = wallet && wallet[0] && wallet[0].privateKey + + if (privateKey){ + await dispatch(handlePrivateKeyLogin(privateKey)) + } + +} + +export const onSubmitLoginFormSuccess = () => () => { +} + +export const onSubmitLoginFormFail = () => (dispatch) => { + dispatch(stopSubmit(FORM_LOGIN_PAGE, { password: 'Wrong password' })) + dispatch({ type: NETWORK_RESET_LOGIN_SUBMITTING }) + +} + +export const validateRecoveryForm = (mnemonic) => (dispatch, getState) => { + const state = getState() + + const { selectedWallet } = state.get('persistAccount') + + return dispatch(validateMnemonicForAccount(selectedWallet, mnemonic)) + +} + +export const initRecoverAccountPage = () => (dispatch) => { + dispatch(resetNewMnemonic()) + dispatch({ type: NETWORK_SET_ACCOUNT_RECOVERY_MODE }) +} + +export const onSubmitRecoverAccountForm = (mnemonic) => (dispatch) => { + const validForm = dispatch(validateRecoveryForm(mnemonic)) + + if (!validForm) { + throw new SubmissionError({ _error: 'Mnemonic incorrect for this wallet' }) + } + + dispatch({ type: NETWORK_SET_NEW_MNEMONIC, mnemonic }) +} + +export const onSubmitRecoverAccountFormSuccess = () => (dispatch) => { + dispatch(navigateToResetPasswordPage()) +} + +export const onSubmitRecoverAccountFormFail = () => (dispatch) => { + dispatch(stopSubmit(FORM_RECOVER_ACCOUNT, { pk: 'Wrong private key' })) + +} + +export const initResetPasswordPage = () => (dispatch, getState) => { + const state = getState() + + const { accountRecoveryMode } = state.get('network') + + if (!accountRecoveryMode){ + dispatch(navigateToRecoverAccountPage()) + } +} + +export const onSubmitResetAccountPasswordForm = (password) => async (dispatch, getState) => { + const state = getState() + + const { newAccountMnemonic } = state.get('network') + const { selectedWallet } = state.get('persistAccount') + + await dispatch(resetPasswordAccount(selectedWallet, newAccountMnemonic, password)) + +} + +export const onSubmitResetAccountPasswordSuccess = () => (dispatch) => { + dispatch({ type: NETWORK_RESET_ACCOUNT_RECOVERY_MODE }) + dispatch(navigateToLoginPage()) + +} + +export const onSubmitResetAccountPasswordFail = (error, dispatch, submitError) => (dispatch) => { + dispatch(stopSubmit(FORM_RESET_PASSWORD, { _error: 'Error' })) + +} + +export const onWalletSelect = (wallet) => (dispatch) => { + dispatch(accountSelect(wallet)) + + dispatch(navigateToLoginPage()) +} + +export const handlePrivateKeyLogin = (privateKey) => async (dispatch, getState) => { + let state = getState() + + dispatch(loading()) + dispatch(clearErrors()) + const provider = privateKeyProvider.getPrivateKeyProvider(privateKey.slice(2), networkService.getProviderSettings(), state.get('multisigWallet')) + + networkService.selectAccount(provider.ethereum.getAddress()) + await networkService.setup(provider) + + state = getState() + const { selectedAccount, selectedProviderId, selectedNetworkId } = state.get(DUCK_NETWORK) + + dispatch(clearErrors()) + + const isPassed = await networkService.checkNetwork( + selectedAccount, + selectedProviderId, + selectedNetworkId, + ) + + if (isPassed) { + networkService.createNetworkSession( + selectedAccount, + selectedProviderId, + selectedNetworkId, + ) + dispatch(login(selectedAccount)) + } + +} + +export const handleMnemonicLogin = (mnemonic) => async (dispatch, getState) => { + const web3 = new Web3() + const accounts = new Accounts(new web3.providers.HttpProvider(networkService.getProviderSettings().url)) + await accounts.wallet.clear() + + dispatch(loading()) + dispatch(clearErrors()) + const provider = mnemonicProvider.getMnemonicProvider(mnemonic, networkService.getProviderSettings()) + networkService.selectAccount(provider.ethereum.getAddress()) + await networkService.setup(provider) + + const state = getState() + + const { selectedAccount, selectedProviderId, selectedNetworkId } = state.get(DUCK_NETWORK) + + dispatch(clearErrors()) + + const isPassed = await networkService.checkNetwork( + selectedAccount, + selectedProviderId, + selectedNetworkId, + ) + + if (isPassed) { + networkService.createNetworkSession( + selectedAccount, + selectedProviderId, + selectedNetworkId, + ) + dispatch(login(selectedAccount)) + } + +} + export const getPrivateKeyFromBlockchain = (blockchain: string) => { switch (blockchain) { case 'Ethereum': diff --git a/packages/login/redux/network/reducer.js b/packages/login/redux/network/reducer.js index 45572a6b9..c88316efa 100644 --- a/packages/login/redux/network/reducer.js +++ b/packages/login/redux/network/reducer.js @@ -5,6 +5,8 @@ import { getNetworksByProvider, providerMap } from '../../network/settings' import * as actions from './actions' +import { NETWORK_SET_IMPORT_PRIVATE_KEY } from "./actions"; +import { NETWORK_SET_LOGIN_LOADING } from "./actions"; const initialState = { isLoading: false, @@ -31,6 +33,12 @@ const initialState = { selectedProviderId: null, networks: [], selectedNetworkId: null, + newAccountName: null, + newAccountPassword: null, + newAccountMnemonic: null, + newAccountPrivateKey: null, + isLoginSubmitting: false, + accountRecoveryMode: false, } export default (state = initialState, action) => { @@ -74,6 +82,63 @@ export default (state = initialState, action) => { isLoading: false, errors: [...state.errors, action.error], } + case actions.NETWORK_SET_NEW_ACCOUNT_CREDENTIALS: + return { + ...state, + newAccountName: action.walletName, + newAccountPassword: action.walletPassword, + } + case actions.NETWORK_SET_NEW_MNEMONIC: + return { + ...state, + newAccountMnemonic: action.mnemonic, + } + case actions.NETWORK_RESET_NEW_MNEMONIC: + return { + ...state, + newAccountMnemonic: null, + } + case actions.NETWORK_SET_IMPORT_PRIVATE_KEY: + return { + ...state, + newAccountPrivateKey: action.privateKey, + } + case actions.NETWORK_RESET_IMPORT_PRIVATE_KEY: + return { + ...state, + newAccountPrivateKey: null, + } + case actions.NETWORK_SET_IMPORT_ACCOUNT_MODE: + return { + ...state, + importAccountMode: true, + } + case actions.NETWORK_RESET_IMPORT_ACCOUNT_MODE: + return { + ...state, + importAccountMode: false, + } + case actions.NETWORK_SET_LOGIN_SUBMITTING: + return { + ...state, + isLoginSubmitting: true, + } + case actions.NETWORK_RESET_LOGIN_SUBMITTING: + return { + ...state, + isLoginSubmitting: false, + } + case actions.NETWORK_SET_ACCOUNT_RECOVERY_MODE: + return { + ...state, + accountRecoveryMode: true, + } + case actions.NETWORK_RESET_ACCOUNT_RECOVERY_MODE: + return { + ...state, + accountRecoveryMode: false, + } + default: return state } diff --git a/src/assets/img/appstore-white.svg b/src/assets/img/appstore-white.svg new file mode 100644 index 000000000..7bc45022d --- /dev/null +++ b/src/assets/img/appstore-white.svg @@ -0,0 +1,55 @@ + + + + + + + + 2d0745e2-571f-4313-890f-7945a689d7ff + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/chronowalletlogobright.svg b/src/assets/img/chronowalletlogobright.svg new file mode 100644 index 000000000..8f3ea19f5 --- /dev/null +++ b/src/assets/img/chronowalletlogobright.svg @@ -0,0 +1,28 @@ + + + + + 1edbc86b-d425-405d-9bd8-60ee0f49b94a + + diff --git a/src/assets/img/chronowallettextbright.svg b/src/assets/img/chronowallettextbright.svg new file mode 100644 index 000000000..8bfe78bd7 --- /dev/null +++ b/src/assets/img/chronowallettextbright.svg @@ -0,0 +1,67 @@ + + + + + a0f5ff71-36c6-4417-b91e-1ebd618450b1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/facebook.svg b/src/assets/img/facebook.svg new file mode 100644 index 000000000..2fb8ce2fb --- /dev/null +++ b/src/assets/img/facebook.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/github.svg b/src/assets/img/github.svg new file mode 100644 index 000000000..87609b98e --- /dev/null +++ b/src/assets/img/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/icons/back.svg b/src/assets/img/icons/back.svg new file mode 100644 index 000000000..fa7e43a29 --- /dev/null +++ b/src/assets/img/icons/back.svg @@ -0,0 +1,18 @@ + + + + + 90f1a8dc-9f06-4e17-987d-0d12e8d7cd42 + + + + + diff --git a/src/assets/img/icons/check-green.svg b/src/assets/img/icons/check-green.svg new file mode 100644 index 000000000..e2462aa8a --- /dev/null +++ b/src/assets/img/icons/check-green.svg @@ -0,0 +1,16 @@ + + + + + 8220b91f-6a4a-4ebe-98b8-0d701ad8170e + + + diff --git a/src/assets/img/icons/delete-white.svg b/src/assets/img/icons/delete-white.svg new file mode 100644 index 000000000..cc63bdfd8 --- /dev/null +++ b/src/assets/img/icons/delete-white.svg @@ -0,0 +1,16 @@ + + + + + ebffa863-a7aa-455a-9cb6-07c24cf83aed + + + diff --git a/src/assets/img/icons/file-white.svg b/src/assets/img/icons/file-white.svg new file mode 100644 index 000000000..ce8746523 --- /dev/null +++ b/src/assets/img/icons/file-white.svg @@ -0,0 +1,16 @@ + + + + + 5a5fafd5-b2c5-47ea-b2c0-7ef57e280622 + + + diff --git a/src/assets/img/icons/key-white.svg b/src/assets/img/icons/key-white.svg new file mode 100644 index 000000000..603e73829 --- /dev/null +++ b/src/assets/img/icons/key-white.svg @@ -0,0 +1,16 @@ + + + + + 8d956c6a-7586-4e0b-a30f-371b3563407a + + + diff --git a/src/assets/img/icons/ledger-nano-white.svg b/src/assets/img/icons/ledger-nano-white.svg new file mode 100644 index 000000000..d5e8fae74 --- /dev/null +++ b/src/assets/img/icons/ledger-nano-white.svg @@ -0,0 +1,21 @@ + + + + + 65a7d0dd-b6fe-4fe5-a4db-e0696a8d7a5f + + + + + + + + diff --git a/src/assets/img/icons/list.svg b/src/assets/img/icons/list.svg new file mode 100644 index 000000000..462dbe514 --- /dev/null +++ b/src/assets/img/icons/list.svg @@ -0,0 +1,16 @@ + + + + + 635d99ca-d8c6-4878-b277-83d9fe988a37 + + + diff --git a/src/assets/img/icons/mnemonic-white.svg b/src/assets/img/icons/mnemonic-white.svg new file mode 100644 index 000000000..716e3632e --- /dev/null +++ b/src/assets/img/icons/mnemonic-white.svg @@ -0,0 +1,16 @@ + + + + + 76b6e7c7-b5b0-4710-8f20-506b658dcd6b + + + diff --git a/src/assets/img/icons/plugin-white.svg b/src/assets/img/icons/plugin-white.svg new file mode 100644 index 000000000..3c7387d6a --- /dev/null +++ b/src/assets/img/icons/plugin-white.svg @@ -0,0 +1,16 @@ + + + + + 4def2330-1322-4f97-bce0-2f128536f1fb + + + diff --git a/src/assets/img/icons/prev-white.svg b/src/assets/img/icons/prev-white.svg new file mode 100644 index 000000000..09d794129 --- /dev/null +++ b/src/assets/img/icons/prev-white.svg @@ -0,0 +1,16 @@ + + + + + 2e847eb1-2f11-470d-8c5e-a3e628575639 + + + diff --git a/src/assets/img/icons/print-white.svg b/src/assets/img/icons/print-white.svg new file mode 100644 index 000000000..38b5d6d4a --- /dev/null +++ b/src/assets/img/icons/print-white.svg @@ -0,0 +1,16 @@ + + + + + 160c1a77-03d7-4834-9669-e304e1093918 + + + diff --git a/src/assets/img/icons/trezor-white.svg b/src/assets/img/icons/trezor-white.svg new file mode 100644 index 000000000..2a6416ee1 --- /dev/null +++ b/src/assets/img/icons/trezor-white.svg @@ -0,0 +1,16 @@ + + + + + bea14c40-229c-4277-a27f-d60338d325f5 + + + diff --git a/src/assets/img/icons/uport.svg b/src/assets/img/icons/uport.svg new file mode 100644 index 000000000..3a17295cd --- /dev/null +++ b/src/assets/img/icons/uport.svg @@ -0,0 +1,16 @@ + + + + + 770e3155-7634-4b44-965d-ccb7195c9cb8 + + + diff --git a/src/assets/img/icons/wallet-white.svg b/src/assets/img/icons/wallet-white.svg new file mode 100644 index 000000000..05c412564 --- /dev/null +++ b/src/assets/img/icons/wallet-white.svg @@ -0,0 +1,16 @@ + + + + + 405250a0-8aac-4199-8932-61078aa77b6d + + + diff --git a/src/assets/img/icons/warning.svg b/src/assets/img/icons/warning.svg new file mode 100644 index 000000000..b97391fc8 --- /dev/null +++ b/src/assets/img/icons/warning.svg @@ -0,0 +1,16 @@ + + + + + 223a9793-bfa9-41d8-8047-9adb426e17c7 + + + diff --git a/src/assets/img/instagram.svg b/src/assets/img/instagram.svg new file mode 100644 index 000000000..c52ee4904 --- /dev/null +++ b/src/assets/img/instagram.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/img/logo-chrono-bank-full.svg b/src/assets/img/logo-chrono-bank-full.svg new file mode 100644 index 000000000..2961dd95c --- /dev/null +++ b/src/assets/img/logo-chrono-bank-full.svg @@ -0,0 +1,34 @@ + + + + + a92693b9-efd8-4fb5-92ed-1a8cc052713c + + diff --git a/src/assets/img/play-white.svg b/src/assets/img/play-white.svg new file mode 100644 index 000000000..933f5e26e --- /dev/null +++ b/src/assets/img/play-white.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dba20148-1f77-467e-8fb4-b06aeb3fe101 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/img/reddit.svg b/src/assets/img/reddit.svg new file mode 100644 index 000000000..b4f687be9 --- /dev/null +++ b/src/assets/img/reddit.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/img/spinningwheel-1.gif b/src/assets/img/spinningwheel-1.gif new file mode 100644 index 000000000..a28fc9ffd Binary files /dev/null and b/src/assets/img/spinningwheel-1.gif differ diff --git a/src/assets/img/spinningwheel.gif b/src/assets/img/spinningwheel.gif new file mode 100644 index 000000000..a28fc9ffd Binary files /dev/null and b/src/assets/img/spinningwheel.gif differ diff --git a/src/assets/img/stripes-2-crop-footer.jpg b/src/assets/img/stripes-2-crop-footer.jpg new file mode 100644 index 000000000..12d9b4bfd Binary files /dev/null and b/src/assets/img/stripes-2-crop-footer.jpg differ diff --git a/src/assets/img/stripes-2-crop.jpg b/src/assets/img/stripes-2-crop.jpg new file mode 100644 index 000000000..727e46587 Binary files /dev/null and b/src/assets/img/stripes-2-crop.jpg differ diff --git a/src/assets/img/telegramm.svg b/src/assets/img/telegramm.svg new file mode 100644 index 000000000..f268b5332 --- /dev/null +++ b/src/assets/img/telegramm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/twitter.svg b/src/assets/img/twitter.svg new file mode 100644 index 000000000..5213d28ef --- /dev/null +++ b/src/assets/img/twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/wallet-title-bg.png b/src/assets/img/wallet-title-bg.png new file mode 100644 index 000000000..44edb7e56 Binary files /dev/null and b/src/assets/img/wallet-title-bg.png differ diff --git a/src/components/common/ui/Button/Button.jsx b/src/components/common/ui/Button/Button.jsx index 97ad6f608..d710a1806 100644 --- a/src/components/common/ui/Button/Button.jsx +++ b/src/components/common/ui/Button/Button.jsx @@ -68,12 +68,15 @@ export default class Button extends PureComponent { if (flat) { buttonType = 'flat' } + + const buttonClasses = classnames('button', buttonType) + return (
    + + +
    +
    + + {} + +
    + + + +
    + ) + } +} diff --git a/src/layouts/Footer/Footer.scss b/src/layouts/Footer/Footer.scss new file mode 100644 index 000000000..7c9eba6b6 --- /dev/null +++ b/src/layouts/Footer/Footer.scss @@ -0,0 +1,246 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ +@import "~styles/partials/mixins"; +@import "~styles/partials/variables"; + +$copyRightPadding: 200px; + +.root { + background-image: $bg-gradient-0; + position: relative; + display: flex; + min-height: 100vh; + flex-direction: column; + + @include md-only { + padding-bottom: 230px; + } + + &:after { + // hack for preloading material icons and avoid FUOC on render + content: ''; + font-family: 'Material Icons'; + } +} + +.content { + flex: 1; + width: 470px; + margin: 0 auto; + padding-top: 40px; + + @include md-only { + padding-top: 80px; + } + @include xs-only { + width: 100vw; + } +} + +.footer { + position: relative; + display: flex; + flex-direction: column; + bottom: 0; + left: 0; + right: 0; + min-width: 700px; + padding: 17px 80px; + align-items: center; + justify-content: flex-start; + overflow: hidden; + + @include md-only { + padding: 20px; + min-width: 0; + } + + .subscription { + position: absolute; + display: flex; + bottom: 0; + left: 335px; + } + + .subscription-input { + width: 280px; + display: flex; + } + + .subscription-button { + width: 280px; + margin-left: 30px; + display: flex; + align-items: flex-end; + + .button { + button { + background: $color-light-blue; + font-size: 14px; + border-radius: 50px; + padding: 0 17px; + min-height: 30px; + + &:hover { + background: #E2A864; + } + } + } + } + + .copyright { + display: flex; + margin-top: 30px; + width: 870px; + z-index: 2; + + @include md-only { + width: auto; + text-align: center; + } + } + + .copyright-text { + color: #9997B2; + font-weight: 400; + font-size: 12px; + line-height: 14px; + text-align: left; + } + + .footer-container { + display: flex; + z-index: 2; + position: relative; + + @include md-only { + display: none; + } + } + + .navigation-chrono-logo-container { + overflow: hidden; + display: flex; + } + + .navigation-chrono-logo { + width: 250px; + display: block; + margin-left: -67px; + } + + .navigation { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 310px; + padding-left: 30px; + padding-right: 30px; + } + + + .navigation-list { + + display: flex; + flex-direction: column; + list-style-type: none; + + li { + display: flex; + color: $color-light-blue; + font-weight: 500; + font-size: 14px; + margin-top: 20px; + + &.first { + margin: 0; + } + } + + } + + .title-container { + display: flex; + margin: 10px 0 30px; + } + + .title { + color: #E2A864; + font-weight: 700; + font-size: 20px; + line-height: 24px; + } + + .downloads { + display: flex; + flex-direction: column; + width: 310px; + padding-left: 30px; + padding-right: 30px; + + .market-logo-container { + display: block; + margin-bottom: 20px; + } + + .ios-market-logo { + display: block; + width: 125px; + } + + .android-market-logo { + display: block; + width: 125px; + } + + } + + .connect { + display: flex; + flex-direction: column; + width: 310px; + + padding-left: 30px; + padding-right: 30px; + + .logos-container { + display: flex; + width: 125px; + height: 130px; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; + } + + .logo { + display: flex; + } + + .img-logo { + width: 32px; + height: 32px; + } + } +} + +.link { + color: white; + margin: auto 45px; + text-decoration: none; + font-size: 18px; + +} + +.background { + position: absolute; + bottom: 0; + left: 0; + right: -750px; + margin: auto; + + @include md-only { + display: none; + } +} diff --git a/src/layouts/Footer/lang.js b/src/layouts/Footer/lang.js new file mode 100644 index 000000000..efa7f6b92 --- /dev/null +++ b/src/layouts/Footer/lang.js @@ -0,0 +1,13 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +export const prefix = `Splash` + +export default { + en: { + copyright: 'Copyright ©2018 LaborX Australia Pty Ltd. All Rights Reserved.', + subscribe: 'Subscribe', + }, +} diff --git a/src/layouts/Footer/styles.js b/src/layouts/Footer/styles.js new file mode 100644 index 000000000..ccc86af92 --- /dev/null +++ b/src/layouts/Footer/styles.js @@ -0,0 +1,30 @@ +export default { + textField: { + style: { + height: 62, + }, + underlineStyle: { + borderColor: '#A3A3CC', + bottom: 0, + }, + underlineFocusStyle: { + borderColor: '#E2A864', + bottom: 0, + }, + inputStyle: { + color: '#9997B2', + textAlign: 'center', + marginTop: 0, + paddingTop: 18, + }, + floatingLabelStyle: { + color: '#A3A3CC', + top: 28, + left: 0, + right: 0, + margin: 'auto', + textAlign: 'center', + transformOrigin: 'center center', + }, + }, +} diff --git a/src/layouts/Markup.jsx b/src/layouts/Markup.jsx index c82973551..169a8854c 100644 --- a/src/layouts/Markup.jsx +++ b/src/layouts/Markup.jsx @@ -116,7 +116,7 @@ export default class Markup extends PureComponent {
    {this.renderPageTitle()}
    - + {/**/}
    @@ -136,7 +136,7 @@ export default class Markup extends PureComponent {
    - + {/**/}
    diff --git a/src/layouts/Splash/Splash.js b/src/layouts/Splash/Splash.js index d44331a4b..f79e16b76 100644 --- a/src/layouts/Splash/Splash.js +++ b/src/layouts/Splash/Splash.js @@ -3,38 +3,70 @@ * Licensed under the AGPL Version 3 license. */ -import { Translate } from 'react-redux-i18n' import PropTypes from 'prop-types' import React, { Component } from 'react' -import ChronoBankLogo from 'components/common/ChronoBankLogo/ChronoBankLogo' + +import WalletTitleBG from 'assets/img/wallet-title-bg.png' +import StripesToCrop from 'assets/img/stripes-2-crop.jpg' +import ChronoWalletLogoBright from 'assets/img/chronowalletlogobright.svg' +import ChronoWalletTextBright from 'assets/img/chronowallettextbright.svg' +import BackIcon from 'assets/img/icons/back.svg' + +import Footer from '../Footer/Footer' +import PersistWrapper from '../partials/PersistWrapper/PersistWrapper' import './Splash.scss' -class Splash extends Component { +export default class Splash extends Component { static propTypes = { children: PropTypes.node, + goBack: PropTypes.func, + navigatorText: PropTypes.string, + } + + static defaultProps = { + goBack: null, + navigatorText: '', } render () { + const { children, goBack, navigatorText } = this.props + return (
    -
    - - {this.props.children} +
    +
    + +
    +
    + +
    +
    + + +
    - {!window.isMobile && ( -
    -
    -
    - - - + + { + goBack ? ( +
    + + + { navigatorText } +
    -
    - )} + ) : null + } + + + {children ? children: null} + + + {!window.isMobile && (
    )}
    ) } } -export default Splash diff --git a/src/layouts/Splash/Splash.scss b/src/layouts/Splash/Splash.scss index 51dc29f46..07b08b2f5 100644 --- a/src/layouts/Splash/Splash.scss +++ b/src/layouts/Splash/Splash.scss @@ -8,15 +8,16 @@ $copyRightPadding: 200px; .root { - background-image: $bg-gradient-0; + background-color: #242045; position: relative; - padding-bottom: 130px; // for footer display: flex; min-height: 100vh; flex-direction: column; + font-family: $font-proxima; @include md-only { - padding-bottom: 230px; + min-height: 100vh; + padding: 0; } &:after { @@ -26,20 +27,6 @@ $copyRightPadding: 200px; } } -.content { - flex: 1; - width: 470px; - margin: 0 auto; - padding-top: 40px; - - @include md-only { - padding-top: 80px; - } - @include xs-only { - width: 100vw; - } -} - .footer { display: flex; background-color: rgba(255, 255, 255, 0.1); @@ -55,6 +42,94 @@ $copyRightPadding: 200px; } } +.header-container { + display: flex; + justify-content: center; + height: 340px; + overflow: hidden; + margin-bottom: 20px; + position: relative; + +} + +.header-images { + +} + +.header-navigator { + position: absolute; + display: none; + align-items: center; + top: 0; + left: 0; + width: 100%; + line-height: 70px; + height: 70px; + padding-left: 20px; + + @include xs-only { + display: flex; + } +} + +.back-button { + background: transparent; + border:0; + cursor: pointer; + margin-right: 20px; + + img { + width: 24px; + height: 24px; + display: block; + } +} + +.navigator-text { + color: #fff; + font-weight: 700; +} + +.header-picture { + display: flex; + position: absolute; + height: 320px; + overflow: hidden; + flex: 1; +} + +.header-picture-crop { + display: flex; + position: absolute; + height: 400px; + top: -115px; + flex: 1; +} + +.header-logos { + z-index: 100; + display: flex; + position: relative; + flex-direction: column; + align-items: center; + top: 90px; + width: 300px; + height: 200px; +} + +.chrono-wallet-logo-bright { + display: flex; + width: 114px; + height: 117px; + margin-bottom: 40px; +} + +.chrono-wallet-text-bright { + display: flex; + height: 59px; + width: 282px; +} + .copyright { max-width: $copyRightPadding; font-size: 12px; @@ -62,6 +137,25 @@ $copyRightPadding: 200px; color: white; } +.create-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 10px; + text-align: center; +} + +.create-title-description { + font-weight: 400; + color: $color-description; + font-size: 16px; + line-height: 22px; + margin-bottom: 10px; + text-align: center; + width: 520px; +} + .links { padding-right: $copyRightPadding; // compensation for centering text-align: center; @@ -79,14 +173,41 @@ $copyRightPadding: 200px; } } -.link { +.actions { color: white; + text-align: center; + line-height: 30px; + margin-bottom: 45px; +} + +.link { + color: $border-color; margin: auto 45px; text-decoration: none; - font-family: $font-family-main; - font-size: 18px; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } @include md-only { margin: 0 0 10px; } } + +.fields-block { + margin: 0 auto 50px; + max-width: 300px; +} + +.button { + margin-bottom: 25px; + + button { + font: 700 16px $font-proxima; + line-height: 45px; + padding: 0 50px; + border-radius: 50px; + white-space: nowrap; + } +} diff --git a/src/layouts/Splash/styles.js b/src/layouts/Splash/styles.js new file mode 100644 index 000000000..6bd9566ea --- /dev/null +++ b/src/layouts/Splash/styles.js @@ -0,0 +1,35 @@ +export default { + textField: { + style: { + height: 62, + }, + underlineStyle: { + borderColor: '#424066', + bottom: 0, + }, + underlineFocusStyle: { + borderColor: '#FFB54E', + bottom: 0, + }, + inputStyle: { + color: '#A3A3CC', + textAlign: 'center', + marginTop: 0, + paddingTop: 18, + }, + floatingLabelStyle: { + color: '#A3A3CC', + top: 28, + left: 0, + right: 0, + margin: 'auto', + textAlign: 'center', + transformOrigin: 'center center', + }, + errorStyle: { + bottom: 0, + marginTop: 5, + textAlign: 'center', + }, + }, +} diff --git a/src/layouts/partials/LocaleDropDown/LocaleDropDown.jsx b/src/layouts/partials/LocaleDropDown/LocaleDropDown.jsx index e3a9c2228..4a695f630 100644 --- a/src/layouts/partials/LocaleDropDown/LocaleDropDown.jsx +++ b/src/layouts/partials/LocaleDropDown/LocaleDropDown.jsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux' import i18n from 'i18n' import { Button } from 'components' import { changeMomentLocale } from 'redux/ui/actions' +import classnames from 'classnames' import './LocaleDropDown.scss' @@ -64,6 +65,7 @@ export default class LocaleDropDown extends PureComponent { } render () { + const { locale } = this.props const locales = Object.entries(i18n).map(([ name, dictionary ]) => ({ name, title: dictionary.title, @@ -75,7 +77,7 @@ export default class LocaleDropDown extends PureComponent { styleName='langButton' onClick={this.handleClick} > - {this.props.locale} + {locale} - - {locales.map((item) => ( - + {locales.map((item, i) => ( +
  • this.handleChangeLocale(item.name)} - value={item.name} - key={item.name} - primaryText={item.title} - /> + > + {item.title} +
  • ))} -
    +
    ) diff --git a/src/layouts/partials/LocaleDropDown/LocaleDropDown.scss b/src/layouts/partials/LocaleDropDown/LocaleDropDown.scss index 3fe03508c..da6e0f604 100644 --- a/src/layouts/partials/LocaleDropDown/LocaleDropDown.scss +++ b/src/layouts/partials/LocaleDropDown/LocaleDropDown.scss @@ -6,25 +6,44 @@ @import "~styles/partials/variables"; .LocaleDropDown { + border-radius: 20px; + border-top: 3px solid #E2A864; + overflow: hidden; + background: #fff; + margin-top: 10px; + @include xs-only { width: 100%; top: 10px; } } +.LocaleDropDownItem { + padding: 15px 20px; + font-size: 14px; + font-family: Arial, Helvetica Neue, Helvetica, sans-serif; + cursor: pointer; + + &:hover, &.LocaleDropDownItemActive { + background: #F2F2F2; + } +} + .root { } .langButton { button { - color: $color-white; - border-radius: 50%; - width: 40px; - height: 40px; - margin: 5px; - padding: 0; - font-size: 20px; - font-weight: lighter; + color: #5DB3ED; + border:1px solid #5DB3ED; + background: transparent; + font-size: 16px; + text-transform: uppercase; + + &:hover { + color:#fff; + border-color: transparent; + } } } diff --git a/src/layouts/partials/PersistWrapper/PersistWrapper.js b/src/layouts/partials/PersistWrapper/PersistWrapper.js new file mode 100644 index 000000000..d0cab5d0e --- /dev/null +++ b/src/layouts/partials/PersistWrapper/PersistWrapper.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { connect } from 'react-redux' +import spinner from 'assets/img/spinningwheel-1.gif' + +import './PersistWrapper.scss' + +const mapStateToProps = (state) => { + return { + rehydrated: state.get('persistAccount').rehydrated, + } +} + +class PersistWrapper extends React.Component { + static propTypes = { + rehydrated: PropTypes.bool, + } + + static contextTypes = { + store: PropTypes.object.isRequired, + } + + constructor (props, context) { + super(props, context) + this.store = context.store + } + + renderLoader(){ + return ( +
    + {/*
    Log In
    */} + +
    + ) + } + + render () { + const { rehydrated, children } = this.props + if (!rehydrated){ + return this.renderLoader() + } + + return children + } + +} + +export default connect(mapStateToProps, null)(PersistWrapper) diff --git a/src/layouts/partials/PersistWrapper/PersistWrapper.scss b/src/layouts/partials/PersistWrapper/PersistWrapper.scss new file mode 100644 index 000000000..1165ce933 --- /dev/null +++ b/src/layouts/partials/PersistWrapper/PersistWrapper.scss @@ -0,0 +1,23 @@ +.loadingMessage { + text-align: center; + padding: 40px 130px; + color: #fff; + font-size: 20px; + font-weight: bold; + line-height: 24px; + margin-bottom: 45px; + + @media (max-width: 640px) { + padding: 40px 20px; + } + + +} + +.loadingMessageHeader { + line-height: 24px; + font-weight: bold; + font-size: 20px; + color: #fff; + margin-bottom: 50px; +} diff --git a/src/layouts/partials/index.js b/src/layouts/partials/index.js index 212fc811b..e0c00fb84 100644 --- a/src/layouts/partials/index.js +++ b/src/layouts/partials/index.js @@ -23,6 +23,7 @@ import DrawerMainMenu from './DrawerMainMenu/DrawerMainMenu' import DepositsContent from './DepositsContent/DepositsContent' import DepositContent from './DepositContent/DepositContent' import TwoFAContent from './TwoFAContent/TwoFAContent' +import PersistWrapper from './PersistWrapper/PersistWrapper' export { HeaderPartial, @@ -45,6 +46,7 @@ export { DepositContent, AddWalletContent, TwoFAContent, + PersistWrapper, } export default { @@ -68,4 +70,5 @@ export default { DepositContent, AddWalletContent, TwoFAContent, + PersistWrapper, } diff --git a/src/models/persistAccount/AbstractWalletModel.js b/src/models/persistAccount/AbstractWalletModel.js new file mode 100644 index 000000000..92d878628 --- /dev/null +++ b/src/models/persistAccount/AbstractWalletModel.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types' + +export default class AbstractWalletModel { + constructor (props, schema) { + PropTypes.checkPropTypes(schema, props, 'prop', '' + this.class) + Object.assign(this, props) + } +} diff --git a/src/models/persistAccount/SignerModel.js b/src/models/persistAccount/SignerModel.js new file mode 100644 index 000000000..628ad2574 --- /dev/null +++ b/src/models/persistAccount/SignerModel.js @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' + +const schema = { + address: PropTypes.string.isRequired, + sign: PropTypes.func.isRequired, + signTransaction: PropTypes.func.isRequired, +} + +export default class SignerModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, props) + Object.freeze(this) + } +} diff --git a/src/models/persistAccount/WalletEntryModel.js b/src/models/persistAccount/WalletEntryModel.js new file mode 100644 index 000000000..a4b758aaf --- /dev/null +++ b/src/models/persistAccount/WalletEntryModel.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' + +const schema = { + key: PropTypes.string, + name: PropTypes.string, + types: PropTypes.object, + encrypted: PropTypes.array, +} + +export default class WalletEntryModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, { + key: '', + name: '', + types: {}, + }, props) + Object.freeze(this) + } +} diff --git a/src/models/persistAccount/WalletModel.js b/src/models/persistAccount/WalletModel.js new file mode 100644 index 000000000..1d8459a2b --- /dev/null +++ b/src/models/persistAccount/WalletModel.js @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' +import WalletEntryModel from './WalletEntryModel' +import SignerModel from './SignerModel' + +const schema = { + wallet: PropTypes.object, + entry: PropTypes.instanceOf(WalletEntryModel), +} + +export default class WalletModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, props) + Object.freeze(this) + } + + get signer () { + // TODO @ipavlenko: Implement custom signers for hardware tokens + return new SignerModel({ + address: this.wallet[0].address.toLowerCase(), + sign: this.wallet[0].sign, + signTransaction: this.wallet[0].signTransaction, + }) + } +} diff --git a/src/models/persistAccount/index.js b/src/models/persistAccount/index.js new file mode 100644 index 000000000..569753291 --- /dev/null +++ b/src/models/persistAccount/index.js @@ -0,0 +1,11 @@ +import AbstractWalletModel from './AbstractWalletModel' +import SignerModel from './SignerModel' +import WalletEntryModel from './WalletEntryModel' +import WalletModel from './WalletModel' + +export { + AbstractWalletModel, + SignerModel, + WalletEntryModel, + WalletModel, +} diff --git a/src/models/persistWallet/AbstractWalletModel.js b/src/models/persistWallet/AbstractWalletModel.js new file mode 100644 index 000000000..92d878628 --- /dev/null +++ b/src/models/persistWallet/AbstractWalletModel.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types' + +export default class AbstractWalletModel { + constructor (props, schema) { + PropTypes.checkPropTypes(schema, props, 'prop', '' + this.class) + Object.assign(this, props) + } +} diff --git a/src/models/persistWallet/SignerModel.js b/src/models/persistWallet/SignerModel.js new file mode 100644 index 000000000..628ad2574 --- /dev/null +++ b/src/models/persistWallet/SignerModel.js @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' + +const schema = { + address: PropTypes.string.isRequired, + sign: PropTypes.func.isRequired, + signTransaction: PropTypes.func.isRequired, +} + +export default class SignerModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, props) + Object.freeze(this) + } +} diff --git a/src/models/persistWallet/WalletEntryModel.js b/src/models/persistWallet/WalletEntryModel.js new file mode 100644 index 000000000..a4b758aaf --- /dev/null +++ b/src/models/persistWallet/WalletEntryModel.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' + +const schema = { + key: PropTypes.string, + name: PropTypes.string, + types: PropTypes.object, + encrypted: PropTypes.array, +} + +export default class WalletEntryModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, { + key: '', + name: '', + types: {}, + }, props) + Object.freeze(this) + } +} diff --git a/src/models/persistWallet/WalletModel.js b/src/models/persistWallet/WalletModel.js new file mode 100644 index 000000000..1d8459a2b --- /dev/null +++ b/src/models/persistWallet/WalletModel.js @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types' +import AbstractWalletModel from './AbstractWalletModel' +import WalletEntryModel from './WalletEntryModel' +import SignerModel from './SignerModel' + +const schema = { + wallet: PropTypes.object, + entry: PropTypes.instanceOf(WalletEntryModel), +} + +export default class WalletModel extends AbstractWalletModel { + constructor (props) { + super(props, schema) + Object.assign(this, props) + Object.freeze(this) + } + + get signer () { + // TODO @ipavlenko: Implement custom signers for hardware tokens + return new SignerModel({ + address: this.wallet[0].address.toLowerCase(), + sign: this.wallet[0].sign, + signTransaction: this.wallet[0].signTransaction, + }) + } +} diff --git a/src/models/persistWallet/index.js b/src/models/persistWallet/index.js new file mode 100644 index 000000000..569753291 --- /dev/null +++ b/src/models/persistWallet/index.js @@ -0,0 +1,11 @@ +import AbstractWalletModel from './AbstractWalletModel' +import SignerModel from './SignerModel' +import WalletEntryModel from './WalletEntryModel' +import WalletModel from './WalletModel' + +export { + AbstractWalletModel, + SignerModel, + WalletEntryModel, + WalletModel, +} diff --git a/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.js b/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.js new file mode 100644 index 000000000..37e9ef3ce --- /dev/null +++ b/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.js @@ -0,0 +1,184 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { connect } from 'react-redux' +import { push } from 'react-router-redux' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { Button } from 'components' +import { + initConfirmMnemonicPage, + navigateToConfirmMnemonicPage, + onSubmitConfirmMnemonic, + onSubmitConfirmMnemonicSuccess, + onSubmitConfirmMnemonicFail, +} from '@chronobank/login/redux/network/actions' + +import './ConfirmMnemonicPage.scss' +import { FORM_CREATE_ACCOUNT } from "../CreateAccountPage/CreateAccountPage"; + +export const FORM_CONFIRM_MNEMONIC = 'ConfirmMnemonicForm' + +function mapStateToProps (state) { + + return { + mnemonic: state.get('network').newAccountMnemonic, + } +} + +function mapDispatchToProps (dispatch) { + return { + navigateToConfirmPage: () => dispatch(navigateToConfirmMnemonicPage()), + initConfirmMnemonicPage: () => dispatch(initConfirmMnemonicPage()), + onSubmit: (values) => { + const confirmMnemonic = values.get('mnemonic') + + dispatch(onSubmitConfirmMnemonic(confirmMnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitConfirmMnemonicSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitConfirmMnemonicFail(errors, dispatch, submitErrors)), + } +} + +class ConfirmMnemonicPage extends Component { + static propTypes = { + mnemonic: PropTypes.string, + initConfirmMnemonicPage: PropTypes.func, + } + + static defaultProps = { + mnemonic: '', + } + + constructor (props){ + super(props) + + const wordsArray = props.mnemonic ? + props.mnemonic.split(' ').map((word, index) => { + return { index, word } + }) : [] + + this.state = { + confirmPhrase: [], + currentWordsArray: wordsArray.sort((a,b) => a.word < b.word), + } + } + + componentDidMount(){ + this.props.initConfirmMnemonicPage() + } + + getCurrentMnemonic (){ + return this.state.confirmPhrase.map((item) => item.word).join(' ') + } + + getWordsButtons (){ + return this.state.currentWordsArray.map((item, index) => { + const wordSelected = this.state.confirmPhrase.includes(item) + + return ( + + )} + ) + } + + onClickWord (word, e){ + const { dispatch, change } = this.props + + if (!this.state.confirmPhrase.includes(word)) { + this.setState( + { confirmPhrase: this.state.confirmPhrase.concat(word) }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + } + + clearMnemonic (){ + const { dispatch, change } = this.props + + this.setState( + { confirmPhrase: [] }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + + clearLastWord (){ + const { dispatch, change } = this.props + + this.setState( + { confirmPhrase: this.state.confirmPhrase.slice(0, -1) }, + () => change('mnemonic', this.getCurrentMnemonic()) + ) + } + + render () { + const { handleSubmit, error } = this.props + console.log('confirm mnemonic page', this.props) + + return ( + +
    +
    +
    Confirm back-up phrase
    + +

    Click on back-up phrase words in the correct order.

    + +
    +
    { this.getCurrentMnemonic() }
    + + +
    + +
    + {error} +
    + +
    + { this.getWordsButtons() } +
    + +
    +
    Start Over
    +
    Undo
    +
    + +
    + + + Back +
    + +
    +
    +
    +
    +
    +
    + + + ) + } +} + +const form = reduxForm({ form: FORM_CONFIRM_MNEMONIC })(ConfirmMnemonicPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.scss b/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.scss new file mode 100644 index 000000000..50acb912b --- /dev/null +++ b/src/pages/ConfirmMnemonicPage/ConfirmMnemonicPage.scss @@ -0,0 +1,216 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + + +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; +} + +.description { + margin-bottom: 30px; + color: $additionalData-color-1; + +} + +.passPhraseWrapper { + position: relative; + margin-bottom: 20px; + background: $background-color-1; + min-height: 100px; + border-radius: 2px; + padding: 20px 45px 20px 30px; +} + +.passPhrase { + font-size: 16px; + font-weight: 700; + line-height: 24px; + color: $button-color-2; + text-align: left; + min-height: 46px; + width: 100%; + + @include md-only { + min-height: 70px; + color: $synced-color; + } +} + +.wordsBlock { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.word { + width: 23%; + margin-bottom: 10px; + + button { + white-space: nowrap; + width: 100%; + cursor: pointer; + user-select: none; + color: #FFFFFF; + background-color: #614DBA; + text-transform: lowercase; + line-height: 62px; + border-radius: 2px; + font-size: 16px; + font-weight: 700; + padding: 12px 0; + } + + &:hover { + background-color: $button-color-2; + color: $background-color-1; + } + + @include md-only { + width: 31%; + } +} + +.submitButton { + background: #00A0D2; + font-size: 14px; + font-weight: 500; + color: #fff; + border-radius: 21px; + cursor: pointer; + line-height: 40px; + height: 40px; + min-width: 150px; + border: none; + margin-bottom: 40px; + padding: 0; + box-shadow: none; + + &:disabled { + cursor: not-allowed; + } + + @include md-only { + min-width: 180px; + } + + &:hover { + background: #0088C3; + } +} + +.progressBlock { + width: 50px; + margin: 0 auto 20px; + display: flex; + justify-content: space-between; +} + +.progressPoint { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progressPointInactive { + background: $color-blue-4; + } +} + +.clearMnemonic { + width: 23px; + height: 23px; + display: inline-block; + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin: auto; + padding: 11px; + cursor: pointer; + box-sizing: content-box; + + img { + width: 100%; + } +} + +.controlsBlock { + display: flex; + justify-content: space-between; + margin-bottom: 35px; + + .control { + width: 49%; + line-height: 42px; + font-size: 16px; + border: 1px solid $color-blue-4; + border-radius: 2px; + color: $color-blue-4; + cursor: pointer; + font-weight: 700; + + &:hover { + background: $color-hover; + color: $background-color-1; + border: 0; + } + } + +} + +.form { + text-align: center; + width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + width: auto; + padding: 0 20px; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.error { + color: red; + margin-bottom: 20px; + display: none; + text-align: center; +} + +.visible { + display: block; +} diff --git a/src/pages/CreateAccountPage/CreateAccountPage.js b/src/pages/CreateAccountPage/CreateAccountPage.js new file mode 100644 index 000000000..f254f2c62 --- /dev/null +++ b/src/pages/CreateAccountPage/CreateAccountPage.js @@ -0,0 +1,177 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' + +import { Button } from 'components' +import { + onSubmitCreateAccountPage, + onSubmitCreateAccountPageSuccess, + onSubmitCreateAccountPageFail, +} from '@chronobank/login/redux/network/actions' +import AutomaticProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/AutomaticProviderSelector' +import ManualProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/ManualProviderSelector' +import web3Provider from '@chronobank/login/network/Web3Provider' + +import validate from './validate' + +import styles from 'layouts/Splash/styles' +import fieldStyles from './styles' +import './CreateAccountPage.scss' + +const STRATEGY_MANUAL = 'manual' +const STRATEGY_AUTOMATIC = 'automatic' + +const nextStrategy = { + [STRATEGY_AUTOMATIC]: STRATEGY_MANUAL, + [STRATEGY_MANUAL]: STRATEGY_AUTOMATIC, +} + +export const FORM_CREATE_ACCOUNT = 'CreateAccountForm' + +function mapStateToProps (state, ownProps) { + + return { + isImportMode: state.get('network').importAccountMode, + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + const walletName = values.get('walletName') + const password = values.get('password') + + await dispatch(onSubmitCreateAccountPage(walletName, password)) + }, + onSubmitSuccess: () => dispatch(onSubmitCreateAccountPageSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitCreateAccountPageFail(errors, dispatch, submitErrors)), + } +} + +class CreateAccountPage extends PureComponent { + static propTypes = { + isImportMode: PropTypes.bool, + } + + constructor(){ + super() + + this.state = { + isShowProvider: true, + strategy: STRATEGY_AUTOMATIC, + } + } + + handleToggleProvider = (isShowProvider) => this.setState({ isShowProvider }) + + handleSelectorSwitch = (currentStrategy) => this.setState({ strategy: nextStrategy[currentStrategy] }) + + renderProviderSelector () { + switch (this.state.strategy) { + case STRATEGY_MANUAL: + return this.renderManualProviderSelector() + case STRATEGY_AUTOMATIC: + return this.renderAutomaticProviderSelector() + default: + return null + } + } + + renderAutomaticProviderSelector () { + return ( + + ) + } + + renderManualProviderSelector () { + return ( + + ) + } + + render () { + const { handleSubmit, pristine, valid, initialValues, isImportMode } = this.props + + return ( + +
    +
    + Create New Account +
    + +
    + Created wallet will be encrypted using given password and stored in your + browser's local storage. +
    + +
    + { this.renderProviderSelector() } +
    + +
    + + + +
    + +
    + + or
    + Use an existing account +
    + +
    + +
    + ) + } +} + +const form = reduxForm({ form: FORM_CREATE_ACCOUNT, validate })(CreateAccountPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) + diff --git a/src/pages/CreateAccountPage/CreateAccountPage.scss b/src/pages/CreateAccountPage/CreateAccountPage.scss new file mode 100644 index 000000000..3048ae140 --- /dev/null +++ b/src/pages/CreateAccountPage/CreateAccountPage.scss @@ -0,0 +1,197 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ +@import "~styles/partials/mixins"; +@import "~styles/partials/variables"; + +$copyRightPadding: 200px; + +.root { + background-color: #242045; + position: relative; + display: flex; + min-height: 100vh; + flex-direction: column; + font-family: $font-proxima; + + @include md-only { + min-height: 100vh; + padding: 0; + } + + &:after { + // hack for preloading material icons and avoid FUOC on render + content: ''; + font-family: 'Material Icons'; + } +} + +.form { + flex: 1; + justify-content: center; + flex-direction: column; + display: flex; + margin: 0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.content { + display: flex; + flex: 1; + justify-content: center; + flex-direction: column; + margin: 0 auto; + + @include xs-only { + padding: 80px 20px 0; + } +} + +.footer { + display: flex; + background-color: rgba(255, 255, 255, 0.1); + bottom: 0; + left: 0; + right: 0; + position: absolute; + padding: 17px 80px; + + @include md-only { + padding: 20px; + flex-direction: column-reverse; + } +} + +.header-container { + display: flex; + justify-content: center; + height: 340px; + overflow: hidden; + margin-bottom: 20px; + + @include md-only { + display: none; + } +} + +.header-picture { + display: flex; + position: absolute; + height: 320px; + overflow: hidden; + flex: 1; +} + +.header-picture-crop { + display: flex; + position: absolute; + height: 400px; + top: -115px; + flex: 1; +} + +.header-logos { + z-index: 100; + display: flex; + position: relative; + flex-direction: column; + align-items: center; + top: 90px; + width: 300px; + height: 200px; +} + +.chrono-wallet-logo-bright { + display: flex; + width: 114px; + height: 117px; + margin-bottom: 40px; +} + +.chrono-wallet-text-bright { + display: flex; + height: 59px; + width: 282px; +} + +.copyright { + max-width: $copyRightPadding; + font-size: 12px; + font-weight: 300; + color: white; +} + +.create-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 10px; + text-align: center; +} + +.create-title-description { + font-weight: 400; + color: $color-description; + font-size: 16px; + line-height: 22px; + margin-bottom: 10px; + text-align: center; + max-width: 520px; +} + +.links { + padding-right: $copyRightPadding; // compensation for centering + text-align: center; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + + @include md-only { + padding: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } +} + +.actions { + color: white; + text-align: center; + line-height: 30px; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.fields-block { + margin: 0 auto 50px; + max-width: 300px; +} + +.button { + margin-bottom: 25px; +} + +.selector { + color: #fff; +} diff --git a/src/pages/CreateAccountPage/styles.js b/src/pages/CreateAccountPage/styles.js new file mode 100644 index 000000000..265d41576 --- /dev/null +++ b/src/pages/CreateAccountPage/styles.js @@ -0,0 +1,8 @@ +export default { + textField: { + underlineStyle: { + borderColor: '#A3A3CC', + bottom: 0, + }, + }, +} diff --git a/src/pages/CreateAccountPage/validate.js b/src/pages/CreateAccountPage/validate.js new file mode 100644 index 000000000..183b6eb86 --- /dev/null +++ b/src/pages/CreateAccountPage/validate.js @@ -0,0 +1,32 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import ErrorList from 'platform/ErrorList' +import * as validator from 'models/validator' + +const validateEqualPasswords = (password, confirmPassword) => password === confirmPassword ? null : 'Wrong password' + +export default (values) => { + const walletName = values.get('walletName') + + let walletNameErrors = new ErrorList() + walletNameErrors.add(validator.required(walletName)) + + const password = values.get('password') + + let passwordErrors = new ErrorList() + passwordErrors.add(validator.required(password)) + + const confirmPassword = values.get('confirmPassword') + let confirmPasswordErrors = new ErrorList() + confirmPasswordErrors.add(validator.required(confirmPassword)) + confirmPasswordErrors.add(validateEqualPasswords(password, confirmPassword)) + + return { + walletName: walletNameErrors.getErrors(), + password: passwordErrors.getErrors(), + confirmPassword: confirmPasswordErrors.getErrors(), + } +} diff --git a/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.js b/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.js new file mode 100644 index 000000000..76195d515 --- /dev/null +++ b/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.js @@ -0,0 +1,83 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import { connect } from 'react-redux' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { Button } from 'components' +import { + downloadWallet, +} from 'redux/persistAccount/actions' +import { + navigateToLoginPage, +} from '@chronobank/login/redux/network/actions' + +import Wallet from 'assets/img/icons/wallet-white.svg' + +import './DownloadWalletFilePage.scss' + +function mapDispatchToProps (dispatch) { + return { + downloadWallet: () => dispatch(downloadWallet()), + navigateToLoginPage: () => dispatch(navigateToLoginPage()), + } +} + +@connect(null, mapDispatchToProps) +export default class MnemonicPage extends Component { + static propTypes = { + downloadWallet: PropTypes.func, + navigateToLoginPage: PropTypes.func, + } + + render () { + const { downloadWallet, navigateToLoginPage } = this.props + + return ( + +
    +
    +
    Download a Wallet File
    + +

    + You can use this wallet file in password recovery option to + make your account available in another browser, for example. + The file is protected by the same password as your created before. +

    + +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ) + } +} diff --git a/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.scss b/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.scss new file mode 100644 index 000000000..ed40e91e3 --- /dev/null +++ b/src/pages/DownloadWalletFilePage/DownloadWalletFilePage.scss @@ -0,0 +1,109 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + padding: 0 20px; + } +} + +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; +} + +.description { + margin-bottom: 50px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; + + @include md-only { + margin-bottom: 20px; + } +} + +.progress-block { + width: 50px; + margin: 0 auto 20px; + display: flex; + justify-content: space-between; +} + +.progress-point { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progress-point-inactive { + background: $color-blue-4; + } +} + +.row { + margin: 0 auto 50px; +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.wallet-img { + width: 48px; + height: 48px; + display: block; + margin: 0 auto; +} + +.button { + text-align: center; + margin-bottom: 50px; + + button { + font-size: 14px; + text-transform: none; + width: 110px; + height: 100px; + padding: 0; + border-radius: 3px; + } + + img { + width: 50px; + margin-bottom: 5px; + } +} diff --git a/src/pages/ImportMethodsPage/ImportMethodsPage.js b/src/pages/ImportMethodsPage/ImportMethodsPage.js new file mode 100644 index 000000000..1652d1559 --- /dev/null +++ b/src/pages/ImportMethodsPage/ImportMethodsPage.js @@ -0,0 +1,136 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { Link } from 'react-router' +import { connect } from 'react-redux' +import { Button } from 'components' + +import { + navigateToMnemonicImportMethod, + navigateToPrivateKeyImportMethod, + navigateToCreateAccount, + initImportMethodsPage, + navigateToCreateAccountWithoutImport, +} from '@chronobank/login/redux/network/actions' + +import Trezor from 'assets/img/icons/trezor-white.svg' +import Ledger from 'assets/img/icons/ledger-nano-white.svg' +import Plugin from 'assets/img/icons/plugin-white.svg' +import Mnemonic from 'assets/img/icons/mnemonic-white.svg' +import Key from 'assets/img/icons/key-white.svg' +import Wallet from 'assets/img/icons/wallet-white.svg' +import Uport from 'assets/img/icons/uport.svg' + +// import styles from 'layouts/Splash/styles' +import './ImportMethodsPage.scss' + +function mapDispatchToProps (dispatch) { + return { + navigateToMnemonicImportMethod: () => dispatch(navigateToMnemonicImportMethod()), + navigateToPrivateKeyImportMethod: () => dispatch(navigateToPrivateKeyImportMethod()), + navigateToCreateAccount: () => dispatch(navigateToCreateAccount()), + navigateToCreateAccountWithoutImport: () => dispatch(navigateToCreateAccountWithoutImport()), + initImportMethodsPage: () => dispatch(initImportMethodsPage()), + } +} + +@connect(null, mapDispatchToProps) +export default class ImportMethodsPage extends PureComponent { + static propTypes = { + navigateToMnemonicImportMethod: PropTypes.func, + navigateToPrivateKeyImportMethod: PropTypes.func, + initImportMethodsPage: PropTypes.func, + navigateToCreateAccountWithoutImport: PropTypes.func, + } + + componentDidMount(){ + this.props.initImportMethodsPage() + } + + handleMnemonicLogin = () => this.props.navigateToMnemonicImportMethod() + + handlePrivateKeyLogin = () => this.props.navigateToPrivateKeyImportMethod() + + handleWalletFileLogin = () => {} + + handleCreateAccount = () => this.props.navigateToCreateAccountWithoutImport() + + render () { + return ( + +
    + +
    Add an Existing Account
    + +
    + + + + + + + + + + + + + +
    + +
    + or
    + Create New Account +
    + +
    +
    + ) + } +} diff --git a/src/pages/ImportMethodsPage/ImportMethodsPage.scss b/src/pages/ImportMethodsPage/ImportMethodsPage.scss new file mode 100644 index 000000000..d18a725c9 --- /dev/null +++ b/src/pages/ImportMethodsPage/ImportMethodsPage.scss @@ -0,0 +1,124 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.page { + max-width: 350px; + margin:0 auto; + +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.methods { + margin-bottom: 15px; + display: flex; + justify-content: space-around; + flex-wrap: wrap; +} + +.button { + width: 31%; + margin-bottom: 10px; + + &.button-uport { + display: none; + + @include xs-only { + display: inline-block; + } + } + + &.button-trezor, + &.button-ledger, + &.button-plugin { + @include xs-only { + display: none; + } + } + + button { + width: 100%; + height: 100px; + padding: 0; + text-transform: none; + border-radius: 3px; + } + + img { + width: 50px; + margin-bottom: 5px; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + cursor: pointer; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/src/pages/LoginMethods/Mnemonic/Mnemonic.js b/src/pages/LoginMethods/Mnemonic/Mnemonic.js new file mode 100644 index 000000000..4103884c4 --- /dev/null +++ b/src/pages/LoginMethods/Mnemonic/Mnemonic.js @@ -0,0 +1,76 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { reduxForm, Field } from 'redux-form/immutable' +import { Link } from 'react-router' +import { TextField } from 'redux-form-material-ui' +import styles from 'layouts/Splash/styles' +import { Button } from 'components' +import { + onSubmitMnemonicLoginForm, + onSubmitMnemonicLoginFormSuccess, + onSubmitMnemonicLoginFormFail, +} from '@chronobank/login/redux/network/actions' + +import './Mnemonic.scss' +import { FORM_LOGIN_PAGE } from "../../LoginPage/LoginPage"; + +export const FORM_MNEMONIC_LOGIN_PAGE = 'MnemonicLoginPageForm' + +function mapDispatchToProps (dispatch) { + return { + onSubmit: (values) => { + const confirmMnemonic = values.get('mnemonic') + dispatch(onSubmitMnemonicLoginForm(confirmMnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitMnemonicLoginFormSuccess()), + onSubmitFail: () => dispatch(onSubmitMnemonicLoginFormFail()), + } +} + +class MnemonicLoginPage extends PureComponent { + render () { + const { handleSubmit } = this.props + + return ( + +
    + +
    Mnemonic form
    + +
    + +
    + +
    + + or  + back +
    + +
    +
    + ) + } +} + +const form = reduxForm({ form: FORM_MNEMONIC_LOGIN_PAGE })(MnemonicLoginPage) +export default connect(null, mapDispatchToProps)(form) diff --git a/src/pages/LoginMethods/Mnemonic/Mnemonic.scss b/src/pages/LoginMethods/Mnemonic/Mnemonic.scss new file mode 100644 index 000000000..6d524ee18 --- /dev/null +++ b/src/pages/LoginMethods/Mnemonic/Mnemonic.scss @@ -0,0 +1,94 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/src/pages/LoginMethods/PrivateKey/PrivateKey.js b/src/pages/LoginMethods/PrivateKey/PrivateKey.js new file mode 100644 index 000000000..f566c002a --- /dev/null +++ b/src/pages/LoginMethods/PrivateKey/PrivateKey.js @@ -0,0 +1,75 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import styles from 'layouts/Splash/styles' +import { Button } from 'components' +import { + onSubmitPrivateKeyLoginForm, + onSubmitPrivateKeyLoginFormSuccess, + onSubmitPrivateKeyLoginFormFail, +} from '@chronobank/login/redux/network/actions' + +import './PrivateKey.scss' + +export const FORM_PRIVATE_KEY_LOGIN_PAGE = 'PrivateKeyLoginPageForm' + +function mapDispatchToProps (dispatch) { + return { + onSubmit: (values) => { + const privateKey = values.get('pk') + dispatch(onSubmitPrivateKeyLoginForm(privateKey)) + }, + onSubmitSuccess: () => dispatch(onSubmitPrivateKeyLoginFormSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitPrivateKeyLoginFormFail(errors, dispatch, submitErrors)), + } +} + +class MnemonicLoginPage extends PureComponent { + render () { + const { handleSubmit } = this.props + + return ( + +
    + +
    Private key form
    + +
    + +
    + +
    + + or  + back +
    + +
    +
    + ) + } +} + +const form = reduxForm({ form: FORM_PRIVATE_KEY_LOGIN_PAGE })(MnemonicLoginPage) +export default connect(null, mapDispatchToProps)(form) diff --git a/src/pages/LoginMethods/PrivateKey/PrivateKey.scss b/src/pages/LoginMethods/PrivateKey/PrivateKey.scss new file mode 100644 index 000000000..6d524ee18 --- /dev/null +++ b/src/pages/LoginMethods/PrivateKey/PrivateKey.scss @@ -0,0 +1,94 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/src/pages/LoginMethods/WalletFile/UploadWalletPage.js b/src/pages/LoginMethods/WalletFile/UploadWalletPage.js new file mode 100644 index 000000000..58ad77677 --- /dev/null +++ b/src/pages/LoginMethods/WalletFile/UploadWalletPage.js @@ -0,0 +1,82 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' + +import FileIcon from 'assets/img/icons/file-white.svg' +import DeleteIcon from 'assets/img/icons/delete-white.svg' +import SpinnerGif from 'assets/img/spinningwheel.gif' +import WarningIcon from 'assets/img/icons/warning.svg' +import CheckIcon from 'assets/img/icons/check-green.svg' + +import './UploadWalletPage.scss' + +export default class MnemonicPage extends Component { + static propTypes = { + mnemonic: PropTypes.string, + } + + static defaultProps = { + mnemonic: '', + } + + render () { + return ( + +
    +
    +
    Upload a Wallet File
    + +

    + Upload a wallet file to add the login information to your browser. + We provide the file on New Account Creation. +

    + +
    + + + + + + + +
    + +
    + + or +
    + Back +
    + +
    +
    +
    + ) + } +} diff --git a/src/pages/LoginMethods/WalletFile/UploadWalletPage.scss b/src/pages/LoginMethods/WalletFile/UploadWalletPage.scss new file mode 100644 index 000000000..6eea2ae81 --- /dev/null +++ b/src/pages/LoginMethods/WalletFile/UploadWalletPage.scss @@ -0,0 +1,108 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + padding: 0 20px; + } +} + +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; +} + +.description { + margin-bottom: 50px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; + + @include md-only { + margin-bottom: 20px; + } +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.row { + margin-bottom: 50px; +} + +.button { + button { + display: flex; + align-items: center; + padding: 0 20px; + line-height: 45px; + min-width: 280px; + margin: 0 auto 20px; + text-transform: none; + + &:disabled { + background: $background-color-1; + } + } + + &.button-warning button { + background: $color-red-2; + } + .button-text { + flex: 1; + text-align: left; + } + + .before-img { + width: 24px; + display: inline-block; + margin-right: 5px; + } + + .after-img { + width: 24px; + display: inline-block; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: $additionalData-color-1; + line-height: 30px; +} + +.submit { + margin-bottom: 25px; + + button:disabled { + background: $background-color-1; + } +} diff --git a/src/pages/LoginMethods/index.js b/src/pages/LoginMethods/index.js new file mode 100644 index 000000000..b287c51ee --- /dev/null +++ b/src/pages/LoginMethods/index.js @@ -0,0 +1,2 @@ +export { default as MnemonicLoginPage, FORM_MNEMONIC_LOGIN_PAGE } from './Mnemonic/Mnemonic' +export { default as PrivateKeyLoginPage, FORM_PRIVATE_KEY_LOGIN_PAGE } from './PrivateKey/PrivateKey' diff --git a/src/pages/LoginPage/LoginPage.js b/src/pages/LoginPage/LoginPage.js index 247fdca19..4197c428f 100644 --- a/src/pages/LoginPage/LoginPage.js +++ b/src/pages/LoginPage/LoginPage.js @@ -3,29 +3,167 @@ * Licensed under the AGPL Version 3 license. */ -import LocaleDropDown from 'layouts/partials/LocaleDropDown/LocaleDropDown' -import LoginForm from '@chronobank/login-ui/components/LoginForm/LoginForm' -import { MuiThemeProvider } from 'material-ui' +import PropTypes from 'prop-types' +import { MuiThemeProvider, CircularProgress } from 'material-ui' import React, { PureComponent } from 'react' -import { styles } from '@chronobank/login-ui/settings' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import { connect } from 'react-redux' +import { Translate } from 'react-redux-i18n' +import { UserRow, Button } from 'components' +import { + onSubmitLoginForm, + onSubmitLoginFormFail, + initLoginPage, + navigateToSelectWallet, +} from '@chronobank/login/redux/network/actions' +import AutomaticProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/AutomaticProviderSelector' +import ManualProviderSelector from '@chronobank/login-ui/components/ProviderSelectorSwitcher/ManualProviderSelector' +import styles from 'layouts/Splash/styles' import './LoginPage.scss' +const STRATEGY_MANUAL = 'manual' +const STRATEGY_AUTOMATIC = 'automatic' + +const nextStrategy = { + [STRATEGY_AUTOMATIC]: STRATEGY_MANUAL, + [STRATEGY_MANUAL]: STRATEGY_AUTOMATIC, +} + +export const FORM_LOGIN_PAGE = 'FormLoginPage' + +function mapStateToProps (state, ownProps) { + + return { + selectedWallet: state.get('persistAccount').selectedWallet, + isLoginSubmitting: state.get('network').isLoginSubmitting, + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + const password = values.get('password') + + await dispatch(onSubmitLoginForm(password)) + }, + onSubmitFail: () => dispatch(onSubmitLoginFormFail()), + initLoginPage: () => dispatch(initLoginPage()), + navigateToSelectWallet: () => dispatch(navigateToSelectWallet()), + } +} + class LoginPage extends PureComponent { + static propTypes = { + initLoginPage: PropTypes.func, + navigateToSelectWallet: PropTypes.func, + isLoginSubmitting: PropTypes.bool, + } + + constructor(props){ + super(props) + + this.state = { + isShowProvider: true, + strategy: STRATEGY_AUTOMATIC, + } + } + + componentWillMount(){ + this.props.initLoginPage() + } + + handleToggleProvider = (isShowProvider) => this.setState({ isShowProvider }) + + handleSelectorSwitch = (currentStrategy) => this.setState({ strategy: nextStrategy[currentStrategy] }) + + renderProviderSelector () { + switch (this.state.strategy) { + case STRATEGY_MANUAL: + return this.renderManualProviderSelector() + case STRATEGY_AUTOMATIC: + return this.renderAutomaticProviderSelector() + default: + return null + } + } + + renderAutomaticProviderSelector () { + return ( + + ) + } + + renderManualProviderSelector () { + return ( + + ) + } + render () { + const { handleSubmit, pristine, valid, initialValues, isImportMode, onSubmit, selectedWallet, + navigateToSelectWallet, isLoginSubmitting } = this.props + return ( -
    - -
      -
    • - -
    • -
    -
    +
    + +
    Log In
    + +
    + { this.renderProviderSelector() } +
    + +
    + + +
    + +
    + +
    +
    +
    + +
    ) } } -export default LoginPage +const form = reduxForm({ form: FORM_LOGIN_PAGE })(LoginPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/src/pages/LoginPage/LoginPage.scss b/src/pages/LoginPage/LoginPage.scss index 1b58ae2a5..8514b0650 100644 --- a/src/pages/LoginPage/LoginPage.scss +++ b/src/pages/LoginPage/LoginPage.scss @@ -40,3 +40,57 @@ ul.actions { } } +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.selector { + color: white; +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/src/pages/MnemonicPage/MnemonicPage.js b/src/pages/MnemonicPage/MnemonicPage.js new file mode 100644 index 000000000..40c560608 --- /dev/null +++ b/src/pages/MnemonicPage/MnemonicPage.js @@ -0,0 +1,119 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { push } from 'react-router-redux' +import { connect } from 'react-redux' +import classnames from 'classnames' +import { MuiThemeProvider } from 'material-ui' +import { reduxForm, Field } from 'redux-form/immutable' +import React, { Component } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' +import { initMnemonicPage, navigateToConfirmMnemonicPage } from '@chronobank/login/redux/network/actions' + +import PrintIcon from 'assets/img/icons/print-white.svg' + +import './MnemonicPage.scss' + +function mapStateToProps (state, ownProps) { + + return { + mnemonic: state.get('network').newAccountMnemonic, + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + initMnemonicPage: () => dispatch(initMnemonicPage()), + navigateToConfirmPage: () => dispatch(navigateToConfirmMnemonicPage()), + } +} + +@connect(mapStateToProps, mapDispatchToProps) +export default class MnemonicPage extends Component { + static propTypes = { + mnemonic: PropTypes.string, + initMnemonicPage: PropTypes.func, + navigateToConfirmPage: PropTypes.func, + } + + static defaultProps = { + mnemonic: '', + } + + componentDidMount(){ + this.props.initMnemonicPage() + } + + navigateToConfirmPage(){ + this.props.navigateToConfirmPage() + } + + render () { + return ( + +
    +
    +
    Write down back-up phrase
    + +

    + You can use this phrase to login and access your wallet, + even if you forgot your password. You may also print the key + which will be provided with a QR code. Use this QR code to + scan on phone on ChronoWallet recover page. +

    + +
    +
    { this.props.mnemonic }
    +
    +
    {}}> + +
    +
    +
    + +
    +
    Important! Read the security guidelines
    + +
      +
    1. +

      + Don't share your back-up phrase (mnemonic key) with someone you don't trust. +  Double check services you're giving your mnemonic to and don't share your phrase with anyone. +

      +
    2. + +
    3. +

      + Don't loose your back-up phrase (mnemonic key). +  We do not store this information and Your account will be lost + together with all your funds and history. +

      +
    4. +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + ) + } +} diff --git a/src/pages/MnemonicPage/MnemonicPage.scss b/src/pages/MnemonicPage/MnemonicPage.scss new file mode 100644 index 000000000..c7d7994be --- /dev/null +++ b/src/pages/MnemonicPage/MnemonicPage.scss @@ -0,0 +1,167 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.wrapper { + text-align: center; + max-width: 600px; + margin: 0 auto; + color: #fff; + + @include md-only { + padding: 0 20px; + } +} + +.page-title { + font-size: 30px; + margin-bottom: 10px; + font-weight: 700; +} + +.description { + margin-bottom: 30px; + color: $additionalData-color-1; + line-height: 22px; + font-weight: 400; + + @include md-only { + margin-bottom: 20px; + } +} + +.passPhraseWrapper { + position: relative; + margin-bottom: 20px; + background: $background-color-1; + min-height: 100px; + border-radius: 2px; + padding: 30px 60px 20px 30px; + + @include md-only { + margin-bottom: 0; + } +} + +.passPhrase { + font-size: 16px; + font-weight: 700; + line-height: 24px; + color: $button-color-2; + text-align: left; + min-height: 46px; + width: 100%; + + @include md-only { + min-height: 55px; + } +} + + +.progressBlock { + width: 50px; + margin: 0 auto 20px; + display: flex; + justify-content: space-between; +} + +.progressPoint { + background: $button-color-2; + width: 10px; + height: 10px; + border-radius: 50%; + + &.progressPointInactive { + background: $color-blue-4; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + color: #fff; + line-height: 30px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.submit { + margin-bottom: 25px; +} + +.infoBlock { + text-align: left; + padding: 32px 30px 10px; + border-top: 5px solid #FF6D6B; + background: $background-color-1; + border-radius: 3px; + margin-bottom: 40px; + + @include md-only { + border-radius: 0; + } +} + +.infoBlockList { + font-weight: bold; + font-size: 16px; + margin-left: 15px; +} + +.listItemContent { + font-weight: 400; + color: #A3A3CC; + line-height: 22px; + margin-bottom: 20px; + + b { + color: #fff; + font-weight: bold; + } +} + +.infoBlockHeader { + color: #FF6D6B; + font-weight: 700; + font-size: 20px; + margin-bottom: 20px; +} + +.printButtonWrapper { + position: absolute; + right: 0; + width: 45px; + top: 0; + bottom: 0; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.printButton { + width: 24px; + opacity: 0.25; + cursor: pointer; + + &:hover { + opacity: 1; + } +} + diff --git a/src/pages/RecoverAccountPage/RecoverAccountPage.js b/src/pages/RecoverAccountPage/RecoverAccountPage.js new file mode 100644 index 000000000..3b9cc4b86 --- /dev/null +++ b/src/pages/RecoverAccountPage/RecoverAccountPage.js @@ -0,0 +1,128 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { Link } from 'react-router' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import { + WalletEntryModel, +} from 'models/persistAccount' +import { + onSubmitRecoverAccountForm, + onSubmitRecoverAccountFormSuccess, + onSubmitRecoverAccountFormFail, + initRecoverAccountPage, +} from '@chronobank/login/redux/network/actions' + +import { Button, UserRow } from 'components' + +import styles from 'layouts/Splash/styles' +import './RecoverAccountPage.scss' + +export const FORM_RECOVER_ACCOUNT = 'RecoverAccountPage' + +function mapStateToProps (state, ownProps) { + const selectedWallet = state.get('persistAccount').selectedWallet + return { + selectedWallet: new WalletEntryModel({...selectedWallet}), + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + let words = [], mnemonic = '' + + for (let i = 1; i <= 12; i++) { + const word = values.get(`word-${i}`) + word && words.push(word) + } + + mnemonic = words.join(' ') + + await dispatch(onSubmitRecoverAccountForm(mnemonic)) + }, + onSubmitSuccess: () => dispatch(onSubmitRecoverAccountFormSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitRecoverAccountFormFail(errors, dispatch, submitErrors)), + initRecoverAccountPage: () => dispatch(initRecoverAccountPage()), + } +} + +class RecoverAccountPage extends PureComponent { + static propTypes = { + selectedWallet: PropTypes.instanceOf(WalletEntryModel), + initRecoverAccountPage: PropTypes.func, + } + + componentWillMount(){ + this.props.initRecoverAccountPage() + } + + get getSelectedWalletName(){ + const { selectedWallet } = this.props + return selectedWallet && selectedWallet.name || '' + } + + render () { + const { handleSubmit, selectedWallet } = this.props + + const wordsArray = new Array(12).fill() + + return ( + +
    +
    + Enter mnemonic to reset password +
    + +
    + +
    + +
    + { + wordsArray.map((item, i) => ( + + )) + } +
    + +
    + + or
    + Back +
    + +
    + +
    + ) + } +} + +const form = reduxForm({ form: FORM_RECOVER_ACCOUNT })(RecoverAccountPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/src/pages/RecoverAccountPage/RecoverAccountPage.scss b/src/pages/RecoverAccountPage/RecoverAccountPage.scss new file mode 100644 index 000000000..ceaa1309e --- /dev/null +++ b/src/pages/RecoverAccountPage/RecoverAccountPage.scss @@ -0,0 +1,77 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.actions { + text-align: center; + margin-bottom: 45px; + line-height: 30px; + color: #fff; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.button { + text-align: center; + margin-bottom: 22px; + + button { + padding: 0 30px; + } +} + +.actionIcon { + transform: matrix(-1,0,0,-1,0,0); +} + +.user-row { + border-top: 1px solid $color-purpule; + margin-bottom: 10px; + color: #fff; +} + +.fields-block { + margin-bottom: 50px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.field { + width: 23% !important; +} diff --git a/src/pages/ResetPasswordPage/ResetPasswordPage.js b/src/pages/ResetPasswordPage/ResetPasswordPage.js new file mode 100644 index 000000000..71a8962fb --- /dev/null +++ b/src/pages/ResetPasswordPage/ResetPasswordPage.js @@ -0,0 +1,116 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { MuiThemeProvider } from 'material-ui' +import React, { PureComponent } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { + WalletEntryModel, +} from 'models/persistAccount' +import { + onSubmitResetAccountPasswordForm, + onSubmitResetAccountPasswordSuccess, + onSubmitResetAccountPasswordFail, + initResetPasswordPage, +} from '@chronobank/login/redux/network/actions' +import { reduxForm, Field } from 'redux-form/immutable' +import { TextField } from 'redux-form-material-ui' +import { UserRow, Button } from 'components' + +import styles from 'layouts/Splash/styles' +import validate from './validate' +import './ResetPasswordPage.scss' + +export const FORM_RESET_PASSWORD = 'ResetPasswordPage' + +function mapStateToProps (state, ownProps) { + const selectedWallet = state.get('persistAccount').selectedWallet + return { + selectedWallet: new WalletEntryModel({...selectedWallet}), + } +} + +function mapDispatchToProps (dispatch, ownProps) { + return { + onSubmit: async (values) => { + const password = values.get('password') + + await dispatch(onSubmitResetAccountPasswordForm(password)) + }, + onSubmitSuccess: () => dispatch(onSubmitResetAccountPasswordSuccess()), + onSubmitFail: (errors, dispatch, submitErrors) => dispatch(onSubmitResetAccountPasswordFail(errors, dispatch, submitErrors)), + initResetPasswordPage: () => dispatch(initResetPasswordPage()), + } +} + +class ResetPasswordPage extends PureComponent { + static propTypes = { + selectedWallet: PropTypes.instanceOf(WalletEntryModel), + initResetPasswordPage: PropTypes.func, + } + + componentWillMount(){ + this.props.initResetPasswordPage() + } + + get getSelectedWalletName(){ + const { selectedWallet } = this.props + return selectedWallet && selectedWallet.name || '' + } + render () { + const { handleSubmit, selectedWallet } = this.props + + return ( + +
    + +
    Reset password
    + +
    + {}} + /> +
    + +
    + + +
    + +
    + +
    + +
    +
    + ) + } +} + +const form = reduxForm({ form: FORM_RESET_PASSWORD, validate })(ResetPasswordPage) +export default connect(mapStateToProps, mapDispatchToProps)(form) diff --git a/src/pages/ResetPasswordPage/ResetPasswordPage.scss b/src/pages/ResetPasswordPage/ResetPasswordPage.scss new file mode 100644 index 000000000..d1ce6b9bf --- /dev/null +++ b/src/pages/ResetPasswordPage/ResetPasswordPage.scss @@ -0,0 +1,92 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +ul.actions { + position: absolute; + top: 30px; + right: 80px; + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + padding: 0; + list-style: none; + font-size: 18px; + line-height: 48px; + + @include md-only { + top: 10px; + right: 10px; + flex: 1 1 auto; + } + + li { + + flex: 0 0 auto; + + a { + padding: 10px 20px; + text-decoration: none; + color: $color-white; + + &:hover { + text-decoration: underline; + } + } + } +} + +.form { + max-width: 380px; + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.user-row { + border-top: 1px solid $color-purpule; +} + +.field { + margin-bottom: 50px; +} + +.button { + text-align: center; + margin-bottom: 30px; +} + +.actions { + text-align: center; + margin-bottom: 45px; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} diff --git a/src/pages/ResetPasswordPage/validate.js b/src/pages/ResetPasswordPage/validate.js new file mode 100644 index 000000000..a23dc2614 --- /dev/null +++ b/src/pages/ResetPasswordPage/validate.js @@ -0,0 +1,27 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import ErrorList from 'platform/ErrorList' +import * as validator from 'models/validator' + +const validateEqualPasswords = (password, confirmPassword) => password === confirmPassword ? null : 'Wrong password' + +export default (values) => { + + const password = values.get('password') + + let passwordErrors = new ErrorList() + passwordErrors.add(validator.required(password)) + + const confirmPassword = values.get('confirmPassword') + let confirmPasswordErrors = new ErrorList() + confirmPasswordErrors.add(validator.required(confirmPassword)) + confirmPasswordErrors.add(validateEqualPasswords(password, confirmPassword)) + + return { + password: passwordErrors.getErrors(), + confirmPassword: confirmPasswordErrors.getErrors(), + } +} diff --git a/src/pages/SelectWalletPage/SelectWalletPage.js b/src/pages/SelectWalletPage/SelectWalletPage.js new file mode 100644 index 000000000..7a6f258bc --- /dev/null +++ b/src/pages/SelectWalletPage/SelectWalletPage.js @@ -0,0 +1,111 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import PropTypes from 'prop-types' +import { MuiThemeProvider } from 'material-ui' +import { connect } from 'react-redux' +import React, { PureComponent } from 'react' +import { Link } from 'react-router' +import { UserRow, Button } from 'components' +import { navigateToSelectImportMethod, onWalletSelect } from '@chronobank/login/redux/network/actions' +import { + WalletEntryModel, +} from 'models/persistAccount' + +import arrow from 'assets/img/icons/prev-white.svg' +import './SelectWalletPage.scss' + +function mapDispatchToProps (dispatch) { + return { + navigateToSelectImportMethod: () => dispatch(navigateToSelectImportMethod()), + onWalletSelect: (wallet) => dispatch(onWalletSelect(wallet)), + } +} + +function mapStateToProps (state) { + return { + walletsList: state.get('persistAccount').walletsList.map( + (wallet) => new WalletEntryModel({...wallet}) + ), + } +} + +@connect(mapStateToProps, mapDispatchToProps) +export default class SelectWalletPage extends PureComponent { + static propTypes = { + onWalletSelect: PropTypes.func, + walletsList: PropTypes.arrayOf( + PropTypes.instanceOf(WalletEntryModel) + ), + navigateToSelectImportMethod: PropTypes.func, + } + + static defaultProps = { + onWalletSelect: () => {}, + walletsList: [], + } + + renderWalletsList (){ + const { onWalletSelect, walletsList } = this.props + + if (!walletsList || !walletsList.length){ + return ( +
    + Sorry, there are no accounts to display +
    + ) + } + + return ( +
    + { + walletsList ? walletsList.map((w, i) => ( + onWalletSelect(w)} + /> + )) : null + } +
    + ) + } + + render () { + return ( + +
    + +
    My Accounts
    + +
    + Browse account stored on your device. +
    + If you have created an account before you may use Add an Existing Account option below. +
    + +
    + { this.renderWalletsList() } + +
    + + or
    + Create New Account +
    +
    + +
    +
    + ) + } +} diff --git a/src/pages/SelectWalletPage/SelectWalletPage.scss b/src/pages/SelectWalletPage/SelectWalletPage.scss new file mode 100644 index 000000000..168f3cf01 --- /dev/null +++ b/src/pages/SelectWalletPage/SelectWalletPage.scss @@ -0,0 +1,93 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +@import "~styles/partials/variables"; +@import "~styles/partials/mixins"; + +.wrapper { + margin:0 auto; + + @include xs-only { + padding: 0 20px; + } +} + +.content { + margin: 0 auto; + max-width: 380px; +} + +.page-title { + color: $color-white; + font-weight: 700; + font-size: 30px; + line-height: 42px; + margin-bottom: 40px; + text-align: center; +} + +.wallets-list { + border-top: 1px solid $color-purpule; + margin-bottom: 45px; + color: #fff; +} + +.empty-list { + border-top: 1px solid $color-purpule; + border-bottom: 1px solid $color-purpule; + padding: 18px 0; + margin-bottom: 30px; + text-align: center; + color: #fff; + width: 380px; + + @include xs-only { + width: 100%; + } +} + +.actions { + text-align: center; + margin-bottom: 45px; + line-height: 30px; + color: #fff; +} + +.link { + color: $border-color; + margin: 0 auto 45px; + text-decoration: none; + font: 700 16px $font-proxima; + + &:hover { + color: #FFB54E; + } + + @include md-only { + margin: 0 0 10px; + } +} + +.button { + text-align: center; + margin-bottom: 22px; + + button { + padding: 0 30px; + } +} + +.actionIcon { + transform: matrix(-1,0,0,-1,0,0); +} + +.description { + font-weight: 400; + color: $additionalData-color-1; + font-size: 16px; + line-height: 22px; + margin-bottom: 40px; + text-align: center; +} diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 000000000..76af6bc56 --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,18 @@ +export { default as PageBase } from './PageBase' +export { default as LoginPage, FORM_LOGIN_PAGE } from './LoginPage/LoginPage' +export { default as NotFoundPage } from './NotFound/NotFound' +export { default as CreateAccountPage, FORM_CREATE_ACCOUNT } from './CreateAccountPage/CreateAccountPage' +export { default as SelectWalletPage } from './SelectWalletPage/SelectWalletPage' +export { default as RecoverAccountPage, FORM_RECOVER_ACCOUNT } from './RecoverAccountPage/RecoverAccountPage' +export { default as ResetPasswordPage, FORM_RESET_PASSWORD } from './ResetPasswordPage/ResetPasswordPage' +export { default as ImportMethodsPage } from './ImportMethodsPage/ImportMethodsPage' +export { default as ConfirmMnemonicPage, FORM_CONFIRM_MNEMONIC } from './ConfirmMnemonicPage/ConfirmMnemonicPage' +export { default as MnemonicPage } from './MnemonicPage/MnemonicPage' +export { default as DownloadWalletFilePage } from './DownloadWalletFilePage/DownloadWalletFilePage' +export { default as UploadWalletPage } from './LoginMethods/WalletFile/UploadWalletPage' +export { + MnemonicLoginPage, + PrivateKeyLoginPage, + FORM_MNEMONIC_LOGIN_PAGE, + FORM_PRIVATE_KEY_LOGIN_PAGE, +} from './LoginMethods' diff --git a/src/redux/configureStore.js b/src/redux/configureStore.js index aa62a9aa2..34f1e6c9d 100644 --- a/src/redux/configureStore.js +++ b/src/redux/configureStore.js @@ -84,11 +84,13 @@ const configureStore = () => { const i18nState = state.get('i18n') const mainWalletsState = state.get('mainWallet') const walletsState = state.get('multisigWallet') + const persistAccount = state.get('persistAccount') state = new Immutable.Map() state = state .set('i18n', i18nState) .set('multisigWallet', walletsState) .set('mainWallet', mainWalletsState) + .set('persistAccount', persistAccount) } return appReducer(state, action) } @@ -117,7 +119,8 @@ export const store = configureStore() store.dispatch(globalWatcher()) const persistorConfig = { - whitelist: ['multisigWallet', 'mainWallet'], + key: 'root', + whitelist: ['multisigWallet', 'mainWallet', 'persistAccount'], transforms: [transformer()], } store.__persistor = persistStore(store, persistorConfig) diff --git a/src/redux/ducks.js b/src/redux/ducks.js index 54ee07874..a47f3398c 100644 --- a/src/redux/ducks.js +++ b/src/redux/ducks.js @@ -24,6 +24,7 @@ import * as wallet from './wallet' import * as watcher from './watcher' import * as tokens from './tokens' import * as assetsHolder from './assetsHolder' +import * as persistAccount from './persistAccount' export default { ui, @@ -46,5 +47,6 @@ export default { assetsManager, tokens, assetsHolder, + persistAccount, ...Login, } diff --git a/src/redux/mainWallet/reducer.js b/src/redux/mainWallet/reducer.js index c8af0bd48..16e91913a 100644 --- a/src/redux/mainWallet/reducer.js +++ b/src/redux/mainWallet/reducer.js @@ -51,6 +51,7 @@ export default (state = initialState, action) => { )) case a.WALLET_SET_NAME: return state.names(state.names().set(`${action.blockchain}-${action.address}`, action.name)) + default: return state } diff --git a/src/redux/persistAccount/actions.js b/src/redux/persistAccount/actions.js new file mode 100644 index 000000000..5fc78dd8e --- /dev/null +++ b/src/redux/persistAccount/actions.js @@ -0,0 +1,169 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import uniqid from 'uniqid' +import bip39 from 'bip39' +import Web3 from 'web3' +import Accounts from 'web3-eth-accounts' + +import web3Provider from '@chronobank/login/network/Web3Provider' +import { + WalletModel, + WalletEntryModel, +} from 'models/persistAccount' +import networkService from '@chronobank/login/network/NetworkService' +import mnemonicProvider from '@chronobank/login/network/mnemonicProvider' +import privateKeyProvider from '@chronobank/login/network/privateKeyProvider' +import { clearErrors, loading } from '@chronobank/login/redux/network/actions' +import { getSelectedNetwork } from './selectors' + +export const WALLETS_ADD = 'persistAccount/WALLETS_ADD' +export const WALLETS_SELECT = 'persistAccount/WALLETS_SELECT' +export const WALLETS_LOAD = 'persistAccount/WALLETS_LOAD' +export const WALLETS_UPDATE_LIST = 'persistAccount/WALLETS_UPDATE_LIST' +export const WALLETS_REMOVE = 'persistAccount/WALLETS_REMOVE' + +export const accountAdd = (wallet) => (dispatch) => { + dispatch({ type: WALLETS_ADD, wallet }) +} + +export const accountSelect = (wallet) => (dispatch) => { + dispatch({ type: WALLETS_SELECT, wallet }) +} + +export const accountLoad = (wallet) => (dispatch) => { + dispatch({ type: WALLETS_LOAD, wallet }) +} + +export const accountUpdateList = (walletList) => (dispatch) => { + dispatch({ type: WALLETS_UPDATE_LIST, walletList }) +} + +export const accountUpdate = (wallet) => (dispatch, getState) => { + const state = getState() + + const { walletsList } = state.get('persistAccount') + + let index = walletsList.findIndex((item) => item.key === wallet.key) + + let copyWalletList = [...walletsList] + + copyWalletList.splice(index, 1, wallet) + + dispatch({ type: WALLETS_UPDATE_LIST, walletsList: copyWalletList }) + +} + +export const decryptAccount = (entry, password) => async (dispatch, getState) => { + const state = getState() + + const web3 = new Web3() + const accounts = new Accounts(new web3.providers.HttpProvider(networkService.getProviderSettings().url)) + await accounts.wallet.clear() + + let wallet = await accounts.wallet.decrypt(entry.encrypted, password) + + const model = new WalletModel({ + entry, + wallet, + }) + + dispatch(accountLoad(model)) + + return wallet + +} + +export const validateAccountName = (name) => (dispatch, getState) => { + const state = getState() + + const { walletsList } = state.get('persistAccount') + + return !walletsList.find((item) => item.name === name) +} + +export const validateMnemonicForAccount = (wallet, mnemonic) => async () => { + let host = networkService.getProviderSettings().url + + const web3 = new Web3() + const accounts = new Accounts(new web3.providers.HttpProvider(host)) + accounts.wallet.clear() + + const walletAddress = wallet && wallet.encrypted && wallet.encrypted[0] && wallet.encrypted[0].address || '' + + const addressFromWallet = `0x${walletAddress}` + + const account = accounts.privateKeyToAccount(`0x${bip39.mnemonicToSeedHex(mnemonic)}`) + const address = account && account.address && account.address.toLowerCase() + + return addressFromWallet === address +} + +export const resetPasswordAccount = (wallet, mnemonic, password) => async (dispatch) => { + let host = networkService.getProviderSettings().url + + const web3 = new Web3() + const accounts = new Accounts(new web3.providers.HttpProvider(host)) + accounts.wallet.clear() + + const newCopy = await dispatch(createAccount({ name: wallet.name, mnemonic, password })) + + let newWallet = { + ...wallet, + encrypted: newCopy.encrypted, + } + + dispatch(accountUpdate(newWallet)) + + dispatch(accountSelect(newWallet)) + +} + +export const createAccount = ({ name, password, privateKey, mnemonic, numberOfAccounts = 0, types = {} }) => async (dispatch, getState) => { + const state = getState() + + let wallet, hex = privateKey || bip39.mnemonicToSeedHex(mnemonic) || '' + + let host = networkService.getProviderSettings().url + + const web3 = new Web3() + const accounts = new Accounts(new web3.providers.HttpProvider(host)) + accounts.wallet.clear() + + wallet = await accounts.wallet.create(numberOfAccounts) + const account = accounts.privateKeyToAccount(`0x${hex}`) + wallet.add(account) + + return new WalletEntryModel({ + key: uniqid(), + name, + types, + encrypted: wallet && wallet.encrypt(password), + }) + +} + +export const downloadWallet = () => (dispatch, getState) => { + const state = getState() + + const { selectedWallet } = state.get('persistAccount') + + if (selectedWallet) { + const text = JSON.stringify(selectedWallet.encrypted.length > 1 ? selectedWallet.encrypted : selectedWallet.encrypted[0]) + const element = document.createElement('a') + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)) + element.setAttribute('download', `Wallet.wlt`) + element.style.display = 'none' + document.body.appendChild(element) + element.click() + document.body.removeChild(element) + } +} + +export const logout = () => (dispatch) => { + dispatch(accountSelect(null)) + dispatch(accountLoad(null)) + // Router.pushRoute('/') +} diff --git a/src/redux/persistAccount/index.js b/src/redux/persistAccount/index.js new file mode 100644 index 000000000..30fd4fe33 --- /dev/null +++ b/src/redux/persistAccount/index.js @@ -0,0 +1,8 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import persistAccount from './reducer' + +export { persistAccount } diff --git a/src/redux/persistAccount/reducer.js b/src/redux/persistAccount/reducer.js new file mode 100644 index 000000000..813559a68 --- /dev/null +++ b/src/redux/persistAccount/reducer.js @@ -0,0 +1,70 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { persistReducer, REHYDRATE } from 'redux-persist' +import storage from 'redux-persist/lib/storage' +import * as a from './actions' +import { removeWallet } from './utils' + +const persistConfig = { + key: 'wallet', + storage: storage, + blacklist: ['decryptedWallet'], +} + +const initialState = { + walletsList: [], + selectedWallet: null, + decryptedWallet: null, + rehydrated: false, +} + +const persistAccount = (state = initialState, action) => { + switch (action.type) { + case REHYDRATE: + return { + ...state, + ...action.payload.persistAccount, + rehydrated: true, + } + case a.WALLETS_ADD : + return { + ...state, + walletsList: [ + ...state.walletsList, + action.wallet, + ], + } + + case a.WALLETS_SELECT : + return { + ...state, + selectedWallet: action.wallet, + } + + case a.WALLETS_LOAD : + return { + ...state, + decryptedWallet: action.wallet, + } + + case a.WALLETS_UPDATE_LIST : + return { + ...state, + walletsList: action.walletsList, + } + + case a.WALLETS_REMOVE : + return { + ...state, + walletsList: removeWallet(state.walletsList, action.name), + } + + default: + return state + } +} + +export default persistReducer(persistConfig, persistAccount) diff --git a/src/redux/persistAccount/selectors.js b/src/redux/persistAccount/selectors.js new file mode 100644 index 000000000..0accdd635 --- /dev/null +++ b/src/redux/persistAccount/selectors.js @@ -0,0 +1,19 @@ +/** + * Copyright 2017–2018, LaborX PTY + * Licensed under the AGPL Version 3 license. + */ + +import { createSelector } from 'reselect' + +export const getSelectedNetwork = () => createSelector( + (state) => state.get('network'), + (network) => { + if (!network.selectedNetworkId){ + return null + } + + return network.networks && network.networks.find( + (item) => item.id === network.selectedNetworkId + ) + }, +) diff --git a/src/redux/persistAccount/utils.js b/src/redux/persistAccount/utils.js new file mode 100644 index 000000000..c083c2708 --- /dev/null +++ b/src/redux/persistAccount/utils.js @@ -0,0 +1,13 @@ +export const replaceWallet = (wallet, walletList) => { + let index = walletList.findIndex((item) => item.key === wallet.key) + + let copyWalletList = [...walletList] + + copyWalletList.splice(index, 1, wallet) + + return copyWalletList +} + +export const removeWallet = (walletsList, name) => { + return walletsList.filter((w) => w.name !== name) +} diff --git a/src/redux/wallet/reducer.js b/src/redux/wallet/reducer.js index 3e71c4bd7..218b1abad 100644 --- a/src/redux/wallet/reducer.js +++ b/src/redux/wallet/reducer.js @@ -9,6 +9,9 @@ const initialState = { isMultisig: false, blockchain: null, address: null, + walletsList: [], + selectedWallet: null, + decryptedWallet: null, } export default (state = initialState, action) => { @@ -24,6 +27,7 @@ export default (state = initialState, action) => { blockchain: action.blockchain, address: action.address, } + default: return state } diff --git a/src/router.js b/src/router.js index 9cf769d91..2efccb1f0 100644 --- a/src/router.js +++ b/src/router.js @@ -6,9 +6,22 @@ import Markup from 'layouts/Markup' import { Provider } from 'react-redux' import React from 'react' -import { Route, Router } from 'react-router' -import NotFoundPage from 'pages/NotFound/NotFound' -import LoginPage from 'pages/LoginPage/LoginPage' +import { Route, Router, IndexRoute, Redirect } from 'react-router' +import { + NotFoundPage, + LoginForm, + CreateAccount, + AccountSelector, + RecoverAccount, + ResetPassword, + LoginWithOptions, + ConfirmMnemonic, + GenerateMnemonic, + GenerateWallet, + UploadWalletPage, + LoginWithMnemonic, + LoginWithPrivateKey, +} from '@chronobank/login-ui/components' import Splash from 'layouts/Splash/Splash' import { AssetsPage, @@ -54,6 +67,7 @@ function hashLinkScroll () { const router = ( + @@ -72,8 +86,19 @@ const router = ( - - + + + + + + + + + + + + + diff --git a/src/styles/partials/_variables.scss b/src/styles/partials/_variables.scss index 3c3d8e516..73d53d7de 100644 --- a/src/styles/partials/_variables.scss +++ b/src/styles/partials/_variables.scss @@ -13,8 +13,11 @@ $color-primary-light: #e9efff; $color-primary-light-1: #f5f5ff; $color-secondary: #2962ff; +$color-description: #9997B2; + $color-white: #FFFFFF; $color-blue: #2962FF; +$color-light-blue: #5DB3ED; $color-blue-2: #758EFF; $color-blue-2-hover: #6980E5; $color-blue405: #4058b8; @@ -27,6 +30,7 @@ $color-red: #FF1744; $error-color: #F44336; $color-red-1: #FF1645; $color-red-1-hover: #E51441; +$color-orange: #E2A864; $color-black: #222; $color-error: $color-red; @@ -81,8 +85,8 @@ $highlight-green: $color-green; $highlight-red: #{material-color('red', 'a400')}; // fonts -$font-family-main: 'Roboto', -sans-serif; +$font-family-main: 'Roboto', sans-serif; +$font-proxima: 'proxima nova', sans-serif; $font-size-base: 16px; $font-weight-bold: 600; diff --git a/yarn.lock b/yarn.lock index e87a95036..24454a3f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2831,7 +2831,7 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" -crypto-browserify@^3.11.0: +crypto-browserify@3.12.0, crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" dependencies: @@ -4146,6 +4146,14 @@ eth-lib@0.1.27: ws "^3.0.0" xhr-request-promise "^0.1.2" +eth-lib@0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.7.tgz#2f93f17b1e23aec3759cd4a3fe20c1286a3fc1ca" + dependencies: + bn.js "^4.11.6" + elliptic "^6.4.0" + xhr-request-promise "^0.1.2" + eth-lib@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/eth-lib/-/eth-lib-0.2.8.tgz#b194058bef4b220ad12ea497431d6cb6aa0623c8" @@ -10943,7 +10951,7 @@ script-ext-html-webpack-plugin@^1.8.8: dependencies: debug "^3.1.0" -scrypt.js@^0.2.0: +scrypt.js@0.2.0, scrypt.js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/scrypt.js/-/scrypt.js-0.2.0.tgz#af8d1465b71e9990110bedfc593b9479e03a8ada" dependencies: @@ -12467,6 +12475,10 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" +uuid@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.1.tgz#c2a30dedb3e535d72ccf82e343941a50ba8533ac" + uuid@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" @@ -12674,6 +12686,21 @@ web3-eth-abi@1.0.0-beta.34: web3-core-helpers "1.0.0-beta.34" web3-utils "1.0.0-beta.34" +web3-eth-accounts@^1.0.0-beta.34: + version "1.0.0-beta.34" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-1.0.0-beta.34.tgz#e09142eeecc797ac3459b75e9b23946d3695f333" + dependencies: + any-promise "1.3.0" + crypto-browserify "3.12.0" + eth-lib "0.2.7" + scrypt.js "0.2.0" + underscore "1.8.3" + uuid "2.0.1" + web3-core "1.0.0-beta.34" + web3-core-helpers "1.0.0-beta.34" + web3-core-method "1.0.0-beta.34" + web3-utils "1.0.0-beta.34" + web3-eth-contract@1.0.0-beta.34: version "1.0.0-beta.34" resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-1.0.0-beta.34.tgz#9dbb38fae7643a808427a20180470ec7415c91e6"