Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #737 from LiskHQ/559-remeber-account
Browse files Browse the repository at this point in the history
Add option to remember account for read-only access - Closes #559
  • Loading branch information
slaweet authored Sep 19, 2017
2 parents 0556d3c + 51402be commit 92ed825
Show file tree
Hide file tree
Showing 43 changed files with 917 additions and 350 deletions.
47 changes: 47 additions & 0 deletions features/accountManagement.feature
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 18 additions & 0 deletions features/step_definitions/generic.step.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
Expand Down Expand Up @@ -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/');
Expand Down Expand Up @@ -151,5 +165,9 @@ defineSupportCode(({ Given, When, Then, setDefaultTimeout }) => {
});
});
});

When('I Refresh the page', (callback) => {
browser.refresh().then(callback);
});
});

13 changes: 11 additions & 2 deletions src/actions/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export const accountLoggedIn = data => ({
data,
});

export const passphraseUsed = data => ({
type: actionTypes.passphraseUsed,
data,
});

/**
*
*/
Expand All @@ -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({
Expand All @@ -84,6 +91,7 @@ export const delegateRegistered = ({ activePeer, account, username, secondPassph
const actionObj = errorAlertDialogDisplayed({ text });
dispatch(actionObj);
});
dispatch(passphraseUsed(passphrase));
};

/**
Expand All @@ -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));
};
1 change: 1 addition & 0 deletions src/actions/peers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 9 additions & 7 deletions src/actions/voting.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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));
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/components/account/account.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
position: absolute;
top: 5px;
right: 5px;
z-index: 1;
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/components/account/address.js
Original file line number Diff line number Diff line change
@@ -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 ?
Expand All @@ -26,6 +37,11 @@ const Address = (props) => {
<div className={`${grid['col-sm-12']} ${grid['col-xs-8']}`}>
<div className={styles['value-wrapper']}>
{content}
<span className="status">
<TooltipWrapper tooltip={getStatusTooltip(props)}>
<i className="material-icons">{props.passphrase && !props.secondSignature ? 'lock_open' : 'lock'}</i>
</TooltipWrapper>
</span>
</div>
</div>
</div>
Expand Down
46 changes: 46 additions & 0 deletions src/components/authInputs/authInputs.js
Original file line number Diff line number Diff line change
@@ -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 <span>
{(!this.props.account.passphrase &&
<PassphraseInput label='Passphrase'
className='passphrase'
error={this.props.passphrase.error}
value={this.props.passphrase.value}
onChange={this.onChange.bind(this, 'passphrase')} />)}
{(this.props.account.secondSignature &&
<PassphraseInput label='Second Passphrase'
className='second-passphrase'
error={this.props.secondPassphrase.error}
value={this.props.secondPassphrase.value}
onChange={this.onChange.bind(this, 'secondPassphrase')} />)}
</span>;
}
}

export default AuthInputs;

73 changes: 73 additions & 0 deletions src/components/authInputs/authInputs.test.js
Original file line number Diff line number Diff line change
@@ -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(<AuthInputs {...props} />);
expect(wrapper.find('Input')).to.have.lengthOf(1);
});

it('should render null if !props.account.secondSignature', () => {
props.account.secondSignature = false;
wrapper = mount(<AuthInputs {...props} />);
expect(wrapper.html()).to.equal('<span></span>');
});

it('should render null if !props.account.secondSignature', () => {
props.account.secondSignature = false;
wrapper = mount(<AuthInputs {...props} />);
expect(wrapper.html()).to.equal('<span></span>');
});

it('should call props.onChange when input value changes', () => {
props.account.secondSignature = true;
wrapper = mount(<AuthInputs {...props} />);
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(<AuthInputs {...props} />);
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(<AuthInputs {...props} />);
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(<AuthInputs {...props} />);
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');
});
});
9 changes: 9 additions & 0 deletions src/components/authInputs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import AuthInputs from './authInputs';

const mapStateToProps = state => ({
account: state.account,
});

export default connect(mapStateToProps)(AuthInputs);

27 changes: 27 additions & 0 deletions src/components/authInputs/index.test.js
Original file line number Diff line number Diff line change
@@ -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(<Provider store={store}>
<AuthInputsHOC {...props}/>
</Provider>);
expect(wrapper.find('AuthInputs').props().account).to.deep.equal(account);
});
});
Loading

0 comments on commit 92ed825

Please sign in to comment.