diff --git a/features/accountManagement.feature b/features/accountManagement.feature
new file mode 100644
index 000000000..fd7967e36
--- /dev/null
+++ b/features/accountManagement.feature
@@ -0,0 +1,47 @@
+Feature: Account management
+ Scenario: should allow to save account locally, after page reload it should require passphrase to do the first transaction, and remember the passphrase for next transactions
+ Given I'm logged in as "genesis"
+ When I click "save account" in main menu
+ And I click "save account button"
+ And I wait 1 seconds
+ And I should see text "Account saved" in "toast" element
+ And I Refresh the page
+ And I wait 2 seconds
+ Then I should be logged in
+ And I click "send button"
+ And I should see empty "passphrase" field
+ And I fill in "1" to "amount" field
+ And I fill in "537318935439898807L" to "recipient" field
+ And I fill in passphrase of "genesis" to "passphrase" field
+ And I click "submit button"
+ And I click "ok button"
+ And I wait 1 seconds
+ And I click "send button"
+ And I fill in "2" to "amount" field
+ And I fill in "537318935439898807L" to "recipient" field
+ And I click "submit button"
+ And I should see alert dialog with title "Success" and text "Your transaction of 2 LSK to 537318935439898807L was accepted and will be processed in a few seconds."
+
+ Scenario: should allow to forget locally saved account
+ Given I'm logged in as "any account"
+ When I click "save account" in main menu
+ And I click "save account button"
+ And I Refresh the page
+ And I wait 2 seconds
+ And I click "forget account" in main menu
+ And I wait 1 seconds
+ Then I should see text "Account was successfully forgotten." in "toast" element
+ And I Refresh the page
+ And I should be on login page
+
+ Scenario: should allow to exit save account dialog with "cancel button"
+ Given I'm logged in as "any account"
+ When I click "save account" in main menu
+ And I click "cancel button"
+ Then I should see no "modal dialog"
+
+ Scenario: should allow to exit save account dialog with "x button"
+ Given I'm logged in as "any account"
+ When I click "save account" in main menu
+ And I click "x button"
+ Then I should see no "modal dialog"
diff --git a/features/step_definitions/generic.step.js b/features/step_definitions/generic.step.js
index c8d3d8a6f..ce6dc887f 100644
--- a/features/step_definitions/generic.step.js
+++ b/features/step_definitions/generic.step.js
@@ -32,6 +32,13 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
waitForElemAndSendKeys(`${selectorClass} input, ${selectorClass} textarea`, secondPassphrase, callback);
});
+ When('I fill in passphrase of "{accountName}" to "{fieldName}" field', (accountName, fieldName, callback) => {
+ const selectorClass = `.${fieldName.replace(/ /g, '-')}`;
+ const passphrase = accounts[accountName].passphrase;
+ browser.sleep(500);
+ waitForElemAndSendKeys(`${selectorClass} input, ${selectorClass} textarea`, passphrase, callback);
+ });
+
When('I wait {seconds} seconds', (seconds, callback) => {
browser.sleep(seconds * 1000).then(callback);
});
@@ -43,6 +50,12 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
.and.notify(callback);
});
+ Then('I should see empty "{fieldName}" field', (fieldName, callback) => {
+ const elem = element(by.css(`.${fieldName.replace(/ /g, '-')} input, .${fieldName.replace(/ /g, '-')} textarea`));
+ expect(elem.getAttribute('value')).to.eventually.equal('')
+ .and.notify(callback);
+ });
+
When('I click "{elementName}"', (elementName, callback) => {
const selector = `.${elementName.replace(/\s+/g, '-')}`;
waitForElemAndClickIt(selector, callback);
@@ -110,6 +123,7 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
browser.ignoreSynchronization = true;
browser.driver.manage().window().setSize(1000, 1000);
browser.get('http://localhost:8080/');
+ localStorage.clear();
localStorage.setItem('address', 'http://localhost:4000');
localStorage.setItem('network', 2);
browser.get('http://localhost:8080/');
@@ -151,5 +165,9 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
});
});
});
+
+ When('I Refresh the page', (callback) => {
+ browser.refresh().then(callback);
+ });
});
diff --git a/src/actions/account.js b/src/actions/account.js
index ef800538e..566d67ae9 100644
--- a/src/actions/account.js
+++ b/src/actions/account.js
@@ -40,6 +40,11 @@ export const accountLoggedIn = data => ({
data,
});
+export const passphraseUsed = data => ({
+ type: actionTypes.passphraseUsed,
+ data,
+});
+
/**
*
*/
@@ -59,14 +64,16 @@ export const secondPassphraseRegistered = ({ activePeer, secondPassphrase, accou
const text = (error && error.message) ? error.message : 'An error occurred while registering your second passphrase. Please try again.';
dispatch(errorAlertDialogDisplayed({ text }));
});
+ dispatch(passphraseUsed(account.passphrase));
};
/**
*
*/
-export const delegateRegistered = ({ activePeer, account, username, secondPassphrase }) =>
+export const delegateRegistered = ({
+ activePeer, account, passphrase, username, secondPassphrase }) =>
(dispatch) => {
- registerDelegate(activePeer, username, account.passphrase, secondPassphrase)
+ registerDelegate(activePeer, username, passphrase, secondPassphrase)
.then((data) => {
// dispatch to add to pending transaction
dispatch(transactionAdded({
@@ -84,6 +91,7 @@ export const delegateRegistered = ({ activePeer, account, username, secondPassph
const actionObj = errorAlertDialogDisplayed({ text });
dispatch(actionObj);
});
+ dispatch(passphraseUsed(passphrase));
};
/**
@@ -107,4 +115,5 @@ export const sent = ({ activePeer, account, recipientId, amount, passphrase, sec
const text = error && error.message ? `${error.message}.` : 'An error occurred while creating the transaction.';
dispatch(errorAlertDialogDisplayed({ text }));
});
+ dispatch(passphraseUsed(passphrase));
};
diff --git a/src/actions/peers.js b/src/actions/peers.js
index 72aff148d..74752fe3d 100644
--- a/src/actions/peers.js
+++ b/src/actions/peers.js
@@ -34,6 +34,7 @@ export const activePeerSet = (data) => {
return {
data: Object.assign({
passphrase: data.passphrase,
+ publicKey: data.publicKey,
activePeer: Lisk.api(config),
}),
type: actionTypes.activePeerSet,
diff --git a/src/actions/voting.js b/src/actions/voting.js
index bcb4e9053..e99b11101 100644
--- a/src/actions/voting.js
+++ b/src/actions/voting.js
@@ -1,8 +1,9 @@
-import actionTypes from '../constants/actions';
-import { vote } from '../utils/api/delegate';
-import { transactionAdded } from './transactions';
import { errorAlertDialogDisplayed } from './dialog';
+import { passphraseUsed } from './account';
+import { transactionAdded } from './transactions';
+import { vote } from '../utils/api/delegate';
import Fees from '../constants/fees';
+import actionTypes from '../constants/actions';
/**
* Add pending variable to the list of voted delegates and list of unvoted delegates
@@ -21,12 +22,13 @@ export const clearVoteLists = () => ({
/**
*
*/
-export const votePlaced = ({ activePeer, account, votedList, unvotedList, secondSecret }) =>
+export const votePlaced = ({
+ activePeer, passphrase, account, votedList, unvotedList, secondSecret }) =>
(dispatch) => {
// Make the Api call
vote(
activePeer,
- account.passphrase,
+ passphrase,
account.publicKey,
votedList,
unvotedList,
@@ -45,11 +47,11 @@ export const votePlaced = ({ activePeer, account, votedList, unvotedList, second
fee: Fees.vote,
type: 3,
}));
- })
- .catch((error) => {
+ }).catch((error) => {
const text = error && error.message ? `${error.message}.` : 'An error occurred while placing your vote.';
dispatch(errorAlertDialogDisplayed({ text }));
});
+ dispatch(passphraseUsed(account.passphrase));
};
/**
diff --git a/src/components/account/account.css b/src/components/account/account.css
index 3d696b4eb..744613b6e 100644
--- a/src/components/account/account.css
+++ b/src/components/account/account.css
@@ -71,6 +71,7 @@
position: absolute;
top: 5px;
right: 5px;
+ z-index: 1;
}
}
diff --git a/src/components/account/address.js b/src/components/account/address.js
index ea8dd8ab0..2e584e4ae 100644
--- a/src/components/account/address.js
+++ b/src/components/account/address.js
@@ -1,7 +1,18 @@
import React from 'react';
import grid from 'flexboxgrid/dist/flexboxgrid.css';
+
+import { TooltipWrapper } from '../timestamp';
import styles from './account.css';
+const getStatusTooltip = (props) => {
+ if (props.secondSignature) {
+ return 'This account is protected by a second passphrase';
+ } else if (props.passphrase) {
+ return 'Passphrase of the acount is saved till the end of the session.';
+ }
+ return 'Passphrase of the acount will be required to perform any transaction.';
+};
+
const Address = (props) => {
const title = props.isDelegate ? 'Delegate' : 'Address';
const content = props.isDelegate ?
@@ -26,6 +37,11 @@ const Address = (props) => {
{content}
+
+
+ {props.passphrase && !props.secondSignature ? 'lock_open' : 'lock'}
+
+
diff --git a/src/components/authInputs/authInputs.js b/src/components/authInputs/authInputs.js
new file mode 100644
index 000000000..f48d24393
--- /dev/null
+++ b/src/components/authInputs/authInputs.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import PassphraseInput from '../passphraseInput';
+import { extractPublicKey } from '../../utils/api/account';
+
+class AuthInputs extends React.Component {
+ componentDidMount() {
+ if (this.props.account.secondSignature) {
+ this.props.onChange('secondPassphrase', '');
+ }
+ }
+
+ onChange(name, value, error) {
+ if (!error) {
+ const publicKeyMap = {
+ passphrase: 'publicKey',
+ secondPassphrase: 'secondPublicKey',
+ };
+ const expectedPublicKey = this.props.account[publicKeyMap[name]];
+
+ if (expectedPublicKey && expectedPublicKey !== extractPublicKey(value)) {
+ error = 'Entered passphrase does not belong to the active account';
+ }
+ }
+ this.props.onChange(name, value, error);
+ }
+
+ render() {
+ return
+ {(!this.props.account.passphrase &&
+ )}
+ {(this.props.account.secondSignature &&
+ )}
+ ;
+ }
+}
+
+export default AuthInputs;
+
diff --git a/src/components/authInputs/authInputs.test.js b/src/components/authInputs/authInputs.test.js
new file mode 100644
index 000000000..612b3688e
--- /dev/null
+++ b/src/components/authInputs/authInputs.test.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import sinon from 'sinon';
+import AuthInputs from './authInputs';
+
+
+describe('AuthInputs', () => {
+ let wrapper;
+ let props;
+ const passphrase = 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit';
+
+ beforeEach(() => {
+ props = {
+ onChange: sinon.spy(),
+ secondPassphrase: { },
+ account: {
+ passphrase,
+ },
+ passphrase: {
+ value: passphrase,
+ },
+ };
+ });
+
+ it('should render Input if props.account.secondSignature', () => {
+ props.account.secondSignature = true;
+ wrapper = mount( );
+ expect(wrapper.find('Input')).to.have.lengthOf(1);
+ });
+
+ it('should render null if !props.account.secondSignature', () => {
+ props.account.secondSignature = false;
+ wrapper = mount( );
+ expect(wrapper.html()).to.equal(' ');
+ });
+
+ it('should render null if !props.account.secondSignature', () => {
+ props.account.secondSignature = false;
+ wrapper = mount( );
+ expect(wrapper.html()).to.equal(' ');
+ });
+
+ it('should call props.onChange when input value changes', () => {
+ props.account.secondSignature = true;
+ wrapper = mount( );
+ wrapper.find('.second-passphrase input').simulate('change', { target: { value: passphrase } });
+ expect(props.onChange).to.have.been.calledWith('secondPassphrase', passphrase);
+ });
+
+ it('should call props.onChange with an error if entered secondPassphrase does not belong to secondPublicKey', () => {
+ const error = 'Entered passphrase does not belong to the active account';
+ props.account.secondSignature = true;
+ props.account.secondPublicKey = 'fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88';
+ wrapper = mount( );
+ wrapper.find('.second-passphrase input').simulate('change', { target: { value: passphrase } });
+ expect(props.onChange).to.have.been.calledWith('secondPassphrase', passphrase, error);
+ });
+
+ it('should call props.onChange(\'secondPassphrase\', \'Required\') when input value changes to \'\'', () => {
+ props.account.secondSignature = true;
+ wrapper = mount( );
+ wrapper.find('.second-passphrase input').simulate('change', { target: { value: '' } });
+ expect(props.onChange).to.have.been.calledWith('secondPassphrase', '', 'Required');
+ });
+
+ it('should call props.onChange(\'secondPassphrase\', \'Invalid passphrase\') when input value changes to \'test\'', () => {
+ props.account.secondSignature = true;
+ wrapper = mount( );
+ wrapper.find('.second-passphrase input').simulate('change', { target: { value: 'test' } });
+ expect(props.onChange).to.have.been.calledWith('secondPassphrase', 'test', 'Passphrase should have 12 words, entered passphrase has 1');
+ });
+});
diff --git a/src/components/authInputs/index.js b/src/components/authInputs/index.js
new file mode 100644
index 000000000..c122d318f
--- /dev/null
+++ b/src/components/authInputs/index.js
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import AuthInputs from './authInputs';
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+export default connect(mapStateToProps)(AuthInputs);
+
diff --git a/src/components/authInputs/index.test.js b/src/components/authInputs/index.test.js
new file mode 100644
index 000000000..772185bfd
--- /dev/null
+++ b/src/components/authInputs/index.test.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import configureMockStore from 'redux-mock-store';
+import AuthInputsHOC from './index';
+
+describe('AuthInputsHOC', () => {
+ let wrapper;
+ const passphrase = 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit';
+ const props = {
+ onChange: () => {},
+ secondPassphrase: {},
+ };
+ const account = {
+ secondSignature: 1,
+ passphrase,
+ };
+
+ it('should render AuthInputs with props.account equal to state.account ', () => {
+ const store = configureMockStore([])({ account });
+ wrapper = mount(
+
+ );
+ expect(wrapper.find('AuthInputs').props().account).to.deep.equal(account);
+ });
+});
diff --git a/src/components/header/header.js b/src/components/header/header.js
index bac432ad0..8f66b5a69 100644
--- a/src/components/header/header.js
+++ b/src/components/header/header.js
@@ -1,6 +1,6 @@
import React from 'react';
import { Button } from 'react-toolbox/lib/button';
-import { IconMenu, MenuItem } from 'react-toolbox/lib/menu';
+import { IconMenu, MenuItem, MenuDivider } from 'react-toolbox/lib/menu';
import grid from 'flexboxgrid/dist/flexboxgrid.css';
import logo from '../../assets/images/LISK-nano.png';
import styles from './header.css';
@@ -11,6 +11,7 @@ import Send from '../send';
import PrivateWrapper from '../privateWrapper';
import SecondPassphraseMenu from '../secondPassphrase';
import offlineStyle from '../offlineWrapper/offlineWrapper.css';
+import SaveAccountButton from '../saveAccountButton';
const Header = props => (
@@ -53,6 +54,8 @@ const Header = props => (
childComponent: VerifyMessage,
})}
/>
+
+
{props.t('logout')}
!inDictionary(word));
- if (invalidWord) {
- if (invalidWord.length >= 2 && invalidWord.length <= 8) {
- const validWord = findSimilarWord(invalidWord);
- if (validWord) {
- return `Word "${invalidWord}" is not on the passphrase Word List. Most similar word on the list is "${findSimilarWord(invalidWord)}"`;
- }
- }
- return `Word "${invalidWord}" is not on the passphrase Word List.`;
- }
- return 'Passphrase is not valid';
- }
-
- changeHandler(name, value) {
+ changeHandler(name, value, error) {
const validator = this.validators[name] || (() => ({}));
this.setState({
[name]: value,
- ...validator(value),
+ ...validator(value, error),
});
}
+ autologin() {
+ const savedAccounts = localStorage.getItem('accounts');
+ if (savedAccounts && !this.props.account.afterLogout) {
+ const account = JSON.parse(savedAccounts)[0];
+ const network = Object.assign({}, networksRaw[account.network]);
+ if (account.network === 2) {
+ network.address = account.address;
+ }
+
+ // set active peer
+ this.props.activePeerSet({
+ publicKey: account.publicKey,
+ network,
+ });
+ }
+ }
+
onLoginSubmission(passphrase) {
const network = Object.assign({}, networksRaw[this.state.network]);
if (this.state.network === 2) {
@@ -181,20 +175,12 @@ class Login extends React.Component {
error={this.state.addressValidity}
onChange={this.changeHandler.bind(this, 'address')} />
}
-
-
{
let wrapper;
// Mocking store
@@ -42,12 +39,6 @@ describe('Login', () => {
expect(wrapper.find('.address')).to.have.lengthOf(1);
});
- it('should allow to change passphrase field to type="text"', () => {
- expect(wrapper.find('.passphrase input').props().type).to.equal('password');
- wrapper.setState({ showPassphrase: true });
- expect(wrapper.find('.passphrase input').props().type).to.equal('text');
- });
-
it('should show error about passphrase length if passphrase is have wrong length', () => {
const passphrase = 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin';
const expectedError = 'Passphrase should have 12 words, entered passphrase has 11';
@@ -167,42 +158,6 @@ describe('Login', () => {
});
});
- describe('validatePassphrase', () => {
- beforeEach('', () => {
- wrapper = shallow( );
- });
-
- it('should set passphraseValidity="" for a valid passphrase', () => {
- const passphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble';
- const data = wrapper.instance().validatePassphrase(passphrase);
- const expectedData = {
- passphrase,
- passphraseValidity: '',
- };
- expect(data).to.deep.equal(expectedData);
- });
-
- it('should set passphraseValidity="Empty passphrase" for an empty string', () => {
- const passphrase = '';
- const data = wrapper.instance().validatePassphrase(passphrase);
- const expectedData = {
- passphrase,
- passphraseValidity: 'Empty passphrase',
- };
- expect(data).to.deep.equal(expectedData);
- });
-
- it.skip('should set passphraseValidity="Invalid passphrase" for a non-empty invalid passphrase', () => {
- const passphrase = 'invalid passphrase';
- const data = wrapper.instance().validatePassphrase(passphrase);
- const expectedData = {
- passphrase,
- passphraseValidity: 'URL is invalid',
- };
- expect(data).to.deep.equal(expectedData);
- });
- });
-
describe('changeHandler', () => {
it('call setState with matching data', () => {
wrapper = shallow( );
diff --git a/src/components/passphraseInput/index.js b/src/components/passphraseInput/index.js
new file mode 100644
index 000000000..e1c790e1c
--- /dev/null
+++ b/src/components/passphraseInput/index.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import Input from 'react-toolbox/lib/input';
+import Tooltip from 'react-toolbox/lib/tooltip';
+import { IconButton } from 'react-toolbox/lib/button';
+import { isValidPassphrase } from '../../utils/passphrase';
+import { findSimilarWord, inDictionary } from '../../utils/similarWord';
+import styles from './passphraseInput.css';
+
+// eslint-disable-next-line new-cap
+const TooltipIconButton = Tooltip(IconButton);
+
+class PassphraseInput extends React.Component {
+ constructor() {
+ super();
+ this.state = { inputType: 'password' };
+ }
+
+ handleValueChange(value) {
+ let error;
+ if (!value) {
+ error = 'Required';
+ } else if (!isValidPassphrase(value)) {
+ error = this.getPassphraseValidationError(value);
+ }
+ this.props.onChange(value, error);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ getPassphraseValidationError(passphrase) {
+ const mnemonic = passphrase.trim().split(' ');
+ if (mnemonic.length < 12) {
+ return `Passphrase should have 12 words, entered passphrase has ${mnemonic.length}`;
+ }
+
+ const invalidWord = mnemonic.find(word => !inDictionary(word.toLowerCase()));
+ if (invalidWord) {
+ if (invalidWord.length >= 2 && invalidWord.length <= 8) {
+ const validWord = findSimilarWord(invalidWord);
+ if (validWord) {
+ return `Word "${invalidWord}" is not on the passphrase Word List. Most similar word on the list is "${findSimilarWord(invalidWord)}"`;
+ }
+ }
+ return `Word "${invalidWord}" is not on the passphrase Word List.`;
+ }
+ return 'Passphrase is not valid';
+ }
+
+ toggleInputType() {
+ this.setState({ inputType: this.state.inputType === 'password' ? 'text' : 'password' });
+ }
+
+ render() {
+ return (
+
+
+
+
);
+ }
+}
+
+export default PassphraseInput;
+
diff --git a/src/components/passphraseInput/index.test.js b/src/components/passphraseInput/index.test.js
new file mode 100644
index 000000000..7f57b5d18
--- /dev/null
+++ b/src/components/passphraseInput/index.test.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { expect } from 'chai';
+import { spy } from 'sinon';
+import { mount } from 'enzyme';
+import PassphraseInput from './index';
+
+describe('PassphraseInput', () => {
+ let wrapper;
+ let props;
+ let onChangeSpy;
+
+ beforeEach('', () => {
+ props = {
+ error: '',
+ value: '',
+ onChange: () => {},
+ };
+ onChangeSpy = spy(props, 'onChange');
+ wrapper = mount( );
+ });
+
+ afterEach('', () => {
+ onChangeSpy.restore();
+ });
+
+ it('should call props.onChange with error=undefined if a valid passphrase is entered', () => {
+ const passphrase = 'wagon stock borrow episode laundry kitten salute link globe zero feed marble';
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, undefined);
+ });
+
+ it('should call props.onChange with error="Required" if an empty passphrase is entered', () => {
+ const passphrase = '';
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, 'Required');
+ });
+
+ const ONLY_ONE_WORD_ERROR = 'Passphrase should have 12 words, entered passphrase has 1';
+ it(`should call props.onChange with error="${ONLY_ONE_WORD_ERROR}" if an "test" passphrase is entered`, () => {
+ const passphrase = 'test';
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, ONLY_ONE_WORD_ERROR);
+ });
+
+ const INVALID_WORD = 'INVALID_WORD';
+ const INVALID_WORD_ERROR = `Word "${INVALID_WORD}" is not on the passphrase Word List.`;
+ it(`should call props.onChange with error='${INVALID_WORD_ERROR}' if a passphrase with an invalid word is entered`, () => {
+ const passphrase = `${INVALID_WORD} stock borrow episode laundry kitten salute link globe zero feed marble`;
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, INVALID_WORD_ERROR);
+ });
+
+ const SIMILAR_WORD_ERROR = 'Word "wagot" is not on the passphrase Word List. Most similar word on the list is "wagon"';
+ it(`should call props.onChange with error='${SIMILAR_WORD_ERROR}' if an passphrase with a typo is entered`, () => {
+ const passphrase = 'wagot stock borrow episode laundry kitten salute link globe zero feed marble';
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, SIMILAR_WORD_ERROR);
+ });
+
+ const NOT_VALID_ERROR = 'Passphrase is not valid';
+ it(`should call props.onChange with error="${NOT_VALID_ERROR}" if an otherwise invalid passphrase is entered`, () => {
+ const passphrase = 'stock wagon borrow episode laundry kitten salute link globe zero feed marble';
+ wrapper.find('input').simulate('change', { target: { value: passphrase } });
+ expect(wrapper.props().onChange).to.have.been.calledWith(passphrase, NOT_VALID_ERROR);
+ });
+
+ it('should allow to change the input field to type="text" and back', () => {
+ expect(wrapper.find('input').props().type).to.equal('password');
+ wrapper.find('.show-passphrase-toggle').simulate('click');
+ expect(wrapper.find('input').props().type).to.equal('text');
+ wrapper.find('.show-passphrase-toggle').simulate('click');
+ expect(wrapper.find('input').props().type).to.equal('password');
+ });
+});
diff --git a/src/components/secondPassphraseInput/secondPassphraseInput.css b/src/components/passphraseInput/passphraseInput.css
similarity index 95%
rename from src/components/secondPassphraseInput/secondPassphraseInput.css
rename to src/components/passphraseInput/passphraseInput.css
index 34b5e3138..66080d7b0 100644
--- a/src/components/secondPassphraseInput/secondPassphraseInput.css
+++ b/src/components/passphraseInput/passphraseInput.css
@@ -7,7 +7,7 @@
position: hidden;
}
-.label {
+.eyeIcon {
position: absolute;
right: 0;
bottom: 50%;
diff --git a/src/components/registerDelegate/registerDelegate.js b/src/components/registerDelegate/registerDelegate.js
index 616fbfdde..d2cadff2f 100644
--- a/src/components/registerDelegate/registerDelegate.js
+++ b/src/components/registerDelegate/registerDelegate.js
@@ -3,28 +3,38 @@ import Input from 'react-toolbox/lib/input';
import InfoParagraph from '../infoParagraph';
import ActionBar from '../actionBar';
import Fees from '../../constants/fees';
+import AuthInputs from '../authInputs';
+import { handleChange, authStatePrefill, authStateIsValid } from '../../utils/form';
class RegisterDelegate extends React.Component {
constructor() {
super();
this.state = {
- name: '',
- nameError: '',
+ name: {
+ value: '',
+ },
+ ...authStatePrefill(),
};
}
-
- changeHandler(name, value) {
- this.setState({ [name]: value });
+ componentDidMount() {
+ const newState = {
+ name: {
+ value: '',
+ },
+ ...authStatePrefill(this.props.account),
+ };
+ this.setState(newState);
}
- register(username, secondPassphrase) {
+ register() {
// @todo I'm not handling this part: this.setState({ nameError: error.message });
this.props.delegateRegistered({
activePeer: this.props.peers.data,
account: this.props.account,
- username,
- secondPassphrase,
+ username: this.state.name.value,
+ passphrase: this.state.passphrase.value,
+ secondPassphrase: this.state.secondPassphrase.value,
});
}
@@ -34,17 +44,13 @@ class RegisterDelegate extends React.Component {
- {
- this.props.account.secondSignature &&
-
- }
+ onChange={handleChange.bind(this, this, 'name')}
+ error={this.state.name.error}
+ value={this.state.name.value} />
+
Becoming a delegate requires registration. You may choose your own
@@ -60,10 +66,10 @@ class RegisterDelegate extends React.Component {
label: 'Register',
fee: Fees.registerDelegate,
className: 'register-button',
- disabled: !this.state.name ||
+ disabled: (!this.state.name.value ||
this.props.account.isDelegate ||
- (this.props.account.secondSignature && !this.state.secondSecret),
- onClick: this.register.bind(this, this.state.name, this.state.secondSecret),
+ !authStateIsValid(this.state)),
+ onClick: this.register.bind(this),
}} />
);
diff --git a/src/components/registerDelegate/registerDelegate.test.js b/src/components/registerDelegate/registerDelegate.test.js
index 9ecbd925f..96a5aa0cf 100644
--- a/src/components/registerDelegate/registerDelegate.test.js
+++ b/src/components/registerDelegate/registerDelegate.test.js
@@ -10,12 +10,14 @@ import * as delegateApi from '../../utils/api/delegate';
const normalAccount = {
+ passphrase: 'pass',
isDelegate: false,
address: '16313739661670634666L',
balance: 1000e8,
};
const delegateAccount = {
+ passphrase: 'pass',
isDelegate: true,
address: '16313739661670634666L',
balance: 1000e8,
@@ -25,6 +27,7 @@ const delegateAccount = {
};
const withSecondSecretAccount = {
+ passphrase: 'pass',
address: '16313739661670634666L',
balance: 1000e8,
delegate: {
@@ -121,7 +124,7 @@ describe('RegisterDelegate', () => {
it('allows register as delegate for a non delegate account with second secret', () => {
wrapper.find('.username input').simulate('change', { target: { value: 'sample_username' } });
- wrapper.find('.second-secret input').simulate('change', { target: { value: 'sample phrase' } });
+ wrapper.find('.second-passphrase input').simulate('change', { target: { value: 'sample phrase' } });
expect(props.delegateRegistered).to.have.been.calledWith();
});
});
diff --git a/src/components/saveAccount/index.js b/src/components/saveAccount/index.js
new file mode 100644
index 000000000..2579cb58c
--- /dev/null
+++ b/src/components/saveAccount/index.js
@@ -0,0 +1,16 @@
+import { connect } from 'react-redux';
+import { successToastDisplayed } from '../../actions/toaster';
+import SaveAccount from './saveAccount';
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+const mapDispatchToProps = dispatch => ({
+ successToast: data => dispatch(successToastDisplayed(data)),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(SaveAccount);
diff --git a/src/components/saveAccount/index.test.js b/src/components/saveAccount/index.test.js
new file mode 100644
index 000000000..c582a5b17
--- /dev/null
+++ b/src/components/saveAccount/index.test.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import sinon from 'sinon';
+import * as toasterActions from '../../actions/toaster';
+import SaveAccountHOC from './index';
+import store from '../../store';
+
+
+describe('SaveAccountHOC', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount( );
+ });
+
+ it('should render SaveAccount', () => {
+ expect(wrapper.find('SaveAccount')).to.have.lengthOf(1);
+ });
+
+ it('should bind dialogDisplayed action to SaveAccount props.successToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed');
+ wrapper.find('SaveAccount').props().successToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+});
+
diff --git a/src/components/saveAccount/saveAccount.js b/src/components/saveAccount/saveAccount.js
new file mode 100644
index 000000000..7a5873083
--- /dev/null
+++ b/src/components/saveAccount/saveAccount.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import InfoParagraph from '../infoParagraph';
+import ActionBar from '../actionBar';
+import { setSavedAccount } from '../../utils/saveAccount';
+
+export default class SaveAccount extends React.Component {
+ save() {
+ setSavedAccount(this.props.account);
+ this.props.closeDialog();
+ this.props.successToast({ label: 'Account saved' });
+ this.props.done();
+ }
+
+ render() {
+ return (
+
+
+ This will save public key of your account on this device,
+ so next time it will launch without the need to log in.
+ However, you will be propted to enter the passphrase once
+ you want to do any transaction.
+
+
+
+ );
+ }
+}
+
diff --git a/src/components/saveAccount/saveAccount.test.js b/src/components/saveAccount/saveAccount.test.js
new file mode 100644
index 000000000..c4c4fd451
--- /dev/null
+++ b/src/components/saveAccount/saveAccount.test.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import { spy } from 'sinon';
+import SaveAccount from './saveAccount';
+import store from '../../store';
+
+
+describe('SaveAccount', () => {
+ let wrapper;
+ let closeDialogSpy;
+ let successToastSpy;
+ let localStorageSpy;
+
+ const props = {
+ account: {
+ publicKey: 'fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88',
+ },
+ closeDialog: () => {},
+ successToast: () => {},
+ done: () => {},
+ };
+
+ beforeEach(() => {
+ closeDialogSpy = spy(props, 'closeDialog');
+ successToastSpy = spy(props, 'successToast');
+ localStorageSpy = spy(localStorage, 'setItem');
+ wrapper = mount( );
+ });
+
+ afterEach(() => {
+ closeDialogSpy.restore();
+ successToastSpy.restore();
+ localStorageSpy.restore();
+ });
+
+ it('should render ActionBar', () => {
+ expect(wrapper.find('ActionBar')).to.have.lengthOf(1);
+ });
+
+ it('should call props.closeDialog, props.successToast and localStorage.setItem on "save button" click', () => {
+ wrapper.find('.save-account-button').simulate('click');
+ const componentProps = wrapper.find(SaveAccount).props();
+ expect(componentProps.closeDialog).to.have.been.calledWith();
+ expect(componentProps.successToast).to.have.been.calledWith({ label: 'Account saved' });
+ const expectedValue = '[{"publicKey":"fab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88","network":"0","address":null}]';
+ expect(localStorageSpy).to.have.been.calledWith('accounts', expectedValue);
+ });
+});
+
diff --git a/src/components/saveAccountButton/index.js b/src/components/saveAccountButton/index.js
new file mode 100644
index 000000000..a0ddf7b54
--- /dev/null
+++ b/src/components/saveAccountButton/index.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import { dialogDisplayed } from '../../actions/dialog';
+import { successToastDisplayed } from '../../actions/toaster';
+import SaveAccountButton from './saveAccountButton';
+
+const mapStateToProps = state => ({
+ account: state.account,
+});
+
+const mapDispatchToProps = dispatch => ({
+ setActiveDialog: data => dispatch(dialogDisplayed(data)),
+ successToast: data => dispatch(successToastDisplayed(data)),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(SaveAccountButton);
diff --git a/src/components/saveAccountButton/index.test.js b/src/components/saveAccountButton/index.test.js
new file mode 100644
index 000000000..c68289cb1
--- /dev/null
+++ b/src/components/saveAccountButton/index.test.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import sinon from 'sinon';
+import * as toasterActions from '../../actions/toaster';
+import * as dialogActions from '../../actions/dialog';
+import store from '../../store';
+import SaveAccountButtonHOC from './index';
+import SaveAccountButton from './saveAccountButton';
+
+describe('SaveAccountButtonHOC', () => {
+ let props;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount( );
+ props = wrapper.find(SaveAccountButton).props();
+ });
+
+ it('should render the SaveAccountButton with props.successToast and props.setActiveDialog', () => {
+ expect(wrapper.find(SaveAccountButton).exists()).to.equal(true);
+ expect(typeof props.successToast).to.equal('function');
+ expect(typeof props.setActiveDialog).to.equal('function');
+ });
+
+ it('should bind successToastDisplayed action to SaveAccountButton props.successToast', () => {
+ const actionsSpy = sinon.spy(toasterActions, 'successToastDisplayed');
+ props.successToast({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+
+ it('should bind dialogDisplayed action to SaveAccountButton props.setActiveDialog', () => {
+ const actionsSpy = sinon.spy(dialogActions, 'dialogDisplayed');
+ props.setActiveDialog({});
+ expect(actionsSpy).to.be.calledWith();
+ actionsSpy.restore();
+ });
+});
+
diff --git a/src/components/saveAccountButton/saveAccountButton.js b/src/components/saveAccountButton/saveAccountButton.js
new file mode 100644
index 000000000..c16b230a8
--- /dev/null
+++ b/src/components/saveAccountButton/saveAccountButton.js
@@ -0,0 +1,33 @@
+import { MenuItem } from 'react-toolbox/lib/menu';
+import React from 'react';
+
+import { getSavedAccount, removeSavedAccount } from '../../utils/saveAccount';
+import SaveAccount from '../saveAccount';
+
+export default class SaveAccountButton extends React.Component {
+ removeSavedAccount() {
+ removeSavedAccount();
+ this.props.successToast({ label: 'Account was successfully forgotten.' });
+ this.forceUpdate();
+ }
+
+ render() {
+ return (getSavedAccount() ?
+ :
+ this.props.setActiveDialog({
+ title: 'Remember this account',
+ childComponent: SaveAccount,
+ childComponentProps: {
+ done: this.forceUpdate.bind(this),
+ },
+ })}
+ />
+ );
+ }
+}
+
diff --git a/src/components/saveAccountButton/saveAccountButton.test.js b/src/components/saveAccountButton/saveAccountButton.test.js
new file mode 100644
index 000000000..e3521e809
--- /dev/null
+++ b/src/components/saveAccountButton/saveAccountButton.test.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { expect } from 'chai';
+import { mount } from 'enzyme';
+import { Provider } from 'react-redux';
+import sinon from 'sinon';
+import * as saveAccountUtils from '../../utils/saveAccount';
+import store from '../../store';
+import SaveAccountButton from './saveAccountButton';
+
+describe('SaveAccountButton', () => {
+ const props = {
+ successToast: () => {},
+ setActiveDialog: () => {},
+ };
+
+ it('Allows to remove saved account from localStorage if accoutn saved already', () => {
+ const removeSavedAccountSpy = sinon.spy(saveAccountUtils, 'removeSavedAccount');
+ const successToastSpy = sinon.spy(props, 'successToast');
+
+ const wrapper = mount( );
+ wrapper.find('MenuItem').simulate('click');
+ expect(removeSavedAccountSpy).to.have.been.calledWith();
+ expect(successToastSpy).to.have.been.calledWith({ label: 'Account was successfully forgotten.' });
+
+ removeSavedAccountSpy.restore();
+ successToastSpy.restore();
+ });
+
+ it('Allows to open SaveAccount modal if account not yet saved', () => {
+ const getSavedAccountMock = sinon.mock(saveAccountUtils);
+ getSavedAccountMock.expects('getSavedAccount').returns();
+ const setActiveDialogSpy = sinon.spy(props, 'setActiveDialog');
+
+ const wrapper = mount( );
+ wrapper.find('MenuItem').simulate('click');
+ expect(setActiveDialogSpy).to.have.been.calledWith();
+
+ setActiveDialogSpy.restore();
+ getSavedAccountMock.restore();
+ });
+});
diff --git a/src/components/secondPassphraseInput/index.js b/src/components/secondPassphraseInput/index.js
deleted file mode 100644
index 0afac594e..000000000
--- a/src/components/secondPassphraseInput/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { connect } from 'react-redux';
-import SecondPassphraseInput from './secondPassphraseInput';
-
-const mapStateToProps = state => ({
- hasSecondPassphrase: !!state.account.secondSignature,
-});
-
-export default connect(mapStateToProps)(SecondPassphraseInput);
-
diff --git a/src/components/secondPassphraseInput/index.test.js b/src/components/secondPassphraseInput/index.test.js
deleted file mode 100644
index bd18075dc..000000000
--- a/src/components/secondPassphraseInput/index.test.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-import { expect } from 'chai';
-import { mount } from 'enzyme';
-import { Provider } from 'react-redux';
-import configureMockStore from 'redux-mock-store';
-import SecondPassphraseInputHOC from './index';
-
-describe('SecondPassphraseInputHOC', () => {
- let wrapper;
-
- it('should render SecondPassphraseInput with props.hasSecondPassphrase if store.account.secondSignature is truthy', () => {
- const store = configureMockStore([])({ account: { secondSignature: 1 } });
- wrapper = mount(
- {} } />
- );
- expect(wrapper.find('SecondPassphraseInput')).to.have.lengthOf(1);
- expect(wrapper.find('SecondPassphraseInput').props().hasSecondPassphrase).to.equal(true);
- });
-
- it('should render SecondPassphraseInput with !props.hasSecondPassphrase if store.account.secondSignature is falsy', () => {
- const store = configureMockStore([])({ account: { secondSignature: 0 } });
- wrapper = mount(
- {} } />
- );
- expect(wrapper.find('SecondPassphraseInput').props().hasSecondPassphrase).to.equal(false);
- });
-});
diff --git a/src/components/secondPassphraseInput/secondPassphraseInput.js b/src/components/secondPassphraseInput/secondPassphraseInput.js
deleted file mode 100644
index 47855a517..000000000
--- a/src/components/secondPassphraseInput/secondPassphraseInput.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import FontIcon from 'react-toolbox/lib/font_icon';
-import Input from 'react-toolbox/lib/input';
-import { isValidPassphrase } from '../../utils/passphrase';
-import styles from './secondPassphraseInput.css';
-
-class SecondPassphraseInput extends React.Component {
- constructor() {
- super();
- this.state = { inputType: 'password' };
- }
-
- componentDidMount() {
- if (this.props.hasSecondPassphrase) {
- this.props.onChange('');
- }
- }
-
- handleValueChange(value) {
- let error;
- if (!value) {
- error = 'Required';
- } else if (!isValidPassphrase(value)) {
- error = 'Invalid passphrase';
- }
- this.props.onChange(value, error);
- }
-
- setInputType(event) {
- this.setState({ inputType: event.target.checked ? 'text' : 'password' });
- }
-
- render() {
- return (this.props.hasSecondPassphrase ?
-
-
-
-
-
-
-
:
- null);
- }
-}
-
-export default SecondPassphraseInput;
-
diff --git a/src/components/secondPassphraseInput/secondPassphraseInput.test.js b/src/components/secondPassphraseInput/secondPassphraseInput.test.js
deleted file mode 100644
index 45de27c03..000000000
--- a/src/components/secondPassphraseInput/secondPassphraseInput.test.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { expect } from 'chai';
-import { mount } from 'enzyme';
-import sinon from 'sinon';
-import SecondPassphraseInput from './secondPassphraseInput';
-
-
-describe('SecondPassphraseInput', () => {
- let wrapper;
- let props;
- const passphrase = 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit';
-
- beforeEach(() => {
- props = {
- onChange: sinon.spy(),
- };
- });
-
- it('should render Input if props.hasSecondPassphrase', () => {
- props.hasSecondPassphrase = true;
- wrapper = mount( );
- expect(wrapper.find('Input')).to.have.lengthOf(1);
- });
-
- it('should render null if !props.hasSecondPassphrase', () => {
- props.hasSecondPassphrase = false;
- wrapper = mount( );
- expect(wrapper.html()).to.equal(null);
- });
-
- it('should render null if !props.hasSecondPassphrase', () => {
- props.hasSecondPassphrase = false;
- wrapper = mount( );
- expect(wrapper.html()).to.equal(null);
- });
-
- it('should call props.onChange when input value changes', () => {
- props.hasSecondPassphrase = true;
- wrapper = mount( );
- wrapper.find('.second-passphrase input').simulate('change', { target: { value: passphrase } });
- expect(props.onChange).to.have.been.calledWith(passphrase);
- });
-
- it('should call props.onChange(\'Required\') when input value changes to \'\'', () => {
- props.hasSecondPassphrase = true;
- wrapper = mount( );
- wrapper.find('.second-passphrase input').simulate('change', { target: { value: '' } });
- expect(props.onChange).to.have.been.calledWith('', 'Required');
- });
-
- it('should call props.onChange(\'Invalid passphrase\') when input value changes to \'test\'', () => {
- props.hasSecondPassphrase = true;
- wrapper = mount( );
- wrapper.find('.second-passphrase input').simulate('change', { target: { value: 'test' } });
- expect(props.onChange).to.have.been.calledWith('test', 'Invalid passphrase');
- });
-});
diff --git a/src/components/send/send.js b/src/components/send/send.js
index 0c2416436..72d2db250 100644
--- a/src/components/send/send.js
+++ b/src/components/send/send.js
@@ -2,8 +2,9 @@ import React from 'react';
import Input from 'react-toolbox/lib/input';
import { IconMenu, MenuItem } from 'react-toolbox/lib/menu';
import { fromRawLsk, toRawLsk } from '../../utils/lsk';
-import SecondPassphraseInput from '../secondPassphraseInput';
+import AuthInputs from '../authInputs';
import ActionBar from '../actionBar';
+import { authStatePrefill, authStateIsValid } from '../../utils/form';
import styles from './send.css';
@@ -17,9 +18,7 @@ class Send extends React.Component {
amount: {
value: '',
},
- secondPassphrase: {
- value: null,
- },
+ ...authStatePrefill(),
};
this.fee = 0.1;
this.inputValidationRegexps = {
@@ -36,6 +35,7 @@ class Send extends React.Component {
amount: {
value: this.props.amount || '',
},
+ ...authStatePrefill(this.props.account),
};
this.setState(newState);
}
@@ -68,7 +68,7 @@ class Send extends React.Component {
account: this.props.account,
recipientId: this.state.recipient.value,
amount: this.state.amount.value,
- passphrase: this.props.account.passphrase,
+ passphrase: this.state.passphrase.value,
secondPassphrase: this.state.secondPassphrase.value,
});
}
@@ -95,10 +95,10 @@ class Send extends React.Component {
error={this.state.amount.error}
value={this.state.amount.value}
onChange={this.handleChange.bind(this, 'amount')} />
-
+
Fee: {this.fee} LSK
diff --git a/src/components/send/send.test.js b/src/components/send/send.test.js
index f01e7779b..c2e4af0bd 100644
--- a/src/components/send/send.test.js
+++ b/src/components/send/send.test.js
@@ -3,20 +3,28 @@ import { expect } from 'chai';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { Provider } from 'react-redux';
-import store from '../../store';
+import configureStore from 'redux-mock-store';
import Send from './send';
+const fakeStore = configureStore();
describe('Send', () => {
let wrapper;
let props;
beforeEach(() => {
+ const account = {
+ balance: 1000e8,
+ passphrase: 'recipe bomb asset salon coil symbol tiger engine assist pact pumpkin visit',
+ };
+
+ const store = fakeStore({
+ account,
+ });
+
props = {
activePeer: {},
- account: {
- balance: 1000e8,
- },
+ account,
closeDialog: () => {},
sent: sinon.spy(),
};
@@ -78,10 +86,10 @@ describe('Send', () => {
wrapper.find('.recipient input').simulate('change', { target: { value: '11004588490103196952L' } });
wrapper.find('.primary-button button').simulate('click');
expect(props.sent).to.have.been.calledWith({
- account: { balance: 100000000000 },
+ account: props.account,
activePeer: {},
amount: '120.25',
- passphrase: undefined,
+ passphrase: props.account.passphrase,
recipientId: '11004588490103196952L',
secondPassphrase: null,
});
diff --git a/src/components/signMessage/signMessage.js b/src/components/signMessage/signMessage.js
index cdcf406dd..64d1ecd4a 100644
--- a/src/components/signMessage/signMessage.js
+++ b/src/components/signMessage/signMessage.js
@@ -4,22 +4,39 @@ import Lisk from 'lisk-js';
import InfoParagraph from '../infoParagraph';
import SignVerifyResult from '../signVerifyResult';
+import AuthInputs from '../authInputs';
import ActionBar from '../actionBar';
+import { authStatePrefill, authStateIsValid } from '../../utils/form';
class SignMessageComponent extends React.Component {
-
constructor() {
super();
this.state = {
message: '',
result: '',
+ ...authStatePrefill(),
};
}
+ componentDidMount() {
+ this.setState({
+ ...authStatePrefill(this.props.account),
+ });
+ }
+
+ handleChange(name, value, error) {
+ this.setState({
+ [name]: {
+ value,
+ error,
+ },
+ });
+ }
+
sign(message) {
const signedMessage = Lisk.crypto.signMessageWithSecret(message,
- this.props.account.passphrase);
+ this.state.passphrase.value);
const result = Lisk.crypto.printSignedMessage(
message, signedMessage, this.props.account.publicKey);
this.setState({ result, resultIsShown: false, message });
@@ -38,34 +55,40 @@ class SignMessageComponent extends React.Component {
render() {
return (
-
- Signing a message with this tool indicates ownership of a privateKey (secret) and
- provides a level of proof that you are the owner of the key.
- Its important to bear in mind that this is not a 100% proof as computer systems
- can be compromised, but is still an effective tool for proving ownership
- of a particular publicKey/address pair.
-
- Note: Digital Signatures and signed messages are not encrypted!
-
-
- {this.state.resultIsShown ?
-
:
-
- }
+
+ Signing a message with this tool indicates ownership of a privateKey (secret) and
+ provides a level of proof that you are the owner of the key.
+ Its important to bear in mind that this is not a 100% proof as computer systems
+ can be compromised, but is still an effective tool for proving ownership
+ of a particular publicKey/address pair.
+
+ Note: Digital Signatures and signed messages are not encrypted!
+
+
+ {this.state.resultIsShown ?
+
:
+
+ }
);
}
diff --git a/src/components/toaster/toaster.js b/src/components/toaster/toaster.js
index 3f20e2cb9..eafa9b759 100644
--- a/src/components/toaster/toaster.js
+++ b/src/components/toaster/toaster.js
@@ -26,7 +26,7 @@ class Toaster extends Component {
key={toast.index}
label={toast.label}
timeout={4000}
- className={`${styles.toast} ${styles[toast.type]} ${styles[`index-${toast.index}`]}`}
+ className={`toast ${styles.toast} ${styles[toast.type]} ${styles[`index-${toast.index}`]}`}
onTimeout={this.hideToast.bind(this, toast)}
/>
))}
diff --git a/src/components/voteDialog/voteDialog.js b/src/components/voteDialog/voteDialog.js
index 10ba04550..e808b9f33 100644
--- a/src/components/voteDialog/voteDialog.js
+++ b/src/components/voteDialog/voteDialog.js
@@ -4,38 +4,35 @@ import ActionBar from '../actionBar';
import Fees from '../../constants/fees';
import Autocomplete from './voteAutocomplete';
import styles from './voteDialog.css';
-import SecondPassphraseInput from '../secondPassphraseInput';
+import AuthInputs from '../authInputs';
+import { authStatePrefill, authStateIsValid } from '../../utils/form';
export default class VoteDialog extends React.Component {
constructor() {
super();
- this.state = {
- secondSecret: '',
- secondPassphrase: {
- value: null,
- },
- };
+ this.state = authStatePrefill();
}
- confirm() {
- const secondSecret = this.props.account.secondSignature === 1 ?
- this.state.secondPassphrase.value :
- null;
+ componentDidMount() {
+ this.setState(authStatePrefill(this.props.account));
+ }
- // fire first action
+ confirm() {
this.props.votePlaced({
activePeer: this.props.activePeer,
account: this.props.account,
votedList: this.props.votedList,
unvotedList: this.props.unvotedList,
- secondSecret,
+ secondSecret: this.state.secondPassphrase.value,
+ passphrase: this.state.passphrase.value,
});
}
- setSecondPass(name, value, error) {
+
+ handleChange(name, value, error) {
this.setState({
[name]: {
value,
- error,
+ error: typeof error === 'string' ? error : null,
},
});
}
@@ -50,10 +47,10 @@ export default class VoteDialog extends React.Component {
addedToVoteList={this.props.addedToVoteList}
removedFromVoteList={this.props.removedFromVoteList}
activePeer={this.props.activePeer} />
-
+
@@ -78,8 +75,7 @@ export default class VoteDialog extends React.Component {
this.props.unvotedList.length > 33) ||
(this.props.votedList.length === 0 &&
this.props.unvotedList.length === 0) ||
- (!!this.state.secondPassphrase.error ||
- this.state.secondPassphrase.value === '')
+ !authStateIsValid(this.state)
),
onClick: this.confirm.bind(this),
}} />
diff --git a/src/components/voteDialog/voteDialog.test.js b/src/components/voteDialog/voteDialog.test.js
index 86dd3afd5..41d67e314 100644
--- a/src/components/voteDialog/voteDialog.test.js
+++ b/src/components/voteDialog/voteDialog.test.js
@@ -81,6 +81,7 @@ describe('VoteDialog', () => {
expect(props.votePlaced).to.have.been.calledWith({
account: ordinaryAccount,
+ passphrase: ordinaryAccount.passphrase,
activePeer: props.activePeer,
secondSecret: null,
unvotedList: props.unvotedList,
@@ -126,12 +127,13 @@ describe('VoteDialog', () => {
childContextTypes: { store: PropTypes.object.isRequired },
});
const secondPassphrase = 'test second passphrase';
- wrapper.instance().setSecondPass('secondPassphrase', secondPassphrase);
+ wrapper.instance().handleChange('secondPassphrase', secondPassphrase);
wrapper.find('.primary-button button').simulate('click');
expect(props.votePlaced).to.have.been.calledWith({
activePeer: props.activePeer,
account: accountWithSecondPassphrase,
+ passphrase: accountWithSecondPassphrase.passphrase,
votedList: props.votedList,
unvotedList: props.unvotedList,
secondSecret: secondPassphrase,
diff --git a/src/constants/actions.js b/src/constants/actions.js
index 3b240d885..cdbcdc22a 100644
--- a/src/constants/actions.js
+++ b/src/constants/actions.js
@@ -24,6 +24,7 @@ const actionTypes = {
transactionsUpdated: 'TRANSACTIONS_UPDATED',
transactionsLoaded: 'TRANSACTIONS_LOADED',
transactionsReset: 'TRANSACTIONS_RESET',
+ passphraseUsed: 'PASSPHRASE_USED',
};
export default actionTypes;
diff --git a/src/store/middlewares/account.js b/src/store/middlewares/account.js
index 21f480a45..b25c7cd70 100644
--- a/src/store/middlewares/account.js
+++ b/src/store/middlewares/account.js
@@ -85,6 +85,12 @@ const votePlaced = (store, action) => {
}
};
+const passphraseUsed = (store, action) => {
+ if (!store.getState().account.passphrase) {
+ store.dispatch(accountUpdated({ passphrase: action.data }));
+ }
+};
+
const accountMiddleware = store => next => (action) => {
next(action);
switch (action.type) {
@@ -95,6 +101,9 @@ const accountMiddleware = store => next => (action) => {
delegateRegistration(store, action);
votePlaced(store, action);
break;
+ case actionTypes.passphraseUsed:
+ passphraseUsed(store, action);
+ break;
default: break;
}
};
diff --git a/src/store/middlewares/account.test.js b/src/store/middlewares/account.test.js
index 99676625f..9bdd8b006 100644
--- a/src/store/middlewares/account.test.js
+++ b/src/store/middlewares/account.test.js
@@ -1,12 +1,14 @@
import { expect } from 'chai';
import { spy, stub } from 'sinon';
-import middleware from './account';
+
+import { SYNC_ACTIVE_INTERVAL, SYNC_INACTIVE_INTERVAL } from '../../constants/api';
+import { accountUpdated } from '../../actions/account';
+import { clearVoteLists } from '../../actions/voting';
import * as accountApi from '../../utils/api/account';
-import * as delegateApi from '../../utils/api/delegate';
import actionTypes from '../../constants/actions';
+import * as delegateApi from '../../utils/api/delegate';
+import middleware from './account';
import transactionTypes from '../../constants/transactionTypes';
-import { SYNC_ACTIVE_INTERVAL, SYNC_INACTIVE_INTERVAL } from '../../constants/api';
-import { clearVoteLists } from '../../actions/voting';
describe('Account middleware', () => {
let store;
@@ -15,6 +17,7 @@ describe('Account middleware', () => {
let stubGetAccount;
let stubGetAccountStatus;
let stubTransactions;
+ const passphrase = 'right cat soul renew under climb middle maid powder churn cram coconut';
const transactionsUpdatedAction = {
type: actionTypes.transactionsUpdated,
@@ -169,5 +172,23 @@ describe('Account middleware', () => {
middleware(store)(next)(transactionsUpdatedAction);
expect(store.dispatch).to.have.been.calledWith(clearVoteLists());
});
-});
+ it(`should dispatch accountUpdated({passphrase}) action on ${actionTypes.passphraseUsed} action if store.account.passphrase is not set`, () => {
+ const action = {
+ type: actionTypes.passphraseUsed,
+ data: passphrase,
+ };
+ middleware(store)(next)(action);
+ expect(store.dispatch).to.have.been.calledWith(accountUpdated({ passphrase }));
+ });
+
+ it(`should not dispatch accountUpdated action on ${actionTypes.passphraseUsed} action if store.account.passphrase is already set`, () => {
+ const action = {
+ type: actionTypes.passphraseUsed,
+ data: passphrase,
+ };
+ store.getState = () => ({ ...state, account: { ...state.account, passphrase } });
+ middleware(store)(next)(action);
+ expect(store.dispatch).to.not.have.been.calledWith();
+ });
+});
diff --git a/src/store/middlewares/login.js b/src/store/middlewares/login.js
index 1f71084db..d3dafa9e7 100644
--- a/src/store/middlewares/login.js
+++ b/src/store/middlewares/login.js
@@ -12,8 +12,8 @@ const loginMiddleware = store => next => (action) => {
next(Object.assign({}, action, { data: action.data.activePeer }));
const { passphrase } = action.data;
- const publicKey = extractPublicKey(passphrase);
- const address = extractAddress(passphrase);
+ const publicKey = passphrase ? extractPublicKey(passphrase) : action.data.publicKey;
+ const address = extractAddress(publicKey);
const accountBasics = {
passphrase,
publicKey,
diff --git a/src/utils/form.js b/src/utils/form.js
new file mode 100644
index 000000000..5403939a6
--- /dev/null
+++ b/src/utils/form.js
@@ -0,0 +1,25 @@
+
+export const authStatePrefill = account => ({
+ secondPassphrase: {
+ value: null,
+ },
+ passphrase: {
+ value: (account && account.passphrase) || '',
+ },
+});
+
+export const authStateIsValid = state => (
+ !state.passphrase.error &&
+ state.passphrase.value !== '' &&
+ !state.secondPassphrase.error &&
+ state.secondPassphrase.value !== ''
+);
+
+export const handleChange = (component, name, value, error) => {
+ component.setState({
+ [name]: {
+ value,
+ error: typeof error === 'string' ? error : undefined,
+ },
+ });
+};
diff --git a/src/utils/saveAccount.js b/src/utils/saveAccount.js
new file mode 100644
index 000000000..2c2eb05a0
--- /dev/null
+++ b/src/utils/saveAccount.js
@@ -0,0 +1,20 @@
+export const getSavedAccount = () => {
+ const savedAccounts = localStorage.getItem('accounts');
+ let account;
+ if (savedAccounts) {
+ account = JSON.parse(savedAccounts)[0];
+ }
+ return account;
+};
+
+export const setSavedAccount = (account) => {
+ localStorage.setItem('accounts', JSON.stringify([{
+ publicKey: account.publicKey,
+ network: localStorage.getItem('network'),
+ address: localStorage.getItem('address'),
+ }]));
+};
+
+export const removeSavedAccount = () => {
+ localStorage.removeItem('accounts');
+};