diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index b9b2497be..c1c969b13 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -95,7 +95,13 @@ "message": "Browsing a website with an unconnected account selected" }, "alertSettingsUnconnectedAccountDescription": { - "message": "This alert is shown in the popup when you are browsing a connected Web3 site, but the currently selected account is not connected." + "message": "This alert is shown in the popup when you are browsing a connected web3 site, but the currently selected account is not connected." + }, + "alertSettingsWeb3ShimUsage": { + "message": "When a website tries to use the removed window.web3 API" + }, + "alertSettingsWeb3ShimUsageDescription": { + "message": "This alert is shown in the popup when you are browsing a site that tries to use the removed window.web3 API, and may be broken as a result." }, "alerts": { "message": "Alerts" @@ -233,6 +239,9 @@ "bytes": { "message": "Bytes" }, + "canToggleInSettings": { + "message": "You can re-enable this notification in Settings -> Alerts." + }, "cancel": { "message": "Cancel" }, @@ -2095,6 +2104,10 @@ "walletSeed": { "message": "Seed phrase" }, + "web3ShimUsageNotification": { + "message": "We noticed that the current website tried to use the removed window.web3 API. If the site appears to be broken, please click $1 for more information.", + "description": "$1 is a clickable link." + }, "welcome": { "message": "Welcome to MetaMask" }, diff --git a/app/images/icons/connect.svg b/app/images/icons/connect.svg deleted file mode 100644 index ac6577090..000000000 --- a/app/images/icons/connect.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/scripts/controllers/alert.js b/app/scripts/controllers/alert.js index a2dd3a7db..4dc215f35 100644 --- a/app/scripts/controllers/alert.js +++ b/app/scripts/controllers/alert.js @@ -1,4 +1,8 @@ import ObservableStore from 'obs-store' +import { + TOGGLEABLE_ALERT_TYPES, + WEB3_SHIM_USAGE_ALERT_STATES, +} from '../../../shared/constants/alerts' /** * @typedef {Object} AlertControllerInitState @@ -14,14 +18,8 @@ import ObservableStore from 'obs-store' * @property {AlertControllerInitState} initState - The initial controller state */ -export const ALERT_TYPES = { - unconnectedAccount: 'unconnectedAccount', - // enumerated here but has no background state - invalidCustomNetwork: 'invalidCustomNetwork', -} - const defaultState = { - alertEnabledness: Object.keys(ALERT_TYPES).reduce( + alertEnabledness: TOGGLEABLE_ALERT_TYPES.reduce( (alertEnabledness, alertType) => { alertEnabledness[alertType] = true return alertEnabledness @@ -29,11 +27,11 @@ const defaultState = { {}, ), unconnectedAccountAlertShownOrigins: {}, + web3ShimUsageOrigins: {}, } /** - * Controller responsible for maintaining - * alert related state + * Controller responsible for maintaining alert-related state. */ export default class AlertController { /** @@ -41,11 +39,13 @@ export default class AlertController { * @param {AlertControllerOptions} [opts] - Controller configuration parameters */ constructor(opts = {}) { - const { initState, preferencesStore } = opts + const { initState = {}, preferencesStore } = opts const state = { ...defaultState, - ...initState, - unconnectedAccountAlertShownOrigins: {}, + alertEnabledness: { + ...defaultState.alertEnabledness, + ...initState.alertEnabledness, + }, } this.store = new ObservableStore(state) @@ -83,4 +83,48 @@ export default class AlertController { unconnectedAccountAlertShownOrigins[origin] = true this.store.updateState({ unconnectedAccountAlertShownOrigins }) } + + /** + * Gets the web3 shim usage state for the given origin. + * + * @param {string} origin - The origin to get the web3 shim usage state for. + * @returns {undefined | 1 | 2} The web3 shim usage state for the given + * origin, or undefined. + */ + getWeb3ShimUsageState(origin) { + return this.store.getState().web3ShimUsageOrigins[origin] + } + + /** + * Sets the web3 shim usage state for the given origin to RECORDED. + * + * @param {string} origin - The origin the that used the web3 shim. + */ + setWeb3ShimUsageRecorded(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.RECORDED) + } + + /** + * Sets the web3 shim usage state for the given origin to DISMISSED. + * + * @param {string} origin - The origin that the web3 shim notification was + * dismissed for. + */ + setWeb3ShimUsageAlertDismissed(origin) { + this._setWeb3ShimUsageState(origin, WEB3_SHIM_USAGE_ALERT_STATES.DISMISSED) + } + + /** + * @private + * @param {string} origin - The origin to set the state for. + * @param {number} value - The state value to set. + */ + _setWeb3ShimUsageState(origin, value) { + let { web3ShimUsageOrigins } = this.store.getState() + web3ShimUsageOrigins = { + ...web3ShimUsageOrigins, + } + web3ShimUsageOrigins[origin] = value + this.store.updateState({ web3ShimUsageOrigins }) + } } diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js index 142e46f44..21e908000 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-shim-usage.js @@ -13,11 +13,13 @@ const logWeb3ShimUsage = { } export default logWeb3ShimUsage -const recordedWeb3ShimUsage = {} - /** * @typedef {Object} LogWeb3ShimUsageOptions * @property {Function} sendMetrics - A function that registers a metrics event. + * @property {Function} getWeb3ShimUsageState - A function that gets web3 shim + * usage state for the given origin. + * @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim + * usage for a particular origin. */ /** @@ -27,10 +29,16 @@ const recordedWeb3ShimUsage = {} * @param {Function} end - The json-rpc-engine 'end' callback. * @param {LogWeb3ShimUsageOptions} options */ -function logWeb3ShimUsageHandler(req, res, _next, end, { sendMetrics }) { +function logWeb3ShimUsageHandler( + req, + res, + _next, + end, + { sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded }, +) { const { origin } = req - if (!recordedWeb3ShimUsage[origin]) { - recordedWeb3ShimUsage[origin] = true + if (getWeb3ShimUsageState(origin) === undefined) { + setWeb3ShimUsageRecorded(origin) sendMetrics({ event: `Website Accessed window.web3 Shim`, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0e976a994..85135f759 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -514,16 +514,16 @@ export default class MetamaskController extends EventEmitter { */ getApi() { const { + alertController, keyringController, + metaMetricsController, networkController, onboardingController, - alertController, permissionsController, preferencesController, + swapsController, threeBoxController, txController, - swapsController, - metaMetricsController, } = this return { @@ -706,8 +706,12 @@ export default class MetamaskController extends EventEmitter { alertController, ), setUnconnectedAccountAlertShown: nodeify( - this.alertController.setUnconnectedAccountAlertShown, - this.alertController, + alertController.setUnconnectedAccountAlertShown, + alertController, + ), + setWeb3ShimUsageAlertDismissed: nodeify( + alertController.setWeb3ShimUsageAlertDismissed, + alertController, ), // 3Box @@ -1979,6 +1983,12 @@ export default class MetamaskController extends EventEmitter { handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind( this.preferencesController, ), + getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( + this.alertController, + ), + setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind( + this.alertController, + ), }), ) // filter and subscription polyfills diff --git a/shared/constants/alerts.js b/shared/constants/alerts.js new file mode 100644 index 000000000..4b2bed147 --- /dev/null +++ b/shared/constants/alerts.js @@ -0,0 +1,18 @@ +export const ALERT_TYPES = { + unconnectedAccount: 'unconnectedAccount', + web3ShimUsage: 'web3ShimUsage', + invalidCustomNetwork: 'invalidCustomNetwork', +} + +/** + * Alerts that can be enabled or disabled by the user. + */ +export const TOGGLEABLE_ALERT_TYPES = [ + ALERT_TYPES.unconnectedAccount, + ALERT_TYPES.web3ShimUsage, +] + +export const WEB3_SHIM_USAGE_ALERT_STATES = { + RECORDED: 1, + DISMISSED: 2, +} diff --git a/ui/app/components/app/home-notification/home-notification.component.js b/ui/app/components/app/home-notification/home-notification.component.js index ef91cbce8..0ae4d85f9 100644 --- a/ui/app/components/app/home-notification/home-notification.component.js +++ b/ui/app/components/app/home-notification/home-notification.component.js @@ -1,11 +1,14 @@ -import React from 'react' +import React, { useState } from 'react' import classnames from 'classnames' import PropTypes from 'prop-types' import Button from '../../ui/button' +import Checkbox from '../../ui/check-box' import Tooltip from '../../ui/tooltip' const HomeNotification = ({ acceptText, + checkboxText, + checkboxTooltipText, classNames = [], descriptionText, ignoreText, @@ -13,6 +16,17 @@ const HomeNotification = ({ onAccept, onIgnore, }) => { + const [checkboxState, setCheckBoxState] = useState(false) + + const checkboxElement = checkboxText && ( + setCheckBoxState((checked) => !checked)} + /> + ) + return ( @@ -43,11 +57,34 @@ const HomeNotification = ({ onIgnore(checkboxState)} > {ignoreText} ) : null} + {checkboxText ? ( + + {checkboxTooltipText ? ( + + {checkboxElement} + + ) : ( + checkboxElement + )} + + {checkboxText} + + + ) : null} ) @@ -55,6 +92,8 @@ const HomeNotification = ({ HomeNotification.propTypes = { acceptText: PropTypes.node, + checkboxText: PropTypes.node, + checkboxTooltipText: PropTypes.node, classNames: PropTypes.array, descriptionText: PropTypes.node.isRequired, ignoreText: PropTypes.node, diff --git a/ui/app/components/app/home-notification/index.scss b/ui/app/components/app/home-notification/index.scss index da2643a2a..38173145b 100644 --- a/ui/app/components/app/home-notification/index.scss +++ b/ui/app/components/app/home-notification/index.scss @@ -28,10 +28,46 @@ color: $white; } + &__text-link { + @include H7; + + color: $primary-blue; + cursor: pointer; + } + .fa-info-circle { color: #6a737d; } + & &__checkbox-wrapper { + display: flex; + flex-direction: row; + align-items: center; + + @media screen and (max-width: 575px) { + width: 160px; + } + } + + & &__checkbox { + height: 13px; + width: 13px; + font-size: 16px; + cursor: pointer; + } + + & &__checkbox-label { + @include H7; + + color: $white; + margin-left: 10px; + margin-top: 1px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + cursor: pointer; + } + & &__ignore-button { border-color: #6a737d; box-sizing: border-box; diff --git a/ui/app/ducks/alerts/invalid-custom-network.js b/ui/app/ducks/alerts/invalid-custom-network.js index aa7ba3cdb..be2aa20e1 100644 --- a/ui/app/ducks/alerts/invalid-custom-network.js +++ b/ui/app/ducks/alerts/invalid-custom-network.js @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import { ALERT_STATE } from './enums' // Constants diff --git a/ui/app/ducks/alerts/unconnected-account.js b/ui/app/ducks/alerts/unconnected-account.js index 58ca9a569..563af43d2 100644 --- a/ui/app/ducks/alerts/unconnected-account.js +++ b/ui/app/ducks/alerts/unconnected-account.js @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit' import { captureException } from '@sentry/browser' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' import * as actionConstants from '../../store/actionConstants' import { addPermittedAccount, @@ -101,7 +101,7 @@ export const dismissAndDisableAlert = () => { return async (dispatch) => { try { await dispatch(disableAlertRequested()) - await dispatch(setAlertEnabledness(name, false)) + await setAlertEnabledness(name, false) await dispatch(disableAlertSucceeded()) } catch (error) { console.error(error) diff --git a/ui/app/ducks/index.js b/ui/app/ducks/index.js index f414a0892..42fccab06 100644 --- a/ui/app/ducks/index.js +++ b/ui/app/ducks/index.js @@ -1,5 +1,5 @@ import { combineReducers } from 'redux' -import { ALERT_TYPES } from '../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../shared/constants/alerts' import metamaskReducer from './metamask/metamask' import localeMessagesReducer from './locale/locale' import sendReducer from './send/send.duck' diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 61abf2559..24180f0d3 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -1,5 +1,5 @@ import * as actionConstants from '../../store/actionConstants' -import { ALERT_TYPES } from '../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../shared/constants/alerts' export default function reduceMetamask(state = {}, action) { const metamaskState = { @@ -375,12 +375,12 @@ export const getCurrentLocale = (state) => state.metamask.currentLocale export const getAlertEnabledness = (state) => state.metamask.alertEnabledness -export const getInvalidCustomNetworkAlertEnabledness = (state) => - getAlertEnabledness(state)[ALERT_TYPES.invalidCustomNetwork] - export const getUnconnectedAccountAlertEnabledness = (state) => getAlertEnabledness(state)[ALERT_TYPES.unconnectedAccount] +export const getWeb3ShimUsageAlertEnabledness = (state) => + getAlertEnabledness(state)[ALERT_TYPES.web3ShimUsage] + export const getUnconnectedAccountAlertShown = (state) => state.metamask.unconnectedAccountAlertShownOrigins diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 17ddeb3cf..92c8d685d 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -31,6 +31,8 @@ import { const LEARN_MORE_URL = 'https://metamask.zendesk.com/hc/en-us/articles/360045129011-Intro-to-MetaMask-v8-extension' +const LEGACY_WEB3_URL = + 'https://metamask.zendesk.com/hc/en-us/articles/360053147012' export default class Home extends PureComponent { static contextTypes = { @@ -42,7 +44,7 @@ export default class Home extends PureComponent { forgottenPassword: PropTypes.bool, suggestedTokens: PropTypes.object, unconfirmedTransactionsCount: PropTypes.number, - shouldShowSeedPhraseReminder: PropTypes.bool, + shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, isPopup: PropTypes.bool, isNotification: PropTypes.bool.isRequired, threeBoxSynced: PropTypes.bool, @@ -66,6 +68,10 @@ export default class Home extends PureComponent { swapsFetchParams: PropTypes.object, swapsEnabled: PropTypes.bool, isMainnet: PropTypes.bool, + shouldShowWeb3ShimUsageNotification: PropTypes.bool.isRequired, + setWeb3ShimUsageAlertDismissed: PropTypes.func.isRequired, + originOfCurrentTab: PropTypes.string, + disableWeb3ShimUsageAlert: PropTypes.func.isRequired, } state = { @@ -161,10 +167,39 @@ export default class Home extends PureComponent { setShowRestorePromptToFalse, showRestorePrompt, threeBoxLastUpdated, + shouldShowWeb3ShimUsageNotification, + setWeb3ShimUsageAlertDismissed, + originOfCurrentTab, + disableWeb3ShimUsageAlert, } = this.props return ( + {shouldShowWeb3ShimUsageNotification ? ( + + global.platform.openTab({ url: LEGACY_WEB3_URL }) + } + > + {t('here')} + , + ])} + ignoreText={t('dismiss')} + onIgnore={(disable) => { + setWeb3ShimUsageAlertDismissed(originOfCurrentTab) + if (disable) { + disableWeb3ShimUsageAlert() + } + }} + checkboxText={t('dontShowThisAgain')} + checkboxTooltipText={t('canToggleInSettings')} + key="home-web3ShimUsageNotification" + /> + ) : null} {shouldShowSeedPhraseReminder ? ( { @@ -58,6 +68,14 @@ const mapStateToProps = (state) => { ? firstPermissionsRequest.metadata.id : null + const originOfCurrentTab = getOriginOfCurrentTab(state) + const shouldShowWeb3ShimUsageNotification = + isPopup && + getWeb3ShimUsageAlertEnabledness(state) && + activeTabHasPermissions(state) && + getWeb3ShimUsageStateForOrigin(state, originOfCurrentTab) === + WEB3_SHIM_USAGE_ALERT_STATES.RECORDED + return { forgottenPassword, suggestedTokens, @@ -81,6 +99,8 @@ const mapStateToProps = (state) => { swapsFetchParams: swapsState.fetchParams, showAwaitingSwapScreen: swapsState.routeState === 'awaiting', isMainnet: getIsMainnet(state), + originOfCurrentTab, + shouldShowWeb3ShimUsageNotification, } } @@ -103,6 +123,10 @@ const mapDispatchToProps = (dispatch) => ({ onTabClick: (name) => dispatch(setDefaultHomeActiveTabName(name)), setSwapsWelcomeMessageHasBeenShown: () => dispatch(setSwapsWelcomeMessageHasBeenShown()), + setWeb3ShimUsageAlertDismissed: (origin) => + setWeb3ShimUsageAlertDismissed(origin), + disableWeb3ShimUsageAlert: () => + setAlertEnabledness(ALERT_TYPES.web3ShimUsage, false), }) export default compose( diff --git a/ui/app/pages/settings/alerts-tab/alerts-tab.js b/ui/app/pages/settings/alerts-tab/alerts-tab.js index 6bc6b5327..832a4ec17 100644 --- a/ui/app/pages/settings/alerts-tab/alerts-tab.js +++ b/ui/app/pages/settings/alerts-tab/alerts-tab.js @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' -import { ALERT_TYPES } from '../../../../../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../../../../../shared/constants/alerts' import Tooltip from '../../../components/ui/tooltip' import ToggleButton from '../../../components/ui/toggle-button' import { setAlertEnabledness } from '../../../store/actions' @@ -11,7 +11,6 @@ import { useI18nContext } from '../../../hooks/useI18nContext' const AlertSettingsEntry = ({ alertId, description, title }) => { const t = useI18nContext() - const dispatch = useDispatch() const isEnabled = useSelector((state) => getAlertEnabledness(state)[alertId]) return ( @@ -27,7 +26,7 @@ const AlertSettingsEntry = ({ alertId, description, title }) => { dispatch(setAlertEnabledness(alertId, !isEnabled))} + onToggle={() => setAlertEnabledness(alertId, !isEnabled)} value={isEnabled} /> > @@ -48,6 +47,10 @@ const AlertsTab = () => { title: t('alertSettingsUnconnectedAccount'), description: t('alertSettingsUnconnectedAccountDescription'), }, + [ALERT_TYPES.web3ShimUsage]: { + title: t('alertSettingsWeb3ShimUsage'), + description: t('alertSettingsWeb3ShimUsageDescription'), + }, } return ( diff --git a/ui/app/selectors/permissions.js b/ui/app/selectors/permissions.js index 776db2234..a463d09d0 100644 --- a/ui/app/selectors/permissions.js +++ b/ui/app/selectors/permissions.js @@ -244,6 +244,13 @@ export function getPermissionsForActiveTab(state) { }) } +export function activeTabHasPermissions(state) { + const { activeTab, metamask } = state + const { domains = {} } = metamask + + return Boolean(domains[activeTab.origin]?.permissions?.length > 0) +} + export function getLastConnectedInfo(state) { const { permissionsHistory = {} } = state.metamask return Object.keys(permissionsHistory).reduce((acc, origin) => { diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index d9fd7129a..5607476a8 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -356,3 +356,7 @@ export function getIpfsGateway(state) { export function getUSDConversionRate(state) { return state.metamask.usdConversionRate } + +export function getWeb3ShimUsageStateForOrigin(state, origin) { + return state.metamask.web3ShimUsageOrigins[origin] +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 474cdda67..57accb993 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -2481,16 +2481,18 @@ export function setSwapsWelcomeMessageHasBeenShown() { } } -export function setAlertEnabledness(alertId, enabledness) { - return async () => { - await promisifiedBackground.setAlertEnabledness(alertId, enabledness) - } +export async function setAlertEnabledness(alertId, enabledness) { + await promisifiedBackground.setAlertEnabledness(alertId, enabledness) } export async function setUnconnectedAccountAlertShown(origin) { await promisifiedBackground.setUnconnectedAccountAlertShown(origin) } +export async function setWeb3ShimUsageAlertDismissed(origin) { + await promisifiedBackground.setWeb3ShimUsageAlertDismissed(origin) +} + export function loadingMethodDataStarted() { return { type: actionConstants.LOADING_METHOD_DATA_STARTED, diff --git a/ui/index.js b/ui/index.js index 192165280..6c4541f17 100644 --- a/ui/index.js +++ b/ui/index.js @@ -4,7 +4,7 @@ import { clone } from 'lodash' import React from 'react' import { render } from 'react-dom' import { getEnvironmentType } from '../app/scripts/lib/util' -import { ALERT_TYPES } from '../app/scripts/controllers/alert' +import { ALERT_TYPES } from '../shared/constants/alerts' import { SENTRY_STATE } from '../app/scripts/lib/setupSentry' import { ENVIRONMENT_TYPE_POPUP } from '../app/scripts/lib/enums' import Root from './app/pages'