mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Alert user upon switching to unconnected account (#8312)
An alert is now shown when the user switches from an account that is connected to the active tab to an account that is not connected. The alert prompts the user to dismiss the alert or connect the account they're switching to. The "loading" state is handled by disabling the buttons, and the error state is handled by displaying a generic error message and disabling the connect button. The new reducer for this alert has been created with `createSlice` from the Redux Toolkit. This utility is recommended by the Redux team, and represents a new style of writing reducers that I hope we will use more in the future (or at least something similar). `createSlice` constructs a reducer, actions, and action creators automatically. The reducer is constructed using their `createReducer` helper, which uses Immer to allow directly mutating the state in the reducer but exposing these changes as immutable.
This commit is contained in:
parent
7302a14341
commit
53feb20803
@ -588,6 +588,9 @@
|
||||
"failed": {
|
||||
"message": "Failed"
|
||||
},
|
||||
"failureMessage": {
|
||||
"message": "Something went wrong, and we were unable to complete the action"
|
||||
},
|
||||
"fast": {
|
||||
"message": "Fast"
|
||||
},
|
||||
@ -1530,6 +1533,12 @@
|
||||
"unapproved": {
|
||||
"message": "Unapproved"
|
||||
},
|
||||
"unconnectedAccountAlertTitle": {
|
||||
"message": "Not connected"
|
||||
},
|
||||
"unconnectedAccountAlertDescription": {
|
||||
"message": "This account is not connected to this site"
|
||||
},
|
||||
"units": {
|
||||
"message": "units"
|
||||
},
|
||||
|
@ -521,5 +521,8 @@
|
||||
],
|
||||
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
|
||||
"errors": {}
|
||||
},
|
||||
"unconnectedAccount": {
|
||||
"state": "CLOSED"
|
||||
}
|
||||
}
|
||||
|
@ -472,5 +472,8 @@
|
||||
],
|
||||
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
|
||||
"errors": {}
|
||||
},
|
||||
"unconnectedAccount": {
|
||||
"state": "CLOSED"
|
||||
}
|
||||
}
|
||||
|
@ -1403,5 +1403,8 @@
|
||||
"priceAndTimeEstimatesLastRetrieved": 1541527901281,
|
||||
"errors": {}
|
||||
},
|
||||
"confirmTransaction": {}
|
||||
"confirmTransaction": {},
|
||||
"unconnectedAccount": {
|
||||
"state": "CLOSED"
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,7 @@
|
||||
"@metamask/eth-token-tracker": "^2.0.0",
|
||||
"@metamask/etherscan-link": "^1.1.0",
|
||||
"@metamask/inpage-provider": "^5.0.0",
|
||||
"@reduxjs/toolkit": "^1.3.2",
|
||||
"@sentry/browser": "^5.11.1",
|
||||
"@sentry/integrations": "^5.11.1",
|
||||
"@zxing/library": "^0.8.0",
|
||||
|
@ -232,6 +232,7 @@ describe('Actions', function () {
|
||||
|
||||
const expectedActions = [
|
||||
'SHOW_LOADING_INDICATION',
|
||||
'SELECTED_ADDRESS_CHANGED',
|
||||
'UPDATE_METAMASK_STATE',
|
||||
'HIDE_LOADING_INDICATION',
|
||||
'SHOW_ACCOUNTS_PAGE',
|
||||
@ -979,7 +980,7 @@ describe('Actions', function () {
|
||||
it('#showAccountDetail', async function () {
|
||||
setSelectedAddressSpy = sinon.stub(background, 'setSelectedAddress')
|
||||
.callsArgWith(1, null)
|
||||
const store = mockStore()
|
||||
const store = mockStore({ metamask: { selectedAddress: '0x123' } })
|
||||
|
||||
await store.dispatch(actions.showAccountDetail())
|
||||
assert(setSelectedAddressSpy.calledOnce)
|
||||
@ -988,7 +989,7 @@ describe('Actions', function () {
|
||||
it('displays warning if setSelectedAddress throws', async function () {
|
||||
setSelectedAddressSpy = sinon.stub(background, 'setSelectedAddress')
|
||||
.callsArgWith(1, new Error('error'))
|
||||
const store = mockStore()
|
||||
const store = mockStore({ metamask: { selectedAddress: '0x123' } })
|
||||
const expectedActions = [
|
||||
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
|
||||
{ type: 'HIDE_LOADING_INDICATION' },
|
||||
|
19
ui/app/components/app/alerts/alerts.js
Normal file
19
ui/app/components/app/alerts/alerts.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import UnconnectedAccountAlert from './unconnected-account-alert'
|
||||
import { alertIsOpen as unconnectedAccountAlertIsOpen } from '../../../ducks/alerts/unconnected-account'
|
||||
|
||||
const Alerts = () => {
|
||||
const _unconnectedAccountAlertIsOpen = useSelector(unconnectedAccountAlertIsOpen)
|
||||
|
||||
if (_unconnectedAccountAlertIsOpen) {
|
||||
return (
|
||||
<UnconnectedAccountAlert />
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default Alerts
|
1
ui/app/components/app/alerts/alerts.scss
Normal file
1
ui/app/components/app/alerts/alerts.scss
Normal file
@ -0,0 +1 @@
|
||||
@import './unconnected-account-alert/unconnected-account-alert.scss';
|
1
ui/app/components/app/alerts/index.js
Normal file
1
ui/app/components/app/alerts/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './alerts'
|
@ -0,0 +1 @@
|
||||
export { default } from './unconnected-account-alert'
|
@ -0,0 +1,63 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import {
|
||||
ALERT_STATE,
|
||||
connectAccount,
|
||||
dismissAlert,
|
||||
getAlertState,
|
||||
} from '../../../../ducks/alerts/unconnected-account'
|
||||
import { I18nContext } from '../../../../contexts/i18n'
|
||||
import Popover from '../../../ui/popover'
|
||||
import Button from '../../../ui/button'
|
||||
|
||||
const {
|
||||
ERROR,
|
||||
LOADING,
|
||||
} = ALERT_STATE
|
||||
|
||||
const SwitchToUnconnectedAccountAlert = () => {
|
||||
const t = useContext(I18nContext)
|
||||
const dispatch = useDispatch()
|
||||
const alertState = useSelector(getAlertState)
|
||||
|
||||
return (
|
||||
<Popover
|
||||
title={t('unconnectedAccountAlertTitle')}
|
||||
subtitle={t('unconnectedAccountAlertDescription')}
|
||||
onClose={() => dispatch(dismissAlert())}
|
||||
footer={(
|
||||
<>
|
||||
{
|
||||
alertState === ERROR
|
||||
? (
|
||||
<div className="unconnected-account-alert__error">
|
||||
{ t('failureMessage') }
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className="unconnected-account-alert__footer-buttons">
|
||||
<Button
|
||||
disabled={alertState === LOADING}
|
||||
onClick={() => dispatch(dismissAlert())}
|
||||
type="secondary"
|
||||
>
|
||||
{ t('dismiss') }
|
||||
</Button>
|
||||
<Button
|
||||
disabled={alertState === LOADING || alertState === ERROR}
|
||||
onClick={() => dispatch(connectAccount())}
|
||||
type="primary"
|
||||
>
|
||||
{ t('connect') }
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
footerClassName="unconnected-account-alert__footer"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwitchToUnconnectedAccountAlert
|
@ -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;
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
|
||||
@import 'add-token-button/index';
|
||||
|
||||
@import 'alerts/alerts.scss';
|
||||
|
||||
@import 'app-header/index';
|
||||
|
||||
@import 'asset-list/asset-list.scss';
|
||||
|
5
ui/app/ducks/alerts/index.js
Normal file
5
ui/app/ducks/alerts/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import unconnectedAccount from './unconnected-account'
|
||||
|
||||
export default {
|
||||
unconnectedAccount,
|
||||
}
|
90
ui/app/ducks/alerts/unconnected-account.js
Normal file
90
ui/app/ducks/alerts/unconnected-account.js
Normal file
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 && <LoadingNetwork /> }
|
||||
{ this.renderRoutes() }
|
||||
</div>
|
||||
<Alerts />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
20
yarn.lock
20
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"
|
||||
|
Loading…
Reference in New Issue
Block a user