From c438b923f529c83ac04a9f8b7cbe6d206e75b76b Mon Sep 17 00:00:00 2001 From: Vit Stanislav Date: Wed, 8 Nov 2017 15:05:51 +0100 Subject: [PATCH] Implement multiple account management --- i18n/locales/en/common.json | 9 ++- src/actions/savedAccounts.js | 11 +-- src/actions/savedAccounts.test.js | 2 +- src/components/saveAccount/index.js | 8 +- src/components/saveAccount/index.test.js | 14 ++-- src/components/saveAccount/saveAccount.js | 73 +++++++++++++++---- .../saveAccount/saveAccount.test.js | 10 ++- src/utils/saveAccount.js | 43 +++++++---- 8 files changed, 120 insertions(+), 50 deletions(-) diff --git a/i18n/locales/en/common.json b/i18n/locales/en/common.json index f2035c3a6..abdb99fe1 100644 --- a/i18n/locales/en/common.json +++ b/i18n/locales/en/common.json @@ -3,6 +3,7 @@ "About": "About", "Account saved": "Account saved", "Account was successfully forgotten.": "Account was successfully forgotten.", + "Add active account": "Add active account", "Add vote to": "Add vote to", "Address": "Address", "Address copied to clipboard": "Address copied to clipboard", @@ -19,6 +20,7 @@ "Blockchain Application Registration": "Blockchain Application Registration", "Cancel": "Cancel", "Click to send all funds": "Click to send all funds", + "Close": "Close", "Confirm": "Confirm", "Connection re-established": "Connection re-established", "Copy": "Copy", @@ -42,7 +44,7 @@ "Fee": "Fee", "Fee: {{amount}} LSK": "Fee: {{amount}} LSK", "Fee: {{fee}} LSK": "Fee: {{fee}} LSK", - "Forget this account": "Forget this account", + "Forget": "Forget", "Forging": "Forging", "From / To": "From / To", "Help": "Help", @@ -73,6 +75,7 @@ "Move your mouse to generate random bytes": "Move your mouse to generate random bytes", "Multisignature Creation": "Multisignature Creation", "Name": "Name", + "Network": "Network", "New Account": "New Account", "Next": "Next", "No delegates found": "No delegates found", @@ -112,7 +115,6 @@ "Register as delegate": "Register as delegate", "Register second passphrase": "Register second passphrase", "Reload": "Reload", - "Remember this account": "Remember this account", "Remove vote from": "Remove vote from", "Repeat the transaction": "Repeat the transaction", "Report Issue...": "Report Issue...", @@ -120,8 +122,8 @@ "Result": "Result", "Result copied to clipboard": "Result copied to clipboard", "Reward": "Reward", - "Save account": "Save account", "Save your passphrase in a safe place": "Save your passphrase in a safe place", + "Saved accounts": "Saved accounts", "Search": "Search", "Search by username": "Search by username", "Second Passphrase": "Second Passphrase", @@ -143,6 +145,7 @@ "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.": "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.", "Submit": "Submit", "Success": "Success", + "Switch": "Switch", "Testnet": "Testnet", "The URL was invalid": "The URL was invalid", "There are no transactions, yet.": "There are no transactions, yet.", diff --git a/src/actions/savedAccounts.js b/src/actions/savedAccounts.js index 8b90f35b3..7644ebd27 100644 --- a/src/actions/savedAccounts.js +++ b/src/actions/savedAccounts.js @@ -1,5 +1,5 @@ import actionTypes from '../constants/actions'; -import { getSavedAccount, setSavedAccount, removeSavedAccount } from '../utils/saveAccount'; +import { getSavedAccounts, setSavedAccount, removeSavedAccount, setLastActiveAccount } from '../utils/saveAccount'; /** * An action to dispatch accountSaved @@ -7,6 +7,7 @@ import { getSavedAccount, setSavedAccount, removeSavedAccount } from '../utils/s */ export const accountSaved = (data) => { setSavedAccount(data); + setLastActiveAccount(data); return { data, type: actionTypes.accountSaved, @@ -16,10 +17,10 @@ export const accountSaved = (data) => { /** * An action to dispatch accountRemoved */ -export const accountRemoved = (publicKey) => { - removeSavedAccount(publicKey); +export const accountRemoved = (account) => { + removeSavedAccount(account); return { - data: publicKey, + data: account, type: actionTypes.accountRemoved, }; }; @@ -43,7 +44,7 @@ export const accountSwitched = (account) => { * */ export const accountsRetrieved = () => { - const accounts = getSavedAccount(); + const accounts = getSavedAccounts(); const data = accounts !== undefined ? accounts : []; return { data, diff --git a/src/actions/savedAccounts.test.js b/src/actions/savedAccounts.test.js index c2fc3ff7d..ef27f95af 100644 --- a/src/actions/savedAccounts.test.js +++ b/src/actions/savedAccounts.test.js @@ -18,7 +18,7 @@ describe('actions: savedAccount', () => { describe('accountsRetrieved', () => { it('should create an action to retrieved the saved accounts list', () => { - sinon.stub(saveAccountUtils, 'getSavedAccount').returns([data]); + sinon.stub(saveAccountUtils, 'getSavedAccounts').returns([data]); const expectedAction = { data: [data], type: actionTypes.accountsRetrieved, diff --git a/src/components/saveAccount/index.js b/src/components/saveAccount/index.js index 07cba1897..b2e233a56 100644 --- a/src/components/saveAccount/index.js +++ b/src/components/saveAccount/index.js @@ -1,16 +1,18 @@ import { connect } from 'react-redux'; import { translate } from 'react-i18next'; -import { accountSaved } from '../../actions/savedAccounts'; +import { accountSaved, accountRemoved, accountSwitched } from '../../actions/savedAccounts'; import SaveAccount from './saveAccount'; const mapStateToProps = state => ({ - address: state.peers.data.options.address, publicKey: state.account.publicKey, - network: state.peers.data.options.name, + networkOptions: state.peers.options, + savedAccounts: state.savedAccounts, }); const mapDispatchToProps = dispatch => ({ accountSaved: data => dispatch(accountSaved(data)), + accountRemoved: data => dispatch(accountRemoved(data)), + accountSwitched: data => dispatch(accountSwitched(data)), }); export default connect( diff --git a/src/components/saveAccount/index.test.js b/src/components/saveAccount/index.test.js index 656892f33..ce439b103 100644 --- a/src/components/saveAccount/index.test.js +++ b/src/components/saveAccount/index.test.js @@ -17,16 +17,16 @@ describe('SaveAccountHOC', () => { publicKey: 'sample_key', username: 'lisk-nano', }; - const peers = { data: { - options: { - address: 'http://localhost:4000', - network: 'Custom node', - }, - } }; + const options = { + address: 'http://localhost:4000', + network: 'Custom node', + }; + const peers = { data: { options }, options }; const store = configureMockStore([])({ peers, account, activePeerSet: () => {}, + savedAccounts: [], }); beforeEach(() => { @@ -45,7 +45,7 @@ describe('SaveAccountHOC', () => { it('should bind accountSaved action to SaveAccount props.accountSaved', () => { const actionsSpy = sinon.spy(savedAccounts, 'accountSaved'); - wrapper.find('SaveAccount .save-account-button button').simulate('click'); + wrapper.find('SaveAccount button.add-active-account-button').simulate('click'); expect(actionsSpy).to.be.calledWith(); actionsSpy.restore(); }); diff --git a/src/components/saveAccount/saveAccount.js b/src/components/saveAccount/saveAccount.js index da86a6540..771801bee 100644 --- a/src/components/saveAccount/saveAccount.js +++ b/src/components/saveAccount/saveAccount.js @@ -1,41 +1,84 @@ +import { Table, TableHead, TableRow, TableCell } from 'react-toolbox/lib/table'; import React from 'react'; -import InfoParagraph from '../infoParagraph'; +import { IconButton } from 'react-toolbox/lib/button'; import ActionBar from '../actionBar'; +import InfoParagraph from '../infoParagraph'; import networks from '../../constants/networks'; +import getNetwork from '../../utils/getNetwork'; +import { extractAddress } from '../../utils/api/account'; const SaveAccount = ({ - network, - address, + networkOptions, publicKey, closeDialog, accountSaved, + accountRemoved, + accountSwitched, + savedAccounts, t, }) => { const save = () => { - // eslint-disable-next-line arrow-body-style - const index = Object.keys(networks).map((item, i) => { - return (networks[item].name === network) ? i : null; - }).find(item => item !== null); accountSaved({ - network: index, - address, + network: networkOptions.code, + address: networkOptions.address, publicKey, }); - closeDialog(); }; + const isActive = account => ( + account.publicKey === publicKey && + account.network === networkOptions.code); + return (
- - {t('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 prompted to enter the passphrase once you want to do any transaction.')} - + { savedAccounts.length === 0 ? + + {t('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 prompted to enter the passphrase once you want to do any transaction.')} + : +
+ + + {t('Switch')} + {t('Address')} + {t('Network')} + {t('Forget')} + + {savedAccounts.map(account => ( + + + + + + {extractAddress(account.publicKey)} + + + {account.network === networks.customNode.code ? + account.address : + t((getNetwork(account.network) || {}).name)} + + + + + + ))} +
+
+ }
diff --git a/src/components/saveAccount/saveAccount.test.js b/src/components/saveAccount/saveAccount.test.js index d2d6b41cd..81aa3d957 100644 --- a/src/components/saveAccount/saveAccount.test.js +++ b/src/components/saveAccount/saveAccount.test.js @@ -20,6 +20,11 @@ describe('SaveAccount', () => { }, closeDialog: () => {}, accountSaved: () => {}, + accountRemoved: () => {}, + accountSwitched: () => {}, + networkOptions: {}, + publicKey: [], + savedAccounts: [], t: key => key, }; @@ -49,10 +54,9 @@ describe('SaveAccount', () => { expect(wrapper.find('ActionBar')).to.have.lengthOf(1); }); - it('should call props.closeDialog and props.accountSaved on "save button" click', () => { - wrapper.find('button.save-account-button').simulate('click'); + it('should call props.accountSaved on "save button" click', () => { + wrapper.find('button.add-active-account-button').simulate('click'); const componentProps = wrapper.find(SaveAccount).props(); - expect(componentProps.closeDialog).to.have.been.calledWith(); expect(componentProps.accountSaved).to.have.been.calledWith(); }); }); diff --git a/src/utils/saveAccount.js b/src/utils/saveAccount.js index b0149e465..2fa3d37f1 100644 --- a/src/utils/saveAccount.js +++ b/src/utils/saveAccount.js @@ -1,24 +1,41 @@ -export const getSavedAccount = () => { +export const getSavedAccounts = () => { const savedAccounts = localStorage.getItem('accounts'); - let account; + let accounts = []; if (savedAccounts) { - account = JSON.parse(savedAccounts); + accounts = JSON.parse(savedAccounts); } - return account; + return accounts; +}; + +export const getLastActiveAccount = () => ( + getSavedAccounts()[localStorage.getItem('lastActiveAccountIndex') || 0] +); + +export const setLastActiveAccount = ({ publicKey, network, address }) => { + const lastActiveAccountIndex = getSavedAccounts().findIndex(account => ( + account.publicKey === publicKey && + account.network === network && + account.address === address + )); + if (lastActiveAccountIndex > -1) { + localStorage.setItem('lastActiveAccountIndex', lastActiveAccountIndex); + } }; export const setSavedAccount = ({ publicKey, network, address }) => { - localStorage.setItem('accounts', JSON.stringify([{ - publicKey, - network, - address, - }])); + localStorage.setItem('accounts', JSON.stringify([ + ...getSavedAccounts(), + { + publicKey, + network, + address, + }, + ])); }; -export const removeSavedAccount = (publicKey) => { - let accounts = localStorage.getItem('accounts'); - accounts = JSON.parse(accounts); - accounts = accounts.filter(account => account.publicKey !== publicKey); +export const removeSavedAccount = ({ network, publicKey }) => { + const accounts = getSavedAccounts().filter(account => + !(account.publicKey === publicKey && account.network === network)); localStorage.setItem('accounts', JSON.stringify(accounts)); };