diff --git a/src/actions/SelectedAccountActions.js b/src/actions/SelectedAccountActions.js index 4e01c1a5..a5f1a91d 100644 --- a/src/actions/SelectedAccountActions.js +++ b/src/actions/SelectedAccountActions.js @@ -1,12 +1,8 @@ /* @flow */ - -import { LOCATION_CHANGE } from 'react-router-redux'; import * as ACCOUNT from 'actions/constants/account'; -import * as NOTIFICATION from 'actions/constants/notification'; -import * as PENDING from 'actions/constants/pendingTx'; - -import * as stateUtils from 'reducers/utils'; +import * as reducerUtils from 'reducers/utils'; +import { initialState } from 'reducers/SelectedAccountReducer'; import type { AsyncAction, @@ -16,85 +12,216 @@ import type { 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; + title: string; + message?: string; + visible: boolean; +} + 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...', + visible: 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...', + visible: false, + }; + } + + const blockchain = state.blockchain.find(b => b.name === network.network); + if (blockchain && !blockchain.connected) { + return { + type: 'backend', + title: 'Backend is not connected', + visible: 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...', + visible: 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', + visible: false, + }; + } + + // case 3: device is disconnected + return { + type: 'info', + title: `Device ${device.instanceLabel} is disconnected`, + message: 'Connect device to load accounts', + visible: false, + }; + } + + if (discovery.completed) { + // case 4: account not found and discovery is completed + return { + type: 'warning', + title: 'Account does not exist', + visible: false, + }; + } + + // case 6: discovery is not completed yet + return { + type: 'info', + title: 'Loading accounts...', + visible: 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`, + visible: true, + }; + } + if (!device.available) { + return { + type: 'info', + title: `Device ${device.instanceLabel} is unavailable`, + message: 'Change passphrase settings to use this device', + visible: true, + }; + } + + // Additional status: account does exists, but waiting for discovery to complete + if (discovery && !discovery.completed) { + return { + type: 'info', + title: 'Loading accounts...', + visible: true, + }; + } + + return null; +}; + +export const observe = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + // ignore actions dispatched from this process + if (action.type === ACCOUNT.UPDATE_SELECTED_ACCOUNT) { + return; + } + const state: State = getState(); const { location } = state.router; - - // 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); - - const payload: $ElementType = { - location: location.pathname, - account, - network, - discovery, - tokens, - pending, - }; - - 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; - } - }); - - if (needUpdate) { + if (!location.state.account) { + // displayed route is not an account route + if (state.selectedAccount.location.length > 1) { + // reset "selectedAccount" reducer to default dispatch({ type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, - payload, + payload: initialState, }); - - 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; + } + + // 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); + + // prepare new state for "selectedAccount" reducer + const newState: SelectedAccountState = { + location: state.router.location.pathname, + account, + network, + discovery, + tokens, + pending, + notification: null, + visible: false, + }; + + // get "selectedAccount" status from newState + const status = getAccountStatus(state, newState); + newState.notification = status || null; + newState.visible = status ? status.visible : true; + + // check if newState is different than previous state + const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, ['location', 'account', 'network', 'discovery', 'tokens', 'pending', 'status', 'visible']); + if (stateChanged) { + // update values in reducer + dispatch({ + type: ACCOUNT.UPDATE_SELECTED_ACCOUNT, + payload: newState, + }); + + // TODO: move this to send account actions + /* + 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, + }); + }, + }, + ], + }, + }); + }); + } + */ } }; diff --git a/src/actions/WalletActions.js b/src/actions/WalletActions.js index 9126c541..983b86b2 100644 --- a/src/actions/WalletActions.js +++ b/src/actions/WalletActions.js @@ -1,9 +1,7 @@ /* @flow */ - -import { LOCATION_CHANGE } from 'react-router-redux'; import * as WALLET from 'actions/constants/wallet'; -import * as stateUtils from 'reducers/utils'; +import * as reducerUtils from 'reducers/utils'; import type { Device, @@ -81,25 +79,29 @@ export const clearUnavailableDevicesData = (prevState: State, device: Device): T }; -export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { - const locationChange: boolean = action.type === LOCATION_CHANGE; +export const observe = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise => { + // ignore actions dispatched from this process + if (action.type === WALLET.SET_SELECTED_DEVICE || action.type === WALLET.UPDATE_SELECTED_DEVICE) { + return; + } const state: State = getState(); + const locationChanged = reducerUtils.observeChanges(prevState.router.location, state.router.location, ['pathname']); + 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, + }); } } }; \ No newline at end of file diff --git a/src/reducers/SelectedAccountReducer.js b/src/reducers/SelectedAccountReducer.js index 68fb56d6..f64ea582 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, + }, + visible: boolean, }; export const initialState: State = { @@ -26,6 +32,8 @@ export const initialState: State = { tokens: [], pending: [], discovery: null, + notification: null, + visible: 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..c8b8d56f 100644 --- a/src/reducers/utils/index.js +++ b/src/reducers/utils/index.js @@ -128,6 +128,7 @@ export const observeChanges = (prev: ?(Object | Array), current: ?(Object | if (prev instanceof Object && current instanceof Object) { for (let i = 0; i < fields.length; i++) { const key = fields[i]; + if (Array.isArray(prev[key]) && Array.isArray(current[key])) return prev[key].length !== current[key].length; if (prev[key] !== current[key]) return true; } } diff --git a/src/services/WalletService.js b/src/services/WalletService.js index 9455dfbb..881f293b 100644 --- a/src/services/WalletService.js +++ b/src/services/WalletService.js @@ -92,10 +92,10 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa api.dispatch(SendFormActionActions.observe(prevState, action)); // update common values in WallerReducer - api.dispatch(WalletActions.updateSelectedValues(prevState, action)); + api.dispatch(WalletActions.observe(prevState, action)); // update common values in SelectedAccountReducer - api.dispatch(SelectedAccountActions.updateSelectedValues(prevState, action)); + api.dispatch(SelectedAccountActions.observe(prevState, action)); return action; };