diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json
index b0ffbd0f76..3b85c1e37f 100644
--- a/i18n/locales/en/common.json
+++ b/i18n/locales/en/common.json
@@ -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.",
@@ -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.",
@@ -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",
diff --git a/src/actions/account.js b/src/actions/account.js
index 383d45a754..205ea8a0a3 100644
--- a/src/actions/account.js
+++ b/src/actions/account.js
@@ -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,
});
diff --git a/src/actions/account.test.js b/src/actions/account.test.js
index 1805b23415..386edad714 100644
--- a/src/actions/account.test.js
+++ b/src/actions/account.test.js
@@ -2,7 +2,7 @@ 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';
@@ -10,6 +10,8 @@ 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', () => {
@@ -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);
+ });
+ });
});
diff --git a/src/actions/savedAccounts.js b/src/actions/savedAccounts.js
index 2bdf54dfa6..f9f4f93202 100644
--- a/src/actions/savedAccounts.js
+++ b/src/actions/savedAccounts.js
@@ -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,
+});
diff --git a/src/actions/savedAccounts.test.js b/src/actions/savedAccounts.test.js
index 9ee84bc481..2ec27a287d 100644
--- a/src/actions/savedAccounts.test.js
+++ b/src/actions/savedAccounts.test.js
@@ -8,6 +8,7 @@ import {
accountRemoved,
accountsRetrieved,
activeAccountSaved,
+ removeSavedAccountPassphrase,
} from './savedAccounts';
@@ -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);
+ });
+ });
});
diff --git a/src/components/header/header.js b/src/components/header/header.js
index 1364c727ec..b6855164df 100644
--- a/src/components/header/header.js
+++ b/src/components/header/header.js
@@ -49,16 +49,20 @@ class Header extends React.Component {
{this.props.autoLog ?
- {((!this.props.account.expireTime || this.props.account.expireTime === 0)) ?
-
{this.props.t('Account locked!')} :
+ {((this.props.account.expireTime &&
+ this.props.account.expireTime !== 0) &&
+ this.props.account.passphrase) ?
{this.props.t('Address timeout in')}
this.props.removePassphrase()}
- />
-
}
+ onComplete={() => {
+ this.props.removeSavedAccountPassphrase();
+ }
+ }
+ />
+
: }
:
{this.props.account.passphrase ? '' :
diff --git a/src/components/header/index.js b/src/components/header/index.js
index 2e35866ac7..88220b3db7 100644
--- a/src/components/header/index.js
+++ b/src/components/header/index.js
@@ -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 => ({
@@ -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,
diff --git a/src/components/header/index.test.js b/src/components/header/index.test.js
index bf545ac1c3..3fde7d036f 100644
--- a/src/components/header/index.test.js
+++ b/src/components/header/index.test.js
@@ -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';
@@ -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();
+ });
});
diff --git a/src/components/savedAccounts/index.js b/src/components/savedAccounts/index.js
index 85f72caa1d..428336722c 100644
--- a/src/components/savedAccounts/index.js
+++ b/src/components/savedAccounts/index.js
@@ -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 => ({
@@ -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(
diff --git a/src/components/savedAccounts/savedAccounts.css b/src/components/savedAccounts/savedAccounts.css
index 57d1dab1e5..e45e348088 100644
--- a/src/components/savedAccounts/savedAccounts.css
+++ b/src/components/savedAccounts/savedAccounts.css
@@ -182,7 +182,8 @@
}
.network,
-.unlocked {
+.unlocked,
+.unlockedSecured {
font-size: var(--font-size-h6);
position: absolute;
top: 0;
@@ -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 {
diff --git a/src/components/savedAccounts/savedAccounts.js b/src/components/savedAccounts/savedAccounts.js
index 621aa12d67..389f0ee7b3 100644
--- a/src/components/savedAccounts/savedAccounts.js
+++ b/src/components/savedAccounts/savedAccounts.js
@@ -27,6 +27,7 @@ class SavedAccounts extends React.Component {
super();
this.state = {
+ isSecureAppears: {},
};
}
@@ -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,
@@ -102,9 +121,16 @@ class SavedAccounts extends React.Component {
key={account.publicKey + account.network}
onClick={ switchAccount.bind(null, account)} >
{(account.passphrase ?
-
+
- {t('Unlocked')}
+ {t('Lock ID')}
+ :
+ null)}
+ {(this.state.isSecureAppears[`${account.network}${account.publicKey}`] ?
+
+ {t('Your ID is now secured!')}
:
null)}
{(account.network !== networks.mainnet.code ?
diff --git a/src/components/savedAccounts/savedAccounts.test.js b/src/components/savedAccounts/savedAccounts.test.js
index f265ff9b98..91f4be5d18 100644
--- a/src/components/savedAccounts/savedAccounts.test.js
+++ b/src/components/savedAccounts/savedAccounts.test.js
@@ -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';
@@ -26,6 +27,7 @@ describe('SavedAccounts', () => {
{
publicKey: 'hab9d261ea050b9e326d7e11587eccc343a20e64e29d8781b50fd06683cacc88',
network: networks.mainnet.code,
+ passphrase: accounts.genesis.passphrase,
},
{
network: networks.customNode.code,
@@ -39,6 +41,7 @@ describe('SavedAccounts', () => {
{
network: networks.testnet.code,
publicKey: activeAccount.publicKey,
+ passphrase: accounts.genesis.passphrase,
},
];
@@ -46,6 +49,8 @@ describe('SavedAccounts', () => {
closeDialog: () => {},
accountRemoved: spy(),
accountSwitched: spy(),
+ removePassphrase: spy(),
+ removeSavedAccountPassphrase: spy(),
networkOptions: {
code: networks.mainnet.code,
},
@@ -62,6 +67,7 @@ describe('SavedAccounts', () => {
account: {
balance: 100e8,
},
+ isSecureAppears: false,
});
wrapper = mountWithRouter(, {
context: { store, i18n },
@@ -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');
diff --git a/src/constants/actions.js b/src/constants/actions.js
index b8e676284f..7ec7060048 100644
--- a/src/constants/actions.js
+++ b/src/constants/actions.js
@@ -43,6 +43,7 @@ const actionTypes = {
removePassphrase: 'REMOVE_PASSPHRASE',
settingsUpdated: 'SETTINGS_UPDATED',
settingsReset: 'SETTINGS_RESET',
+ removeSavedAccountPassphrase: 'REMOVE_SAVED_ACCOUNT_PASSPHRASE',
};
export default actionTypes;
diff --git a/src/store/middlewares/login.js b/src/store/middlewares/login.js
index ac5f5df31c..f2b1c5f648 100644
--- a/src/store/middlewares/login.js
+++ b/src/store/middlewares/login.js
@@ -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;
diff --git a/src/store/middlewares/login.test.js b/src/store/middlewares/login.test.js
index f9fc24dbd6..f0b9545225 100644
--- a/src/store/middlewares/login.test.js
+++ b/src/store/middlewares/login.test.js
@@ -5,6 +5,7 @@ import middleware from './login';
import actionTypes from '../../constants/actions';
import * as accountApi from '../../utils/api/account';
import * as delegateApi from '../../utils/api/delegate';
+import networks from '../../constants/networks';
describe('Login middleware', () => {
let store;
@@ -12,7 +13,7 @@ describe('Login middleware', () => {
let accountApiMock;
let delegateApiMock;
const { passphrase } = accounts.genesis;
- const activePeer = {};
+ const activePeer = { options: { code: networks.mainnet } };
const activePeerSetAction = {
type: actionTypes.activePeerSet,
data: {
diff --git a/src/store/reducers/savedAccounts.js b/src/store/reducers/savedAccounts.js
index 2cdf77249b..38825612fd 100644
--- a/src/store/reducers/savedAccounts.js
+++ b/src/store/reducers/savedAccounts.js
@@ -59,11 +59,13 @@ const savedAccounts = (state = { accounts: [] }, action) => {
!(account.publicKey === action.data.publicKey &&
account.network === action.data.network)),
};
- case actionTypes.removePassphrase:
+ case actionTypes.removeSavedAccountPassphrase:
return {
...state,
accounts: state.accounts.map((account) => {
- delete account.passphrase;
+ if (!action.data || (`${action.data.network}${action.data.passphrase}` === `${account.network}${account.passphrase}`)) {
+ delete account.passphrase;
+ }
return account;
}),
};
diff --git a/src/store/reducers/savedAccounts.test.js b/src/store/reducers/savedAccounts.test.js
index 224dedc54a..62ec86b321 100644
--- a/src/store/reducers/savedAccounts.test.js
+++ b/src/store/reducers/savedAccounts.test.js
@@ -71,10 +71,11 @@ describe('Reducer: savedAccounts(state, action)', () => {
expect(changedState).to.deep.equal({ accounts: [account2] });
});
- it('should return array same accounts without passphrase if action.type = actionTypes.removePassphrase', () => {
+ it('should return array same accounts without passphrase if action.type = actionTypes.removeSavedAccountPassphrase', () => {
const state = { accounts: [account, account2] };
const action = {
- type: actionTypes.removePassphrase,
+ type: actionTypes.removeSavedAccountPassphrase,
+ data: account2,
};
const changedState = savedAccounts(state, action);
const account2WithoutPassphrase = { ...account2 };
diff --git a/test/integration/accountSwitch.test.js b/test/integration/accountSwitch.test.js
index 10ab3827d9..565b0b6757 100644
--- a/test/integration/accountSwitch.test.js
+++ b/test/integration/accountSwitch.test.js
@@ -34,6 +34,7 @@ describe('@integration: Account switch', () => {
publicKey: accounts.delegate.publicKey,
address: 'http://localhost:8080',
balance: accounts.delegate.balance,
+ passphrase: accounts.genesis.passphrase,
}, {
network: networks.mainnet.code,
publicKey: accounts['empty account'].publicKey,
@@ -100,6 +101,13 @@ describe('@integration: Account switch', () => {
step('Then I should see 2 instances of "saved account card"', () => helper.shouldSeeCountInstancesOf(2, '.saved-account-card'));
});
+ describe('Scenario: should allow to "Lock ID" account', () => {
+ step('Given I\'m on "account switcher" with accounts: "genesis,delegate,empty account"', setupStep);
+ step('Then I should see 1 instance of "Lock ID"', () => helper.shouldSeeCountInstancesOf(1, 'strong.unlocked'));
+ step('When I click "Lock ID"', () => helper.clickOnElement('strong.unlocked'));
+ step('Then I should see 0 instances of "Lock ID"', () => helper.shouldSeeCountInstancesOf(0, 'strong.unlocked'));
+ });
+
describe('Scenario: should allow to switch account', () => {
step('Given I\'m on "account switcher" with accounts: "genesis,delegate,empty account"', setupStep);
step('When I click "saved account card"', () => helper.clickOnElement('.saved-account-card'));
diff --git a/test/integration/login.test.js b/test/integration/login.test.js
index af508fce2a..fda047dd7c 100644
--- a/test/integration/login.test.js
+++ b/test/integration/login.test.js
@@ -19,6 +19,7 @@ import { activePeerSet } from '../../src/actions/peers';
import * as toasterActions from '../../src/actions/toaster';
import Login from './../../src/components/login';
import accounts from '../constants/accounts';
+import networks from './../../src/constants/networks';
import GenericStepDefinition from '../utils/genericStepDefinition';
describe('@integration: Login', () => {
@@ -76,6 +77,7 @@ describe('@integration: Login', () => {
secondSignature: 0,
secondPublicKey: null,
multisignatures: [],
+ network: networks.mainnet.code,
u_multisignatures: [],
});
delegateAPIStub = stub(delegateAPI, 'getDelegate').returnsPromise().rejects();
@@ -97,7 +99,7 @@ describe('@integration: Login', () => {
checkIfInRoute() {
expect(this.store.getState().account).to.have.all.keys('passphrase', 'publicKey', 'address', 'delegate',
'isDelegate', 'expireTime', 'u_multisignatures', 'multisignatures', 'unconfirmedBalance',
- 'secondSignature', 'secondPublicKey', 'balance', 'unconfirmedSignature');
+ 'secondSignature', 'secondPublicKey', 'balance', 'unconfirmedSignature', 'network');
restoreStubs();
}