From e9ce398323325757152552f4f7f6f96a2ae4705a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Thu, 28 Apr 2022 17:24:18 +0200 Subject: [PATCH 1/6] Add logic to inject homepage scripts when page is monted --- app/components/Views/BrowserTab/index.js | 48 ++++++++++++---------- app/core/RPCMethods/RPCMethodMiddleware.ts | 6 +++ 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 4e4b82e554e..157dbe831ed 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -356,6 +356,31 @@ export const BrowserTab = (props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [notifyAllConnections, props.approvedHosts, props.selectedAddress]); + /** + * Inject home page scripts to get the favourites and set analytics key + */ + const injectHomePageScripts = async () => { + const { current } = webviewRef; + const analyticsEnabled = Analytics.getEnabled(); + const disctinctId = await Analytics.getDistinctId(); + const homepageScripts = ` + window.__mmFavorites = ${JSON.stringify(props.bookmarks)}; + window.__mmSearchEngine = "${props.searchEngine}"; + window.__mmMetametrics = ${analyticsEnabled}; + window.__mmDistinctId = "${disctinctId}"; + window.__mmMixpanelToken = "${MM_MIXPANEL_TOKEN}"; + (function () { + try { + window.dispatchEvent(new Event('metamask_onHomepageScriptsInjected')); + } catch (e) { + //Nothing to do + } + })() + `; + + current.injectJavaScript(homepageScripts); + }; + const initializeBackgroundBridge = (urlBridge, isMainFrame) => { const newBridge = new BackgroundBridge({ webview: webviewRef, @@ -381,6 +406,7 @@ export const BrowserTab = (props) => { // Wizard wizardScrollAdjusted, tabId: props.id, + injectHomePageScripts, }), isMainFrame, }); @@ -467,25 +493,6 @@ export const BrowserTab = (props) => { return bookmarks.some(({ url: bookmark }) => bookmark === maskedUrl); }; - /** - * Inject home page scripts to get the favourites and set analytics key - */ - const injectHomePageScripts = async () => { - const { current } = webviewRef; - if (!current) return; - const analyticsEnabled = Analytics.getEnabled(); - const disctinctId = await Analytics.getDistinctId(); - const homepageScripts = ` - window.__mmFavorites = ${JSON.stringify(props.bookmarks)}; - window.__mmSearchEngine = "${props.searchEngine}"; - window.__mmMetametrics = ${analyticsEnabled}; - window.__mmDistinctId = "${disctinctId}"; - window.__mmMixpanelToken = "${MM_MIXPANEL_TOKEN}"; - `; - - current.injectJavaScript(homepageScripts); - }; - /** * Show a phishing modal when a url is not allowed */ @@ -871,9 +878,6 @@ export const BrowserTab = (props) => { } icon.current = null; - if (isHomepage(nativeEvent.url)) { - injectHomePageScripts(); - } // Reset the previous bridges backgroundBridges.current.length && backgroundBridges.current.forEach((bridge) => bridge.onDisconnect()); diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index ef2fde03c41..482eb2396c2 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -48,6 +48,7 @@ interface RPCMethodsMiddleParameters { tabId: string; // For WalletConnect isWalletConnect: boolean; + injectHomePageScripts: () => void; } export const checkActiveAccountAndChainId = ({ address, chainId, activeAccounts }: any) => { @@ -118,6 +119,7 @@ export const getRpcMethodMiddleware = ({ tabId, // For WalletConnect isWalletConnect, + injectHomePageScripts, }: RPCMethodsMiddleParameters) => // all user facing RPC calls not implemented by the provider createAsyncMiddleware(async (req: any, res: any, next: any) => { @@ -510,6 +512,10 @@ export const getRpcMethodMiddleware = ({ res.result = true; }, + metamask_onAppMounted: async () => { + injectHomePageScripts(); + }, + /** * This method is used by the inpage provider to get its state on * initialization. From 26e142915722c38d5d85da80531841cb173f7ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Fri, 29 Apr 2022 10:38:20 +0200 Subject: [PATCH 2/6] Add isHomepage check --- app/core/RPCMethods/RPCMethodMiddleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 482eb2396c2..5bffe4dd14b 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -513,7 +513,10 @@ export const getRpcMethodMiddleware = ({ }, metamask_onAppMounted: async () => { - injectHomePageScripts(); + if (isHomepage()) { + injectHomePageScripts(); + } + res.result = true; }, /** From 20ff9cc993a9b7a5277cb282a36fcc008cfc21f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Fri, 29 Apr 2022 11:22:31 +0200 Subject: [PATCH 3/6] Change naming --- app/core/RPCMethods/RPCMethodMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 5bffe4dd14b..5c3c087a78a 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -512,7 +512,7 @@ export const getRpcMethodMiddleware = ({ res.result = true; }, - metamask_onAppMounted: async () => { + metamask_injectHomepageScripts: async () => { if (isHomepage()) { injectHomePageScripts(); } From 20da493f8bf4f8bb03dcec807fb5664e9884b589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Thu, 5 May 2022 16:04:02 +0100 Subject: [PATCH 4/6] Fix linting --- app/components/Views/BrowserTab/index.js | 2975 ++++++++++---------- app/core/RPCMethods/RPCMethodMiddleware.ts | 1160 ++++---- 2 files changed, 2160 insertions(+), 1975 deletions(-) diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 157dbe831ed..1624c92814a 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -1,16 +1,16 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { - Text, - StyleSheet, - TextInput, - View, - TouchableWithoutFeedback, - Alert, - TouchableOpacity, - Linking, - Keyboard, - BackHandler, - InteractionManager, + Text, + StyleSheet, + TextInput, + View, + TouchableWithoutFeedback, + Alert, + TouchableOpacity, + Linking, + Keyboard, + BackHandler, + InteractionManager, } from 'react-native'; import { withNavigation } from '@react-navigation/compat'; import { WebView } from 'react-native-webview'; @@ -24,10 +24,18 @@ import BackgroundBridge from '../../../core/BackgroundBridge'; import Engine from '../../../core/Engine'; import PhishingModal from '../../UI/PhishingModal'; import WebviewProgressBar from '../../UI/WebviewProgressBar'; -import { baseStyles, fontStyles, colors as importedColors } from '../../../styles/common'; +import { + baseStyles, + fontStyles, + colors as importedColors, +} from '../../../styles/common'; import Logger from '../../../util/Logger'; import onUrlSubmit, { getHost, getUrlObj } from '../../../util/browser'; -import { SPA_urlChangeListener, JS_DESELECT_TEXT, JS_WEBVIEW_URL } from '../../../util/browserScripts'; +import { + SPA_urlChangeListener, + JS_DESELECT_TEXT, + JS_WEBVIEW_URL, +} from '../../../util/browserScripts'; import resolveEnsToIpfsContentId from '../../../lib/ens-ipfs/resolver'; import Button from '../../UI/Button'; import { strings } from '../../../../locales/i18n'; @@ -62,145 +70,145 @@ const MM_MIXPANEL_TOKEN = process.env.MM_MIXPANEL_TOKEN; const ANIMATION_TIMING = 300; const createStyles = (colors) => - StyleSheet.create({ - wrapper: { - ...baseStyles.flexGrow, - backgroundColor: colors.background.default, - }, - hide: { - flex: 0, - opacity: 0, - display: 'none', - width: 0, - height: 0, - }, - progressBarWrapper: { - height: 3, - width: '100%', - left: 0, - right: 0, - top: 0, - position: 'absolute', - zIndex: 999999, - }, - optionsOverlay: { - position: 'absolute', - zIndex: 99999998, - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - optionsWrapper: { - position: 'absolute', - zIndex: 99999999, - width: 200, - borderWidth: 1, - borderColor: colors.border.default, - backgroundColor: colors.background.default, - borderRadius: 10, - paddingBottom: 5, - paddingTop: 10, - }, - optionsWrapperAndroid: { - shadowColor: importedColors.shadow, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.5, - shadowRadius: 3, - bottom: 65, - right: 5, - }, - optionsWrapperIos: { - shadowColor: importedColors.shadow, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.5, - shadowRadius: 3, - bottom: 90, - right: 5, - }, - option: { - paddingVertical: 10, - height: 'auto', - minHeight: 44, - paddingHorizontal: 15, - backgroundColor: colors.background.default, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-start', - marginTop: Device.isAndroid() ? 0 : -5, - }, - optionText: { - fontSize: 16, - lineHeight: 16, - alignSelf: 'center', - justifyContent: 'center', - marginTop: 3, - color: colors.primary.default, - flex: 1, - ...fontStyles.fontPrimary, - }, - optionIconWrapper: { - flex: 0, - borderRadius: 5, - backgroundColor: colors.primary.muted, - padding: 3, - marginRight: 10, - alignSelf: 'center', - }, - optionIcon: { - color: colors.primary.default, - textAlign: 'center', - alignSelf: 'center', - fontSize: 18, - }, - webview: { - ...baseStyles.flexGrow, - zIndex: 1, - }, - urlModalContent: { - flexDirection: 'row', - paddingTop: Device.isAndroid() ? 10 : Device.isIphoneX() ? 50 : 27, - paddingHorizontal: 10, - height: Device.isAndroid() ? 59 : Device.isIphoneX() ? 87 : 65, - backgroundColor: colors.background.default, - }, - searchWrapper: { - flexDirection: 'row', - borderRadius: 30, - backgroundColor: colors.background.alternative, - height: Device.isAndroid() ? 40 : 30, - flex: 1, - }, - clearButton: { paddingHorizontal: 12, justifyContent: 'center' }, - urlModal: { - justifyContent: 'flex-start', - margin: 0, - }, - urlInput: { - ...fontStyles.normal, - fontSize: Device.isAndroid() ? 16 : 14, - paddingLeft: 15, - flex: 1, - color: colors.text.default, - }, - cancelButton: { - marginTop: -6, - marginLeft: 10, - justifyContent: 'center', - }, - cancelButtonText: { - fontSize: 14, - color: colors.primary.default, - ...fontStyles.normal, - }, - bottomModal: { - justifyContent: 'flex-end', - margin: 0, - }, - fullScreenModal: { - flex: 1, - }, - }); + StyleSheet.create({ + wrapper: { + ...baseStyles.flexGrow, + backgroundColor: colors.background.default, + }, + hide: { + flex: 0, + opacity: 0, + display: 'none', + width: 0, + height: 0, + }, + progressBarWrapper: { + height: 3, + width: '100%', + left: 0, + right: 0, + top: 0, + position: 'absolute', + zIndex: 999999, + }, + optionsOverlay: { + position: 'absolute', + zIndex: 99999998, + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + optionsWrapper: { + position: 'absolute', + zIndex: 99999999, + width: 200, + borderWidth: 1, + borderColor: colors.border.default, + backgroundColor: colors.background.default, + borderRadius: 10, + paddingBottom: 5, + paddingTop: 10, + }, + optionsWrapperAndroid: { + shadowColor: importedColors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 3, + bottom: 65, + right: 5, + }, + optionsWrapperIos: { + shadowColor: importedColors.shadow, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 3, + bottom: 90, + right: 5, + }, + option: { + paddingVertical: 10, + height: 'auto', + minHeight: 44, + paddingHorizontal: 15, + backgroundColor: colors.background.default, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + marginTop: Device.isAndroid() ? 0 : -5, + }, + optionText: { + fontSize: 16, + lineHeight: 16, + alignSelf: 'center', + justifyContent: 'center', + marginTop: 3, + color: colors.primary.default, + flex: 1, + ...fontStyles.fontPrimary, + }, + optionIconWrapper: { + flex: 0, + borderRadius: 5, + backgroundColor: colors.primary.muted, + padding: 3, + marginRight: 10, + alignSelf: 'center', + }, + optionIcon: { + color: colors.primary.default, + textAlign: 'center', + alignSelf: 'center', + fontSize: 18, + }, + webview: { + ...baseStyles.flexGrow, + zIndex: 1, + }, + urlModalContent: { + flexDirection: 'row', + paddingTop: Device.isAndroid() ? 10 : Device.isIphoneX() ? 50 : 27, + paddingHorizontal: 10, + height: Device.isAndroid() ? 59 : Device.isIphoneX() ? 87 : 65, + backgroundColor: colors.background.default, + }, + searchWrapper: { + flexDirection: 'row', + borderRadius: 30, + backgroundColor: colors.background.alternative, + height: Device.isAndroid() ? 40 : 30, + flex: 1, + }, + clearButton: { paddingHorizontal: 12, justifyContent: 'center' }, + urlModal: { + justifyContent: 'flex-start', + margin: 0, + }, + urlInput: { + ...fontStyles.normal, + fontSize: Device.isAndroid() ? 16 : 14, + paddingLeft: 15, + flex: 1, + color: colors.text.default, + }, + cancelButton: { + marginTop: -6, + marginLeft: 10, + justifyContent: 'center', + }, + cancelButtonText: { + fontSize: 14, + color: colors.primary.default, + ...fontStyles.normal, + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0, + }, + fullScreenModal: { + flex: 1, + }, + }); const sessionENSNames = {}; const ensIgnoreList = []; @@ -208,162 +216,173 @@ let approvedHosts = {}; const getApprovedHosts = () => approvedHosts; const setApprovedHosts = (hosts) => { - approvedHosts = hosts; + approvedHosts = hosts; }; export const BrowserTab = (props) => { - const [backEnabled, setBackEnabled] = useState(false); - const [forwardEnabled, setForwardEnabled] = useState(false); - const [progress, setProgress] = useState(0); - const [initialUrl, setInitialUrl] = useState(''); - const [firstUrlLoaded, setFirstUrlLoaded] = useState(false); - const [autocompleteValue, setAutocompleteValue] = useState(''); - const [error, setError] = useState(null); - const [showUrlModal, setShowUrlModal] = useState(false); - const [showOptions, setShowOptions] = useState(false); - const [entryScriptWeb3, setEntryScriptWeb3] = useState(null); - const [showPhishingModal, setShowPhishingModal] = useState(false); - const [blockedUrl, setBlockedUrl] = useState(undefined); - - const webviewRef = useRef(null); - const inputRef = useRef(null); - - const url = useRef(''); - const title = useRef(''); - const icon = useRef(null); - const webviewUrlPostMessagePromiseResolve = useRef(null); - const backgroundBridges = useRef([]); - const fromHomepage = useRef(false); - const wizardScrollAdjusted = useRef(false); - - const { colors, themeAppearance } = useAppThemeFromContext() || mockTheme; - const styles = createStyles(colors); - - /** - * Is the current tab the active tab - */ - const isTabActive = useCallback(() => props.activeTab === props.id, [props.activeTab, props.id]); - - /** - * Gets the url to be displayed to the user - * For example, if it's ens then show [site].eth instead of ipfs url - */ - const getMaskedUrl = (url) => { - if (!url) return url; - let replace = null; - if (url.startsWith(AppConstants.IPFS_DEFAULT_GATEWAY_URL)) { - replace = (key) => `${AppConstants.IPFS_DEFAULT_GATEWAY_URL}${sessionENSNames[key].hash}/`; - } else if (url.startsWith(AppConstants.IPNS_DEFAULT_GATEWAY_URL)) { - replace = (key) => `${AppConstants.IPNS_DEFAULT_GATEWAY_URL}${sessionENSNames[key].hostname}/`; - } else if (url.startsWith(AppConstants.SWARM_DEFAULT_GATEWAY_URL)) { - replace = (key) => `${AppConstants.SWARM_GATEWAY_URL}${sessionENSNames[key].hash}/`; - } - - if (replace) { - const key = Object.keys(sessionENSNames).find((ens) => url.startsWith(ens)); - if (key) { - url = url.replace(replace(key), `https://${sessionENSNames[key].hostname}/`); - } - } - return url; - }; - - /** - * Shows or hides the url input modal. - * When opened it sets the current website url on the input. - */ - const toggleUrlModal = useCallback( - ({ urlInput = null } = {}) => { - const goingToShow = !showUrlModal; - const urlToShow = getMaskedUrl(urlInput || url.current); - - if (goingToShow && urlToShow) setAutocompleteValue(urlToShow); - - setShowUrlModal(goingToShow); - }, - [showUrlModal] - ); - - /** - * Checks if it is a ENS website - */ - const isENSUrl = (url) => { - const { hostname } = new URL(url); - const tld = hostname.split('.').pop(); - if (AppConstants.supportedTLDs.indexOf(tld.toLowerCase()) !== -1) { - // Make sure it's not in the ignore list - if (ensIgnoreList.indexOf(hostname) === -1) { - return true; - } - } - return false; - }; - - /** - * Checks if a given url or the current url is the homepage - */ - const isHomepage = useCallback((checkUrl = null) => { - const currentPage = checkUrl || url.current; - const { host: currentHost } = getUrlObj(currentPage); - return currentHost === HOMEPAGE_HOST; - }, []); - - const notifyAllConnections = useCallback( - (payload, restricted = true) => { - const fullHostname = new URL(url.current).hostname; - - // TODO:permissions move permissioning logic elsewhere - backgroundBridges.current.forEach((bridge) => { - if ( - bridge.hostname === fullHostname && - (!props.privacyMode || !restricted || approvedHosts[bridge.hostname]) - ) { - bridge.sendNotification(payload); - } - }); - }, - [props.privacyMode] - ); - - /** - * Manage hosts that were approved to connect with the user accounts - */ - useEffect(() => { - const { approvedHosts: approvedHostsProps, selectedAddress } = props; - - approvedHosts = approvedHostsProps; - - const numApprovedHosts = Object.keys(approvedHosts).length; - - // this will happen if the approved hosts were cleared - if (numApprovedHosts === 0) { - notifyAllConnections( - { - method: NOTIFICATION_NAMES.accountsChanged, - params: [], - }, - false - ); // notification should be sent regardless of approval status - } - - if (numApprovedHosts > 0) { - notifyAllConnections({ - method: NOTIFICATION_NAMES.accountsChanged, - params: [selectedAddress], - }); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [notifyAllConnections, props.approvedHosts, props.selectedAddress]); - - /** - * Inject home page scripts to get the favourites and set analytics key - */ - const injectHomePageScripts = async () => { - const { current } = webviewRef; - const analyticsEnabled = Analytics.getEnabled(); - const disctinctId = await Analytics.getDistinctId(); - const homepageScripts = ` + const [backEnabled, setBackEnabled] = useState(false); + const [forwardEnabled, setForwardEnabled] = useState(false); + const [progress, setProgress] = useState(0); + const [initialUrl, setInitialUrl] = useState(''); + const [firstUrlLoaded, setFirstUrlLoaded] = useState(false); + const [autocompleteValue, setAutocompleteValue] = useState(''); + const [error, setError] = useState(null); + const [showUrlModal, setShowUrlModal] = useState(false); + const [showOptions, setShowOptions] = useState(false); + const [entryScriptWeb3, setEntryScriptWeb3] = useState(null); + const [showPhishingModal, setShowPhishingModal] = useState(false); + const [blockedUrl, setBlockedUrl] = useState(undefined); + + const webviewRef = useRef(null); + const inputRef = useRef(null); + + const url = useRef(''); + const title = useRef(''); + const icon = useRef(null); + const webviewUrlPostMessagePromiseResolve = useRef(null); + const backgroundBridges = useRef([]); + const fromHomepage = useRef(false); + const wizardScrollAdjusted = useRef(false); + + const { colors, themeAppearance } = useAppThemeFromContext() || mockTheme; + const styles = createStyles(colors); + + /** + * Is the current tab the active tab + */ + const isTabActive = useCallback( + () => props.activeTab === props.id, + [props.activeTab, props.id], + ); + + /** + * Gets the url to be displayed to the user + * For example, if it's ens then show [site].eth instead of ipfs url + */ + const getMaskedUrl = (url) => { + if (!url) return url; + let replace = null; + if (url.startsWith(AppConstants.IPFS_DEFAULT_GATEWAY_URL)) { + replace = (key) => + `${AppConstants.IPFS_DEFAULT_GATEWAY_URL}${sessionENSNames[key].hash}/`; + } else if (url.startsWith(AppConstants.IPNS_DEFAULT_GATEWAY_URL)) { + replace = (key) => + `${AppConstants.IPNS_DEFAULT_GATEWAY_URL}${sessionENSNames[key].hostname}/`; + } else if (url.startsWith(AppConstants.SWARM_DEFAULT_GATEWAY_URL)) { + replace = (key) => + `${AppConstants.SWARM_GATEWAY_URL}${sessionENSNames[key].hash}/`; + } + + if (replace) { + const key = Object.keys(sessionENSNames).find((ens) => + url.startsWith(ens), + ); + if (key) { + url = url.replace( + replace(key), + `https://${sessionENSNames[key].hostname}/`, + ); + } + } + return url; + }; + + /** + * Shows or hides the url input modal. + * When opened it sets the current website url on the input. + */ + const toggleUrlModal = useCallback( + ({ urlInput = null } = {}) => { + const goingToShow = !showUrlModal; + const urlToShow = getMaskedUrl(urlInput || url.current); + + if (goingToShow && urlToShow) setAutocompleteValue(urlToShow); + + setShowUrlModal(goingToShow); + }, + [showUrlModal], + ); + + /** + * Checks if it is a ENS website + */ + const isENSUrl = (url) => { + const { hostname } = new URL(url); + const tld = hostname.split('.').pop(); + if (AppConstants.supportedTLDs.indexOf(tld.toLowerCase()) !== -1) { + // Make sure it's not in the ignore list + if (ensIgnoreList.indexOf(hostname) === -1) { + return true; + } + } + return false; + }; + + /** + * Checks if a given url or the current url is the homepage + */ + const isHomepage = useCallback((checkUrl = null) => { + const currentPage = checkUrl || url.current; + const { host: currentHost } = getUrlObj(currentPage); + return currentHost === HOMEPAGE_HOST; + }, []); + + const notifyAllConnections = useCallback( + (payload, restricted = true) => { + const fullHostname = new URL(url.current).hostname; + + // TODO:permissions move permissioning logic elsewhere + backgroundBridges.current.forEach((bridge) => { + if ( + bridge.hostname === fullHostname && + (!props.privacyMode || !restricted || approvedHosts[bridge.hostname]) + ) { + bridge.sendNotification(payload); + } + }); + }, + [props.privacyMode], + ); + + /** + * Manage hosts that were approved to connect with the user accounts + */ + useEffect(() => { + const { approvedHosts: approvedHostsProps, selectedAddress } = props; + + approvedHosts = approvedHostsProps; + + const numApprovedHosts = Object.keys(approvedHosts).length; + + // this will happen if the approved hosts were cleared + if (numApprovedHosts === 0) { + notifyAllConnections( + { + method: NOTIFICATION_NAMES.accountsChanged, + params: [], + }, + false, + ); // notification should be sent regardless of approval status + } + + if (numApprovedHosts > 0) { + notifyAllConnections({ + method: NOTIFICATION_NAMES.accountsChanged, + params: [selectedAddress], + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [notifyAllConnections, props.approvedHosts, props.selectedAddress]); + + /** + * Inject home page scripts to get the favourites and set analytics key + */ + const injectHomePageScripts = async () => { + const { current } = webviewRef; + const analyticsEnabled = Analytics.getEnabled(); + const disctinctId = await Analytics.getDistinctId(); + const homepageScripts = ` window.__mmFavorites = ${JSON.stringify(props.bookmarks)}; window.__mmSearchEngine = "${props.searchEngine}"; window.__mmMetametrics = ${analyticsEnabled}; @@ -378,1157 +397,1247 @@ export const BrowserTab = (props) => { })() `; - current.injectJavaScript(homepageScripts); - }; - - const initializeBackgroundBridge = (urlBridge, isMainFrame) => { - const newBridge = new BackgroundBridge({ - webview: webviewRef, - url: urlBridge, - getRpcMethodMiddleware: ({ hostname, getProviderState }) => - getRpcMethodMiddleware({ - hostname, - getProviderState, - navigation: props.navigation, - getApprovedHosts, - setApprovedHosts, - approveHost: props.approveHost, - // Website info - url, - title, - icon, - // Bookmarks - isHomepage, - // Show autocomplete - fromHomepage, - setAutocompleteValue, - setShowUrlModal, - // Wizard - wizardScrollAdjusted, - tabId: props.id, - injectHomePageScripts, - }), - isMainFrame, - }); - backgroundBridges.current.push(newBridge); - }; - - /** + current.injectJavaScript(homepageScripts); + }; + + const initializeBackgroundBridge = (urlBridge, isMainFrame) => { + const newBridge = new BackgroundBridge({ + webview: webviewRef, + url: urlBridge, + getRpcMethodMiddleware: ({ hostname, getProviderState }) => + getRpcMethodMiddleware({ + hostname, + getProviderState, + navigation: props.navigation, + getApprovedHosts, + setApprovedHosts, + approveHost: props.approveHost, + // Website info + url, + title, + icon, + // Bookmarks + isHomepage, + // Show autocomplete + fromHomepage, + setAutocompleteValue, + setShowUrlModal, + // Wizard + wizardScrollAdjusted, + tabId: props.id, + injectHomePageScripts, + }), + isMainFrame, + }); + backgroundBridges.current.push(newBridge); + }; + + /** * Disabling iframes for now const onFrameLoadStarted = url => { url && initializeBackgroundBridge(url, false); }; */ - /** - * Dismiss the text selection on the current website - */ - const dismissTextSelectionIfNeeded = useCallback(() => { - if (isTabActive() && Device.isAndroid()) { - const { current } = webviewRef; - if (current) { - setTimeout(() => { - current.injectJavaScript(JS_DESELECT_TEXT); - }, 50); - } - } - }, [isTabActive]); - - /** - * Toggle the options menu - */ - const toggleOptions = useCallback(() => { - dismissTextSelectionIfNeeded(); - setShowOptions(!showOptions); - InteractionManager.runAfterInteractions(() => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_BROWSER_OPTIONS); - }); - }, [dismissTextSelectionIfNeeded, showOptions]); - - /** - * Show the options menu - */ - const toggleOptionsIfNeeded = useCallback(() => { - if (showOptions) { - toggleOptions(); - } - }, [showOptions, toggleOptions]); - - /** - * Go back to previous website in history - */ - const goBack = useCallback(() => { - if (!backEnabled) return; - - toggleOptionsIfNeeded(); - const { current } = webviewRef; - current && current.goBack(); - }, [backEnabled, toggleOptionsIfNeeded]); - - /** - * Go forward to the next website in history - */ - const goForward = async () => { - if (!forwardEnabled) return; - - toggleOptionsIfNeeded(); - const { current } = webviewRef; - current && current.goForward && current.goForward(); - }; - - /** - * Check if a hostname is allowed - */ - const isAllowedUrl = useCallback( - (hostname) => { - const { PhishingController } = Engine.context; - return (props.whitelist && props.whitelist.includes(hostname)) || !PhishingController.test(hostname); - }, - [props.whitelist] - ); - - const isBookmark = () => { - const { bookmarks } = props; - const maskedUrl = getMaskedUrl(url.current); - return bookmarks.some(({ url: bookmark }) => bookmark === maskedUrl); - }; - - /** - * Show a phishing modal when a url is not allowed - */ - const handleNotAllowedUrl = (urlToGo) => { - setBlockedUrl(urlToGo); - setTimeout(() => setShowPhishingModal(true), 1000); - }; - - /** - * Get IPFS info from a ens url - */ - const handleIpfsContent = useCallback( - async (fullUrl, { hostname, pathname, query }) => { - const { provider } = Engine.context.NetworkController; - let gatewayUrl; - try { - const { type, hash } = await resolveEnsToIpfsContentId({ - provider, - name: hostname, - }); - if (type === 'ipfs-ns') { - gatewayUrl = `${props.ipfsGateway}${hash}${pathname || '/'}${query || ''}`; - const response = await fetch(gatewayUrl); - const statusCode = response.status; - if (statusCode >= 400) { - Logger.log('Status code ', statusCode, gatewayUrl); - //urlNotFound(gatewayUrl); - return null; - } - } else if (type === 'swarm-ns') { - gatewayUrl = `${AppConstants.SWARM_DEFAULT_GATEWAY_URL}${hash}${pathname || '/'}${query || ''}`; - } else if (type === 'ipns-ns') { - gatewayUrl = `${AppConstants.IPNS_DEFAULT_GATEWAY_URL}${hostname}${pathname || '/'}${query || ''}`; - } - return { - url: gatewayUrl, - hash, - type, - }; - } catch (err) { - // This is a TLD that might be a normal website - // For example .XYZ and might be more in the future - if (hostname.substr(-4) !== '.eth' && err.toString().indexOf('is not standard') !== -1) { - ensIgnoreList.push(hostname); - return { url: fullUrl, reload: true }; - } - if (err?.message?.startsWith('EnsIpfsResolver - no known ens-ipfs registry for chainId')) { - trackErrorAsAnalytics('Browser: Failed to resolve ENS name for chainId', err?.message); - } else { - Logger.error(err, 'Failed to resolve ENS name'); - } - - Alert.alert(strings('browser.failed_to_resolve_ens_name'), err.message); - goBack(); - } - }, - [goBack, props.ipfsGateway] - ); - - /** - * Go to a url - */ - const go = useCallback( - async (url, initialCall) => { - const hasProtocol = url.match(/^[a-z]*:\/\//) || isHomepage(url); - const sanitizedURL = hasProtocol ? url : `${props.defaultProtocol}${url}`; - const { hostname, query, pathname } = new URL(sanitizedURL); - - let urlToGo = sanitizedURL; - const isEnsUrl = isENSUrl(url); - const { current } = webviewRef; - if (isEnsUrl) { - current && current.stopLoading(); - const { url: ensUrl, type, hash, reload } = await handleIpfsContent(url, { hostname, query, pathname }); - if (reload) return go(ensUrl); - urlToGo = ensUrl; - sessionENSNames[urlToGo] = { hostname, hash, type }; - } - - if (isAllowedUrl(hostname)) { - if (initialCall || !firstUrlLoaded) { - setInitialUrl(urlToGo); - setFirstUrlLoaded(true); - } else { - current && current.injectJavaScript(`(function(){window.location.href = '${urlToGo}' })()`); - } - - setProgress(0); - return sanitizedURL; - } - handleNotAllowedUrl(urlToGo); - return null; - }, - [firstUrlLoaded, handleIpfsContent, isAllowedUrl, isHomepage, props.defaultProtocol] - ); - - /** - * Open a new tab - */ - const openNewTab = useCallback( - (url) => { - toggleOptionsIfNeeded(); - dismissTextSelectionIfNeeded(); - props.newTab(url); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [dismissTextSelectionIfNeeded, toggleOptionsIfNeeded] - ); - - /** - * Hide url input modal - */ - const hideUrlModal = useCallback(() => { - setShowUrlModal(false); - - if (isHomepage()) { - const { current } = webviewRef; - const blur = `document.getElementsByClassName('autocomplete-input')[0].blur();`; - current && current.injectJavaScript(blur); - } - }, [isHomepage]); - - /** - * Handle keyboard hide - */ - const keyboardDidHide = useCallback(() => { - if (!isTabActive() || isEmulatorSync()) return false; - if (!fromHomepage.current) { - if (showUrlModal) { - hideUrlModal(); - } - } - }, [hideUrlModal, isTabActive, showUrlModal]); - - /** - * Set keyboard listeners - */ - useEffect(() => { - const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', keyboardDidHide); - return function cleanup() { - keyboardDidHideListener.remove(); - }; - }, [keyboardDidHide]); - - /** - * Reload current page - */ - const reload = useCallback(() => { - const { current } = webviewRef; - current && current.reload(); - }, []); - - /** - * Handle when the drawer (app menu) is opened - */ - const drawerOpenHandler = useCallback(() => { - dismissTextSelectionIfNeeded(); - }, [dismissTextSelectionIfNeeded]); - - /** - * Set initial url, dapp scripts and engine. Similar to componentDidMount - */ - useEffect(() => { - approvedHosts = props.approvedHosts; - const initialUrl = props.initialUrl || HOMEPAGE_URL; - go(initialUrl, true); - - const getEntryScriptWeb3 = async () => { - const entryScriptWeb3 = await EntryScriptWeb3.get(); - setEntryScriptWeb3(entryScriptWeb3 + SPA_urlChangeListener); - }; - - getEntryScriptWeb3(); - - // Specify how to clean up after this effect: - return function cleanup() { - backgroundBridges.current.forEach((bridge) => bridge.onDisconnect()); - - // Remove all Engine listeners - Engine.context.TokensController.hub.removeAllListeners(); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - /** - * Enable the header to toggle the url modal and update other header data - */ - useEffect(() => { - if (props.activeTab === props.id) { - props.navigation.setParams({ - showUrlModal: toggleUrlModal, - url: getMaskedUrl(url.current), - icon: icon.current, - error, - }); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [error, props.activeTab, props.id, toggleUrlModal]); - - useEffect(() => { - if (Device.isAndroid()) { - DrawerStatusTracker.hub.on('drawer::open', drawerOpenHandler); - } - - return function cleanup() { - if (Device.isAndroid()) { - DrawerStatusTracker && - DrawerStatusTracker.hub && - DrawerStatusTracker.hub.removeListener('drawer::open', drawerOpenHandler); - } - }; - }, [drawerOpenHandler]); - - /** - * Set navigation listeners - */ - useEffect(() => { - const handleAndroidBackPress = () => { - if (!isTabActive()) return false; - goBack(); - return true; - }; - - BackHandler.addEventListener('hardwareBackPress', handleAndroidBackPress); - - // Handle hardwareBackPress event only for browser, not components rendered on top - props.navigation.addListener('willFocus', () => { - BackHandler.addEventListener('hardwareBackPress', handleAndroidBackPress); - }); - props.navigation.addListener('willBlur', () => { - BackHandler.removeEventListener('hardwareBackPress', handleAndroidBackPress); - }); - - return function cleanup() { - BackHandler.removeEventListener('hardwareBackPress', handleAndroidBackPress); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [goBack, isTabActive]); - - /** - * Handles state changes for when the url changes - */ - const changeUrl = (siteInfo, type) => { - url.current = siteInfo.url; - title.current = siteInfo.title; - if (siteInfo.icon) icon.current = siteInfo.icon; - }; - - /** - * Handles state changes for when the url changes - */ - const changeAddressBar = (siteInfo, type) => { - setBackEnabled(siteInfo.canGoBack); - setForwardEnabled(siteInfo.canGoForward); - - isTabActive() && - props.navigation.setParams({ - url: getMaskedUrl(siteInfo.url), - icon: siteInfo.icon, - silent: true, - }); - - props.updateTabInfo(getMaskedUrl(siteInfo.url), props.id); - - props.addToBrowserHistory({ - name: siteInfo.title, - url: getMaskedUrl(siteInfo.url), - }); - }; - - /** - * Go to eth-phishing-detect page - */ - const goToETHPhishingDetector = () => { - setShowPhishingModal(false); - go(`https://github.com/metamask/eth-phishing-detect`); - }; - - /** - * Continue to phishing website - */ - const continueToPhishingSite = () => { - const urlObj = new URL(blockedUrl); - props.addToWhitelist(urlObj.hostname); - setShowPhishingModal(false); - blockedUrl !== url.current && - setTimeout(() => { - go(blockedUrl); - setBlockedUrl(undefined); - }, 1000); - }; - - /** - * Go to etherscam website - */ - const goToEtherscam = () => { - setShowPhishingModal(false); - go(`https://etherscamdb.info/domain/meta-mask.com`); - }; - - /** - * Go to eth-phishing-detect issue - */ - const goToFilePhishingIssue = () => { - setShowPhishingModal(false); - go(`https://github.com/metamask/eth-phishing-detect/issues/new`); - }; - - /** - * Go back from phishing website alert - */ - const goBackToSafety = () => { - blockedUrl === url.current && goBack(); - setTimeout(() => { - setShowPhishingModal(false); - setBlockedUrl(undefined); - }, 500); - }; - - /** - * Renders the phishing modal - */ - const renderPhishingModal = () => ( - - - - ); - - const trackEventSearchUsed = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SEARCH_USED, { - option_chosen: 'Search on URL', - number_of_tabs: undefined, - }); - }; - - /** - * Stops normal loading when it's ens, instead call go to be properly set up - */ - const onShouldStartLoadWithRequest = ({ url }) => { - if (isENSUrl(url)) { - go(url.replace(/^http:\/\//, 'https://')); - return false; - } - return true; - }; - - /** - * Website started to load - */ - const onLoadStart = async ({ nativeEvent }) => { - const { hostname } = new URL(nativeEvent.url); - if (!isAllowedUrl(hostname)) { - return handleNotAllowedUrl(nativeEvent.url); - } - webviewUrlPostMessagePromiseResolve.current = null; - setError(false); - - changeUrl(nativeEvent, 'start'); - - //For Android url on the navigation bar should only update upon load. - if (Device.isAndroid()) { - changeAddressBar(nativeEvent, 'start'); - } - - icon.current = null; - - // Reset the previous bridges - backgroundBridges.current.length && backgroundBridges.current.forEach((bridge) => bridge.onDisconnect()); - backgroundBridges.current = []; - const origin = new URL(nativeEvent.url).origin; - initializeBackgroundBridge(origin, true); - }; - - /** - * Sets loading bar progress - */ - const onLoadProgress = ({ nativeEvent: { progress } }) => { - setProgress(progress); - }; - - const onLoad = ({ nativeEvent }) => { - //For iOS url on the navigation bar should only update upon load. - if (Device.isIos()) { - changeUrl(nativeEvent, 'start'); - changeAddressBar(nativeEvent, 'start'); - } - }; - - /** - * When website finished loading - */ - const onLoadEnd = ({ nativeEvent }) => { - if (nativeEvent.loading) return; - const { current } = webviewRef; - - current && current.injectJavaScript(JS_WEBVIEW_URL); - - const promiseResolver = (resolve) => { - webviewUrlPostMessagePromiseResolve.current = resolve; - }; - const promise = current ? new Promise(promiseResolver) : Promise.resolve(url.current); - - promise.then((info) => { - const { hostname: currentHostname } = new URL(url.current); - const { hostname } = new URL(nativeEvent.url); - if (info.url === nativeEvent.url && currentHostname === hostname) { - changeUrl({ ...nativeEvent, icon: info.icon }, 'end-promise'); - changeAddressBar({ ...nativeEvent, icon: info.icon }, 'end-promise'); - } - }); - }; - - /** - * Handle message from website - */ - const onMessage = ({ nativeEvent }) => { - let data = nativeEvent.data; - try { - data = typeof data === 'string' ? JSON.parse(data) : data; - if (!data || (!data.type && !data.name)) { - return; - } - if (data.name) { - backgroundBridges.current.forEach((bridge) => { - if (bridge.isMainFrame) { - const { origin } = data && data.origin && new URL(data.origin); - bridge.url === origin && bridge.onMessage(data); - } else { - bridge.url === data.origin && bridge.onMessage(data); - } - }); - return; - } - - switch (data.type) { - /** + /** + * Dismiss the text selection on the current website + */ + const dismissTextSelectionIfNeeded = useCallback(() => { + if (isTabActive() && Device.isAndroid()) { + const { current } = webviewRef; + if (current) { + setTimeout(() => { + current.injectJavaScript(JS_DESELECT_TEXT); + }, 50); + } + } + }, [isTabActive]); + + /** + * Toggle the options menu + */ + const toggleOptions = useCallback(() => { + dismissTextSelectionIfNeeded(); + setShowOptions(!showOptions); + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_BROWSER_OPTIONS); + }); + }, [dismissTextSelectionIfNeeded, showOptions]); + + /** + * Show the options menu + */ + const toggleOptionsIfNeeded = useCallback(() => { + if (showOptions) { + toggleOptions(); + } + }, [showOptions, toggleOptions]); + + /** + * Go back to previous website in history + */ + const goBack = useCallback(() => { + if (!backEnabled) return; + + toggleOptionsIfNeeded(); + const { current } = webviewRef; + current && current.goBack(); + }, [backEnabled, toggleOptionsIfNeeded]); + + /** + * Go forward to the next website in history + */ + const goForward = async () => { + if (!forwardEnabled) return; + + toggleOptionsIfNeeded(); + const { current } = webviewRef; + current && current.goForward && current.goForward(); + }; + + /** + * Check if a hostname is allowed + */ + const isAllowedUrl = useCallback( + (hostname) => { + const { PhishingController } = Engine.context; + return ( + (props.whitelist && props.whitelist.includes(hostname)) || + !PhishingController.test(hostname) + ); + }, + [props.whitelist], + ); + + const isBookmark = () => { + const { bookmarks } = props; + const maskedUrl = getMaskedUrl(url.current); + return bookmarks.some(({ url: bookmark }) => bookmark === maskedUrl); + }; + + /** + * Show a phishing modal when a url is not allowed + */ + const handleNotAllowedUrl = (urlToGo) => { + setBlockedUrl(urlToGo); + setTimeout(() => setShowPhishingModal(true), 1000); + }; + + /** + * Get IPFS info from a ens url + */ + const handleIpfsContent = useCallback( + async (fullUrl, { hostname, pathname, query }) => { + const { provider } = Engine.context.NetworkController; + let gatewayUrl; + try { + const { type, hash } = await resolveEnsToIpfsContentId({ + provider, + name: hostname, + }); + if (type === 'ipfs-ns') { + gatewayUrl = `${props.ipfsGateway}${hash}${pathname || '/'}${ + query || '' + }`; + const response = await fetch(gatewayUrl); + const statusCode = response.status; + if (statusCode >= 400) { + Logger.log('Status code ', statusCode, gatewayUrl); + //urlNotFound(gatewayUrl); + return null; + } + } else if (type === 'swarm-ns') { + gatewayUrl = `${AppConstants.SWARM_DEFAULT_GATEWAY_URL}${hash}${ + pathname || '/' + }${query || ''}`; + } else if (type === 'ipns-ns') { + gatewayUrl = `${AppConstants.IPNS_DEFAULT_GATEWAY_URL}${hostname}${ + pathname || '/' + }${query || ''}`; + } + return { + url: gatewayUrl, + hash, + type, + }; + } catch (err) { + // This is a TLD that might be a normal website + // For example .XYZ and might be more in the future + if ( + hostname.substr(-4) !== '.eth' && + err.toString().indexOf('is not standard') !== -1 + ) { + ensIgnoreList.push(hostname); + return { url: fullUrl, reload: true }; + } + if ( + err?.message?.startsWith( + 'EnsIpfsResolver - no known ens-ipfs registry for chainId', + ) + ) { + trackErrorAsAnalytics( + 'Browser: Failed to resolve ENS name for chainId', + err?.message, + ); + } else { + Logger.error(err, 'Failed to resolve ENS name'); + } + + Alert.alert(strings('browser.failed_to_resolve_ens_name'), err.message); + goBack(); + } + }, + [goBack, props.ipfsGateway], + ); + + /** + * Go to a url + */ + const go = useCallback( + async (url, initialCall) => { + const hasProtocol = url.match(/^[a-z]*:\/\//) || isHomepage(url); + const sanitizedURL = hasProtocol ? url : `${props.defaultProtocol}${url}`; + const { hostname, query, pathname } = new URL(sanitizedURL); + + let urlToGo = sanitizedURL; + const isEnsUrl = isENSUrl(url); + const { current } = webviewRef; + if (isEnsUrl) { + current && current.stopLoading(); + const { + url: ensUrl, + type, + hash, + reload, + } = await handleIpfsContent(url, { hostname, query, pathname }); + if (reload) return go(ensUrl); + urlToGo = ensUrl; + sessionENSNames[urlToGo] = { hostname, hash, type }; + } + + if (isAllowedUrl(hostname)) { + if (initialCall || !firstUrlLoaded) { + setInitialUrl(urlToGo); + setFirstUrlLoaded(true); + } else { + current && + current.injectJavaScript( + `(function(){window.location.href = '${urlToGo}' })()`, + ); + } + + setProgress(0); + return sanitizedURL; + } + handleNotAllowedUrl(urlToGo); + return null; + }, + [ + firstUrlLoaded, + handleIpfsContent, + isAllowedUrl, + isHomepage, + props.defaultProtocol, + ], + ); + + /** + * Open a new tab + */ + const openNewTab = useCallback( + (url) => { + toggleOptionsIfNeeded(); + dismissTextSelectionIfNeeded(); + props.newTab(url); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [dismissTextSelectionIfNeeded, toggleOptionsIfNeeded], + ); + + /** + * Hide url input modal + */ + const hideUrlModal = useCallback(() => { + setShowUrlModal(false); + + if (isHomepage()) { + const { current } = webviewRef; + const blur = `document.getElementsByClassName('autocomplete-input')[0].blur();`; + current && current.injectJavaScript(blur); + } + }, [isHomepage]); + + /** + * Handle keyboard hide + */ + const keyboardDidHide = useCallback(() => { + if (!isTabActive() || isEmulatorSync()) return false; + if (!fromHomepage.current) { + if (showUrlModal) { + hideUrlModal(); + } + } + }, [hideUrlModal, isTabActive, showUrlModal]); + + /** + * Set keyboard listeners + */ + useEffect(() => { + const keyboardDidHideListener = Keyboard.addListener( + 'keyboardDidHide', + keyboardDidHide, + ); + return function cleanup() { + keyboardDidHideListener.remove(); + }; + }, [keyboardDidHide]); + + /** + * Reload current page + */ + const reload = useCallback(() => { + const { current } = webviewRef; + current && current.reload(); + }, []); + + /** + * Handle when the drawer (app menu) is opened + */ + const drawerOpenHandler = useCallback(() => { + dismissTextSelectionIfNeeded(); + }, [dismissTextSelectionIfNeeded]); + + /** + * Set initial url, dapp scripts and engine. Similar to componentDidMount + */ + useEffect(() => { + approvedHosts = props.approvedHosts; + const initialUrl = props.initialUrl || HOMEPAGE_URL; + go(initialUrl, true); + + const getEntryScriptWeb3 = async () => { + const entryScriptWeb3 = await EntryScriptWeb3.get(); + setEntryScriptWeb3(entryScriptWeb3 + SPA_urlChangeListener); + }; + + getEntryScriptWeb3(); + + // Specify how to clean up after this effect: + return function cleanup() { + backgroundBridges.current.forEach((bridge) => bridge.onDisconnect()); + + // Remove all Engine listeners + Engine.context.TokensController.hub.removeAllListeners(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** + * Enable the header to toggle the url modal and update other header data + */ + useEffect(() => { + if (props.activeTab === props.id) { + props.navigation.setParams({ + showUrlModal: toggleUrlModal, + url: getMaskedUrl(url.current), + icon: icon.current, + error, + }); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, props.activeTab, props.id, toggleUrlModal]); + + useEffect(() => { + if (Device.isAndroid()) { + DrawerStatusTracker.hub.on('drawer::open', drawerOpenHandler); + } + + return function cleanup() { + if (Device.isAndroid()) { + DrawerStatusTracker && + DrawerStatusTracker.hub && + DrawerStatusTracker.hub.removeListener( + 'drawer::open', + drawerOpenHandler, + ); + } + }; + }, [drawerOpenHandler]); + + /** + * Set navigation listeners + */ + useEffect(() => { + const handleAndroidBackPress = () => { + if (!isTabActive()) return false; + goBack(); + return true; + }; + + BackHandler.addEventListener('hardwareBackPress', handleAndroidBackPress); + + // Handle hardwareBackPress event only for browser, not components rendered on top + props.navigation.addListener('willFocus', () => { + BackHandler.addEventListener('hardwareBackPress', handleAndroidBackPress); + }); + props.navigation.addListener('willBlur', () => { + BackHandler.removeEventListener( + 'hardwareBackPress', + handleAndroidBackPress, + ); + }); + + return function cleanup() { + BackHandler.removeEventListener( + 'hardwareBackPress', + handleAndroidBackPress, + ); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [goBack, isTabActive]); + + /** + * Handles state changes for when the url changes + */ + const changeUrl = (siteInfo, type) => { + url.current = siteInfo.url; + title.current = siteInfo.title; + if (siteInfo.icon) icon.current = siteInfo.icon; + }; + + /** + * Handles state changes for when the url changes + */ + const changeAddressBar = (siteInfo, type) => { + setBackEnabled(siteInfo.canGoBack); + setForwardEnabled(siteInfo.canGoForward); + + isTabActive() && + props.navigation.setParams({ + url: getMaskedUrl(siteInfo.url), + icon: siteInfo.icon, + silent: true, + }); + + props.updateTabInfo(getMaskedUrl(siteInfo.url), props.id); + + props.addToBrowserHistory({ + name: siteInfo.title, + url: getMaskedUrl(siteInfo.url), + }); + }; + + /** + * Go to eth-phishing-detect page + */ + const goToETHPhishingDetector = () => { + setShowPhishingModal(false); + go(`https://github.com/metamask/eth-phishing-detect`); + }; + + /** + * Continue to phishing website + */ + const continueToPhishingSite = () => { + const urlObj = new URL(blockedUrl); + props.addToWhitelist(urlObj.hostname); + setShowPhishingModal(false); + blockedUrl !== url.current && + setTimeout(() => { + go(blockedUrl); + setBlockedUrl(undefined); + }, 1000); + }; + + /** + * Go to etherscam website + */ + const goToEtherscam = () => { + setShowPhishingModal(false); + go(`https://etherscamdb.info/domain/meta-mask.com`); + }; + + /** + * Go to eth-phishing-detect issue + */ + const goToFilePhishingIssue = () => { + setShowPhishingModal(false); + go(`https://github.com/metamask/eth-phishing-detect/issues/new`); + }; + + /** + * Go back from phishing website alert + */ + const goBackToSafety = () => { + blockedUrl === url.current && goBack(); + setTimeout(() => { + setShowPhishingModal(false); + setBlockedUrl(undefined); + }, 500); + }; + + /** + * Renders the phishing modal + */ + const renderPhishingModal = () => ( + + + + ); + + const trackEventSearchUsed = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SEARCH_USED, { + option_chosen: 'Search on URL', + number_of_tabs: undefined, + }); + }; + + /** + * Stops normal loading when it's ens, instead call go to be properly set up + */ + const onShouldStartLoadWithRequest = ({ url }) => { + if (isENSUrl(url)) { + go(url.replace(/^http:\/\//, 'https://')); + return false; + } + return true; + }; + + /** + * Website started to load + */ + const onLoadStart = async ({ nativeEvent }) => { + const { hostname } = new URL(nativeEvent.url); + if (!isAllowedUrl(hostname)) { + return handleNotAllowedUrl(nativeEvent.url); + } + webviewUrlPostMessagePromiseResolve.current = null; + setError(false); + + changeUrl(nativeEvent, 'start'); + + //For Android url on the navigation bar should only update upon load. + if (Device.isAndroid()) { + changeAddressBar(nativeEvent, 'start'); + } + + icon.current = null; + + // Reset the previous bridges + backgroundBridges.current.length && + backgroundBridges.current.forEach((bridge) => bridge.onDisconnect()); + backgroundBridges.current = []; + const origin = new URL(nativeEvent.url).origin; + initializeBackgroundBridge(origin, true); + }; + + /** + * Sets loading bar progress + */ + const onLoadProgress = ({ nativeEvent: { progress } }) => { + setProgress(progress); + }; + + const onLoad = ({ nativeEvent }) => { + //For iOS url on the navigation bar should only update upon load. + if (Device.isIos()) { + changeUrl(nativeEvent, 'start'); + changeAddressBar(nativeEvent, 'start'); + } + }; + + /** + * When website finished loading + */ + const onLoadEnd = ({ nativeEvent }) => { + if (nativeEvent.loading) return; + const { current } = webviewRef; + + current && current.injectJavaScript(JS_WEBVIEW_URL); + + const promiseResolver = (resolve) => { + webviewUrlPostMessagePromiseResolve.current = resolve; + }; + const promise = current + ? new Promise(promiseResolver) + : Promise.resolve(url.current); + + promise.then((info) => { + const { hostname: currentHostname } = new URL(url.current); + const { hostname } = new URL(nativeEvent.url); + if (info.url === nativeEvent.url && currentHostname === hostname) { + changeUrl({ ...nativeEvent, icon: info.icon }, 'end-promise'); + changeAddressBar({ ...nativeEvent, icon: info.icon }, 'end-promise'); + } + }); + }; + + /** + * Handle message from website + */ + const onMessage = ({ nativeEvent }) => { + let data = nativeEvent.data; + try { + data = typeof data === 'string' ? JSON.parse(data) : data; + if (!data || (!data.type && !data.name)) { + return; + } + if (data.name) { + backgroundBridges.current.forEach((bridge) => { + if (bridge.isMainFrame) { + const { origin } = data && data.origin && new URL(data.origin); + bridge.url === origin && bridge.onMessage(data); + } else { + bridge.url === data.origin && bridge.onMessage(data); + } + }); + return; + } + + switch (data.type) { + /** * Disabling iframes for now case 'FRAME_READY': { const { url } = data.payload; onFrameLoadStarted(url); break; }*/ - case 'GET_WEBVIEW_URL': { - const { url } = data.payload; - if (url === nativeEvent.url) - webviewUrlPostMessagePromiseResolve.current && - webviewUrlPostMessagePromiseResolve.current(data.payload); - } - } - } catch (e) { - Logger.error(e, `Browser::onMessage on ${url.current}`); - } - }; - - /** - * Go to home page, reload if already on homepage - */ - const goToHomepage = async () => { - toggleOptionsIfNeeded(); - if (url.current === HOMEPAGE_URL) return reload(); - await go(HOMEPAGE_URL); - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_HOME); - }; - - /** - * Render the progress bar - */ - const renderProgressBar = () => ( - - - - ); - - /** - * When url input changes - */ - const onURLChange = (inputValue) => { - setAutocompleteValue(inputValue); - }; - - /** - * Handle url input submit - */ - const onUrlInputSubmit = async (input = null) => { - const inputValue = (typeof input === 'string' && input) || autocompleteValue; - trackEventSearchUsed(); - if (!inputValue) { - toggleUrlModal(); - return; - } - const { defaultProtocol, searchEngine } = props; - const sanitizedInput = onUrlSubmit(inputValue, searchEngine, defaultProtocol); - await go(sanitizedInput); - toggleUrlModal(); - }; - - /** Clear search input and focus */ - const clearSearchInput = () => { - setAutocompleteValue(''); - inputRef.current?.focus?.(); - }; - - /** - * Render url input modal - */ - const renderUrlModal = () => { - if (showUrlModal && inputRef) { - setTimeout(() => { - const { current } = inputRef; - if (current && !current.isFocused()) { - current.focus(); - } - }, ANIMATION_TIMING); - } - - return ( - - - - - {autocompleteValue ? ( - - - - ) : null} - - - {strings('browser.cancel')} - - - - - ); - }; - - /** - * Handle error, for example, ssl certificate error - */ - const onError = ({ nativeEvent: errorInfo }) => { - Logger.log(errorInfo); - props.navigation.setParams({ - error: true, - }); - setError(errorInfo); - }; - - /** - * Track new tab event - */ - const trackNewTabEvent = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_NEW_TAB, { - option_chosen: 'Browser Options', - number_of_tabs: undefined, - }); - }; - - /** - * Track add site to favorites event - */ - const trackAddToFavoritesEvent = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_ADD_FAVORITES, { - dapp_name: title.current || '', - dapp_url: url.current || '', - }); - }; - - /** - * Track share site event - */ - const trackShareEvent = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SHARE_SITE); - }; - - /** - * Track change network event - */ - const trackSwitchNetworkEvent = ({ from }) => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SWITCH_NETWORK, { - from_chain_id: from, - }); - }; - - /** - * Track reload site event - */ - const trackReloadEvent = () => { - AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_RELOAD); - }; - - /** - * Add bookmark - */ - const addBookmark = () => { - toggleOptionsIfNeeded(); - props.navigation.push('AddBookmarkView', { - screen: 'AddBookmark', - params: { - title: title.current || '', - url: getMaskedUrl(url.current), - onAddBookmark: async ({ name, url }) => { - props.addBookmark({ name, url }); - if (Device.isIos()) { - const item = { - uniqueIdentifier: url, - title: name || getMaskedUrl(url), - contentDescription: `Launch ${name || url} on MetaMask`, - keywords: [name.split(' '), url, 'dapp'], - thumbnail: { - uri: icon.current || `https://api.faviconkit.com/${getHost(url)}/256`, - }, - }; - try { - SearchApi.indexSpotlightItem(item); - } catch (e) { - Logger.error(e, 'Error adding to spotlight'); - } - } - }, - }, - }); - trackAddToFavoritesEvent(); - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_ADD_TO_FAVORITE); - }; - - /** - * Share url - */ - const share = () => { - toggleOptionsIfNeeded(); - Share.open({ - url: url.current, - }).catch((err) => { - Logger.log('Error while trying to share address', err); - }); - trackShareEvent(); - }; - - /** - * Open external link - */ - const openInBrowser = () => { - toggleOptionsIfNeeded(); - Linking.openURL(url.current).catch((error) => - Logger.log(`Error while trying to open external link: ${url.current}`, error) - ); - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_OPEN_IN_BROWSER); - }; - - /** - * Handles reload button press - */ - const onReloadPress = () => { - toggleOptionsIfNeeded(); - reload(); - trackReloadEvent(); - }; - - /** - * Render non-homepage options menu - */ - const renderNonHomeOptions = () => { - if (isHomepage()) return null; - - return ( - - - {!isBookmark() && ( - - )} - - - - ); - }; - - /** - * Handle new tab button press - */ - const onNewTabPress = () => { - openNewTab(); - trackNewTabEvent(); - }; - - /** - * Handle switch network press - */ - const switchNetwork = () => { - const { toggleNetworkModal, network } = props; - toggleOptionsIfNeeded(); - toggleNetworkModal(); - trackSwitchNetworkEvent({ from: network }); - }; - - /** - * Render options menu - */ - const renderOptions = () => { - if (showOptions) { - return ( - - - - - {renderNonHomeOptions()} - - - - - ); - } - }; - - /** - * Show the different tabs - */ - const showTabs = () => { - dismissTextSelectionIfNeeded(); - props.showTabs(); - }; - - /** - * Render the bottom (navigation/options) bar - */ - const renderBottomBar = () => ( - - ); - - /** - * Render the onboarding wizard browser step - */ - const renderOnboardingWizard = () => { - const { wizardStep } = props; - if ([6].includes(wizardStep)) { - if (!wizardScrollAdjusted.current) { - setTimeout(() => { - reload(); - }, 1); - wizardScrollAdjusted.current = true; - } - return ; - } - return null; - }; - - /** - * Return to the MetaMask Dapp Homepage - */ - const returnHome = () => { - go(HOMEPAGE_HOST); - }; - - /** - * Main render - */ - return ( - - - - {!!entryScriptWeb3 && firstUrlLoaded && ( - } - source={{ uri: initialUrl }} - injectedJavaScriptBeforeContentLoaded={entryScriptWeb3} - style={styles.webview} - onLoadStart={onLoadStart} - onLoad={onLoad} - onLoadEnd={onLoadEnd} - onLoadProgress={onLoadProgress} - onMessage={onMessage} - onError={onError} - onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - userAgent={USER_AGENT} - sendCookies - javascriptEnabled - allowsInlineMediaPlayback - useWebkit - testID={'browser-webview'} - /> - )} - - {renderProgressBar()} - {isTabActive() && renderPhishingModal()} - {isTabActive() && renderUrlModal()} - {isTabActive() && renderOptions()} - {isTabActive() && renderBottomBar()} - {isTabActive() && renderOnboardingWizard()} - - - ); + case 'GET_WEBVIEW_URL': { + const { url } = data.payload; + if (url === nativeEvent.url) + webviewUrlPostMessagePromiseResolve.current && + webviewUrlPostMessagePromiseResolve.current(data.payload); + } + } + } catch (e) { + Logger.error(e, `Browser::onMessage on ${url.current}`); + } + }; + + /** + * Go to home page, reload if already on homepage + */ + const goToHomepage = async () => { + toggleOptionsIfNeeded(); + if (url.current === HOMEPAGE_URL) return reload(); + await go(HOMEPAGE_URL); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_HOME); + }; + + /** + * Render the progress bar + */ + const renderProgressBar = () => ( + + + + ); + + /** + * When url input changes + */ + const onURLChange = (inputValue) => { + setAutocompleteValue(inputValue); + }; + + /** + * Handle url input submit + */ + const onUrlInputSubmit = async (input = null) => { + const inputValue = + (typeof input === 'string' && input) || autocompleteValue; + trackEventSearchUsed(); + if (!inputValue) { + toggleUrlModal(); + return; + } + const { defaultProtocol, searchEngine } = props; + const sanitizedInput = onUrlSubmit( + inputValue, + searchEngine, + defaultProtocol, + ); + await go(sanitizedInput); + toggleUrlModal(); + }; + + /** Clear search input and focus */ + const clearSearchInput = () => { + setAutocompleteValue(''); + inputRef.current?.focus?.(); + }; + + /** + * Render url input modal + */ + const renderUrlModal = () => { + if (showUrlModal && inputRef) { + setTimeout(() => { + const { current } = inputRef; + if (current && !current.isFocused()) { + current.focus(); + } + }, ANIMATION_TIMING); + } + + return ( + + + + + {autocompleteValue ? ( + + + + ) : null} + + + + {strings('browser.cancel')} + + + + + + ); + }; + + /** + * Handle error, for example, ssl certificate error + */ + const onError = ({ nativeEvent: errorInfo }) => { + Logger.log(errorInfo); + props.navigation.setParams({ + error: true, + }); + setError(errorInfo); + }; + + /** + * Track new tab event + */ + const trackNewTabEvent = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_NEW_TAB, { + option_chosen: 'Browser Options', + number_of_tabs: undefined, + }); + }; + + /** + * Track add site to favorites event + */ + const trackAddToFavoritesEvent = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_ADD_FAVORITES, { + dapp_name: title.current || '', + dapp_url: url.current || '', + }); + }; + + /** + * Track share site event + */ + const trackShareEvent = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SHARE_SITE); + }; + + /** + * Track change network event + */ + const trackSwitchNetworkEvent = ({ from }) => { + AnalyticsV2.trackEvent( + AnalyticsV2.ANALYTICS_EVENTS.BROWSER_SWITCH_NETWORK, + { + from_chain_id: from, + }, + ); + }; + + /** + * Track reload site event + */ + const trackReloadEvent = () => { + AnalyticsV2.trackEvent(AnalyticsV2.ANALYTICS_EVENTS.BROWSER_RELOAD); + }; + + /** + * Add bookmark + */ + const addBookmark = () => { + toggleOptionsIfNeeded(); + props.navigation.push('AddBookmarkView', { + screen: 'AddBookmark', + params: { + title: title.current || '', + url: getMaskedUrl(url.current), + onAddBookmark: async ({ name, url }) => { + props.addBookmark({ name, url }); + if (Device.isIos()) { + const item = { + uniqueIdentifier: url, + title: name || getMaskedUrl(url), + contentDescription: `Launch ${name || url} on MetaMask`, + keywords: [name.split(' '), url, 'dapp'], + thumbnail: { + uri: + icon.current || + `https://api.faviconkit.com/${getHost(url)}/256`, + }, + }; + try { + SearchApi.indexSpotlightItem(item); + } catch (e) { + Logger.error(e, 'Error adding to spotlight'); + } + } + }, + }, + }); + trackAddToFavoritesEvent(); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_ADD_TO_FAVORITE); + }; + + /** + * Share url + */ + const share = () => { + toggleOptionsIfNeeded(); + Share.open({ + url: url.current, + }).catch((err) => { + Logger.log('Error while trying to share address', err); + }); + trackShareEvent(); + }; + + /** + * Open external link + */ + const openInBrowser = () => { + toggleOptionsIfNeeded(); + Linking.openURL(url.current).catch((error) => + Logger.log( + `Error while trying to open external link: ${url.current}`, + error, + ), + ); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_OPEN_IN_BROWSER); + }; + + /** + * Handles reload button press + */ + const onReloadPress = () => { + toggleOptionsIfNeeded(); + reload(); + trackReloadEvent(); + }; + + /** + * Render non-homepage options menu + */ + const renderNonHomeOptions = () => { + if (isHomepage()) return null; + + return ( + + + {!isBookmark() && ( + + )} + + + + ); + }; + + /** + * Handle new tab button press + */ + const onNewTabPress = () => { + openNewTab(); + trackNewTabEvent(); + }; + + /** + * Handle switch network press + */ + const switchNetwork = () => { + const { toggleNetworkModal, network } = props; + toggleOptionsIfNeeded(); + toggleNetworkModal(); + trackSwitchNetworkEvent({ from: network }); + }; + + /** + * Render options menu + */ + const renderOptions = () => { + if (showOptions) { + return ( + + + + + {renderNonHomeOptions()} + + + + + ); + } + }; + + /** + * Show the different tabs + */ + const showTabs = () => { + dismissTextSelectionIfNeeded(); + props.showTabs(); + }; + + /** + * Render the bottom (navigation/options) bar + */ + const renderBottomBar = () => ( + + ); + + /** + * Render the onboarding wizard browser step + */ + const renderOnboardingWizard = () => { + const { wizardStep } = props; + if ([6].includes(wizardStep)) { + if (!wizardScrollAdjusted.current) { + setTimeout(() => { + reload(); + }, 1); + wizardScrollAdjusted.current = true; + } + return ; + } + return null; + }; + + /** + * Return to the MetaMask Dapp Homepage + */ + const returnHome = () => { + go(HOMEPAGE_HOST); + }; + + /** + * Main render + */ + return ( + + + + {!!entryScriptWeb3 && firstUrlLoaded && ( + ( + + )} + source={{ uri: initialUrl }} + injectedJavaScriptBeforeContentLoaded={entryScriptWeb3} + style={styles.webview} + onLoadStart={onLoadStart} + onLoad={onLoad} + onLoadEnd={onLoadEnd} + onLoadProgress={onLoadProgress} + onMessage={onMessage} + onError={onError} + onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} + userAgent={USER_AGENT} + sendCookies + javascriptEnabled + allowsInlineMediaPlayback + useWebkit + testID={'browser-webview'} + /> + )} + + {renderProgressBar()} + {isTabActive() && renderPhishingModal()} + {isTabActive() && renderUrlModal()} + {isTabActive() && renderOptions()} + {isTabActive() && renderBottomBar()} + {isTabActive() && renderOnboardingWizard()} + + + ); }; BrowserTab.propTypes = { - /** - * The ID of the current tab - */ - id: PropTypes.number, - /** - * The ID of the active tab - */ - activeTab: PropTypes.number, - /** - * InitialUrl - */ - initialUrl: PropTypes.string, - /** - * Called to approve account access for a given hostname - */ - approveHost: PropTypes.func, - /** - * Map of hostnames with approved account access - */ - approvedHosts: PropTypes.object, - /** - * Protocol string to append to URLs that have none - */ - defaultProtocol: PropTypes.string, - /** - * A string that of the chosen ipfs gateway - */ - ipfsGateway: PropTypes.string, - /** - * Object containing the information for the current transaction - */ - transaction: PropTypes.object, - /** - * react-navigation object used to switch between screens - */ - navigation: PropTypes.object, - /** - * A string representing the network id - */ - network: PropTypes.string, - /** - * Indicates whether privacy mode is enabled - */ - privacyMode: PropTypes.bool, - /** - * A string that represents the selected address - */ - selectedAddress: PropTypes.string, - /** - * whitelisted url to bypass the phishing detection - */ - whitelist: PropTypes.array, - /** - * Url coming from an external source - * For ex. deeplinks - */ - url: PropTypes.string, - /** - * Function to toggle the network switcher modal - */ - toggleNetworkModal: PropTypes.func, - /** - * Function to open a new tab - */ - newTab: PropTypes.func, - /** - * Function to store bookmarks - */ - addBookmark: PropTypes.func, - /** - * Array of bookmarks - */ - bookmarks: PropTypes.array, - /** - * String representing the current search engine - */ - searchEngine: PropTypes.string, - /** - * Function to store the a page in the browser history - */ - addToBrowserHistory: PropTypes.func, - /** - * Function to store the a website in the browser whitelist - */ - addToWhitelist: PropTypes.func, - /** - * Function to update the tab information - */ - updateTabInfo: PropTypes.func, - /** - * Function to update the tab information - */ - showTabs: PropTypes.func, - /** - * Action to set onboarding wizard step - */ - setOnboardingWizardStep: PropTypes.func, - /** - * Current onboarding wizard step - */ - wizardStep: PropTypes.number, - /** - * the current version of the app - */ - app_version: PropTypes.string, + /** + * The ID of the current tab + */ + id: PropTypes.number, + /** + * The ID of the active tab + */ + activeTab: PropTypes.number, + /** + * InitialUrl + */ + initialUrl: PropTypes.string, + /** + * Called to approve account access for a given hostname + */ + approveHost: PropTypes.func, + /** + * Map of hostnames with approved account access + */ + approvedHosts: PropTypes.object, + /** + * Protocol string to append to URLs that have none + */ + defaultProtocol: PropTypes.string, + /** + * A string that of the chosen ipfs gateway + */ + ipfsGateway: PropTypes.string, + /** + * Object containing the information for the current transaction + */ + transaction: PropTypes.object, + /** + * react-navigation object used to switch between screens + */ + navigation: PropTypes.object, + /** + * A string representing the network id + */ + network: PropTypes.string, + /** + * Indicates whether privacy mode is enabled + */ + privacyMode: PropTypes.bool, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + * whitelisted url to bypass the phishing detection + */ + whitelist: PropTypes.array, + /** + * Url coming from an external source + * For ex. deeplinks + */ + url: PropTypes.string, + /** + * Function to toggle the network switcher modal + */ + toggleNetworkModal: PropTypes.func, + /** + * Function to open a new tab + */ + newTab: PropTypes.func, + /** + * Function to store bookmarks + */ + addBookmark: PropTypes.func, + /** + * Array of bookmarks + */ + bookmarks: PropTypes.array, + /** + * String representing the current search engine + */ + searchEngine: PropTypes.string, + /** + * Function to store the a page in the browser history + */ + addToBrowserHistory: PropTypes.func, + /** + * Function to store the a website in the browser whitelist + */ + addToWhitelist: PropTypes.func, + /** + * Function to update the tab information + */ + updateTabInfo: PropTypes.func, + /** + * Function to update the tab information + */ + showTabs: PropTypes.func, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func, + /** + * Current onboarding wizard step + */ + wizardStep: PropTypes.number, + /** + * the current version of the app + */ + app_version: PropTypes.string, }; BrowserTab.defaultProps = { - defaultProtocol: 'https://', + defaultProtocol: 'https://', }; const mapStateToProps = (state) => ({ - approvedHosts: state.privacy.approvedHosts, - bookmarks: state.bookmarks, - ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, - network: state.engine.backgroundState.NetworkController.network, - selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress?.toLowerCase(), - privacyMode: state.privacy.privacyMode, - searchEngine: state.settings.searchEngine, - whitelist: state.browser.whitelist, - activeTab: state.browser.activeTab, - wizardStep: state.wizard.step, + approvedHosts: state.privacy.approvedHosts, + bookmarks: state.bookmarks, + ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, + network: state.engine.backgroundState.NetworkController.network, + selectedAddress: + state.engine.backgroundState.PreferencesController.selectedAddress?.toLowerCase(), + privacyMode: state.privacy.privacyMode, + searchEngine: state.settings.searchEngine, + whitelist: state.browser.whitelist, + activeTab: state.browser.activeTab, + wizardStep: state.wizard.step, }); const mapDispatchToProps = (dispatch) => ({ - approveHost: (hostname) => dispatch(approveHost(hostname)), - addBookmark: (bookmark) => dispatch(addBookmark(bookmark)), - addToBrowserHistory: ({ url, name }) => dispatch(addToHistory({ url, name })), - addToWhitelist: (url) => dispatch(addToWhitelist(url)), - toggleNetworkModal: () => dispatch(toggleNetworkModal()), - setOnboardingWizardStep: (step) => dispatch(setOnboardingWizardStep(step)), + approveHost: (hostname) => dispatch(approveHost(hostname)), + addBookmark: (bookmark) => dispatch(addBookmark(bookmark)), + addToBrowserHistory: ({ url, name }) => dispatch(addToHistory({ url, name })), + addToWhitelist: (url) => dispatch(addToWhitelist(url)), + toggleNetworkModal: () => dispatch(toggleNetworkModal()), + setOnboardingWizardStep: (step) => dispatch(setOnboardingWizardStep(step)), }); -export default connect(mapStateToProps, mapDispatchToProps)(withNavigation(BrowserTab)); +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withNavigation(BrowserTab)); diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 5c3c087a78a..cb5fd76e47c 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -5,7 +5,10 @@ import { ethErrors } from 'eth-json-rpc-errors'; import RPCMethods from './index.js'; import { RPC } from '../../constants/network'; import { NetworksChainId, NetworkType } from '@metamask/controllers'; -import Networks, { blockTagParamIndex, getAllNetworks } from '../../util/networks'; +import Networks, { + blockTagParamIndex, + getAllNetworks, +} from '../../util/networks'; import { polyfillGasPrice } from './utils'; import ImportedEngine from '../Engine'; import { strings } from '../../../locales/i18n'; @@ -19,558 +22,631 @@ const Engine = ImportedEngine as any; let appVersion = ''; export enum ApprovalTypes { - CONNECT_ACCOUNTS = 'CONNECT_ACCOUNTS', - SIGN_MESSAGE = 'SIGN_MESSAGE', - ADD_ETHEREUM_CHAIN = 'ADD_ETHEREUM_CHAIN', - SWITCH_ETHEREUM_CHAIN = 'SWITCH_ETHEREUM_CHAIN', + CONNECT_ACCOUNTS = 'CONNECT_ACCOUNTS', + SIGN_MESSAGE = 'SIGN_MESSAGE', + ADD_ETHEREUM_CHAIN = 'ADD_ETHEREUM_CHAIN', + SWITCH_ETHEREUM_CHAIN = 'SWITCH_ETHEREUM_CHAIN', } interface RPCMethodsMiddleParameters { - hostname: string; - getProviderState: () => any; - navigation: any; - getApprovedHosts: any; - setApprovedHosts: (approvedHosts: any) => void; - approveHost: (fullHostname: string) => void; - url: { current: string }; - title: { current: string }; - icon: { current: string }; - approvalRequest: { current: { resolve: (value: boolean) => void; reject: () => void } }; - // Bookmarks - isHomepage: () => boolean; - // Show autocomplete - fromHomepage: { current: boolean }; - setAutocompleteValue: (value: string) => void; - setShowUrlModal: (showUrlModal: boolean) => void; - // Wizard - wizardScrollAdjusted: { current: boolean }; - // For the browser - tabId: string; - // For WalletConnect - isWalletConnect: boolean; - injectHomePageScripts: () => void; + hostname: string; + getProviderState: () => any; + navigation: any; + getApprovedHosts: any; + setApprovedHosts: (approvedHosts: any) => void; + approveHost: (fullHostname: string) => void; + url: { current: string }; + title: { current: string }; + icon: { current: string }; + approvalRequest: { + current: { resolve: (value: boolean) => void; reject: () => void }; + }; + // Bookmarks + isHomepage: () => boolean; + // Show autocomplete + fromHomepage: { current: boolean }; + setAutocompleteValue: (value: string) => void; + setShowUrlModal: (showUrlModal: boolean) => void; + // Wizard + wizardScrollAdjusted: { current: boolean }; + // For the browser + tabId: string; + // For WalletConnect + isWalletConnect: boolean; + injectHomePageScripts: () => void; } -export const checkActiveAccountAndChainId = ({ address, chainId, activeAccounts }: any) => { - if (address) { - if (!activeAccounts || !activeAccounts.length || address.toLowerCase() !== activeAccounts?.[0]?.toLowerCase()) { - throw ethErrors.rpc.invalidParams({ - message: `Invalid parameters: must provide an Ethereum address.`, - }); - } - } - - if (chainId) { - const { provider } = Engine.context.NetworkController.state; - const networkProvider = provider; - const networkType = provider.type as NetworkType; - const isInitialNetwork = networkType && getAllNetworks().includes(networkType); - let activeChainId; - - if (isInitialNetwork) { - activeChainId = NetworksChainId[networkType]; - } else if (networkType === RPC) { - activeChainId = networkProvider.chainId; - } - - if (activeChainId && !activeChainId.startsWith('0x')) { - // Convert to hex - activeChainId = `0x${parseInt(activeChainId, 10).toString(16)}`; - } - - let chainIdRequest = String(chainId); - if (chainIdRequest && !chainIdRequest.startsWith('0x')) { - // Convert to hex - chainIdRequest = `0x${parseInt(chainIdRequest, 10).toString(16)}`; - } - - if (activeChainId !== chainIdRequest) { - Alert.alert(`Active chainId is ${activeChainId} but received ${chainIdRequest}`); - throw ethErrors.rpc.invalidParams({ - message: `Invalid parameters: active chainId is different than the one provided.`, - }); - } - } +export const checkActiveAccountAndChainId = ({ + address, + chainId, + activeAccounts, +}: any) => { + if (address) { + if ( + !activeAccounts || + !activeAccounts.length || + address.toLowerCase() !== activeAccounts?.[0]?.toLowerCase() + ) { + throw ethErrors.rpc.invalidParams({ + message: `Invalid parameters: must provide an Ethereum address.`, + }); + } + } + + if (chainId) { + const { provider } = Engine.context.NetworkController.state; + const networkProvider = provider; + const networkType = provider.type as NetworkType; + const isInitialNetwork = + networkType && getAllNetworks().includes(networkType); + let activeChainId; + + if (isInitialNetwork) { + activeChainId = NetworksChainId[networkType]; + } else if (networkType === RPC) { + activeChainId = networkProvider.chainId; + } + + if (activeChainId && !activeChainId.startsWith('0x')) { + // Convert to hex + activeChainId = `0x${parseInt(activeChainId, 10).toString(16)}`; + } + + let chainIdRequest = String(chainId); + if (chainIdRequest && !chainIdRequest.startsWith('0x')) { + // Convert to hex + chainIdRequest = `0x${parseInt(chainIdRequest, 10).toString(16)}`; + } + + if (activeChainId !== chainIdRequest) { + Alert.alert( + `Active chainId is ${activeChainId} but received ${chainIdRequest}`, + ); + throw ethErrors.rpc.invalidParams({ + message: `Invalid parameters: active chainId is different than the one provided.`, + }); + } + } }; /** * Handle RPC methods called by dapps */ export const getRpcMethodMiddleware = ({ - hostname, - getProviderState, - navigation, - getApprovedHosts, - setApprovedHosts, - approveHost, - // Website info - url, - title, - icon, - // Bookmarks - isHomepage, - // Show autocomplete - fromHomepage, - setAutocompleteValue, - setShowUrlModal, - // Wizard - wizardScrollAdjusted, - // For the browser - tabId, - // For WalletConnect - isWalletConnect, - injectHomePageScripts, + hostname, + getProviderState, + navigation, + getApprovedHosts, + setApprovedHosts, + approveHost, + // Website info + url, + title, + icon, + // Bookmarks + isHomepage, + // Show autocomplete + fromHomepage, + setAutocompleteValue, + setShowUrlModal, + // Wizard + wizardScrollAdjusted, + // For the browser + tabId, + // For WalletConnect + isWalletConnect, + injectHomePageScripts, }: RPCMethodsMiddleParameters) => - // all user facing RPC calls not implemented by the provider - createAsyncMiddleware(async (req: any, res: any, next: any) => { - const getAccounts = (): string[] => { - const { - privacy: { privacyMode }, - } = store.getState(); - - const selectedAddress = Engine.context.PreferencesController.state.selectedAddress?.toLowerCase(); - - const isEnabled = isWalletConnect || !privacyMode || getApprovedHosts()[hostname]; - - return isEnabled && selectedAddress ? [selectedAddress] : []; - }; - - const checkTabActive = () => { - if (!tabId) return true; - const { browser } = store.getState(); - if (tabId !== browser.activeTab) throw ethErrors.provider.userRejectedRequest(); - }; - - const requestUserApproval = async ({ type = '', requestData = {} }) => { - checkTabActive(); - await Engine.context.ApprovalController.clear(ethErrors.provider.userRejectedRequest()); - - const responseData = await Engine.context.ApprovalController.add({ - origin: hostname, - type, - requestData: { - ...requestData, - pageMeta: { url: url.current, title: title.current, icon: icon.current }, - }, - id: random(), - }); - return responseData; - }; - - const rpcMethods: any = { - eth_getTransactionByHash: async () => { - res.result = await polyfillGasPrice('getTransactionByHash', req.params); - }, - eth_getTransactionByBlockHashAndIndex: async () => { - res.result = await polyfillGasPrice('getTransactionByBlockHashAndIndex', req.params); - }, - eth_getTransactionByBlockNumberAndIndex: async () => { - res.result = await polyfillGasPrice('getTransactionByBlockNumberAndIndex', req.params); - }, - eth_chainId: async () => { - const { provider } = Engine.context.NetworkController.state; - const networkProvider = provider; - const networkType = provider.type as NetworkType; - const isInitialNetwork = networkType && getAllNetworks().includes(networkType); - let chainId; - - if (isInitialNetwork) { - chainId = NetworksChainId[networkType]; - } else if (networkType === RPC) { - chainId = networkProvider.chainId; - } - - if (chainId && !chainId.startsWith('0x')) { - // Convert to hex - res.result = `0x${parseInt(chainId, 10).toString(16)}`; - } - }, - net_version: async () => { - const { - provider: { type: networkType }, - } = Engine.context.NetworkController.state; - - const isInitialNetwork = networkType && getAllNetworks().includes(networkType); - if (isInitialNetwork) { - res.result = (Networks as any)[networkType].networkId; - } else { - return next(); - } - }, - eth_requestAccounts: async () => { - const { params } = req; - const { - privacy: { privacyMode }, - } = store.getState(); - - let { selectedAddress } = Engine.context.PreferencesController.state; - - selectedAddress = selectedAddress?.toLowerCase(); - - if (isWalletConnect || !privacyMode || ((!params || !params.force) && getApprovedHosts()[hostname])) { - res.result = [selectedAddress]; - } else { - try { - await requestUserApproval({ type: ApprovalTypes.CONNECT_ACCOUNTS, requestData: { hostname } }); - const fullHostname = new URL(url.current).hostname; - approveHost?.(fullHostname); - setApprovedHosts?.({ ...getApprovedHosts?.(), [fullHostname]: true }); - - res.result = selectedAddress ? [selectedAddress] : []; - } catch (e) { - throw ethErrors.provider.userRejectedRequest('User denied account authorization.'); - } - } - }, - eth_accounts: async () => { - res.result = await getAccounts(); - }, - - eth_coinbase: async () => { - const accounts = await getAccounts(); - res.result = accounts.length > 0 ? accounts[0] : null; - }, - eth_sendTransaction: () => { - checkTabActive(); - checkActiveAccountAndChainId({ - address: req.params[0].from, - chainId: req.params[0].chainId, - activeAccounts: getAccounts(), - }); - next(); - }, - eth_signTransaction: async () => { - // This is implemented later in our middleware stack – specifically, in - // eth-json-rpc-middleware – but our UI does not support it. - throw ethErrors.rpc.methodNotSupported(); - }, - eth_sign: async () => { - const { MessageManager } = Engine.context; - const pageMeta = { - meta: { - url: url.current, - title: title.current, - icon: icon.current, - }, - }; - - checkTabActive(); - checkActiveAccountAndChainId({ address: req.params[0].from, activeAccounts: getAccounts() }); - - if (req.params[1].length === 66 || req.params[1].length === 67) { - const rawSig = await MessageManager.addUnapprovedMessageAsync({ - data: req.params[1], - from: req.params[0], - ...pageMeta, - origin: hostname, - }); - - res.result = rawSig; - } else { - throw ethErrors.rpc.invalidParams('eth_sign requires 32 byte message hash'); - } - }, - - personal_sign: async () => { - const { PersonalMessageManager } = Engine.context; - const firstParam = req.params[0]; - const secondParam = req.params[1]; - const params = { - data: firstParam, - from: secondParam, - }; - - if (resemblesAddress(firstParam) && !resemblesAddress(secondParam)) { - params.data = secondParam; - params.from = firstParam; - } - - const pageMeta = { - meta: { - url: url.current, - title: title.current, - icon: icon.current, - }, - }; - - checkTabActive(); - checkActiveAccountAndChainId({ address: params.from, activeAccounts: getAccounts() }); - - const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ - ...params, - ...pageMeta, - origin: hostname, - }); - - res.result = rawSig; - }, - - eth_signTypedData: async () => { - const { TypedMessageManager } = Engine.context; - const pageMeta = { - meta: { - url: url.current, - title: title.current, - icon: icon.current, - }, - }; - - checkTabActive(); - checkActiveAccountAndChainId({ address: req.params[1], activeAccounts: getAccounts() }); - - const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( - { - data: req.params[0], - from: req.params[1], - ...pageMeta, - origin: hostname, - }, - 'V1' - ); - - res.result = rawSig; - }, - - eth_signTypedData_v3: async () => { - const { TypedMessageManager } = Engine.context; - - const data = JSON.parse(req.params[1]); - const chainId = data.domain.chainId; - - const pageMeta = { - meta: { - url: url.current, - title: title.current, - icon: icon.current, - }, - }; - - checkTabActive(); - checkActiveAccountAndChainId({ address: req.params[0], chainId, activeAccounts: getAccounts() }); - - const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( - { - data: req.params[1], - from: req.params[0], - ...pageMeta, - origin: hostname, - }, - 'V3' - ); - - res.result = rawSig; - }, - - eth_signTypedData_v4: async () => { - const { TypedMessageManager } = Engine.context; - - const data = JSON.parse(req.params[1]); - const chainId = data.domain.chainId; - - const pageMeta = { - meta: { - url: url.current, - title: title.current, - icon: icon.current, - }, - }; - - checkTabActive(); - checkActiveAccountAndChainId({ address: req.params[0], chainId, activeAccounts: getAccounts() }); - - const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( - { - data: req.params[1], - from: req.params[0], - ...pageMeta, - origin: hostname, - }, - 'V4' - ); - - res.result = rawSig; - }, - - web3_clientVersion: async () => { - if (!appVersion) { - appVersion = await getVersion(); - } - res.result = `MetaMask/${appVersion}/Mobile`; - }, - - wallet_scanQRCode: () => - new Promise((resolve, reject) => { - checkTabActive(); - navigation.navigate('QRScanner', { - onScanSuccess: (data: any) => { - const regex = new RegExp(req.params[0]); - if (regex && !regex.exec(data)) { - reject({ message: 'NO_REGEX_MATCH', data }); - } else if (!regex && !/^(0x){1}[0-9a-fA-F]{40}$/i.exec(data.target_address)) { - reject({ message: 'INVALID_ETHEREUM_ADDRESS', data: data.target_address }); - } - let result = data; - if (data.target_address) { - result = data.target_address; - } else if (data.scheme) { - result = JSON.stringify(data); - } - res.result = result; - resolve(); - }, - onScanError: (e: { toString: () => any }) => { - throw ethErrors.rpc.internal(e.toString()); - }, - }); - }), - - wallet_watchAsset: async () => { - const { - params: { - options: { address, decimals, image, symbol }, - type, - }, - } = req; - const { TokensController } = Engine.context; - - checkTabActive(); - try { - const watchAssetResult = await TokensController.watchAsset( - { address, symbol, decimals, image }, - type - ); - await watchAssetResult.result; - res.result = true; - } catch (error) { - if ((error as Error).message === 'User rejected to watch the asset.') { - throw ethErrors.provider.userRejectedRequest(); - } - throw error; - } - }, - - metamask_removeFavorite: async () => { - checkTabActive(); - if (!isHomepage()) { - throw ethErrors.provider.unauthorized('Forbidden.'); - } - - const { bookmarks } = store.getState(); - - Alert.alert(strings('browser.remove_bookmark_title'), strings('browser.remove_bookmark_msg'), [ - { - text: strings('browser.cancel'), - onPress: () => { - res.result = { - favorites: bookmarks, - }; - }, - style: 'cancel', - }, - { - text: strings('browser.yes'), - onPress: () => { - const bookmark = { url: req.params[0] }; - - store.dispatch(removeBookmark(bookmark)); - - res.result = { - favorites: bookmarks, - }; - }, - }, - ]); - }, - - metamask_showTutorial: async () => { - checkTabActive(); - if (!isHomepage()) { - throw ethErrors.provider.unauthorized('Forbidden.'); - } - wizardScrollAdjusted.current = false; - - store.dispatch(setOnboardingWizardStep(1)); - - navigation.navigate('WalletView'); - - res.result = true; - }, - - metamask_showAutocomplete: async () => { - checkTabActive(); - if (!isHomepage()) { - throw ethErrors.provider.unauthorized('Forbidden.'); - } - fromHomepage.current = true; - setAutocompleteValue(''); - setShowUrlModal(true); - - setTimeout(() => { - fromHomepage.current = false; - }, 1500); - - res.result = true; - }, - - metamask_injectHomepageScripts: async () => { - if (isHomepage()) { - injectHomePageScripts(); - } - res.result = true; - }, - - /** - * This method is used by the inpage provider to get its state on - * initialization. - */ - metamask_getProviderState: async () => { - res.result = { - ...getProviderState(), - accounts: await getAccounts(), - }; - }, - - /** - * This method is sent by the window.web3 shim. It can be used to - * record web3 shim usage metrics. These metrics are already collected - * in the extension, and can optionally be added to mobile as well. - * - * For now, we need to respond to this method to not throw errors on - * the page, and we implement it as a no-op. - */ - metamask_logWeb3ShimUsage: () => (res.result = null), - wallet_addEthereumChain: () => { - checkTabActive(); - return RPCMethods.wallet_addEthereumChain({ - req, - res, - requestUserApproval, - }); - }, - - wallet_switchEthereumChain: () => { - checkTabActive(); - return RPCMethods.wallet_switchEthereumChain({ - req, - res, - requestUserApproval, - }); - }, - }; - - const blockRefIndex = blockTagParamIndex(req); - if (blockRefIndex) { - const blockRef = req.params?.[blockRefIndex]; - // omitted blockRef implies "latest" - if (blockRef === undefined) { - req.params[blockRefIndex] = 'latest'; - } - } - - if (!rpcMethods[req.method]) { - return next(); - } - await rpcMethods[req.method](); - }); + // all user facing RPC calls not implemented by the provider + createAsyncMiddleware(async (req: any, res: any, next: any) => { + const getAccounts = (): string[] => { + const { + privacy: { privacyMode }, + } = store.getState(); + + const selectedAddress = + Engine.context.PreferencesController.state.selectedAddress?.toLowerCase(); + + const isEnabled = + isWalletConnect || !privacyMode || getApprovedHosts()[hostname]; + + return isEnabled && selectedAddress ? [selectedAddress] : []; + }; + + const checkTabActive = () => { + if (!tabId) return true; + const { browser } = store.getState(); + if (tabId !== browser.activeTab) + throw ethErrors.provider.userRejectedRequest(); + }; + + const requestUserApproval = async ({ type = '', requestData = {} }) => { + checkTabActive(); + await Engine.context.ApprovalController.clear( + ethErrors.provider.userRejectedRequest(), + ); + + const responseData = await Engine.context.ApprovalController.add({ + origin: hostname, + type, + requestData: { + ...requestData, + pageMeta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }, + id: random(), + }); + return responseData; + }; + + const rpcMethods: any = { + eth_getTransactionByHash: async () => { + res.result = await polyfillGasPrice('getTransactionByHash', req.params); + }, + eth_getTransactionByBlockHashAndIndex: async () => { + res.result = await polyfillGasPrice( + 'getTransactionByBlockHashAndIndex', + req.params, + ); + }, + eth_getTransactionByBlockNumberAndIndex: async () => { + res.result = await polyfillGasPrice( + 'getTransactionByBlockNumberAndIndex', + req.params, + ); + }, + eth_chainId: async () => { + const { provider } = Engine.context.NetworkController.state; + const networkProvider = provider; + const networkType = provider.type as NetworkType; + const isInitialNetwork = + networkType && getAllNetworks().includes(networkType); + let chainId; + + if (isInitialNetwork) { + chainId = NetworksChainId[networkType]; + } else if (networkType === RPC) { + chainId = networkProvider.chainId; + } + + if (chainId && !chainId.startsWith('0x')) { + // Convert to hex + res.result = `0x${parseInt(chainId, 10).toString(16)}`; + } + }, + net_version: async () => { + const { + provider: { type: networkType }, + } = Engine.context.NetworkController.state; + + const isInitialNetwork = + networkType && getAllNetworks().includes(networkType); + if (isInitialNetwork) { + res.result = (Networks as any)[networkType].networkId; + } else { + return next(); + } + }, + eth_requestAccounts: async () => { + const { params } = req; + const { + privacy: { privacyMode }, + } = store.getState(); + + let { selectedAddress } = Engine.context.PreferencesController.state; + + selectedAddress = selectedAddress?.toLowerCase(); + + if ( + isWalletConnect || + !privacyMode || + ((!params || !params.force) && getApprovedHosts()[hostname]) + ) { + res.result = [selectedAddress]; + } else { + try { + await requestUserApproval({ + type: ApprovalTypes.CONNECT_ACCOUNTS, + requestData: { hostname }, + }); + const fullHostname = new URL(url.current).hostname; + approveHost?.(fullHostname); + setApprovedHosts?.({ + ...getApprovedHosts?.(), + [fullHostname]: true, + }); + + res.result = selectedAddress ? [selectedAddress] : []; + } catch (e) { + throw ethErrors.provider.userRejectedRequest( + 'User denied account authorization.', + ); + } + } + }, + eth_accounts: async () => { + res.result = await getAccounts(); + }, + + eth_coinbase: async () => { + const accounts = await getAccounts(); + res.result = accounts.length > 0 ? accounts[0] : null; + }, + eth_sendTransaction: () => { + checkTabActive(); + checkActiveAccountAndChainId({ + address: req.params[0].from, + chainId: req.params[0].chainId, + activeAccounts: getAccounts(), + }); + next(); + }, + eth_signTransaction: async () => { + // This is implemented later in our middleware stack – specifically, in + // eth-json-rpc-middleware – but our UI does not support it. + throw ethErrors.rpc.methodNotSupported(); + }, + eth_sign: async () => { + const { MessageManager } = Engine.context; + const pageMeta = { + meta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }; + + checkTabActive(); + checkActiveAccountAndChainId({ + address: req.params[0].from, + activeAccounts: getAccounts(), + }); + + if (req.params[1].length === 66 || req.params[1].length === 67) { + const rawSig = await MessageManager.addUnapprovedMessageAsync({ + data: req.params[1], + from: req.params[0], + ...pageMeta, + origin: hostname, + }); + + res.result = rawSig; + } else { + throw ethErrors.rpc.invalidParams( + 'eth_sign requires 32 byte message hash', + ); + } + }, + + personal_sign: async () => { + const { PersonalMessageManager } = Engine.context; + const firstParam = req.params[0]; + const secondParam = req.params[1]; + const params = { + data: firstParam, + from: secondParam, + }; + + if (resemblesAddress(firstParam) && !resemblesAddress(secondParam)) { + params.data = secondParam; + params.from = firstParam; + } + + const pageMeta = { + meta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }; + + checkTabActive(); + checkActiveAccountAndChainId({ + address: params.from, + activeAccounts: getAccounts(), + }); + + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + ...params, + ...pageMeta, + origin: hostname, + }); + + res.result = rawSig; + }, + + eth_signTypedData: async () => { + const { TypedMessageManager } = Engine.context; + const pageMeta = { + meta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }; + + checkTabActive(); + checkActiveAccountAndChainId({ + address: req.params[1], + activeAccounts: getAccounts(), + }); + + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: req.params[0], + from: req.params[1], + ...pageMeta, + origin: hostname, + }, + 'V1', + ); + + res.result = rawSig; + }, + + eth_signTypedData_v3: async () => { + const { TypedMessageManager } = Engine.context; + + const data = JSON.parse(req.params[1]); + const chainId = data.domain.chainId; + + const pageMeta = { + meta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }; + + checkTabActive(); + checkActiveAccountAndChainId({ + address: req.params[0], + chainId, + activeAccounts: getAccounts(), + }); + + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: req.params[1], + from: req.params[0], + ...pageMeta, + origin: hostname, + }, + 'V3', + ); + + res.result = rawSig; + }, + + eth_signTypedData_v4: async () => { + const { TypedMessageManager } = Engine.context; + + const data = JSON.parse(req.params[1]); + const chainId = data.domain.chainId; + + const pageMeta = { + meta: { + url: url.current, + title: title.current, + icon: icon.current, + }, + }; + + checkTabActive(); + checkActiveAccountAndChainId({ + address: req.params[0], + chainId, + activeAccounts: getAccounts(), + }); + + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: req.params[1], + from: req.params[0], + ...pageMeta, + origin: hostname, + }, + 'V4', + ); + + res.result = rawSig; + }, + + web3_clientVersion: async () => { + if (!appVersion) { + appVersion = await getVersion(); + } + res.result = `MetaMask/${appVersion}/Mobile`; + }, + + wallet_scanQRCode: () => + new Promise((resolve, reject) => { + checkTabActive(); + navigation.navigate('QRScanner', { + onScanSuccess: (data: any) => { + const regex = new RegExp(req.params[0]); + if (regex && !regex.exec(data)) { + reject({ message: 'NO_REGEX_MATCH', data }); + } else if ( + !regex && + !/^(0x){1}[0-9a-fA-F]{40}$/i.exec(data.target_address) + ) { + reject({ + message: 'INVALID_ETHEREUM_ADDRESS', + data: data.target_address, + }); + } + let result = data; + if (data.target_address) { + result = data.target_address; + } else if (data.scheme) { + result = JSON.stringify(data); + } + res.result = result; + resolve(); + }, + onScanError: (e: { toString: () => any }) => { + throw ethErrors.rpc.internal(e.toString()); + }, + }); + }), + + wallet_watchAsset: async () => { + const { + params: { + options: { address, decimals, image, symbol }, + type, + }, + } = req; + const { TokensController } = Engine.context; + + checkTabActive(); + try { + const watchAssetResult = await TokensController.watchAsset( + { address, symbol, decimals, image }, + type, + ); + await watchAssetResult.result; + res.result = true; + } catch (error) { + if ( + (error as Error).message === 'User rejected to watch the asset.' + ) { + throw ethErrors.provider.userRejectedRequest(); + } + throw error; + } + }, + + metamask_removeFavorite: async () => { + checkTabActive(); + if (!isHomepage()) { + throw ethErrors.provider.unauthorized('Forbidden.'); + } + + const { bookmarks } = store.getState(); + + Alert.alert( + strings('browser.remove_bookmark_title'), + strings('browser.remove_bookmark_msg'), + [ + { + text: strings('browser.cancel'), + onPress: () => { + res.result = { + favorites: bookmarks, + }; + }, + style: 'cancel', + }, + { + text: strings('browser.yes'), + onPress: () => { + const bookmark = { url: req.params[0] }; + + store.dispatch(removeBookmark(bookmark)); + + res.result = { + favorites: bookmarks, + }; + }, + }, + ], + ); + }, + + metamask_showTutorial: async () => { + checkTabActive(); + if (!isHomepage()) { + throw ethErrors.provider.unauthorized('Forbidden.'); + } + wizardScrollAdjusted.current = false; + + store.dispatch(setOnboardingWizardStep(1)); + + navigation.navigate('WalletView'); + + res.result = true; + }, + + metamask_showAutocomplete: async () => { + checkTabActive(); + if (!isHomepage()) { + throw ethErrors.provider.unauthorized('Forbidden.'); + } + fromHomepage.current = true; + setAutocompleteValue(''); + setShowUrlModal(true); + + setTimeout(() => { + fromHomepage.current = false; + }, 1500); + + res.result = true; + }, + + metamask_injectHomepageScripts: async () => { + if (isHomepage()) { + injectHomePageScripts(); + } + res.result = true; + }, + + /** + * This method is used by the inpage provider to get its state on + * initialization. + */ + metamask_getProviderState: async () => { + res.result = { + ...getProviderState(), + accounts: await getAccounts(), + }; + }, + + /** + * This method is sent by the window.web3 shim. It can be used to + * record web3 shim usage metrics. These metrics are already collected + * in the extension, and can optionally be added to mobile as well. + * + * For now, we need to respond to this method to not throw errors on + * the page, and we implement it as a no-op. + */ + metamask_logWeb3ShimUsage: () => (res.result = null), + wallet_addEthereumChain: () => { + checkTabActive(); + return RPCMethods.wallet_addEthereumChain({ + req, + res, + requestUserApproval, + }); + }, + + wallet_switchEthereumChain: () => { + checkTabActive(); + return RPCMethods.wallet_switchEthereumChain({ + req, + res, + requestUserApproval, + }); + }, + }; + + const blockRefIndex = blockTagParamIndex(req); + if (blockRefIndex) { + const blockRef = req.params?.[blockRefIndex]; + // omitted blockRef implies "latest" + if (blockRef === undefined) { + req.params[blockRefIndex] = 'latest'; + } + } + + if (!rpcMethods[req.method]) { + return next(); + } + await rpcMethods[req.method](); + }); export default getRpcMethodMiddleware; From b568ddf54fef8d63aa5dda2bdb8a813f2a3cea41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Wed, 11 May 2022 22:46:28 +0100 Subject: [PATCH 5/6] Fix bookmarks update --- app/components/Views/BrowserTab/index.js | 6 +++--- app/core/RPCMethods/RPCMethodMiddleware.ts | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 6e87469faee..beefa483b17 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -64,7 +64,7 @@ import { getRpcMethodMiddleware } from '../../../core/RPCMethods/RPCMethodMiddle import { useAppThemeFromContext, mockTheme } from '../../../util/theme'; const { HOMEPAGE_URL, USER_AGENT, NOTIFICATION_NAMES } = AppConstants; -const HOMEPAGE_HOST = 'localhost:3001'; +const HOMEPAGE_HOST = 'home.metamask.io'; const MM_MIXPANEL_TOKEN = process.env.MM_MIXPANEL_TOKEN; const ANIMATION_TIMING = 300; @@ -378,12 +378,12 @@ export const BrowserTab = (props) => { /** * Inject home page scripts to get the favourites and set analytics key */ - const injectHomePageScripts = async () => { + const injectHomePageScripts = async (bookmarks) => { const { current } = webviewRef; const analyticsEnabled = Analytics.getEnabled(); const disctinctId = await Analytics.getDistinctId(); const homepageScripts = ` - window.__mmFavorites = ${JSON.stringify(props.bookmarks)}; + window.__mmFavorites = ${JSON.stringify(bookmarks || props.bookmarks)}; window.__mmSearchEngine = "${props.searchEngine}"; window.__mmMetametrics = ${analyticsEnabled}; window.__mmDistinctId = "${disctinctId}"; diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index cb5fd76e47c..7a889771b58 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -53,7 +53,7 @@ interface RPCMethodsMiddleParameters { tabId: string; // For WalletConnect isWalletConnect: boolean; - injectHomePageScripts: () => void; + injectHomePageScripts: (bookmarks?: []) => void; } export const checkActiveAccountAndChainId = ({ @@ -549,6 +549,12 @@ export const getRpcMethodMiddleware = ({ store.dispatch(removeBookmark(bookmark)); + const { bookmarks: updatedBookmarks } = store.getState(); + + if (isHomepage()) { + injectHomePageScripts(updatedBookmarks); + } + res.result = { favorites: bookmarks, }; From cd52ce33d9ff3e2bd8467eb4b2290ff5218e9343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Fatia?= Date: Fri, 13 May 2022 10:55:15 +0100 Subject: [PATCH 6/6] Use dynamic hostname instead of hardcoded value --- app/components/Views/BrowserTab/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index df196191220..fe128be17cd 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -61,7 +61,7 @@ import downloadFile from '../../../util/browser/downloadFile'; import { createBrowserUrlModalNavDetails } from '../BrowserUrlModal/BrowserUrlModal'; const { HOMEPAGE_URL, USER_AGENT, NOTIFICATION_NAMES } = AppConstants; -const HOMEPAGE_HOST = 'home.metamask.io'; +const HOMEPAGE_HOST = new URL(HOMEPAGE_URL)?.hostname; const MM_MIXPANEL_TOKEN = process.env.MM_MIXPANEL_TOKEN; const createStyles = (colors) =>