diff --git a/js/package.json b/js/package.json
index 0618f9d2d..011ecc897 100644
--- a/js/package.json
+++ b/js/package.json
@@ -146,6 +146,7 @@
"mobx-react-devtools": "4.2.10",
"moment": "2.17.0",
"phoneformat.js": "1.0.3",
+ "push.js": "0.0.11",
"qs": "6.3.0",
"react": "15.4.1",
"react-ace": "4.1.0",
diff --git a/js/src/index.js b/js/src/index.js
index 6938a46f8..46d6c9c74 100644
--- a/js/src/index.js
+++ b/js/src/index.js
@@ -67,7 +67,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
const api = new SecureApi(`ws://${parityUrl}`, token);
ContractInstances.create(api);
-const store = initStore(api);
+const store = initStore(api, hashHistory);
store.dispatch({ type: 'initAll', api });
store.dispatch(setApi(api));
diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js
index bb11cf32f..14bc9b0a6 100644
--- a/js/src/redux/middleware.js
+++ b/js/src/redux/middleware.js
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
import thunk from 'redux-thunk';
+import { routerMiddleware } from 'react-router-redux';
import ErrorsMiddleware from '~/ui/Errors/middleware';
import SettingsMiddleware from '~/views/Settings/middleware';
@@ -22,12 +23,13 @@ import SignerMiddleware from './providers/signerMiddleware';
import statusMiddleware from '~/views/Status/middleware';
import CertificationsMiddleware from './providers/certifications/middleware';
-export default function (api) {
+export default function (api, browserHistory) {
const errors = new ErrorsMiddleware();
const signer = new SignerMiddleware(api);
const settings = new SettingsMiddleware();
const status = statusMiddleware();
const certifications = new CertificationsMiddleware();
+ const routeMiddleware = routerMiddleware(browserHistory);
const middleware = [
settings.toMiddleware(),
@@ -36,5 +38,5 @@ export default function (api) {
certifications.toMiddleware()
];
- return middleware.concat(status, thunk);
+ return middleware.concat(status, routeMiddleware, thunk);
}
diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js
index 65f831008..f8cfb2c1e 100644
--- a/js/src/redux/providers/balancesActions.js
+++ b/js/src/redux/providers/balancesActions.js
@@ -15,11 +15,14 @@
// along with Parity. If not, see .
import { range, uniq, isEqual } from 'lodash';
+import BigNumber from 'bignumber.js';
+import { push } from 'react-router-redux';
import { hashToImageUrl } from './imagesReducer';
import { setAddressImage } from './imagesActions';
import * as ABIS from '~/contracts/abi';
+import { notifyTransaction } from '~/util/notifications';
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
const ETH = {
@@ -28,7 +31,64 @@ const ETH = {
image: imagesEthereum
};
-export function setBalances (balances) {
+function setBalances (_balances) {
+ return (dispatch, getState) => {
+ const state = getState();
+
+ const accounts = state.personal.accounts;
+ const nextBalances = _balances;
+ const prevBalances = state.balances.balances;
+ const balances = { ...prevBalances };
+
+ Object.keys(nextBalances).forEach((address) => {
+ if (!balances[address]) {
+ balances[address] = Object.assign({}, nextBalances[address]);
+ return;
+ }
+
+ const balance = Object.assign({}, balances[address]);
+ const { tokens, txCount = balance.txCount } = nextBalances[address];
+ const nextTokens = [].concat(balance.tokens);
+
+ tokens.forEach((t) => {
+ const { token, value } = t;
+ const { tag } = token;
+
+ const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag);
+
+ if (tokenIndex === -1) {
+ nextTokens.push({
+ token,
+ value
+ });
+ } else {
+ const oldValue = nextTokens[tokenIndex].value;
+
+ // If received a token/eth (old value < new value), notify
+ if (oldValue.lt(value) && accounts[address]) {
+ const account = accounts[address];
+ const txValue = value.minus(oldValue);
+
+ const redirectToAccount = () => {
+ const route = `/account/${account.address}`;
+ dispatch(push(route));
+ };
+
+ notifyTransaction(account, token, txValue, redirectToAccount);
+ }
+
+ nextTokens[tokenIndex] = { token, value };
+ }
+ });
+
+ balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens };
+ });
+
+ dispatch(_setBalances(balances));
+ };
+}
+
+function _setBalances (balances) {
return {
type: 'setBalances',
balances
@@ -123,14 +183,14 @@ export function fetchBalances (_addresses) {
const fullFetch = addresses.length === 1;
- const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts)));
+ const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
return Promise
- .all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch)))
+ .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))
.then((accountsBalances) => {
const balances = {};
- fetchedAddresses.forEach((addr, idx) => {
+ addressesToFetch.forEach((addr, idx) => {
balances[addr] = accountsBalances[idx];
});
@@ -146,10 +206,12 @@ export function fetchBalances (_addresses) {
export function updateTokensFilter (_addresses, _tokens) {
return (dispatch, getState) => {
const { api, balances, personal } = getState();
- const { visibleAccounts } = personal;
+ const { visibleAccounts, accounts } = personal;
const { tokensFilter } = balances;
- const addresses = uniq(_addresses || visibleAccounts || []).sort();
+ const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
+ const addresses = uniq(_addresses || addressesToFetch || []).sort();
+
const tokens = _tokens || Object.values(balances.tokens) || [];
const tokenAddresses = tokens.map((t) => t.address).sort();
@@ -221,8 +283,10 @@ export function updateTokensFilter (_addresses, _tokens) {
export function queryTokensFilter (tokensFilter) {
return (dispatch, getState) => {
const { api, personal, balances } = getState();
- const { visibleAccounts } = personal;
+ const { visibleAccounts, accounts } = personal;
+
const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase());
+ const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts)));
Promise
.all([
@@ -237,18 +301,16 @@ export function queryTokensFilter (tokensFilter) {
.concat(logsTo)
.forEach((log) => {
const tokenAddress = log.address;
+
const fromAddress = '0x' + log.topics[1].slice(-40);
const toAddress = '0x' + log.topics[2].slice(-40);
- const fromIdx = visibleAddresses.indexOf(fromAddress);
- const toIdx = visibleAddresses.indexOf(toAddress);
-
- if (fromIdx > -1) {
- addresses.push(visibleAccounts[fromIdx]);
+ if (addressesToFetch.includes(fromAddress)) {
+ addresses.push(fromAddress);
}
- if (toIdx > -1) {
- addresses.push(visibleAccounts[toIdx]);
+ if (addressesToFetch.includes(toAddress)) {
+ addresses.push(toAddress);
}
tokenAddresses.push(tokenAddress);
@@ -269,9 +331,10 @@ export function queryTokensFilter (tokensFilter) {
export function fetchTokensBalances (_addresses = null, _tokens = null) {
return (dispatch, getState) => {
const { api, personal, balances } = getState();
- const { visibleAccounts } = personal;
+ const { visibleAccounts, accounts } = personal;
- const addresses = _addresses || visibleAccounts;
+ const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
+ const addresses = _addresses || addressesToFetch;
const tokens = _tokens || Object.values(balances.tokens);
if (addresses.length === 0) {
diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js
index 01923a4f4..4b6950498 100644
--- a/js/src/redux/providers/balancesReducer.js
+++ b/js/src/redux/providers/balancesReducer.js
@@ -15,7 +15,6 @@
// along with Parity. If not, see .
import { handleActions } from 'redux-actions';
-import BigNumber from 'bignumber.js';
const initialState = {
balances: {},
@@ -26,39 +25,7 @@ const initialState = {
export default handleActions({
setBalances (state, action) {
- const nextBalances = action.balances;
- const prevBalances = state.balances;
- const balances = { ...prevBalances };
-
- Object.keys(nextBalances).forEach((address) => {
- if (!balances[address]) {
- balances[address] = Object.assign({}, nextBalances[address]);
- return;
- }
-
- const balance = Object.assign({}, balances[address]);
- const { tokens, txCount = balance.txCount } = nextBalances[address];
- const nextTokens = [].concat(balance.tokens);
-
- tokens.forEach((t) => {
- const { token, value } = t;
- const { tag } = token;
-
- const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag);
-
- if (tokenIndex === -1) {
- nextTokens.push({
- token,
- value
- });
- } else {
- nextTokens[tokenIndex] = { token, value };
- }
- });
-
- balances[address] = Object.assign({}, { txCount: txCount || new BigNumber(0), tokens: nextTokens });
- });
-
+ const { balances } = action;
return Object.assign({}, state, { balances });
},
diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js
index 563378caa..a90d8b62c 100644
--- a/js/src/redux/providers/index.js
+++ b/js/src/redux/providers/index.js
@@ -21,11 +21,11 @@ export Status from './status';
export apiReducer from './apiReducer';
export balancesReducer from './balancesReducer';
+export blockchainReducer from './blockchainReducer';
+export compilerReducer from './compilerReducer';
export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer';
export signerReducer from './signerReducer';
-export statusReducer from './statusReducer';
-export blockchainReducer from './blockchainReducer';
-export compilerReducer from './compilerReducer';
export snackbarReducer from './snackbarReducer';
+export statusReducer from './statusReducer';
export walletReducer from './walletReducer';
diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js
index 92388df65..642dfe403 100644
--- a/js/src/redux/reducers.js
+++ b/js/src/redux/reducers.js
@@ -17,7 +17,12 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
-import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers';
+import {
+ apiReducer, balancesReducer, blockchainReducer,
+ compilerReducer, imagesReducer, personalReducer,
+ signerReducer, statusReducer as nodeStatusReducer,
+ snackbarReducer, walletReducer
+} from './providers';
import certificationsReducer from './providers/certifications/reducer';
import errorReducer from '~/ui/Errors/reducers';
diff --git a/js/src/redux/store.js b/js/src/redux/store.js
index 2ff50ea53..1d62f9ea5 100644
--- a/js/src/redux/store.js
+++ b/js/src/redux/store.js
@@ -32,9 +32,9 @@ const storeCreation = window.devToolsExtension
? window.devToolsExtension()(createStore)
: createStore;
-export default function (api) {
+export default function (api, browserHistory) {
const reducers = initReducers();
- const middleware = initMiddleware(api);
+ const middleware = initMiddleware(api, browserHistory);
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
new BalancesProvider(store, api).start();
diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js
new file mode 100644
index 000000000..479448234
--- /dev/null
+++ b/js/src/util/notifications.js
@@ -0,0 +1,45 @@
+// Copyright 2015, 2016 Ethcore (UK) Ltd.
+// This file is part of Parity.
+
+// Parity is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Parity is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Parity. If not, see .
+
+import Push from 'push.js';
+import BigNumber from 'bignumber.js';
+import { noop } from 'lodash';
+
+import { fromWei } from '~/api/util/wei';
+
+import ethereumIcon from '~/../assets/images/contracts/ethereum-black-64x64.png';
+import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png';
+
+export function notifyTransaction (account, token, _value, onClick) {
+ const name = account.name || account.address;
+ const value = token.tag.toLowerCase() === 'eth'
+ ? fromWei(_value)
+ : _value.div(new BigNumber(token.format || 1));
+
+ const icon = token.tag.toLowerCase() === 'eth'
+ ? ethereumIcon
+ : (token.image || unkownIcon);
+
+ Push.create(`${name}`, {
+ body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`,
+ icon: {
+ x16: icon,
+ x32: icon
+ },
+ timeout: 20000,
+ onClick: onClick || noop
+ });
+}