Notify user on transaction received (#3782)
* Notify user on new transaction #2556 * Add routing to account on notification click * Timeout of notif set to 20s
This commit is contained in:
parent
aa30619b6f
commit
b0f1665f11
@ -146,6 +146,7 @@
|
|||||||
"mobx-react-devtools": "4.2.10",
|
"mobx-react-devtools": "4.2.10",
|
||||||
"moment": "2.17.0",
|
"moment": "2.17.0",
|
||||||
"phoneformat.js": "1.0.3",
|
"phoneformat.js": "1.0.3",
|
||||||
|
"push.js": "0.0.11",
|
||||||
"qs": "6.3.0",
|
"qs": "6.3.0",
|
||||||
"react": "15.4.1",
|
"react": "15.4.1",
|
||||||
"react-ace": "4.1.0",
|
"react-ace": "4.1.0",
|
||||||
|
@ -67,7 +67,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
|
|||||||
const api = new SecureApi(`ws://${parityUrl}`, token);
|
const api = new SecureApi(`ws://${parityUrl}`, token);
|
||||||
ContractInstances.create(api);
|
ContractInstances.create(api);
|
||||||
|
|
||||||
const store = initStore(api);
|
const store = initStore(api, hashHistory);
|
||||||
store.dispatch({ type: 'initAll', api });
|
store.dispatch({ type: 'initAll', api });
|
||||||
store.dispatch(setApi(api));
|
store.dispatch(setApi(api));
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
|
import { routerMiddleware } from 'react-router-redux';
|
||||||
|
|
||||||
import ErrorsMiddleware from '~/ui/Errors/middleware';
|
import ErrorsMiddleware from '~/ui/Errors/middleware';
|
||||||
import SettingsMiddleware from '~/views/Settings/middleware';
|
import SettingsMiddleware from '~/views/Settings/middleware';
|
||||||
@ -22,12 +23,13 @@ import SignerMiddleware from './providers/signerMiddleware';
|
|||||||
import statusMiddleware from '~/views/Status/middleware';
|
import statusMiddleware from '~/views/Status/middleware';
|
||||||
import CertificationsMiddleware from './providers/certifications/middleware';
|
import CertificationsMiddleware from './providers/certifications/middleware';
|
||||||
|
|
||||||
export default function (api) {
|
export default function (api, browserHistory) {
|
||||||
const errors = new ErrorsMiddleware();
|
const errors = new ErrorsMiddleware();
|
||||||
const signer = new SignerMiddleware(api);
|
const signer = new SignerMiddleware(api);
|
||||||
const settings = new SettingsMiddleware();
|
const settings = new SettingsMiddleware();
|
||||||
const status = statusMiddleware();
|
const status = statusMiddleware();
|
||||||
const certifications = new CertificationsMiddleware();
|
const certifications = new CertificationsMiddleware();
|
||||||
|
const routeMiddleware = routerMiddleware(browserHistory);
|
||||||
|
|
||||||
const middleware = [
|
const middleware = [
|
||||||
settings.toMiddleware(),
|
settings.toMiddleware(),
|
||||||
@ -36,5 +38,5 @@ export default function (api) {
|
|||||||
certifications.toMiddleware()
|
certifications.toMiddleware()
|
||||||
];
|
];
|
||||||
|
|
||||||
return middleware.concat(status, thunk);
|
return middleware.concat(status, routeMiddleware, thunk);
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,14 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { range, uniq, isEqual } from 'lodash';
|
import { range, uniq, isEqual } from 'lodash';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { push } from 'react-router-redux';
|
||||||
|
|
||||||
import { hashToImageUrl } from './imagesReducer';
|
import { hashToImageUrl } from './imagesReducer';
|
||||||
import { setAddressImage } from './imagesActions';
|
import { setAddressImage } from './imagesActions';
|
||||||
|
|
||||||
import * as ABIS from '~/contracts/abi';
|
import * as ABIS from '~/contracts/abi';
|
||||||
|
import { notifyTransaction } from '~/util/notifications';
|
||||||
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
|
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
|
||||||
|
|
||||||
const ETH = {
|
const ETH = {
|
||||||
@ -28,7 +31,64 @@ const ETH = {
|
|||||||
image: imagesEthereum
|
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 {
|
return {
|
||||||
type: 'setBalances',
|
type: 'setBalances',
|
||||||
balances
|
balances
|
||||||
@ -123,14 +183,14 @@ export function fetchBalances (_addresses) {
|
|||||||
|
|
||||||
const fullFetch = addresses.length === 1;
|
const fullFetch = addresses.length === 1;
|
||||||
|
|
||||||
const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts)));
|
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
|
||||||
|
|
||||||
return Promise
|
return Promise
|
||||||
.all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch)))
|
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))
|
||||||
.then((accountsBalances) => {
|
.then((accountsBalances) => {
|
||||||
const balances = {};
|
const balances = {};
|
||||||
|
|
||||||
fetchedAddresses.forEach((addr, idx) => {
|
addressesToFetch.forEach((addr, idx) => {
|
||||||
balances[addr] = accountsBalances[idx];
|
balances[addr] = accountsBalances[idx];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,10 +206,12 @@ export function fetchBalances (_addresses) {
|
|||||||
export function updateTokensFilter (_addresses, _tokens) {
|
export function updateTokensFilter (_addresses, _tokens) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { api, balances, personal } = getState();
|
const { api, balances, personal } = getState();
|
||||||
const { visibleAccounts } = personal;
|
const { visibleAccounts, accounts } = personal;
|
||||||
const { tokensFilter } = balances;
|
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 tokens = _tokens || Object.values(balances.tokens) || [];
|
||||||
const tokenAddresses = tokens.map((t) => t.address).sort();
|
const tokenAddresses = tokens.map((t) => t.address).sort();
|
||||||
|
|
||||||
@ -221,8 +283,10 @@ export function updateTokensFilter (_addresses, _tokens) {
|
|||||||
export function queryTokensFilter (tokensFilter) {
|
export function queryTokensFilter (tokensFilter) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { api, personal, balances } = getState();
|
const { api, personal, balances } = getState();
|
||||||
const { visibleAccounts } = personal;
|
const { visibleAccounts, accounts } = personal;
|
||||||
|
|
||||||
const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase());
|
const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase());
|
||||||
|
const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts)));
|
||||||
|
|
||||||
Promise
|
Promise
|
||||||
.all([
|
.all([
|
||||||
@ -237,18 +301,16 @@ export function queryTokensFilter (tokensFilter) {
|
|||||||
.concat(logsTo)
|
.concat(logsTo)
|
||||||
.forEach((log) => {
|
.forEach((log) => {
|
||||||
const tokenAddress = log.address;
|
const tokenAddress = log.address;
|
||||||
|
|
||||||
const fromAddress = '0x' + log.topics[1].slice(-40);
|
const fromAddress = '0x' + log.topics[1].slice(-40);
|
||||||
const toAddress = '0x' + log.topics[2].slice(-40);
|
const toAddress = '0x' + log.topics[2].slice(-40);
|
||||||
|
|
||||||
const fromIdx = visibleAddresses.indexOf(fromAddress);
|
if (addressesToFetch.includes(fromAddress)) {
|
||||||
const toIdx = visibleAddresses.indexOf(toAddress);
|
addresses.push(fromAddress);
|
||||||
|
|
||||||
if (fromIdx > -1) {
|
|
||||||
addresses.push(visibleAccounts[fromIdx]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toIdx > -1) {
|
if (addressesToFetch.includes(toAddress)) {
|
||||||
addresses.push(visibleAccounts[toIdx]);
|
addresses.push(toAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenAddresses.push(tokenAddress);
|
tokenAddresses.push(tokenAddress);
|
||||||
@ -269,9 +331,10 @@ export function queryTokensFilter (tokensFilter) {
|
|||||||
export function fetchTokensBalances (_addresses = null, _tokens = null) {
|
export function fetchTokensBalances (_addresses = null, _tokens = null) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { api, personal, balances } = 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);
|
const tokens = _tokens || Object.values(balances.tokens);
|
||||||
|
|
||||||
if (addresses.length === 0) {
|
if (addresses.length === 0) {
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
balances: {},
|
balances: {},
|
||||||
@ -26,39 +25,7 @@ const initialState = {
|
|||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
setBalances (state, action) {
|
setBalances (state, action) {
|
||||||
const nextBalances = action.balances;
|
const { balances } = action;
|
||||||
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 });
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign({}, state, { balances });
|
return Object.assign({}, state, { balances });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@ export Status from './status';
|
|||||||
|
|
||||||
export apiReducer from './apiReducer';
|
export apiReducer from './apiReducer';
|
||||||
export balancesReducer from './balancesReducer';
|
export balancesReducer from './balancesReducer';
|
||||||
|
export blockchainReducer from './blockchainReducer';
|
||||||
|
export compilerReducer from './compilerReducer';
|
||||||
export imagesReducer from './imagesReducer';
|
export imagesReducer from './imagesReducer';
|
||||||
export personalReducer from './personalReducer';
|
export personalReducer from './personalReducer';
|
||||||
export signerReducer from './signerReducer';
|
export signerReducer from './signerReducer';
|
||||||
export statusReducer from './statusReducer';
|
|
||||||
export blockchainReducer from './blockchainReducer';
|
|
||||||
export compilerReducer from './compilerReducer';
|
|
||||||
export snackbarReducer from './snackbarReducer';
|
export snackbarReducer from './snackbarReducer';
|
||||||
|
export statusReducer from './statusReducer';
|
||||||
export walletReducer from './walletReducer';
|
export walletReducer from './walletReducer';
|
||||||
|
@ -17,7 +17,12 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { routerReducer } from 'react-router-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 certificationsReducer from './providers/certifications/reducer';
|
||||||
|
|
||||||
import errorReducer from '~/ui/Errors/reducers';
|
import errorReducer from '~/ui/Errors/reducers';
|
||||||
|
@ -32,9 +32,9 @@ const storeCreation = window.devToolsExtension
|
|||||||
? window.devToolsExtension()(createStore)
|
? window.devToolsExtension()(createStore)
|
||||||
: createStore;
|
: createStore;
|
||||||
|
|
||||||
export default function (api) {
|
export default function (api, browserHistory) {
|
||||||
const reducers = initReducers();
|
const reducers = initReducers();
|
||||||
const middleware = initMiddleware(api);
|
const middleware = initMiddleware(api, browserHistory);
|
||||||
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
|
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
|
||||||
|
|
||||||
new BalancesProvider(store, api).start();
|
new BalancesProvider(store, api).start();
|
||||||
|
45
js/src/util/notifications.js
Normal file
45
js/src/util/notifications.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user