diff --git a/src/actions/accounts.js b/src/actions/accounts.js index ed8da2ad..3f8f36d6 100644 --- a/src/actions/accounts.js +++ b/src/actions/accounts.js @@ -1,9 +1,5 @@ // @flow -import { createAction } from 'redux-actions' - -import type { Dispatch } from 'redux' - import db from 'helpers/db' import type { Account } from 'types/common' @@ -14,24 +10,17 @@ export const addAccount: AddAccount = payload => ({ payload, }) -export type EditAccount = Account => { type: string, payload: Account } -export const editAccount: AddAccount = payload => ({ - type: 'DB:EDIT_ACCOUNT', +export type UpdateAccount = Account => { type: string, payload: Account } +export const updateAccount: AddAccount = payload => ({ + type: 'DB:UPDATE_ACCOUNT', payload, }) type FetchAccounts = () => { type: string } -export const fetchAccounts: FetchAccounts = () => ({ - type: 'FETCH_ACCOUNTS', - payload: db('accounts'), -}) - -const setAccountData = createAction('DB:SET_ACCOUNT_DATA', (accountID, data) => ({ - accountID, - data, -})) - -export const syncAccount: Function = account => async (dispatch: Dispatch<*>) => { - const { id, ...data } = account - dispatch(setAccountData(id, data)) +export const fetchAccounts: FetchAccounts = () => { + const payload = db.get('accounts') + return { + type: 'SET_ACCOUNTS', + payload, + } } diff --git a/src/actions/settings.js b/src/actions/settings.js index 537cee84..1b942e93 100644 --- a/src/actions/settings.js +++ b/src/actions/settings.js @@ -11,7 +11,7 @@ export const saveSettings: SaveSettings = payload => ({ }) export const fetchSettings: Function = () => dispatch => { - const settings = db('settings') + const settings = db.get('settings') if (Object.keys(settings).length === 0) { return } diff --git a/src/components/AccountPage.js b/src/components/AccountPage.js index a17a0e1c..9d8f84b2 100644 --- a/src/components/AccountPage.js +++ b/src/components/AccountPage.js @@ -4,6 +4,7 @@ import React, { PureComponent, Fragment } from 'react' import { compose } from 'redux' import { connect } from 'react-redux' import { translate } from 'react-i18next' +import { Redirect } from 'react-router' import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'constants' @@ -42,6 +43,11 @@ class AccountPage extends PureComponent { render() { const { account, accountData, openModal, t } = this.props + // Don't even throw if we jumped in wrong account route + if (!account) { + return + } + return ( diff --git a/src/components/DashboardPage.js b/src/components/DashboardPage.js index 506a0305..84f04c66 100644 --- a/src/components/DashboardPage.js +++ b/src/components/DashboardPage.js @@ -40,7 +40,7 @@ type Props = { type State = { tab: number, - datas: Object, + fakeDatas: Array, } const ACCOUNTS_BY_LINE = 3 @@ -52,7 +52,7 @@ const itemsTimes = [ { key: 'year', name: 'Last year' }, ] -const generateData = v => ({ +const generateFakeData = v => ({ name: `Day ${v}`, value: random(10, 100), }) @@ -60,11 +60,11 @@ const generateData = v => ({ class DashboardPage extends PureComponent { state = { tab: 0, - datas: this.generateDatas(), + fakeDatas: this.generateFakeDatas(), } componentDidMount() { - this.addDatasOnAccounts() + this.addFakeDatasOnAccounts() } componentWillUnmount() { @@ -74,42 +74,33 @@ class DashboardPage extends PureComponent { getAccountsChunk() { const { accounts } = this.props - const listAccounts = Object.values(accounts) + // create shallow copy of accounts, to be mutated + const listAccounts = [...accounts] while (listAccounts.length % ACCOUNTS_BY_LINE !== 0) listAccounts.push(null) - return chunk(listAccounts, ACCOUNTS_BY_LINE) } - generateDatas() { + generateFakeDatas() { const { accounts } = this.props - - return Object.keys(accounts).reduce((result, key) => { - result[key] = [...Array(25).keys()].map(v => generateData(v + 1)) - - return result - }, {}) + return accounts.map(() => [...Array(25).keys()].map(v => generateFakeData(v + 1))) } - addDatasOnAccounts = () => { + addFakeDatasOnAccounts = () => { this._timeout = setTimeout(() => { const { accounts } = this.props this.setState(prev => ({ - datas: { - ...Object.keys(accounts).reduce((result, key) => { - if (result[key]) { - const nextIndex = result[key].length - - result[key][nextIndex] = generateData(nextIndex) - } - - return result - }, prev.datas), - }, + fakeDatas: accounts.reduce((res, acc, i) => { + if (res[i]) { + const nextIndex = res[i].length + res[i][nextIndex] = generateFakeData(nextIndex) + } + return res + }, prev.fakeDatas), })) - this.addDatasOnAccounts() + this.addFakeDatasOnAccounts() }, TIMEOUT_REFRESH_DATAS) } @@ -119,9 +110,9 @@ class DashboardPage extends PureComponent { render() { const { totalBalance, push, accounts } = this.props - const { tab, datas } = this.state + const { tab, fakeDatas } = this.state - const totalAccounts = Object.keys(accounts).length + const totalAccounts = accounts.length return ( @@ -171,17 +162,14 @@ class DashboardPage extends PureComponent { { - const data = datas[key] - + fakeDatas.reduce((res, data) => { data.forEach((d, i) => { - result[i] = { + res[i] = { name: d.name, - value: (result[i] ? result[i].value : 0) + d.value, + value: (res[i] ? res[i].value : 0) + d.value, } }) - - return result + return res }, []), 25, )} @@ -216,7 +204,7 @@ class DashboardPage extends PureComponent { {account.data && formatBTC(account.data.balance)} - + ), )} diff --git a/src/components/IsUnlocked/index.js b/src/components/IsUnlocked.js similarity index 97% rename from src/components/IsUnlocked/index.js rename to src/components/IsUnlocked.js index 13e21ba4..36336fcb 100644 --- a/src/components/IsUnlocked/index.js +++ b/src/components/IsUnlocked.js @@ -81,6 +81,10 @@ class IsUnlocked extends Component { return true } + if (!nextProps.isLocked && this.props.isLocked) { + return true + } + return nextProps.children !== this.props.children } diff --git a/src/components/SelectAccount/index.js b/src/components/SelectAccount/index.js index a3f15ecb..6966340f 100644 --- a/src/components/SelectAccount/index.js +++ b/src/components/SelectAccount/index.js @@ -18,7 +18,7 @@ import Box from 'components/base/Box' import Text from 'components/base/Text' const mapStateToProps: MapStateToProps<*, *, *> = state => ({ - accounts: Object.entries(getVisibleAccounts(state)).map(([, account]: [string, any]) => account), + accounts: getVisibleAccounts(state), }) const renderItem = item => ( diff --git a/src/components/SideBar/index.js b/src/components/SideBar/index.js index b4bbd41d..7e68309e 100644 --- a/src/components/SideBar/index.js +++ b/src/components/SideBar/index.js @@ -111,11 +111,11 @@ class SideBar extends PureComponent { - {Object.entries(accounts).map(([id, account]: [string, any]) => ( + {accounts.map(account => ( {account.name} diff --git a/src/components/TopBar.js b/src/components/TopBar.js index 9d542a2c..ac9a6c6f 100644 --- a/src/components/TopBar.js +++ b/src/components/TopBar.js @@ -32,7 +32,7 @@ const Container = styled(Box).attrs({ ` const mapStateToProps: MapStateToProps<*, *, *> = state => ({ - hasAccounts: Object.keys(getAccounts(state)).length > 0, + hasAccounts: getAccounts(state).length > 0, hasPassword: hasPassword(state), }) diff --git a/src/components/modals/AddAccount/index.js b/src/components/modals/AddAccount/index.js index e595fbc9..1c75ddd2 100644 --- a/src/components/modals/AddAccount/index.js +++ b/src/components/modals/AddAccount/index.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux' import { compose } from 'redux' import { translate } from 'react-i18next' import { ipcRenderer } from 'electron' +import differenceBy from 'lodash/differenceBy' import { MODAL_ADD_ACCOUNT } from 'constants' @@ -74,7 +75,7 @@ const Steps = { ), listAccounts: (props: Object) => { - const accounts = Object.entries(props.accounts).map(([, account]: [string, any]) => account) + const { accounts } = props const emptyAccounts = accounts.filter(account => account.transactions.length === 0) const existingAccounts = accounts.filter(account => account.transactions.length > 0) const canCreateAccount = props.canCreateAccount && emptyAccounts.length === 1 @@ -110,7 +111,7 @@ type Props = { type State = { inputValue: InputValue, step: Step, - accounts: Object, + accounts: Accounts, progress: null | Object, } @@ -129,7 +130,7 @@ const defaultState = { inputValue: { wallet: '', }, - accounts: {}, + accounts: [], progress: null, step: 'chooseWallet', } @@ -146,12 +147,7 @@ class AddAccountModal extends PureComponent { componentWillReceiveProps(nextProps) { if (nextProps.accounts) { this.setState(prev => ({ - accounts: Object.keys(prev.accounts).reduce((result, value) => { - if (!nextProps.accounts[value]) { - result[value] = prev.accounts[value] - } - return result - }, {}), + accounts: differenceBy(prev.accounts, nextProps.accounts, 'id'), })) } } @@ -183,7 +179,7 @@ class AddAccountModal extends PureComponent { sendEvent('usb', 'wallet.getAccounts', { path: currentDevice.path, wallet: inputValue.wallet, - currentAccounts: Object.keys(accounts), + currentAccounts: accounts.map(acc => acc.id), }) } diff --git a/src/components/modals/SettingsAccount.js b/src/components/modals/SettingsAccount.js index 524864d5..962f17ad 100644 --- a/src/components/modals/SettingsAccount.js +++ b/src/components/modals/SettingsAccount.js @@ -9,7 +9,7 @@ import { MODAL_SETTINGS_ACCOUNT } from 'constants' import type { Account } from 'types/common' -import { editAccount } from 'actions/accounts' +import { updateAccount } from 'actions/accounts' import { setDataModal, closeModal } from 'reducers/modals' import Box from 'components/base/Box' @@ -27,14 +27,14 @@ type State = { type Props = { closeModal: Function, - editAccount: Function, + updateAccount: Function, setDataModal: Function, push: Function, } const mapDispatchToProps = { closeModal, - editAccount, + updateAccount, setDataModal, push, } @@ -91,9 +91,9 @@ class SettingsAccount extends PureComponent { handleSubmitName = (account: Account) => (e: SyntheticEvent) => { e.preventDefault() - const { editAccount, setDataModal } = this.props + const { updateAccount, setDataModal } = this.props - editAccount(account) + updateAccount(account) setDataModal(MODAL_SETTINGS_ACCOUNT, { account }) this.setState({ @@ -102,9 +102,9 @@ class SettingsAccount extends PureComponent { } handleArchiveAccount = (account: Account) => () => { - const { push, closeModal, editAccount } = this.props + const { push, closeModal, updateAccount } = this.props - editAccount({ + updateAccount({ ...account, archived: true, }) diff --git a/src/helpers/db.js b/src/helpers/db.js index aa0418a8..dfe8da71 100644 --- a/src/helpers/db.js +++ b/src/helpers/db.js @@ -2,23 +2,37 @@ import Store from 'electron-store' const encryptionKey = {} -const store = type => +const store = key => new Store({ - name: type, - defaults: {}, - encryptionKey: encryptionKey[type], + name: key, + defaults: { + data: null, + }, + encryptionKey: encryptionKey[key], }) -export function setEncryptionKey(type, value) { - encryptionKey[type] = value +export function setEncryptionKey(key, value) { + encryptionKey[key] = value } -export default (type, values) => { - const db = store(type) +export default { + // If the db doesn't exists for that key, init it, with the default value provided + init: (key, defaults) => { + const db = store(key) + const data = db.get('data') + if (!data) { + db.set('data', defaults) + } + }, - if (values) { - db.store = values - } + get: key => { + const db = store(key) + return db.get('data') + }, - return db.store + set: (key, val) => { + const db = store(key) + db.set('data', val) + return db.get('data') + }, } diff --git a/src/internals/usb/wallet/accounts.js b/src/internals/usb/wallet/accounts.js index 5513381e..bc310bc4 100644 --- a/src/internals/usb/wallet/accounts.js +++ b/src/internals/usb/wallet/accounts.js @@ -105,7 +105,7 @@ export default async ({ return encodeBase58Check(xpub) } - const getAllAccounts = async (currentAccount = 0, accounts = {}) => { + const getAllAccounts = async (currentAccount = 0, accounts = []) => { const xpub58 = await getXpub58ByAccount({ account: currentAccount, network }) if (currentAccounts.includes(xpub58)) { @@ -122,10 +122,10 @@ export default async ({ const hasTransactions = account.transactions.length > 0 - accounts[xpub58] = { + accounts.push({ id: xpub58, ...account, - } + }) if (hasTransactions) { return getAllAccounts(currentAccount + 1, accounts) diff --git a/src/middlewares/db.js b/src/middlewares/db.js index b77e4e32..a9e10495 100644 --- a/src/middlewares/db.js +++ b/src/middlewares/db.js @@ -17,6 +17,6 @@ export default store => next => action => { const state = getState() const { settings } = state - db('settings', settings) - db('accounts', getAccounts(state)) + db.set('settings', settings) + db.set('accounts', getAccounts(state)) } diff --git a/src/reducers/accounts.js b/src/reducers/accounts.js index 2ab6d4d6..d21c7bd8 100644 --- a/src/reducers/accounts.js +++ b/src/reducers/accounts.js @@ -12,64 +12,85 @@ import type { Account, Accounts, AccountData } from 'types/common' export type AccountsState = Accounts -const state: AccountsState = {} +const state: AccountsState = [] -function getAccount(account: Account) { +function orderAccountsTransactions(account: Account) { const transactions = get(account.data, 'transactions', []) - transactions.sort((a, b) => new Date(b.received_at) - new Date(a.received_at)) - return { ...account, data: { - ...(account.data || {}), + ...account.data, transactions, }, } } +const defaultAccountData: AccountData = { + address: '', + balance: 0, + currentIndex: 0, + transactions: [], +} + const handlers: Object = { - ADD_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({ - ...state, - [account.id]: getAccount(account), - }), - EDIT_ACCOUNT: (state: AccountsState, { payload: account }: { payload: Account }) => ({ - ...state, - [account.id]: { - ...state[account.id], - ...getAccount(account), - }, - }), - FETCH_ACCOUNTS: (state: AccountsState, { payload: accounts }: { payload: Accounts }) => accounts, - SET_ACCOUNT_DATA: ( + SET_ACCOUNTS: ( state: AccountsState, - { payload: { accountID, data } }: { payload: { accountID: string, data: AccountData } }, - ): AccountsState => { - const account = state[accountID] - const { data: accountData } = account - - const transactions = uniqBy( - [...get(accountData, 'transactions', []), ...data.transactions], - tx => tx.hash, - ) - const currentIndex = data.currentIndex ? data.currentIndex : get(accountData, 'currentIndex', 0) - - account.data = { - ...accountData, - ...data, - balance: transactions.reduce((result, v) => { - result += v.balance - return result - }, 0), - currentIndex, - transactions, - } + { payload: accounts }: { payload: Accounts }, + ): AccountsState => accounts, - return { - ...state, - [accountID]: getAccount(account), - } + ADD_ACCOUNT: ( + state: AccountsState, + { payload: account }: { payload: Account }, + ): AccountsState => { + account = orderAccountsTransactions({ + ...account, + data: { + ...defaultAccountData, + ...account.data, + }, + }) + return [...state, account] }, + + UPDATE_ACCOUNT: ( + state: AccountsState, + { payload: account }: { payload: Account }, + ): AccountsState => + state.map(existingAccount => { + if (existingAccount.id !== account.id) { + return existingAccount + } + + const existingData = get(existingAccount, 'data', {}) + const data = get(account, 'data', {}) + + const transactions = uniqBy( + [...get(existingData, 'transactions', []), ...get(data, 'transactions', [])], + tx => tx.hash, + ) + + const currentIndex = data.currentIndex + ? data.currentIndex + : get(existingData, 'currentIndex', 0) + + const updatedAccount = { + ...existingAccount, + ...account, + data: { + ...existingData, + ...data, + balance: transactions.reduce((result, v) => { + result += v.balance + return result + }, 0), + currentIndex, + transactions, + }, + } + + return orderAccountsTransactions(updatedAccount) + }), } // Selectors @@ -78,35 +99,24 @@ export function getTotalBalance(state: { accounts: AccountsState }) { return reduce( state.accounts, (result, account) => { - result += account.data.balance + result += get(account, 'data.balance', 0) return result }, 0, ) } -export function getAccounts(state: { accounts: AccountsState }) { - return Object.keys(state.accounts).reduce((result, key) => { - result[key] = getAccount(state.accounts[key]) - return result - }, {}) +export function getAccounts(state: { accounts: AccountsState }): Array { + return state.accounts } -export function getVisibleAccounts(state: { accounts: AccountsState }) { - const accounts = getAccounts(state) - return Object.keys(accounts).reduce((result, key) => { - const account = accounts[key] - - if (account.archived !== true) { - result[key] = account - } - - return result - }, {}) +export function getVisibleAccounts(state: { accounts: AccountsState }): Array { + return getAccounts(state).filter(account => account.archived !== true) } -export function getAccountById(state: { accounts: AccountsState }, id: string) { - return getAccounts(state)[id] +export function getAccountById(state: { accounts: AccountsState }, id: string): Account | null { + const account = getAccounts(state).find(account => account.id === id) + return account || null } export function getAccountData(state: State, id: string): AccountData | null { @@ -114,7 +124,7 @@ export function getAccountData(state: State, id: string): AccountData | null { } export function canCreateAccount(state: State): boolean { - return every(getAccounts(state), a => a.data.transactions.length > 0) + return every(getAccounts(state), a => get(a, 'data.transactions.length', 0) > 0) } export default handleActions(handlers, state) diff --git a/src/renderer/events.js b/src/renderer/events.js index 94cd7114..a1ff17a1 100644 --- a/src/renderer/events.js +++ b/src/renderer/events.js @@ -10,7 +10,7 @@ import type { Accounts } from 'types/common' import { CHECK_UPDATE_TIMEOUT, SYNC_ACCOUNT_TIMEOUT } from 'constants' import { updateDevices, addDevice, removeDevice } from 'actions/devices' -import { syncAccount } from 'actions/accounts' +import { updateAccount } from 'actions/accounts' import { setUpdateStatus } from 'reducers/update' import { getAccountData, getAccounts } from 'reducers/accounts' @@ -41,11 +41,11 @@ export function sendSyncEvent(channel: string, msgType: string, data: any): any export function startSyncAccounts(accounts: Accounts) { syncAccounts = true sendEvent('accounts', 'sync.all', { - accounts: Object.entries(accounts).map(([id, account]: [string, any]) => { + accounts: accounts.map(account => { const currentIndex = get(account, 'data.currentIndex', 0) const allAddresses = get(account, 'data.allAddresses', []) return { - id, + id: account.id, allAddresses, currentIndex, } @@ -75,7 +75,7 @@ export default ({ store, locked }: { store: Object, locked: boolean }) => { ) if (currentAccountData.transactions.length !== transactions.length) { - store.dispatch(syncAccount(account)) + store.dispatch(updateAccount(account)) } } }, diff --git a/src/renderer/index.js b/src/renderer/index.js index 2a2f1a8f..ba86a525 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -16,6 +16,8 @@ import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' import { getLanguage } from 'reducers/settings' +import db from 'helpers/db' + import App from 'components/App' import 'styles/global' @@ -25,6 +27,10 @@ if (__PROD__ && __SENTRY_URL__) { window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) } +// init db with defaults if needed +db.init('accounts', []) +db.init('settings', {}) + const history = createHistory() const store = createStore(history) const rootNode = document.getElementById('app') diff --git a/src/types/common.js b/src/types/common.js index fe576260..19497fe3 100644 --- a/src/types/common.js +++ b/src/types/common.js @@ -33,7 +33,7 @@ export type Account = { data?: AccountData, } -export type Accounts = { [_: string]: Account } +export type Accounts = Array // -------------------- Settings