mirror of
https://github.com/trezor/trezor-wallet
synced 2024-11-24 09:18:09 +00:00
Merge pull request #89 from satoshilabs/feature/notifications
Feature/notifications
This commit is contained in:
commit
49aa31774e
@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
|
|
||||||
import * as PENDING from 'actions/constants/pendingTx';
|
import * as PENDING from 'actions/constants/pendingTx';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Action, ThunkAction, GetState, Dispatch,
|
||||||
|
} from 'flowtype';
|
||||||
import type { State, PendingTx } from 'reducers/PendingTxReducer';
|
import type { State, PendingTx } from 'reducers/PendingTxReducer';
|
||||||
|
|
||||||
|
|
||||||
export type PendingTxAction = {
|
export type PendingTxAction = {
|
||||||
type: typeof PENDING.FROM_STORAGE,
|
type: typeof PENDING.FROM_STORAGE,
|
||||||
payload: State
|
payload: State
|
||||||
@ -15,9 +20,34 @@ export type PendingTxAction = {
|
|||||||
tx: PendingTx,
|
tx: PendingTx,
|
||||||
receipt?: Object,
|
receipt?: Object,
|
||||||
} | {
|
} | {
|
||||||
type: typeof PENDING.TX_NOT_FOUND,
|
type: typeof PENDING.TX_REJECTED,
|
||||||
tx: PendingTx,
|
tx: PendingTx,
|
||||||
} | {
|
} | {
|
||||||
type: typeof PENDING.TX_TOKEN_ERROR,
|
type: typeof PENDING.TX_TOKEN_ERROR,
|
||||||
tx: PendingTx,
|
tx: PendingTx,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
};
|
@ -1,100 +1,210 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||||
|
import * as WALLET from 'actions/constants/wallet';
|
||||||
import * as ACCOUNT from 'actions/constants/account';
|
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 PENDING from 'actions/constants/pendingTx';
|
||||||
|
|
||||||
import * as stateUtils from 'reducers/utils';
|
import * as reducerUtils from 'reducers/utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AsyncAction,
|
PayloadAction,
|
||||||
Action,
|
Action,
|
||||||
GetState,
|
GetState,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
State,
|
State,
|
||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
|
|
||||||
|
type SelectedAccountState = $ElementType<State, 'selectedAccount'>;
|
||||||
|
|
||||||
export type SelectedAccountAction = {
|
export type SelectedAccountAction = {
|
||||||
type: typeof ACCOUNT.DISPOSE,
|
type: typeof ACCOUNT.DISPOSE,
|
||||||
} | {
|
} | {
|
||||||
type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||||
payload: $ElementType<State, 'selectedAccount'>
|
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 => ({
|
export const dispose = (): Action => ({
|
||||||
type: ACCOUNT.DISPOSE,
|
type: ACCOUNT.DISPOSE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): ?AccountStatus => {
|
||||||
const locationChange: boolean = action.type === LOCATION_CHANGE;
|
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<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
|
||||||
|
// ignore not listed actions
|
||||||
|
if (actions.indexOf(action.type) < 0) return false;
|
||||||
|
|
||||||
const state: State = getState();
|
const state: State = getState();
|
||||||
const { location } = state.router;
|
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)
|
// get new values for selected account
|
||||||
if (locationChange
|
const account = reducerUtils.getSelectedAccount(state);
|
||||||
|| prevState.accounts !== state.accounts
|
const network = reducerUtils.getSelectedNetwork(state);
|
||||||
|| prevState.discovery !== state.discovery
|
const discovery = reducerUtils.getDiscoveryProcess(state);
|
||||||
|| prevState.tokens !== state.tokens
|
const tokens = reducerUtils.getAccountTokens(state, account);
|
||||||
|| prevState.pending !== state.pending) {
|
const pending = reducerUtils.getAccountPendingTx(state.pending, account);
|
||||||
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<State, 'selectedAccount'> = {
|
// prepare new state for "selectedAccount" reducer
|
||||||
location: location.pathname,
|
const newState: SelectedAccountState = {
|
||||||
|
location: state.router.location.pathname,
|
||||||
account,
|
account,
|
||||||
network,
|
network,
|
||||||
discovery,
|
discovery,
|
||||||
tokens,
|
tokens,
|
||||||
pending,
|
pending,
|
||||||
|
notification: null,
|
||||||
|
shouldRender: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let needUpdate: boolean = false;
|
// get "selectedAccount" status from newState
|
||||||
Object.keys(payload).forEach((key) => {
|
const status = getAccountStatus(state, newState);
|
||||||
if (Array.isArray(payload[key])) {
|
newState.notification = status || null;
|
||||||
if (Array.isArray(state.selectedAccount[key]) && payload[key].length !== state.selectedAccount[key].length) {
|
newState.shouldRender = status ? status.shouldRender : true;
|
||||||
needUpdate = true;
|
// check if newState is different than previous state
|
||||||
}
|
const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, {
|
||||||
} else if (payload[key] !== state.selectedAccount[key]) {
|
account: ['balance', 'nonce'],
|
||||||
needUpdate = true;
|
discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'],
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (needUpdate) {
|
if (stateChanged) {
|
||||||
|
// update values in reducer
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
type: ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||||
payload,
|
payload: newState,
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import TrezorConnect from 'trezor-connect';
|
import TrezorConnect from 'trezor-connect';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
|
import * as ACCOUNT from 'actions/constants/account';
|
||||||
import * as NOTIFICATION from 'actions/constants/notification';
|
import * as NOTIFICATION from 'actions/constants/notification';
|
||||||
import * as SEND from 'actions/constants/send';
|
import * as SEND from 'actions/constants/send';
|
||||||
import * as WEB3 from 'actions/constants/web3';
|
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,
|
type: typeof SEND.TOGGLE_ADVANCED | typeof SEND.TX_SENDING | typeof SEND.TX_ERROR,
|
||||||
} | SendTxAction;
|
} | 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 => {
|
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();
|
const currentState = getState();
|
||||||
// do not proceed if it's not "send" url
|
// do not proceed if it's not "send" url
|
||||||
if (!currentState.router.location.state.send) return;
|
if (!currentState.router.location.state.send) return;
|
||||||
@ -75,16 +87,9 @@ export const observe = (prevState: ReducersState, action: Action): ThunkAction =
|
|||||||
|
|
||||||
let shouldUpdate: boolean = false;
|
let shouldUpdate: boolean = false;
|
||||||
// check if "selectedAccount" reducer changed
|
// check if "selectedAccount" reducer changed
|
||||||
const selectedAccountChanged = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, ['account', 'tokens', 'pending']);
|
shouldUpdate = reducerUtils.observeChanges(prevState.selectedAccount, currentState.selectedAccount, {
|
||||||
if (selectedAccountChanged) {
|
account: ['balance', 'nonce'],
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if "sendForm" reducer changed
|
// check if "sendForm" reducer changed
|
||||||
if (!shouldUpdate) {
|
if (!shouldUpdate) {
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||||
|
import { DEVICE } from 'trezor-connect';
|
||||||
import * as WALLET from 'actions/constants/wallet';
|
import * as WALLET from 'actions/constants/wallet';
|
||||||
import * as stateUtils from 'reducers/utils';
|
import * as reducerUtils from 'reducers/utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Device,
|
Device,
|
||||||
TrezorDevice,
|
TrezorDevice,
|
||||||
RouterLocationState,
|
RouterLocationState,
|
||||||
ThunkAction,
|
ThunkAction,
|
||||||
AsyncAction,
|
PayloadAction,
|
||||||
Action,
|
Action,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
GetState,
|
GetState,
|
||||||
@ -80,16 +80,29 @@ 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<boolean> => (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<void> => {
|
|
||||||
const locationChange: boolean = action.type === LOCATION_CHANGE;
|
|
||||||
const state: State = getState();
|
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)
|
// handle devices state change (from trezor-connect events or location change)
|
||||||
if (locationChange || prevState.devices !== state.devices) {
|
if (locationChanged || selectedDeviceChanged) {
|
||||||
const device = stateUtils.getSelectedDevice(state);
|
if (device && reducerUtils.isSelectedDevice(state.wallet.selectedDevice, device)) {
|
||||||
if (state.wallet.selectedDevice !== device) {
|
|
||||||
if (device && stateUtils.isSelectedDevice(state.wallet.selectedDevice, device)) {
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: WALLET.UPDATE_SELECTED_DEVICE,
|
type: WALLET.UPDATE_SELECTED_DEVICE,
|
||||||
device,
|
device,
|
||||||
@ -100,6 +113,7 @@ export const updateSelectedValues = (prevState: State, action: Action): AsyncAct
|
|||||||
device,
|
device,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
};
|
};
|
@ -139,7 +139,7 @@ export const resolvePendingTransactions = (network: string): PromiseAction<void>
|
|||||||
const status = await instance.web3.eth.getTransaction(tx.id);
|
const status = await instance.web3.eth.getTransaction(tx.id);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PENDING.TX_NOT_FOUND,
|
type: PENDING.TX_REJECTED,
|
||||||
tx,
|
tx,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage';
|
export const FROM_STORAGE: 'pending__from_storage' = 'pending__from_storage';
|
||||||
export const ADD: 'pending__add' = 'pending__add';
|
export const ADD: 'pending__add' = 'pending__add';
|
||||||
export const TX_RESOLVED: 'pending__tx_resolved' = 'pending__tx_resolved';
|
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';
|
export const TX_TOKEN_ERROR: 'pending__tx_token_error' = 'pending__tx_token_error';
|
0
src/components/notifications/App/Container.js
Normal file
0
src/components/notifications/App/Container.js
Normal file
0
src/components/notifications/App/index.js
Normal file
0
src/components/notifications/App/index.js
Normal file
@ -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 (
|
||||||
|
<Notification
|
||||||
|
type="error"
|
||||||
|
title={notification.title}
|
||||||
|
message={notification.message}
|
||||||
|
actions={
|
||||||
|
[{
|
||||||
|
label: 'Connect',
|
||||||
|
callback: async () => {
|
||||||
|
await props.blockchainReconnect('trop');
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// return (<Notification type="error" title={notification.title} message={notification.message} />);
|
||||||
|
}
|
||||||
|
return (<Notification type={notification.type} title={notification.title} message={notification.message} />);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
@ -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 => (
|
||||||
|
<Notification
|
||||||
|
key={n.title}
|
||||||
|
type={n.type}
|
||||||
|
title={n.title}
|
||||||
|
message={n.message}
|
||||||
|
cancelable={n.cancelable}
|
||||||
|
actions={n.actions}
|
||||||
|
close={close}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
@ -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<Notification> = [];
|
||||||
|
// if (location.state.device) {
|
||||||
|
// notifications.push(<Notification key="example" type="info" title="Static example" />);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
};
|
55
src/components/notifications/Context/index.js
Normal file
55
src/components/notifications/Context/index.js
Normal file
@ -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<State, 'router'>;
|
||||||
|
notifications: $ElementType<State, 'notifications'>;
|
||||||
|
selectedAccount: $ElementType<State, 'selectedAccount'>;
|
||||||
|
wallet: $ElementType<State, 'wallet'>;
|
||||||
|
blockchain: $ElementType<State, 'blockchain'>;
|
||||||
|
children?: React.Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DispatchProps = {
|
||||||
|
close: typeof NotificationActions.close;
|
||||||
|
blockchainReconnect: typeof reconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Props = StateProps & DispatchProps;
|
||||||
|
|
||||||
|
type OwnProps = {};
|
||||||
|
|
||||||
|
const Notifications = (props: Props) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<StaticNotifications {...props} />
|
||||||
|
<AccountNotifications {...props} />
|
||||||
|
<ActionNotifications {...props} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||||
|
router: state.router,
|
||||||
|
notifications: state.notifications,
|
||||||
|
selectedAccount: state.selectedAccount,
|
||||||
|
wallet: state.wallet,
|
||||||
|
blockchain: state.blockchain,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||||
|
close: bindActionCreators(NotificationActions.close, dispatch),
|
||||||
|
blockchainReconnect: bindActionCreators(reconnect, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);
|
@ -65,7 +65,7 @@ export default function pending(state: State = initialState, action: Action): St
|
|||||||
// return add(state, action.payload);
|
// return add(state, action.payload);
|
||||||
case PENDING.TX_RESOLVED:
|
case PENDING.TX_RESOLVED:
|
||||||
return remove(state, action.tx.id);
|
return remove(state, action.tx.id);
|
||||||
case PENDING.TX_NOT_FOUND:
|
case PENDING.TX_REJECTED:
|
||||||
return reject(state, action.tx.id);
|
return reject(state, action.tx.id);
|
||||||
|
|
||||||
case PENDING.FROM_STORAGE:
|
case PENDING.FROM_STORAGE:
|
||||||
|
@ -11,12 +11,18 @@ import type {
|
|||||||
} from 'flowtype';
|
} from 'flowtype';
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
location?: string;
|
location: string;
|
||||||
account: ?Account;
|
account: ?Account;
|
||||||
network: ?Coin;
|
network: ?Coin;
|
||||||
tokens: Array<Token>,
|
tokens: Array<Token>,
|
||||||
pending: Array<PendingTx>,
|
pending: Array<PendingTx>,
|
||||||
discovery: ?Discovery
|
discovery: ?Discovery,
|
||||||
|
notification: ?{
|
||||||
|
type: string,
|
||||||
|
title: string,
|
||||||
|
message?: string,
|
||||||
|
},
|
||||||
|
shouldRender: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialState: State = {
|
export const initialState: State = {
|
||||||
@ -26,6 +32,8 @@ export const initialState: State = {
|
|||||||
tokens: [],
|
tokens: [],
|
||||||
pending: [],
|
pending: [],
|
||||||
discovery: null,
|
discovery: null,
|
||||||
|
notification: null,
|
||||||
|
shouldRender: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state: State = initialState, action: Action): State => {
|
export default (state: State = initialState, action: Action): State => {
|
||||||
|
@ -116,21 +116,58 @@ export const getWeb3 = (state: State): ?Web3Instance => {
|
|||||||
return state.web3.find(w3 => w3.network === locationState.network);
|
return state.web3.find(w3 => w3.network === locationState.network);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const observeChanges = (prev: ?(Object | Array<any>), current: ?(Object | Array<any>), fields?: Array<string>): boolean => {
|
export const observeChanges = (prev: ?Object, current: ?Object, filter?: {[k: string]: Array<string>}): boolean => {
|
||||||
if (prev !== current) {
|
// 1. both objects are the same (solves simple types like string, boolean and number)
|
||||||
// 1. one of the objects is null/undefined
|
if (prev === current) return false;
|
||||||
|
// 2. one of the objects is null/undefined
|
||||||
if (!prev || !current) return true;
|
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;
|
const prevType = Object.prototype.toString.call(current);
|
||||||
// 3. no nested field to check
|
const currentType = Object.prototype.toString.call(current);
|
||||||
if (!Array.isArray(fields)) return true;
|
// 3. one of the objects has different type then other
|
||||||
// 4. validate nested field
|
if (prevType !== currentType) return true;
|
||||||
if (prev instanceof Object && current instanceof Object) {
|
|
||||||
for (let i = 0; i < fields.length; i++) {
|
if (currentType === '[object Array]') {
|
||||||
const key = fields[i];
|
// 4. Array length is different
|
||||||
if (prev[key] !== current[key]) return true;
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -110,9 +110,12 @@ const LocalStorageService: Middleware = (api: MiddlewareAPI) => (next: Middlewar
|
|||||||
|
|
||||||
case SEND.TX_COMPLETE:
|
case SEND.TX_COMPLETE:
|
||||||
case PENDING.TX_RESOLVED:
|
case PENDING.TX_RESOLVED:
|
||||||
case PENDING.TX_NOT_FOUND:
|
case PENDING.TX_REJECTED:
|
||||||
save(api.dispatch, api.getState);
|
save(api.dispatch, api.getState);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
|
@ -21,7 +21,7 @@ import type {
|
|||||||
/**
|
/**
|
||||||
* Middleware
|
* Middleware
|
||||||
*/
|
*/
|
||||||
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => async (action: Action): Promise<Action> => {
|
||||||
const prevState = api.getState();
|
const prevState = api.getState();
|
||||||
|
|
||||||
// Application live cycle starts HERE!
|
// Application live cycle starts HERE!
|
||||||
@ -88,14 +88,14 @@ const WalletService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispa
|
|||||||
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
|
api.dispatch(NotificationActions.clear(prevLocation.state, currentLocation.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
// observe send form props changes
|
// 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));
|
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));
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
};
|
};
|
||||||
|
@ -12,13 +12,15 @@ import type { State } from 'flowtype';
|
|||||||
import Header from 'components/Header';
|
import Header from 'components/Header';
|
||||||
import Footer from 'components/Footer';
|
import Footer from 'components/Footer';
|
||||||
import ModalContainer from 'components/modals';
|
import ModalContainer from 'components/modals';
|
||||||
import Notifications from 'components/Notification';
|
import ContextNotifications from 'components/notifications/Context';
|
||||||
|
|
||||||
import Log from 'components/Log';
|
import Log from 'components/Log';
|
||||||
|
|
||||||
import LeftNavigation from './components/LeftNavigation/Container';
|
import LeftNavigation from './components/LeftNavigation/Container';
|
||||||
import TopNavigationAccount from './components/TopNavigationAccount';
|
import TopNavigationAccount from './components/TopNavigationAccount';
|
||||||
import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings';
|
import TopNavigationDeviceSettings from './components/TopNavigationDeviceSettings';
|
||||||
|
|
||||||
|
|
||||||
type WalletContainerProps = {
|
type WalletContainerProps = {
|
||||||
wallet: $ElementType<State, 'wallet'>,
|
wallet: $ElementType<State, 'wallet'>,
|
||||||
children?: React.Node
|
children?: React.Node
|
||||||
@ -89,7 +91,7 @@ const Wallet = (props: WalletContainerProps) => (
|
|||||||
<Route path="/device/:device/network/:network/account/:account" component={TopNavigationAccount} />
|
<Route path="/device/:device/network/:network/account/:account" component={TopNavigationAccount} />
|
||||||
<Route path="/device/:device/device-settings" component={TopNavigationDeviceSettings} />
|
<Route path="/device/:device/device-settings" component={TopNavigationDeviceSettings} />
|
||||||
</Navigation>
|
</Navigation>
|
||||||
<Notifications />
|
<ContextNotifications />
|
||||||
<Log />
|
<Log />
|
||||||
<Body>
|
<Body>
|
||||||
{ props.children }
|
{ props.children }
|
||||||
|
@ -10,8 +10,6 @@ import colors from 'config/colors';
|
|||||||
import Tooltip from 'components/Tooltip';
|
import Tooltip from 'components/Tooltip';
|
||||||
import { QRCode } from 'react-qr-svg';
|
import { QRCode } from 'react-qr-svg';
|
||||||
|
|
||||||
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
|
||||||
|
|
||||||
import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables';
|
import { FONT_SIZE, FONT_WEIGHT, FONT_FAMILY } from 'config/variables';
|
||||||
|
|
||||||
import type { Props } from './Container';
|
import type { Props } from './Container';
|
||||||
@ -154,7 +152,6 @@ const AccountReceive = (props: Props) => {
|
|||||||
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
const isAddressHidden = !isAddressVerifying && !addressVerified && !addressUnverified;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedAccount {...props}>
|
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<StyledH2>Receive Ethereum or tokens</StyledH2>
|
<StyledH2>Receive Ethereum or tokens</StyledH2>
|
||||||
<AddressWrapper
|
<AddressWrapper
|
||||||
@ -233,7 +230,6 @@ const AccountReceive = (props: Props) => {
|
|||||||
)}
|
)}
|
||||||
</AddressWrapper>
|
</AddressWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</SelectedAccount>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import { FONT_SIZE, FONT_WEIGHT, TRANSITION } from 'config/variables';
|
|||||||
import colors from 'config/colors';
|
import colors from 'config/colors';
|
||||||
import P from 'components/Paragraph';
|
import P from 'components/Paragraph';
|
||||||
import { H2 } from 'components/Heading';
|
import { H2 } from 'components/Heading';
|
||||||
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
|
||||||
import type { Token } from 'flowtype';
|
import type { Token } from 'flowtype';
|
||||||
import AdvancedForm from './components/AdvancedForm';
|
import AdvancedForm from './components/AdvancedForm';
|
||||||
import PendingTransactions from './components/PendingTransactions';
|
import PendingTransactions from './components/PendingTransactions';
|
||||||
@ -191,6 +190,7 @@ const AccountSend = (props: Props) => {
|
|||||||
network,
|
network,
|
||||||
discovery,
|
discovery,
|
||||||
tokens,
|
tokens,
|
||||||
|
shouldRender,
|
||||||
} = props.selectedAccount;
|
} = props.selectedAccount;
|
||||||
const {
|
const {
|
||||||
address,
|
address,
|
||||||
@ -219,6 +219,8 @@ const AccountSend = (props: Props) => {
|
|||||||
onSend,
|
onSend,
|
||||||
} = props.sendFormActions;
|
} = props.sendFormActions;
|
||||||
|
|
||||||
|
if (!device || !account || !discovery || !network || !shouldRender) return null;
|
||||||
|
|
||||||
const isCurrentCurrencyToken = networkSymbol !== currency;
|
const isCurrentCurrencyToken = networkSymbol !== currency;
|
||||||
|
|
||||||
let selectedTokenBalance = 0;
|
let selectedTokenBalance = 0;
|
||||||
@ -227,8 +229,6 @@ const AccountSend = (props: Props) => {
|
|||||||
selectedTokenBalance = selectedToken.balance;
|
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 isSendButtonDisabled: boolean = Object.keys(errors).length > 0 || total === '0' || amount.length === 0 || address.length === 0 || sending;
|
||||||
let sendButtonText: string = 'Send';
|
let sendButtonText: string = 'Send';
|
||||||
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
if (networkSymbol !== currency && amount.length > 0 && !errors.amount) {
|
||||||
@ -252,7 +252,6 @@ const AccountSend = (props: Props) => {
|
|||||||
const isAdvancedSettingsHidden = !advanced;
|
const isAdvancedSettingsHidden = !advanced;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedAccount {...props}>
|
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<StyledH2>Send Ethereum or tokens</StyledH2>
|
<StyledH2>Send Ethereum or tokens</StyledH2>
|
||||||
<InputRow>
|
<InputRow>
|
||||||
@ -402,7 +401,6 @@ const AccountSend = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</SelectedAccount>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import Tooltip from 'components/Tooltip';
|
|||||||
|
|
||||||
import CoinLogo from 'components/images/CoinLogo';
|
import CoinLogo from 'components/images/CoinLogo';
|
||||||
import * as stateUtils from 'reducers/utils';
|
import * as stateUtils from 'reducers/utils';
|
||||||
import SelectedAccount from 'views/Wallet/components/SelectedAccount';
|
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
import AccountBalance from './components/AccountBalance';
|
import AccountBalance from './components/AccountBalance';
|
||||||
import AddedToken from './components/AddedToken';
|
import AddedToken from './components/AddedToken';
|
||||||
@ -65,17 +64,18 @@ const AccountSummary = (props: Props) => {
|
|||||||
network,
|
network,
|
||||||
tokens,
|
tokens,
|
||||||
pending,
|
pending,
|
||||||
|
shouldRender,
|
||||||
} = props.selectedAccount;
|
} = props.selectedAccount;
|
||||||
|
|
||||||
// flow
|
// flow
|
||||||
if (!device || !account || !network) return <SelectedAccount {...props} />;
|
if (!device || !account || !network || !shouldRender) return null;
|
||||||
|
|
||||||
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
const explorerLink: string = `${network.explorer.address}${account.address}`;
|
||||||
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
const pendingAmount: BigNumber = stateUtils.getPendingAmount(pending, network.symbol);
|
||||||
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
const balance: string = new BigNumber(account.balance).minus(pendingAmount).toString(10);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectedAccount {...props}>
|
<React.Fragment>
|
||||||
<AccountHeading>
|
<AccountHeading>
|
||||||
<AccountName>
|
<AccountName>
|
||||||
<StyledCoinLogo coinNetwork={account.network} />
|
<StyledCoinLogo coinNetwork={account.network} />
|
||||||
@ -155,7 +155,7 @@ const AccountSummary = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</AddedTokensWrapper>
|
</AddedTokensWrapper>
|
||||||
</SelectedAccount>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user