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

Add option to remember account for read-only access - Closes #559 #757

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
31e71d0
Add saveAccount menu item
slaweet Aug 31, 2017
186ea97
Setup autologin based on saved account
slaweet Aug 31, 2017
06deefa
Add authInputs component
slaweet Aug 31, 2017
fc5e4a8
Update send component to use authInputs
slaweet Aug 31, 2017
154d418
Update voteDialog to use authInputs
slaweet Aug 31, 2017
1bf39c4
Update registerDelegate to use authInputs
slaweet Aug 31, 2017
4f55813
Remove no longer used secondPassphraseInput
slaweet Aug 31, 2017
9653276
Add publicKey to activePeerSet action
slaweet Aug 31, 2017
2ebc105
Remove unused code
slaweet Sep 1, 2017
ae94b34
Separate passphraseInput component
slaweet Sep 12, 2017
aa2d4e7
Use advanced passphrase validation also in passphraseInput
slaweet Sep 12, 2017
275dd0f
Use PassphraseInput component in Login
slaweet Sep 12, 2017
99d428f
Move passphraseInput component to its own folder
slaweet Sep 12, 2017
fbb3cca
Add unit tests for PassphraseInput component
slaweet Sep 12, 2017
750b47b
Delete files re-introduced in merge
slaweet Sep 12, 2017
1f957ec
Prevent React warring
slaweet Sep 12, 2017
32e4f15
Add unit tests for SaveAccount component
slaweet Sep 12, 2017
6b0edab
Refactor our authStateIsValid function
slaweet Sep 15, 2017
ba9adef
Add auth Inputs to SignMessage component
slaweet Sep 15, 2017
869af11
Fix indentation in signMessage
slaweet Sep 15, 2017
fbe5037
Add e2e tests for save account
slaweet Sep 15, 2017
0ed0ec4
Refactor authInputsHOC to pass in the whole account object
slaweet Sep 15, 2017
7f36a16
Add a check that entered passphrase belongs to the active account
slaweet Sep 15, 2017
03a6d2f
Add passphraseUsed action to save passphrase
slaweet Sep 15, 2017
6e6f899
Add lock icon to account
slaweet Sep 15, 2017
39125de
Fix signMessage
slaweet Sep 15, 2017
39e631c
Allow to forget saved account
slaweet Sep 18, 2017
efe06fa
Add e2e test for forget account feature
slaweet Sep 18, 2017
4b704a5
Fix coding style
slaweet Sep 19, 2017
620949b
Add crossed eye icon to passphraseInput
slaweet Sep 19, 2017
7bbe609
Change text 'Save this account locally' to 'Remember this account'
slaweet Sep 19, 2017
d428e0d
Remove "locally" from "Forget account" texts
slaweet Sep 19, 2017
51402be
Fix saveAccountButton unit test
slaweet Sep 19, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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