dispatch(dismissAlert())}
+ footer={(
+ <>
+ {
+ alertState === ERROR
+ ? (
+
+ { t('failureMessage') }
+
+ )
+ : null
+ }
+
+
+
+
+ >
+ )}
+ footerClassName="unconnected-account-alert__footer"
+ />
+ )
+}
+
+export default SwitchToUnconnectedAccountAlert
diff --git a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss
new file mode 100644
index 000000000000..1a1b75f7f1c6
--- /dev/null
+++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss
@@ -0,0 +1,33 @@
+.unconnected-account-alert {
+ &__footer {
+ flex-direction: column;
+
+ :only-child {
+ margin: 0;
+ }
+ }
+
+ &__footer-buttons {
+ display: flex;
+ flex-direction: row;
+
+ button:first-child {
+ margin-right: 24px;
+ }
+
+ button {
+ font-size: 14px;
+ line-height: 20px;
+ padding: 8px;
+ }
+ }
+
+ &__error {
+ margin-bottom: 16px;
+ padding: 16px;
+ font-size: 14px;
+ border: 1px solid #D73A49;
+ background: #F8EAE8;
+ border-radius: 3px;
+ }
+}
diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss
index 933d35ced891..49468037a3c8 100644
--- a/ui/app/components/app/index.scss
+++ b/ui/app/components/app/index.scss
@@ -4,6 +4,8 @@
@import 'add-token-button/index';
+@import 'alerts/alerts.scss';
+
@import 'app-header/index';
@import 'asset-list/asset-list.scss';
diff --git a/ui/app/ducks/alerts/index.js b/ui/app/ducks/alerts/index.js
new file mode 100644
index 000000000000..3a7cd3919a0c
--- /dev/null
+++ b/ui/app/ducks/alerts/index.js
@@ -0,0 +1,5 @@
+import unconnectedAccount from './unconnected-account'
+
+export default {
+ unconnectedAccount,
+}
diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js
new file mode 100644
index 000000000000..92ed697984e0
--- /dev/null
+++ b/ui/app/ducks/alerts/unconnected-account.js
@@ -0,0 +1,90 @@
+import { createSlice } from '@reduxjs/toolkit'
+import { captureException } from '@sentry/browser'
+
+import actionConstants from '../../store/actionConstants'
+import { addPermittedAccount } from '../../store/actions'
+import { getOriginOfCurrentTab, getSelectedAddress } from '../../selectors/selectors'
+
+// Constants
+
+export const ALERT_STATE = {
+ CLOSED: 'CLOSED',
+ ERROR: 'ERROR',
+ LOADING: 'LOADING',
+ OPEN: 'OPEN',
+}
+
+const name = 'unconnectedAccount'
+
+const initialState = {
+ state: ALERT_STATE.CLOSED,
+}
+
+// Slice (reducer plus auto-generated actions and action creators)
+
+const slice = createSlice({
+ name,
+ initialState,
+ reducers: {
+ connectAccountFailed: (state) => {
+ state.state = ALERT_STATE.ERROR
+ },
+ connectAccountRequested: (state) => {
+ state.state = ALERT_STATE.LOADING
+ },
+ connectAccountSucceeded: (state) => {
+ state.state = ALERT_STATE.CLOSED
+ },
+ dismissAlert: (state) => {
+ state.state = ALERT_STATE.CLOSED
+ },
+ switchedToUnconnectedAccount: (state) => {
+ state.state = ALERT_STATE.OPEN
+ },
+ },
+ extraReducers: {
+ [actionConstants.SELECTED_ADDRESS_CHANGED]: (state) => {
+ // close the alert if the account is switched while it's open
+ if (state.state === ALERT_STATE.OPEN) {
+ state.state = ALERT_STATE.CLOSED
+ }
+ },
+ },
+})
+
+const { actions, reducer } = slice
+
+export default reducer
+
+// Selectors
+
+export const getAlertState = (state) => state[name].state
+
+export const alertIsOpen = (state) => state[name].state !== ALERT_STATE.CLOSED
+
+// Actions / action-creators
+
+export const {
+ connectAccountFailed,
+ connectAccountRequested,
+ connectAccountSucceeded,
+ dismissAlert,
+ switchedToUnconnectedAccount,
+} = actions
+
+export const connectAccount = () => {
+ return async (dispatch, getState) => {
+ const state = getState()
+ const selectedAddress = getSelectedAddress(state)
+ const origin = getOriginOfCurrentTab(state)
+ try {
+ await dispatch(connectAccountRequested())
+ await dispatch(addPermittedAccount(origin, selectedAddress))
+ await dispatch(connectAccountSucceeded())
+ } catch (error) {
+ console.error(error)
+ captureException(error)
+ await dispatch(connectAccountFailed())
+ }
+ }
+}
diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js
index d0531dceedd8..a63fbe56ce49 100644
--- a/ui/app/ducks/index.js
+++ b/ui/app/ducks/index.js
@@ -5,8 +5,10 @@ import sendReducer from './send/send.duck'
import appStateReducer from './app/app'
import confirmTransactionReducer from './confirm-transaction/confirm-transaction.duck'
import gasReducer from './gas/gas.duck'
+import alerts from './alerts'
export default combineReducers({
+ ...alerts,
activeTab: (s) => (s === undefined ? null : s),
metamask: metamaskReducer,
appState: appStateReducer,
diff --git a/ui/app/pages/routes/routes.component.js b/ui/app/pages/routes/routes.component.js
index 7a8d9bacf3f4..e02d2bb77073 100644
--- a/ui/app/pages/routes/routes.component.js
+++ b/ui/app/pages/routes/routes.component.js
@@ -29,6 +29,7 @@ import { Modal } from '../../components/app/modals'
import Alert from '../../components/ui/alert'
import AppHeader from '../../components/app/app-header'
import UnlockPage from '../unlock-page'
+import Alerts from '../../components/app/alerts'
import {
ADD_TOKEN_ROUTE,
@@ -251,6 +252,7 @@ export default class Routes extends Component {
{ !isLoading && isLoadingNetwork && }
{ this.renderRoutes() }
+
)
}
diff --git a/ui/app/store/actionConstants.js b/ui/app/store/actionConstants.js
index 02396e2ee952..a1ec4488f212 100644
--- a/ui/app/store/actionConstants.js
+++ b/ui/app/store/actionConstants.js
@@ -18,6 +18,7 @@ export default {
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
+ SELECTED_ADDRESS_CHANGED: 'SELECTED_ADDRESS_CHANGED',
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN',
// unlock screen
diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js
index e44ec27948fa..7cba6e57336d 100644
--- a/ui/app/store/actions.js
+++ b/ui/app/store/actions.js
@@ -15,6 +15,11 @@ import { setCustomGasLimit } from '../ducks/gas/gas.duck'
import txHelper from '../../lib/tx-helper'
import { getEnvironmentType } from '../../../app/scripts/lib/util'
import actionConstants from './actionConstants'
+import {
+ getPermittedAccountsForCurrentTab,
+ getSelectedAddress,
+} from '../selectors/selectors'
+import { switchedToUnconnectedAccount } from '../ducks/alerts/unconnected-account'
let background = null
let promisifiedBackground = null
@@ -1089,12 +1094,21 @@ export function updateMetamaskState (newState) {
return (dispatch, getState) => {
const { metamask: currentState } = getState()
- const { currentLocale } = currentState
- const { currentLocale: newLocale } = newState
+ const {
+ currentLocale,
+ selectedAddress,
+ } = currentState
+ const {
+ currentLocale: newLocale,
+ selectedAddress: newSelectedAddress,
+ } = newState
if (currentLocale && newLocale && currentLocale !== newLocale) {
dispatch(updateCurrentLocale(newLocale))
}
+ if (selectedAddress !== newSelectedAddress) {
+ dispatch({ type: actionConstants.SELECTED_ADDRESS_CHANGED })
+ }
dispatch({
type: actionConstants.UPDATE_METAMASK_STATE,
@@ -1161,10 +1175,17 @@ export function setSelectedAddress (address) {
}
export function showAccountDetail (address) {
- return async (dispatch) => {
+ return async (dispatch, getState) => {
dispatch(showLoadingIndication())
log.debug(`background.setSelectedAddress`)
+ const state = getState()
+ const selectedAddress = getSelectedAddress(state)
+ const permittedAccountsForCurrentTab = getPermittedAccountsForCurrentTab(state)
+ const currentTabIsConnectedToPreviousAddress = permittedAccountsForCurrentTab.includes(selectedAddress)
+ const currentTabIsConnectedToNextAddress = permittedAccountsForCurrentTab.includes(address)
+ const switchingToUnconnectedAddress = currentTabIsConnectedToPreviousAddress && !currentTabIsConnectedToNextAddress
+
let tokens
try {
tokens = await promisifiedBackground.setSelectedAddress(address)
@@ -1179,6 +1200,9 @@ export function showAccountDetail (address) {
type: actionConstants.SHOW_ACCOUNT_DETAIL,
value: address,
})
+ if (switchingToUnconnectedAddress) {
+ dispatch(switchedToUnconnectedAccount())
+ }
dispatch(setSelectedToken())
}
}
diff --git a/yarn.lock b/yarn.lock
index 8bb1123d3c23..6c43c0403569 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1981,6 +1981,16 @@
react-lifecycles-compat "^3.0.4"
warning "^3.0.0"
+"@reduxjs/toolkit@^1.3.2":
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.3.2.tgz#cbd062f0b806eb4611afeb2b30240e5186d6dd27"
+ integrity sha512-IRI9Nx6Ys/u4NDqPvUC0+e8MH+e1VME9vn30xAmd+MBqDsClc0Dhrlv4Scw2qltRy/mrINarU6BqJp4/dcyyFg==
+ dependencies:
+ immer "^6.0.1"
+ redux "^4.0.0"
+ redux-thunk "^2.3.0"
+ reselect "^4.0.0"
+
"@sentry/browser@^5.11.1":
version "5.11.1"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.1.tgz#337ffcb52711b23064c847a07629e966f54a5ebb"
@@ -14209,6 +14219,11 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
+immer@^6.0.1:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.3.tgz#94d5051cd724668160a900d66d85ec02816f29bd"
+ integrity sha512-12VvNrfSrXZdm/BJgi/KDW2soq5freVSf3I1+4CLunUM8mAGx2/0Njy0xBVzi5zewQZiwM7z1/1T+8VaI7NkmQ==
+
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -24342,6 +24357,11 @@ reselect@^3.0.1:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
+reselect@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
+ integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
+
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"