Skip to content

Commit

Permalink
Merge pull request #523 from LiskHQ/496-add-lockid-feature
Browse files Browse the repository at this point in the history
Add LockID feature - Closes #496
  • Loading branch information
slaweet committed Mar 16, 2018
2 parents 3895f6c + b41d11e commit 9a7e00d
Show file tree
Hide file tree
Showing 19 changed files with 180 additions and 21 deletions.
3 changes: 2 additions & 1 deletion i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"Lisk Hub": "Lisk Hub",
"Lisk ID": "Lisk ID",
"Lisk Website": "Lisk Website",
"Lock ID": "Lock ID",
"Lock ID’s automatically after 10 minutes.": "Lock ID’s automatically after 10 minutes.",
"Log in": "Log in",
"Losing access to this passphrase will mean no funds can be sent from this account.": "Losing access to this passphrase will mean no funds can be sent from this account.",
Expand Down Expand Up @@ -202,7 +203,6 @@
"Unable to connect to the node": "Unable to connect to the node",
"Undo": "Undo",
"Unlock account": "Unlock account",
"Unlocked": "Unlocked",
"Update download finished": "Update download finished",
"Update now": "Update now",
"Updates downloaded, application has to be restarted to apply the updates.": "Updates downloaded, application has to be restarted to apply the updates.",
Expand Down Expand Up @@ -231,6 +231,7 @@
"You only need to do this once for each Lisk ID.": "You only need to do this once for each Lisk ID.",
"You will send a small amount of {{fee}} LSK to yourself and therefore initialize your ID.": "You will send a small amount of {{fee}} LSK to yourself and therefore initialize your ID.",
"You've received {{value}} LSK.": "You've received {{value}} LSK.",
"Your ID is now secured!": "Your ID is now secured!",
"Your Lisk IDs": "Your Lisk IDs",
"You’re votes are being processed and will be confirmed. It may take up to 10 minutes to be secured in the blockchain.": "You’re votes are being processed and will be confirmed. It may take up to 10 minutes to be secured in the blockchain.",
"Zero not allowed": "Zero not allowed",
Expand Down
4 changes: 3 additions & 1 deletion src/actions/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import transactionTypes from '../constants/transactionTypes';
/**
* Trigger this action to remove passphrase from account object
*
* @param {Object} data - account data
* @returns {Object} - Action object
*/
export const removePassphrase = () => ({
export const removePassphrase = data => ({
data,
type: actionTypes.removePassphrase,
});

Expand Down
21 changes: 20 additions & 1 deletion src/actions/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { expect } from 'chai';
import sinon from 'sinon';
import actionTypes from '../constants/actions';
import { accountUpdated, accountLoggedOut,
secondPassphraseRegistered, delegateRegistered, sent } from './account';
secondPassphraseRegistered, delegateRegistered, sent, removePassphrase } from './account';
import { transactionAdded, transactionFailed } from './transactions';
import { errorAlertDialogDisplayed } from './dialog';
import * as accountApi from '../utils/api/account';
import * as delegateApi from '../utils/api/delegate';
import Fees from '../constants/fees';
import { toRawLsk } from '../utils/lsk';
import transactionTypes from '../constants/transactionTypes';
import networks from '../constants/networks';
import accounts from '../../test/constants/accounts';

describe('actions: account', () => {
describe('accountUpdated', () => {
Expand Down Expand Up @@ -215,4 +217,21 @@ describe('actions: account', () => {
expect(dispatch).to.have.been.calledWith(expectedAction);
});
});

describe('removePassphrase', () => {
it('should create an action to remove passphrase', () => {
const data = {
publicKey: accounts.genesis.publicKey,
network: networks.testnet,
address: accounts.genesis.address,
};

const expectedAction = {
data,
type: actionTypes.removePassphrase,
};

expect(removePassphrase(data)).to.be.deep.equal(expectedAction);
});
});
});
10 changes: 10 additions & 0 deletions src/actions/savedAccounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,13 @@ export const accountsRetrieved = () => ({
export const activeAccountSaved = () => ({
type: actionTypes.activeAccountSaved,
});

/**
* An action to dispatch removeSavedAccountPassphrase
* @param {Object} data - account data
* @returns {Object} - Action object
*/
export const removeSavedAccountPassphrase = data => ({
data,
type: actionTypes.removeSavedAccountPassphrase,
});
12 changes: 12 additions & 0 deletions src/actions/savedAccounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
accountRemoved,
accountsRetrieved,
activeAccountSaved,
removeSavedAccountPassphrase,
} from './savedAccounts';


Expand Down Expand Up @@ -76,4 +77,15 @@ describe('actions: savedAccount', () => {
expect(activeAccountSaved()).to.be.deep.equal(expectedAction);
});
});

