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