diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b8656abe0..6dadd1b45 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -148,6 +148,18 @@ "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, + "alerts": { + "message": "Alerts" + }, + "alertsSettingsDescription": { + "message": "Enable or disable each alert" + }, + "alertSettingsUnconnectedAccount": { + "message": "Switching to an unconnected account" + }, + "alertSettingsUnconnectedAccountDescription": { + "message": "This alert is shown in the popup when you switch from a connected account to an unconnected account." + }, "allowOriginSpendToken": { "message": "Allow $1 to spend your $2?", "description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend" @@ -462,6 +474,9 @@ "done": { "message": "Done" }, + "dontShowThisAgain": { + "message": "Don't show this again" + }, "downloadGoogleChrome": { "message": "Download Google Chrome" }, @@ -1540,6 +1555,9 @@ "unconnectedAccountAlertDescription": { "message": "This account is not connected to this site" }, + "unconnectedAccountAlertDisableTooltip": { + "message": "This can be changed in \"Settings > Alerts\"" + }, "units": { "message": "units" }, diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js new file mode 100644 index 000000000..5a7b173c8 --- /dev/null +++ b/app/scripts/controllers/alert.js @@ -0,0 +1,54 @@ +import ObservableStore from 'obs-store' + +/** + * @typedef {Object} AlertControllerInitState + * @property {Object} alertEnabledness - A map of any alerts that were suppressed keyed by alert ID, where the value + * is the timestamp of when the user suppressed the alert. + */ + +/** + * @typedef {Object} AlertControllerOptions + * @property {AlertControllerInitState} initState - The initial controller state + */ + +export const ALERT_TYPES = { + unconnectedAccount: 'unconnectedAccount', +} + +const defaultState = { + alertEnabledness: Object.keys(ALERT_TYPES) + .reduce( + (alertEnabledness, alertType) => { + alertEnabledness[alertType] = true + return alertEnabledness + }, + {} + ), +} + +/** + * Controller responsible for maintaining + * alert related state + */ +export default class AlertController { + /** + * @constructor + * @param {AlertControllerOptions} [opts] - Controller configuration parameters + */ + constructor (opts = {}) { + const { initState } = opts + const state = Object.assign( + {}, + defaultState, + initState, + ) + this.store = new ObservableStore(state) + } + + setAlertEnabledness (alertId, enabledness) { + let { alertEnabledness } = this.store.getState() + alertEnabledness = { ...alertEnabledness } + alertEnabledness[alertId] = enabledness + this.store.updateState({ alertEnabledness }) + } +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index e3364cece..a6f99554f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -31,6 +31,7 @@ import PreferencesController from './controllers/preferences' import AppStateController from './controllers/app-state' import InfuraController from './controllers/infura' import CachedBalancesController from './controllers/cached-balances' +import AlertController from './controllers/alert' import OnboardingController from './controllers/onboarding' import ThreeBoxController from './controllers/threebox' import RecentBlocksController from './controllers/recent-blocks' @@ -233,6 +234,8 @@ export default class MetamaskController extends EventEmitter { this.addressBookController = new AddressBookController(undefined, initState.AddressBookController) + this.alertController = new AlertController({ initState: initState.AlertController }) + this.threeBoxController = new ThreeBoxController({ preferencesController: this.preferencesController, addressBookController: this.addressBookController, @@ -305,6 +308,7 @@ export default class MetamaskController extends EventEmitter { NetworkController: this.networkController.store, InfuraController: this.infuraController.store, CachedBalancesController: this.cachedBalancesController.store, + AlertController: this.alertController.store, OnboardingController: this.onboardingController.store, IncomingTransactionsController: this.incomingTransactionsController.store, ABTestController: this.abTestController.store, @@ -331,6 +335,7 @@ export default class MetamaskController extends EventEmitter { AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, InfuraController: this.infuraController.store, + AlertController: this.alertController.store, OnboardingController: this.onboardingController.store, IncomingTransactionsController: this.incomingTransactionsController.store, PermissionsController: this.permissionsController.permissions, @@ -440,6 +445,7 @@ export default class MetamaskController extends EventEmitter { const keyringController = this.keyringController const networkController = this.networkController const onboardingController = this.onboardingController + const alertController = this.alertController const permissionsController = this.permissionsController const preferencesController = this.preferencesController const threeBoxController = this.threeBoxController @@ -556,6 +562,9 @@ export default class MetamaskController extends EventEmitter { // onboarding controller setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController), + // alert controller + setAlertEnabledness: nodeify(alertController.setAlertEnabledness, alertController), + // 3Box setThreeBoxSyncingPermission: nodeify(threeBoxController.setThreeBoxSyncingPermission, threeBoxController), restoreFromThreeBox: nodeify(threeBoxController.restoreFromThreeBox, threeBoxController), diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 068bc90f9..645365e00 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -980,7 +980,7 @@ describe('Actions', function () { it('#showAccountDetail', async function () { setSelectedAddressSpy = sinon.stub(background, 'setSelectedAddress') .callsArgWith(1, null) - const store = mockStore({ metamask: { selectedAddress: '0x123' } }) + const store = mockStore({ metamask: { alertEnabledness: {}, selectedAddress: '0x123' } }) await store.dispatch(actions.showAccountDetail()) assert(setSelectedAddressSpy.calledOnce) @@ -989,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({ metamask: { selectedAddress: '0x123' } }) + const store = mockStore({ metamask: { alertEnabledness: {}, selectedAddress: '0x123' } }) const expectedActions = [ { type: 'SHOW_LOADING_INDICATION', value: undefined }, { type: 'HIDE_LOADING_INDICATION' }, diff --git a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js index a522e7803..6deeda85f 100644 --- a/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js +++ b/ui/app/components/app/alerts/unconnected-account-alert/unconnected-account-alert.js @@ -1,15 +1,18 @@ -import React, { useContext } from 'react' +import React, { useContext, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { ALERT_STATE, connectAccount, dismissAlert, + dismissAndDisableAlert, getAlertState, } from '../../../../ducks/alerts/unconnected-account' import { I18nContext } from '../../../../contexts/i18n' import Popover from '../../../ui/popover' import Button from '../../../ui/button' +import Checkbox from '../../../ui/check-box' +import Tooltip from '../../../ui/tooltip-v2' const { ERROR, @@ -20,12 +23,20 @@ const SwitchToUnconnectedAccountAlert = () => { const t = useContext(I18nContext) const dispatch = useDispatch() const alertState = useSelector(getAlertState) + const [dontShowThisAgain, setDontShowThisAgain] = useState(false) + + const onClose = async () => { + return dontShowThisAgain + ? await dispatch(dismissAndDisableAlert()) + : dispatch(dismissAlert()) + } return ( dispatch(dismissAlert())} + onClose={onClose} footer={( <> { @@ -40,13 +51,13 @@ const SwitchToUnconnectedAccountAlert = () => {