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..3729d870a853
--- /dev/null
+++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.scss
@@ -0,0 +1,27 @@
+.unconnected-account-alert {
+ &__footer {
+ flex-direction: column;
+
+ :only-child {
+ margin: 0;
+ }
+ }
+
+ &__footer-buttons {
+ display: flex;
+ flex-direction: row;
+
+ button:first-child {
+ margin-right: 24px;
+ }
+ }
+
+ &__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..bb2e0cb1a10e
--- /dev/null
+++ b/ui/app/ducks/alerts/unconnected-account.js
@@ -0,0 +1,101 @@
+import { createSlice } from '@reduxjs/toolkit'
+import { captureException } from '@sentry/browser'
+
+import actionConstants from '../../store/actionConstants'
+import { addPermittedAccount } from '../../store/actions'
+import { getOriginOfCurrentTab } 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,
+ address: null,
+}
+
+// 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
+ state.address = null
+ },
+ dismissAlert: (state) => {
+ state.state = ALERT_STATE.CLOSED
+ state.address = null
+ },
+ switchedToUnconnectedAccount: (state, action) => {
+ state.state = ALERT_STATE.OPEN
+ state.address = action.payload
+ },
+ },
+ extraReducers: {
+ [actionConstants.UPDATE_METAMASK_STATE]: (state, action) => {
+ // close the alert if the account is switched while it's open
+ if (
+ state.state === ALERT_STATE.OPEN &&
+ state.address !== action.value.selectedAddress
+ ) {
+ state.state = ALERT_STATE.CLOSED
+ state.address = null
+ }
+ },
+ },
+})
+
+const { actions, reducer } = slice
+
+export default reducer
+
+// Selectors
+
+export const getAlertState = (state) => state[name].state
+
+export const getAlertAccountAddress = (state) => state[name].address
+
+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 address = getAlertAccountAddress(state)
+ const origin = getOriginOfCurrentTab(state)
+ try {
+ await dispatch(connectAccountRequested())
+ await dispatch(addPermittedAccount(origin, address))
+ 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/actions.js b/ui/app/store/actions.js
index 25119530bd72..cbaa4df43940 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
@@ -1161,10 +1166,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 +1191,9 @@ export function showAccountDetail (address) {
type: actionConstants.SHOW_ACCOUNT_DETAIL,
value: address,
})
+ if (switchingToUnconnectedAddress) {
+ dispatch(switchedToUnconnectedAccount(address))
+ }
dispatch(setSelectedToken())
}
}
diff --git a/yarn.lock b/yarn.lock
index e1092b8da128..36e0e27dfaf9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1966,6 +1966,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"
@@ -14194,6 +14204,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"