describe('removeSavedAccountPassphrase', () => {
it('should create an action to remove passphrase', () => {
const expectedAction = {
data,
type: actionTypes.removeSavedAccountPassphrase,
};

expect(removeSavedAccountPassphrase(data)).to.be.deep.equal(expectedAction);
});
});
});
14 changes: 9 additions & 5 deletions src/components/header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,20 @@ class Header extends React.Component {
</div>
<CopyToClipboard value={this.props.account.address} className={`${styles.address} account-information-address`}/>
{this.props.autoLog ? <div className={styles.timer}>
{((!this.props.account.expireTime || this.props.account.expireTime === 0)) ?
<span>{this.props.t('Account locked!')} <FontIcon value='locked' className={styles.lock}/></span> :
{((this.props.account.expireTime &&
this.props.account.expireTime !== 0) &&
this.props.account.passphrase) ?
<div>
{this.props.t('Address timeout in')} <i> </i>
<Countdown
date={this.props.account.expireTime}
renderer={CountDownTemplate}
onComplete={() => this.props.removePassphrase()}
/> <FontIcon value='unlocked' className={styles.lock}/>
</div>}
onComplete={() => {
this.props.removeSavedAccountPassphrase();
}
}
/>
</div> : <div></div>}
</div>
: <div className={styles.timer}>
{this.props.account.passphrase ? '' : <span>
Expand Down
4 changes: 3 additions & 1 deletion src/components/header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { withRouter } from 'react-router';
import { translate } from 'react-i18next';
import { dialogDisplayed } from '../../actions/dialog';
import { accountLoggedOut, removePassphrase } from '../../actions/account';
import { removeSavedAccountPassphrase } from '../../actions/savedAccounts';
import Header from './header';

const mapStateToProps = state => ({
Expand All @@ -15,7 +16,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
setActiveDialog: data => dispatch(dialogDisplayed(data)),
logOut: () => dispatch(accountLoggedOut()),
removePassphrase: () => dispatch(removePassphrase()),
removePassphrase: data => dispatch(removePassphrase(data)),
removeSavedAccountPassphrase: () => dispatch(removeSavedAccountPassphrase()),
});
export default withRouter(connect(
mapStateToProps,
Expand Down
15 changes: 15 additions & 0 deletions src/components/header/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '../../i18n'; // initialized i18next instance
import * as accountActions from '../../actions/account';
import * as dialogActions from '../../actions/dialog';
import * as savedAccountsActions from '../../actions/savedAccounts';
import Header from './header';
import HeaderHOC from './index';

Expand Down Expand Up @@ -52,4 +53,18 @@ describe('HeaderHOC', () => {
expect(actionsSpy).to.be.calledWith();
actionsSpy.restore();
});

it('should dispatch removePassphrase action', () => {
const actionsSpy = sinon.spy(accountActions, 'removePassphrase');
wrapper.find(Header).props().removePassphrase({});
expect(actionsSpy).to.be.calledWith({});
actionsSpy.restore();
});

it('should dispatch removeSavedAccountPassphrase action', () => {
const actionsSpy = sinon.spy(savedAccountsActions, 'removeSavedAccountPassphrase');
wrapper.find(Header).props().removeSavedAccountPassphrase();
expect(actionsSpy).to.be.calledWith();
actionsSpy.restore();
});
});
5 changes: 4 additions & 1 deletion src/components/savedAccounts/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { translate } from 'react-i18next';
import { accountRemoved, accountSwitched } from '../../actions/savedAccounts';
import { accountRemoved, accountSwitched, removeSavedAccountPassphrase } from '../../actions/savedAccounts';
import { removePassphrase } from '../../actions/account';
import SavedAccounts from './savedAccounts';

const mapStateToProps = state => ({
Expand All @@ -12,6 +13,8 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
accountRemoved: data => dispatch(accountRemoved(data)),
accountSwitched: data => dispatch(accountSwitched(data)),
removePassphrase: data => dispatch(removePassphrase(data)),
removeSavedAccountPassphrase: data => dispatch(removeSavedAccountPassphrase(data)),
});

export default connect(
Expand Down
21 changes: 20 additions & 1 deletion src/components/savedAccounts/savedAccounts.css
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
}

.network,
.unlocked {
.unlocked,
.unlockedSecured {
font-size: var(--font-size-h6);
position: absolute;
top: 0;
Expand All @@ -197,6 +198,24 @@

.unlocked {
left: 0;
padding-top: 25px;

& span {
margin-bottom: 3px;
transition: transform ease-in-out 500ms;
}

&:hover {
& span {
transform: scale(1.1);
transform-origin: 100%;
}
}
}

.unlockedSecured {
left: 0;
padding-top: 30px;
}

.network {
Expand Down
30 changes: 28 additions & 2 deletions src/components/savedAccounts/savedAccounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SavedAccounts extends React.Component {
super();

this.state = {
isSecureAppears: {},
};
}

Expand Down Expand Up @@ -57,6 +58,24 @@ class SavedAccounts extends React.Component {
e.stopPropagation();
}

handleRemovePassphrase(account, e) {
e.stopPropagation();

const uniqueID = `${account.network}${account.publicKey}`;
const { savedAccounts } = this.props;
const savedActiveAccount = savedAccounts.find(acc => `${acc.network}${acc.passphrase}` === `${account.network}${account.passphrase}`);
if (savedActiveAccount) {
this.props.removePassphrase(account);
}

this.props.removeSavedAccountPassphrase(account);

this.setState({ isSecureAppears: { ...this.state.isSecureAppears, [uniqueID]: true } });
setTimeout(() => {
this.setState({ isSecureAppears: { ...this.state.isSecureAppears, [uniqueID]: false } });
}, 5000);
}

render() {
const {
closeDialog,
Expand Down Expand Up @@ -102,9 +121,16 @@ class SavedAccounts extends React.Component {
key={account.publicKey + account.network}
onClick={ switchAccount.bind(null, account)} >
{(account.passphrase ?
<strong className={styles.unlocked}>
<strong
className={`unlocked ${styles.unlocked}`}
onClick={this.handleRemovePassphrase.bind(this, account)}>
<FontIcon value='unlocked' />
{t('Unlocked')}
{t('Lock ID')}
</strong> :
null)}
{(this.state.isSecureAppears[`${account.network}${account.publicKey}`] ?
<strong className={`unlockedSecured ${styles.unlockedSecured}`}>
{t('Your ID is now secured!')}
</strong> :
null)}
{(account.network !== networks.mainnet.code ?
Expand Down
32 changes: 31 additions & 1 deletion src/components/savedAccounts/savedAccounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React from 'react';
import { expect } from 'chai';
import { MemoryRouter as Router } from 'react-router-dom';
import { mount } from 'enzyme';
import { spy } from 'sinon';
import { spy, useFakeTimers } from 'sinon';
import configureStore from 'redux-mock-store';
import PropTypes from 'prop-types';
import i18n from '../../i18n';
import networks from '../../constants/networks';
import accounts from '../../../test/constants/accounts';
import SavedAccounts from './savedAccounts';
import routes from '../../constants/routes';

Expand All @@ -26,6 +27,7 @@ describe('SavedAccounts', () => {
{
publicKey: 'hab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88',
network: networks.mainnet.code,
passphrase: accounts.genesis.passphrase,
},
{
network: networks.customNode.code,
Expand All @@ -39,13 +41,16 @@ describe('SavedAccounts', () => {
{
network: networks.testnet.code,
publicKey: activeAccount.publicKey,
passphrase: accounts.genesis.passphrase,
},
];

const props = {
closeDialog: () => {},
accountRemoved: spy(),
accountSwitched: spy(),
removePassphrase: spy(),
removeSavedAccountPassphrase: spy(),
networkOptions: {
code: networks.mainnet.code,
},
Expand All @@ -62,6 +67,7 @@ describe('SavedAccounts', () => {
account: {
balance: 100e8,
},
isSecureAppears: false,
});
wrapper = mountWithRouter(<SavedAccounts {...props} />, {
context: { store, i18n },
Expand All @@ -87,12 +93,36 @@ describe('SavedAccounts', () => {
expect(props.accountRemoved).to.have.been.calledWith(savedAccounts[1]);
});

it('should call props.removePassphrase and props.removeSavedAccountPassphrase on "Lock ID" click when account is in saved', () => {
expect(wrapper.find('strong.unlocked')).to.have.lengthOf(2);
wrapper.find('strong.unlocked').at(1).simulate('click');
expect(props.removePassphrase).to.have.been.calledWith(savedAccounts[3]);
expect(props.removeSavedAccountPassphrase).to.have.been.calledWith(savedAccounts[3]);
});

it('should call props.accountSwitched on the "saved account card" click', () => {
wrapper.find('.saved-account-card').at(1).simulate('click');
expect(props.accountSwitched).to.have.been.calledWith(savedAccounts[1]);
expect(props.history.push).to.have.been.calledWith(`${routes.main.path}${routes.dashboard.path}`);
});

it('should check if "Your ID is now secured!" disapears after 5sec', () => {
const clock = useFakeTimers({
toFake: ['setTimeout', 'clearTimeout', 'Date'],
});

expect(wrapper.find('strong.unlockedSecured')).to.have.lengthOf(0);
wrapper.find('strong.unlocked').at(1).simulate('click');
clock.tick(2000);
expect(wrapper.find('strong.unlockedSecured')).to.have.lengthOf(1);

clock.tick(7000);
wrapper.update();
expect(wrapper.find('strong.unlockedSecured')).to.have.lengthOf(0);

clock.restore();
});

it('should not call props.accountSwitched on the "saved account card" click if in "edit" mode', () => {
wrapper.find('button.edit-button').simulate('click');
wrapper.find('.saved-account-card').at(0).simulate('click');
Expand Down
1 change: 1 addition & 0 deletions src/constants/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const actionTypes = {
removePassphrase: 'REMOVE_PASSPHRASE',
settingsUpdated: 'SETTINGS_UPDATED',
settingsReset: 'SETTINGS_RESET',
removeSavedAccountPassphrase: 'REMOVE_SAVED_ACCOUNT_PASSPHRASE',
};

export default actionTypes;
3 changes: 2 additions & 1 deletion src/store/middlewares/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ const loginMiddleware = store => next => (action) => {

next(Object.assign({}, action, { data: action.data.activePeer }));

const { passphrase } = action.data;
const { passphrase, activePeer: { options: { code } } } = action.data;
const publicKey = passphrase ? extractPublicKey(passphrase) : action.data.publicKey;
const address = extractAddress(publicKey);
const accountBasics = {
passphrase,
publicKey,
address,
network: code,
};
const { activePeer } = action.data;

Expand Down
Loading

0 comments on commit 9a7e00d

Please sign in to comment.