diff --git a/src/actions/PendingTxActions.js b/src/actions/PendingTxActions.js index b4f398d3..a6377171 100644 --- a/src/actions/PendingTxActions.js +++ b/src/actions/PendingTxActions.js @@ -2,8 +2,13 @@ import * as PENDING from 'actions/constants/pendingTx'; + +import type { + Action, ThunkAction, GetState, Dispatch, +} from 'flowtype'; import type { State, PendingTx } from 'reducers/PendingTxReducer'; + export type PendingTxAction = { type: typeof PENDING.FROM_STORAGE, payload: State @@ -15,9 +20,34 @@ export type PendingTxAction = { tx: PendingTx, receipt?: Object, } | { - type: typeof PENDING.TX_NOT_FOUND, + type: typeof PENDING.TX_REJECTED, tx: PendingTx, } | { type: typeof PENDING.TX_TOKEN_ERROR, tx: PendingTx, -} \ No newline at end of file +} + +export const reject = (tx: PendingTx): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + /* + dispatch({ + type: NOTIFICATION.ADD, + payload: { + type: 'warning', + title: 'Pending transaction rejected', + message: `Transaction with id: ${tx.id} not found.`, + cancelable: true, + actions: [ + { + label: 'OK', + callback: () => { + dispatch({ + type: PENDING.TX_RESOLVED, + tx, + }); + }, + }, + ], + }, + }); + */ +}; \ No newline at end of file diff --git a/src/actions/SelectedAccountActions.js b/src/actions/SelectedAccountActions.js index 4e01c1a5..53e8b62f 100644 --- a/src/actions/SelectedAccountActions.js +++ b/src/actions/SelectedAccountActions.js @@ -1,100 +1,210 @@ /* @flow */ - - import { LOCATION_CHANGE } from 'react-router-redux'; +import * as WALLET from 'actions/constants/wallet'; import * as ACCOUNT from 'actions/constants/account'; -import * as NOTIFICATION from 'actions/constants/notification'; +import * as DISCOVERY from 'actions/constants/discovery'; +import * as TOKEN from 'actions/constants/token'; import * as PENDING from 'actions/constants/pendingTx'; -import * as stateUtils from 'reducers/utils'; +import * as reducerUtils from 'reducers/utils'; import type { - AsyncAction, + PayloadAction, Action, GetState, Dispatch, State, } from 'flowtype'; +type SelectedAccountState = $ElementType; + export type SelectedAccountAction = { type: typeof ACCOUNT.DISPOSE, } | { type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT, - payload: $ElementType + payload: SelectedAccountState, }; +type AccountStatus = { + type: string; // notification type + title: string; // notification title + message?: string; // notification message + shouldRender: boolean; // should render account page +} + export const dispose = (): Action => ({ type: ACCOUNT.DISPOSE, }); -export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const locationChange: boolean = action.type === LOCATION_CHANGE; +const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): ?AccountStatus => { + const device = state.wallet.selectedDevice; + if (!device || !device.state) { + return { + type: 'info', + title: 'Loading device...', + shouldRender: false, + }; + } + + const { + account, + discovery, + network, + } = selectedAccount; + + // corner case: accountState didn't finish loading state after LOCATION_CHANGE action + if (!network) { + return { + type: 'info', + title: 'Loading account state...', + shouldRender: false, + }; + } + + const blockchain = state.blockchain.find(b => b.name === network.network); + if (blockchain && !blockchain.connected) { + return { + type: 'backend', + title: `${network.name} backend is not connected`, + shouldRender: false, + }; + } + + // account not found (yet). checking why... + if (!account) { + if (!discovery || discovery.waitingForDevice) { + if (device.connected) { + // case 1: device is connected but discovery not started yet (probably waiting for auth) + if (device.available) { + return { + type: 'info', + title: 'Loading accounts...', + shouldRender: false, + }; + } + // case 2: device is unavailable (created with different passphrase settings) account cannot be accessed + return { + type: 'info', + title: `Device ${device.instanceLabel} is unavailable`, + message: 'Change passphrase settings to use this device', + shouldRender: false, + }; + } + + // case 3: device is disconnected + return { + type: 'info', + title: `Device ${device.instanceLabel} is disconnected`, + message: 'Connect device to load accounts', + shouldRender: false, + }; + } + + if (discovery.completed) { + // case 4: account not found and discovery is completed + return { + type: 'warning', + title: 'Account does not exist', + shouldRender: false, + }; + } + + // case 6: discovery is not completed yet + return { + type: 'info', + title: 'Loading accounts...', + shouldRender: false, + }; + } + + // Additional status: account does exists and it's visible but shouldn't be active + if (!device.connected) { + return { + type: 'info', + title: `Device ${device.instanceLabel} is disconnected`, + shouldRender: true, + }; + } + if (!device.available) { + return { + type: 'info', + title: `Device ${device.instanceLabel} is unavailable`, + message: 'Change passphrase settings to use this device', + shouldRender: true, + }; + } + + // Additional status: account does exists, but waiting for discovery to complete + if (discovery && !discovery.completed) { + return { + type: 'info', + title: 'Loading accounts...', + shouldRender: true, + }; + } + + return null; +}; + +// list of all actions which has influence on "selectedAccount" reducer +// other actions will be ignored +const actions = [ + LOCATION_CHANGE, + WALLET.SET_SELECTED_DEVICE, + WALLET.UPDATE_SELECTED_DEVICE, + ...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT), // exported values got unwanted "__esModule: true" as first element + ...Object.values(DISCOVERY).filter(v => typeof v === 'string'), + ...Object.values(TOKEN).filter(v => typeof v === 'string'), + ...Object.values(PENDING).filter(v => typeof v === 'string'), +]; + +/* +* Called from WalletService +*/ +export const observe = (prevState: State, action: Action): PayloadAction => (dispatch: Dispatch, getState: GetState): boolean => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return false; + const state: State = getState(); const { location } = state.router; + // displayed route is not an account route + if (!location.state.account) return false; - // handle devices state change (from trezor-connect events or location change) - if (locationChange - || prevState.accounts !== state.accounts - || prevState.discovery !== state.discovery - || prevState.tokens !== state.tokens - || prevState.pending !== state.pending) { - const account = stateUtils.getSelectedAccount(state); - const network = stateUtils.getSelectedNetwork(state); - const discovery = stateUtils.getDiscoveryProcess(state); - const tokens = stateUtils.getAccountTokens(state, account); - const pending = stateUtils.getAccountPendingTx(state.pending, account); + // get new values for selected account + const account = reducerUtils.getSelectedAccount(state); + const network = reducerUtils.getSelectedNetwork(state); + const discovery = reducerUtils.getDiscoveryProcess(state); + const tokens = reducerUtils.getAccountTokens(state, account); + const pending = reducerUtils.getAccountPendingTx(state.pending, account); - const payload: $ElementType = { - location: location.pathname, - account, - network, - discovery, - tokens, - pending, - }; + // prepare new state for "selectedAccount" reducer + const newState: SelectedAccountState = { + location: state.router.location.pathname, + account, + network, + discovery, + tokens, + pending, + notification: null, + shouldRender: false, + }; - let needUpdate: boolean = false; - Object.keys(payload).forEach((key) => { - if (Array.isArray(payload[key])) { - if (Array.isArray(state.selectedAccount[key]) && payload[key].length !== state.selectedAccount[key].length) { - needUpdate = true; - } - } else if (payload[key] !== state.selectedAccount[key]) { - needUpdate = true; - } + // get "selectedAccount" status from newState + const status = getAccountStatus(state, newState); + newState.notification = status || null; + newState.shouldRender = status ? status.shouldRender : true; + // check if newState is different than previous state + const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, { + account: ['balance', 'nonce'], + discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'], + }); + + if (stateChanged) { + // update values in reducer + dispatch({ + type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, + payload: newState, }); - - if (needUpdate) { - dispatch({ - type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, - payload, - }); - - if (location.state.send) { - const rejectedTxs = pending.filter(tx => tx.rejected); - rejectedTxs.forEach((tx) => { - dispatch({ - type: NOTIFICATION.ADD, - payload: { - type: 'warning', - title: 'Pending transaction rejected', - message: `Transaction with id: ${tx.id} not found.`, - cancelable: true, - actions: [ - { - label: 'OK', - callback: () => { - dispatch({ - type: PENDING.TX_RESOLVED, - tx, - }); - }, - }, - ], - }, - }); - }); - } - } } + return stateChanged; }; diff --git a/src/actions/SendFormActions.js b/src/actions/SendFormActions.js index 1302d611..f26664f3 100644 --- a/src/actions/SendFormActions.js +++ b/src/actions/SendFormActions.js @@ -2,6 +2,7 @@ import TrezorConnect from 'trezor-connect'; import BigNumber from 'bignumber.js'; +import * as ACCOUNT from 'actions/constants/account'; import * as NOTIFICATION from 'actions/constants/notification'; import * as SEND from 'actions/constants/send'; import * as WEB3 from 'actions/constants/web3'; @@ -45,10 +46,21 @@ export type SendFormAction = { type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR, } | SendTxAction; +// list of all actions which has influence on "sendForm" reducer +// other actions will be ignored +const actions = [ + ACCOUNT.UPDATE_SELECTED_ACCOUNT, + WEB3.GAS_PRICE_UPDATED, + ...Object.values(SEND).filter(v => typeof v === 'string'), +]; + /* -* Called from WalletService on EACH action +* Called from WalletService */ export const observe = (prevState: ReducersState, action: Action): ThunkAction => (dispatch: Dispatch, getState: GetState): void => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return; + const currentState = getState(); // do not proceed if it's not "send" url if (!currentState.router.location.state.send) return; @@ -75,16 +87,9 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction = let shouldUpdate: boolean = false; // check if "selectedAccount" reducer changed - const selectedAccountChanged = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, ['account', 'tokens', 'pending']); - if (selectedAccountChanged) { - // double check - // there are only few fields that we are interested in - // check them to avoid unnecessary calculation and validation - const accountChanged = reducerUtils.observeChanges(prevState.selectedAccount.account, currentState.selectedAccount.account, ['balance', 'nonce']); - const tokensChanged = reducerUtils.observeChanges(prevState.selectedAccount.tokens, currentState.selectedAccount.tokens); - const pendingChanged = reducerUtils.observeChanges(prevState.selectedAccount.pending, currentState.selectedAccount.pending); - shouldUpdate = accountChanged || tokensChanged || pendingChanged; - } + shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, { + account: ['balance', 'nonce'], + }); // check if "sendForm" reducer changed if (!shouldUpdate) { diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 9126c541..0d572f4f 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -1,16 +1,16 @@ /* @flow */ - import { LOCATION_CHANGE } from 'react-router-redux'; +import { DEVICE } from 'trezor-connect'; import * as WALLET from 'actions/constants/wallet'; -import * as stateUtils from 'reducers/utils'; +import * as reducerUtils from 'reducers/utils'; import type { Device, TrezorDevice, RouterLocationState, ThunkAction, - AsyncAction, + PayloadAction, Action, Dispatch, GetState, @@ -80,26 +80,40 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T } }; +// list of all actions which has influence on "selectedDevice" field in "wallet" reducer +// other actions will be ignored +const actions = [ + LOCATION_CHANGE, + ...Object.values(DEVICE).filter(v => typeof v === 'string'), +]; + +/* +* Called from WalletService +*/ +export const observe = (prevState: State, action: Action): PayloadAction => (dispatch: Dispatch, getState: GetState): boolean => { + // ignore not listed actions + if (actions.indexOf(action.type) < 0) return false; -export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const locationChange: boolean = action.type === LOCATION_CHANGE; const state: State = getState(); + const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location); + const device = reducerUtils.getSelectedDevice(state); + const selectedDeviceChanged = reducerUtils.observeChanges(state.wallet.selectedDevice, device); + // handle devices state change (from trezor-connect events or location change) - if (locationChange || prevState.devices !== state.devices) { - const device = stateUtils.getSelectedDevice(state); - if (state.wallet.selectedDevice !== device) { - if (device && stateUtils.isSelectedDevice(state.wallet.selectedDevice, device)) { - dispatch({ - type: WALLET.UPDATE_SELECTED_DEVICE, - device, - }); - } else { - dispatch({ - type: WALLET.SET_SELECTED_DEVICE, - device, - }); - } + if (locationChanged || selectedDeviceChanged) { + if (device && reducerUtils.isSelectedDevice(state.wallet.selectedDevice, device)) { + dispatch({ + type: WALLET.UPDATE_SELECTED_DEVICE, + device, + }); + } else { + dispatch({ + type: WALLET.SET_SELECTED_DEVICE, + device, + }); } + return true; } + return false; }; \ No newline at end of file diff --git a/src/actions/Web3Actions.js b/src/actions/Web3Actions.js index a8d9491c..53c26a3b 100644 --- a/src/actions/Web3Actions.js +++ b/src/actions/Web3Actions.js @@ -139,7 +139,7 @@ export const resolvePendingTransactions = (network: string): PromiseAction const status = await instance.web3.eth.getTransaction(tx.id); if (!status) { dispatch({ - type: PENDING.TX_NOT_FOUND, + type: PENDING.TX_REJECTED, tx, }); } else { diff --git a/src/actions/constants/pendingTx.js b/src/actions/constants/pendingTx.js index 04f8ef91..08376cc4 100644 --- a/src/actions/constants/pendingTx.js +++ b/src/actions/constants/pendingTx.js @@ -4,5 +4,5 @@ export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage'; export const ADD: 'pending__add' = 'pending__add'; export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved'; -export const TX_NOT_FOUND: 'pending__tx_not_found' = 'pending__tx_not_found'; +export const TX_REJECTED: 'pending__tx_rejected' = 'pending__tx_rejected'; export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error'; \ No newline at end of file diff --git a/src/components/notifications/App/Container.js b/src/components/notifications/App/Container.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/notifications/App/index.js b/src/components/notifications/App/index.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/notifications/Context/components/Account/index.js b/src/components/notifications/Context/components/Account/index.js new file mode 100644 index 00000000..fe42bbd1 --- /dev/null +++ b/src/components/notifications/Context/components/Account/index.js @@ -0,0 +1,36 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +// There could be only one account notification +export default (props: Props) => { + const { notification } = props.selectedAccount; + if (notification) { + if (notification.type === 'backend') { + // special case: backend is down + // TODO: this is a different component with "auto resolve" button + + return ( + { + await props.blockchainReconnect('trop'); + }, + }] + } + /> + ); + + // return (); + } + return (); + } + return null; +}; \ No newline at end of file diff --git a/src/components/notifications/Context/components/Action/index.js b/src/components/notifications/Context/components/Action/index.js new file mode 100644 index 00000000..0bf7f0be --- /dev/null +++ b/src/components/notifications/Context/components/Action/index.js @@ -0,0 +1,20 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + const { notifications, close } = props; + return notifications.map(n => ( + + )); +}; \ No newline at end of file diff --git a/src/components/notifications/Context/components/Static/index.js b/src/components/notifications/Context/components/Static/index.js new file mode 100644 index 00000000..47d30b24 --- /dev/null +++ b/src/components/notifications/Context/components/Static/index.js @@ -0,0 +1,17 @@ +/* @flow */ +import * as React from 'react'; +import { Notification } from 'components/Notification'; + +import type { Props } from '../../index'; + +export default (props: Props) => { + const { location } = props.router; + if (!location) return null; + + const notifications: Array = []; + // if (location.state.device) { + // notifications.push(); + // } + + return notifications; +}; \ No newline at end of file diff --git a/src/components/notifications/Context/index.js b/src/components/notifications/Context/index.js new file mode 100644 index 00000000..e28bba0b --- /dev/null +++ b/src/components/notifications/Context/index.js @@ -0,0 +1,55 @@ +/* @flow */ +import * as React from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import type { MapStateToProps, MapDispatchToProps } from 'react-redux'; +import type { State, Dispatch } from 'flowtype'; + +import { reconnect } from 'actions/DiscoveryActions'; +import * as NotificationActions from 'actions/NotificationActions'; + +import StaticNotifications from './components/Static'; +import AccountNotifications from './components/Account'; +import ActionNotifications from './components/Action'; + +export type StateProps = { + router: $ElementType; + notifications: $ElementType; + selectedAccount: $ElementType; + wallet: $ElementType; + blockchain: $ElementType; + children?: React.Node; +} + +export type DispatchProps = { + close: typeof NotificationActions.close; + blockchainReconnect: typeof reconnect; +} + +export type Props = StateProps & DispatchProps; + +type OwnProps = {}; + +const Notifications = (props: Props) => ( + + + + + +); + +const mapStateToProps: MapStateToProps = (state: State): StateProps => ({ + router: state.router, + notifications: state.notifications, + selectedAccount: state.selectedAccount, + wallet: state.wallet, + blockchain: state.blockchain, +}); + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + close: bindActionCreators(NotificationActions.close, dispatch), + blockchainReconnect: bindActionCreators(reconnect, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Notifications); \ No newline at end of file diff --git a/src/reducers/PendingTxReducer.js b/src/reducers/PendingTxReducer.js index 9f4ab635..f89243ea 100644 --- a/src/reducers/PendingTxReducer.js +++ b/src/reducers/PendingTxReducer.js @@ -65,7 +65,7 @@ export default function pending(state: State = initialState, action: Action): St // return add(state, action.payload); case PENDING.TX_RESOLVED: return remove(state, action.tx.id); - case PENDING.TX_NOT_FOUND: + case PENDING.TX_REJECTED: return reject(state, action.tx.id); case PENDING.FROM_STORAGE: diff --git a/src/reducers/SelectedAccountReducer.js b/src/reducers/SelectedAccountReducer.js index 68fb56d6..1b400668 100644 --- a/src/reducers/SelectedAccountReducer.js +++ b/src/reducers/SelectedAccountReducer.js @@ -11,12 +11,18 @@ import type { } from 'flowtype'; export type State = { - location?: string; + location: string; account: ?Account; network: ?Coin; tokens: Array, pending: Array, - discovery: ?Discovery + discovery: ?Discovery, + notification: ?{ + type: string, + title: string, + message?: string, + }, + shouldRender: boolean, }; export const initialState: State = { @@ -26,6 +32,8 @@ export const initialState: State = { tokens: [], pending: [], discovery: null, + notification: null, + shouldRender: false, }; export default (state: State = initialState, action: Action): State => { diff --git a/src/reducers/utils/index.js b/src/reducers/utils/index.js index 9b5ba7f5..d3a8e740 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -116,21 +116,58 @@ export const getWeb3 = (state: State): ?Web3Instance => { return state.web3.find(w3 => w3.network === locationState.network); }; -export const observeChanges = (prev: ?(Object | Array), current: ?(Object | Array), fields?: Array): boolean => { - if (prev !== current) { - // 1. one of the objects is null/undefined - if (!prev || !current) return true; - // 2. object are Arrays and they have different length - if (Array.isArray(prev) && Array.isArray(current)) return prev.length !== current.length; - // 3. no nested field to check - if (!Array.isArray(fields)) return true; - // 4. validate nested field - if (prev instanceof Object && current instanceof Object) { - for (let i = 0; i < fields.length; i++) { - const key = fields[i]; - if (prev[key] !== current[key]) return true; +export const observeChanges = (prev: ?Object, current: ?Object, filter?: {[k: string]: Array}): boolean => { + // 1. both objects are the same (solves simple types like string, boolean and number) + if (prev === current) return false; + // 2. one of the objects is null/undefined + if (!prev || !current) return true; + + const prevType = Object.prototype.toString.call(current); + const currentType = Object.prototype.toString.call(current); + // 3. one of the objects has different type then other + if (prevType !== currentType) return true; + + if (currentType === '[object Array]') { + // 4. Array length is different + if (prev.length !== current.length) return true; + // observe array recursive + for (let i = 0; i < current.length; i++) { + if (observeChanges(prev[i], current[i], filter)) return true; + } + } else if (currentType === '[object Object]') { + const prevKeys = Object.keys(prev); + const currentKeys = Object.keys(prev); + // 5. simple validation of keys length + if (prevKeys.length !== currentKeys.length) return true; + + // 6. "prev" has keys which "current" doesn't have + const prevDifference = prevKeys.find(k => currentKeys.indexOf(k) < 0); + if (prevDifference) return true; + + // 7. "current" has keys which "prev" doesn't have + const currentDifference = currentKeys.find(k => prevKeys.indexOf(k) < 0); + if (currentDifference) return true; + + // 8. observe every key recursive + for (let i = 0; i < currentKeys.length; i++) { + const key = currentKeys[i]; + if (filter && filter.hasOwnProperty(key) && prev[key] && current[key]) { + const prevFiltered = {}; + const currentFiltered = {}; + for (let i2 = 0; i2 < filter[key].length; i2++) { + const field = filter[key][i2]; + prevFiltered[field] = prev[key][field]; + currentFiltered[field] = current[key][field]; + } + if (observeChanges(prevFiltered, currentFiltered)) return true; + } else if (observeChanges(prev[key], current[key])) { + return true; } } + } else if (prev !== current) { + // solve simple types like string, boolean and number + return true; } + return false; }; diff --git a/src/services/LocalStorageService.js b/src/services/LocalStorageService.js index 8bc66798..9a9af858 100644 --- a/src/services/LocalStorageService.js +++ b/src/services/LocalStorageService.js @@ -110,9 +110,12 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar case SEND.TX_COMPLETE: case PENDING.TX_RESOLVED: - case PENDING.TX_NOT_FOUND: + case PENDING.TX_REJECTED: save(api.dispatch, api.getState); break; + + default: + return action; } return action; diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 9455dfbb..76db7638 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -21,7 +21,7 @@ import type { /** * Middleware */ -const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => { +const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => async (action: Action): Promise => { const prevState = api.getState(); // Application live cycle starts HERE! @@ -88,14 +88,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state)); } - // observe send form props changes - api.dispatch(SendFormActionActions.observe(prevState, action)); - - // update common values in WallerReducer - api.dispatch(WalletActions.updateSelectedValues(prevState, action)); - - // update common values in SelectedAccountReducer - api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); + // observe common values in WallerReducer + if (!await api.dispatch(WalletActions.observe(prevState, action))) { + // if "selectedDevice" didn't change observe common values in SelectedAccountReducer + if (!await api.dispatch(SelectedAccountActions.observe(prevState, action))) { + // if "selectedAccount" didn't change observe send form props changes + api.dispatch(SendFormActionActions.observe(prevState, action)); + } + } return action; }; diff --git a/src/views/Wallet/index.js b/src/views/Wallet/index.js index 0bf9849a..c78a3020 100644 --- a/src/views/Wallet/index.js +++ b/src/views/Wallet/index.js @@ -12,13 +12,15 @@ import type { State } from 'flowtype'; import Header from 'components/Header'; import Footer from 'components/Footer'; import ModalContainer from 'components/modals'; -import Notifications from 'components/Notification'; +import ContextNotifications from 'components/notifications/Context'; + import Log from 'components/Log'; import LeftNavigation from './components/LeftNavigation/Container'; import TopNavigationAccount from './components/TopNavigationAccount'; import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings'; + type WalletContainerProps = { wallet: $ElementType, children?: React.Node @@ -89,7 +91,7 @@ const Wallet = (props: WalletContainerProps) => ( - + { props.children } diff --git a/src/views/Wallet/views/AccountReceive/index.js b/src/views/Wallet/views/AccountReceive/index.js index c3864b60..202ceee1 100644 --- a/src/views/Wallet/views/AccountReceive/index.js +++ b/src/views/Wallet/views/AccountReceive/index.js @@ -10,8 +10,6 @@ import colors from 'config/colors'; import Tooltip from 'components/Tooltip'; import { QRCode } from 'react-qr-svg'; -import SelectedAccount from 'views/Wallet/components/SelectedAccount'; - import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables'; import type { Props } from './Container'; @@ -154,87 +152,85 @@ const AccountReceive = (props: Props) => { const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified; return ( - - - Receive Ethereum or tokens - - {((addressVerified || addressUnverified) && !isAddressVerifying) && ( - - {addressUnverified ? ( - - Unverified address. -
- {device.connected && device.available ? 'Show on TREZOR' : 'Connect your TREZOR to verify it.'} -
- ) : ( - - {device.connected ? 'Show on TREZOR' : 'Connect your TREZOR to verify address.'} - - )} - - )} - > - props.showAddress(account.addressPath)} - > - - -
- )} - {address} - - {isAddressVerifying && ( - - - - - Check address on your Trezor - - - )} - {/* {isAddressVerifying && ( - {account.network} account #{account.index + 1} - )} */} - {(addressVerified || addressUnverified) && ( - - )} - {!(addressVerified || addressUnverified) && ( - + Receive Ethereum or tokens + + {((addressVerified || addressUnverified) && !isAddressVerifying) && ( + + {addressUnverified ? ( + + Unverified address. +
+ {device.connected && device.available ? 'Show on TREZOR' : 'Connect your TREZOR to verify it.'} +
+ ) : ( + + {device.connected ? 'Show on TREZOR' : 'Connect your TREZOR to verify address.'} + + )} + + )} + > + props.showAddress(account.addressPath)} - isDisabled={device.connected && !discovery.completed} > - + +
+ )} + {address} + + {isAddressVerifying && ( + + + + - Show full address -
- )} -
-
-
+ Check address on your Trezor + + + )} + {/* {isAddressVerifying && ( + {account.network} account #{account.index + 1} + )} */} + {(addressVerified || addressUnverified) && ( + + )} + {!(addressVerified || addressUnverified) && ( + props.showAddress(account.addressPath)} + isDisabled={device.connected && !discovery.completed} + > + + Show full address + + )} + + ); }; -export default AccountReceive; +export default AccountReceive; \ No newline at end of file diff --git a/src/views/Wallet/views/AccountSend/index.js b/src/views/Wallet/views/AccountSend/index.js index e634c4aa..7007d92d 100644 --- a/src/views/Wallet/views/AccountSend/index.js +++ b/src/views/Wallet/views/AccountSend/index.js @@ -12,7 +12,6 @@ import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables'; import colors from 'config/colors'; import P from 'components/Paragraph'; import { H2 } from 'components/Heading'; -import SelectedAccount from 'views/Wallet/components/SelectedAccount'; import type { Token } from 'flowtype'; import AdvancedForm from './components/AdvancedForm'; import PendingTransactions from './components/PendingTransactions'; @@ -191,6 +190,7 @@ const AccountSend = (props: Props) => { network, discovery, tokens, + shouldRender, } = props.selectedAccount; const { address, @@ -219,6 +219,8 @@ const AccountSend = (props: Props) => { onSend, } = props.sendFormActions; + if (!device || !account || !discovery || !network || !shouldRender) return null; + const isCurrentCurrencyToken = networkSymbol !== currency; let selectedTokenBalance = 0; @@ -227,8 +229,6 @@ const AccountSend = (props: Props) => { selectedTokenBalance = selectedToken.balance; } - if (!device || !account || !discovery || !network) return null; - let isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending; let sendButtonText: string = 'Send'; if (networkSymbol !== currency && amount.length > 0 && !errors.amount) { @@ -252,157 +252,155 @@ const AccountSend = (props: Props) => { const isAdvancedSettingsHidden = !advanced; return ( - - - Send Ethereum or tokens - - onAddressChange(event.target.value)} - /> - + + Send Ethereum or tokens + + onAddressChange(event.target.value)} + /> + - - - Amount - {(isCurrentCurrencyToken && selectedToken) && ( - You have: {selectedTokenBalance} {selectedToken.symbol} - )} - - )} - value={amount} - onChange={event => onAmountChange(event.target.value)} - bottomText={errors.amount || warnings.amount || infos.amount} - sideAddons={[ - ( - onSetMax()} - isActive={setMax} - > - {!setMax && ( - - )} - {setMax && ( - - )} - Set max - - ), - ( - - ), - ]} - /> - - - - - Fee - {gasPriceNeedsUpdate && ( - - - Recommended fees updated. Click here to use them - - )} - - + Amount + {(isCurrentCurrencyToken && selectedToken) && ( + You have: {selectedTokenBalance} {selectedToken.symbol} + )} + )} - + value={amount} + onChange={event => onAmountChange(event.target.value)} + bottomText={errors.amount || warnings.amount || infos.amount} + sideAddons={[ + ( + onSetMax()} + isActive={setMax} + > + {!setMax && ( + + )} + {setMax && ( + + )} + Set max + + ), + ( + + ), + ]} + /> + - {advanced && ( - - onSend()} - > - {sendButtonText} - - - )} + + + Fee + {gasPriceNeedsUpdate && ( + + + Recommended fees updated. Click here to use them + + )} + +