diff --git a/app/components/Views/Browser/index.js b/app/components/Views/Browser/index.js index 607515b18fda..698e137d2aeb 100644 --- a/app/components/Views/Browser/index.js +++ b/app/components/Views/Browser/index.js @@ -1,5 +1,11 @@ import PropTypes from 'prop-types'; -import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { View } from 'react-native'; import { captureScreen } from 'react-native-view-shot'; import { connect, useSelector } from 'react-redux'; @@ -29,10 +35,20 @@ import URL from 'url-parse'; import { useMetrics } from '../../hooks/useMetrics'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { appendURLParams } from '../../../util/browser'; -import { THUMB_WIDTH, THUMB_HEIGHT, IDLE_TIME_CALC_INTERVAL, IDLE_TIME_MAX } from './constants'; +import { + THUMB_WIDTH, + THUMB_HEIGHT, + IDLE_TIME_CALC_INTERVAL, + IDLE_TIME_MAX, +} from './constants'; import { useStyles } from '../../hooks/useStyles'; import styleSheet from './styles'; import Routes from '../../../constants/navigation/Routes'; +///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) +import { selectSelectedInternalAccount } from '../../../selectors/accountsController'; +import { isSolanaAccount } from '../../../core/Multichain/utils'; +import { useFocusEffect } from '@react-navigation/native'; +///: END:ONLY_INCLUDE_IF const MAX_BROWSER_TABS = 5; @@ -71,27 +87,65 @@ export const Browser = (props) => { (state) => state.security.dataCollectionForMarketing, ); - const homePageUrl = useCallback(() => - appendURLParams(AppConstants.HOMEPAGE_URL, { - metricsEnabled: isEnabled(), - marketingEnabled: isDataCollectionForMarketingEnabled ?? false, - }).href, + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + const currentSelectedAccount = useSelector(selectSelectedInternalAccount); + ///: END:ONLY_INCLUDE_IF + + const homePageUrl = useCallback( + () => + appendURLParams(AppConstants.HOMEPAGE_URL, { + metricsEnabled: isEnabled(), + marketingEnabled: isDataCollectionForMarketingEnabled ?? false, + }).href, [isEnabled, isDataCollectionForMarketingEnabled], ); - const newTab = useCallback((url, linkType) => { - // if tabs.length > MAX_BROWSER_TABS, show the max browser tabs modal - if (tabs.length >= MAX_BROWSER_TABS) { - navigation.navigate(Routes.MODAL.MAX_BROWSER_TABS_MODAL); - } else { - // When a new tab is created, a new tab is rendered, which automatically sets the url source on the webview - createNewTab(url || homePageUrl(), linkType); - } - }, [tabs, navigation, homePageUrl, createNewTab]); + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) + // TODO remove after we release Solana dapp connectivity + useFocusEffect( + useCallback(() => { + if (isSolanaAccount(currentSelectedAccount)) { + toastRef?.current?.showToast({ + variant: ToastVariants.Network, + networkImageSource: require('../../../images/solana-logo.png'), + labelOptions: [ + { + label: `${strings( + 'browser.toast.solana_dapp_connection_coming_soon.title', + )} \n`, + isBold: true, + }, + { + label: `${strings( + 'browser.toast.solana_dapp_connection_coming_soon.message', + )}`, + }, + ], + }); + } + }, [toastRef, currentSelectedAccount]), + ); + ///: END:ONLY_INCLUDE_IF - const updateTabInfo = useCallback((tabID, info) => { - updateTab(tabID, info); - }, [updateTab]); + const newTab = useCallback( + (url, linkType) => { + // if tabs.length > MAX_BROWSER_TABS, show the max browser tabs modal + if (tabs.length >= MAX_BROWSER_TABS) { + navigation.navigate(Routes.MODAL.MAX_BROWSER_TABS_MODAL); + } else { + // When a new tab is created, a new tab is rendered, which automatically sets the url source on the webview + createNewTab(url || homePageUrl(), linkType); + } + }, + [tabs, navigation, homePageUrl, createNewTab], + ); + + const updateTabInfo = useCallback( + (tabID, info) => { + updateTab(tabID, info); + }, + [updateTab], + ); const hideTabsAndUpdateUrl = (url) => { navigation.setParams({ @@ -125,7 +179,8 @@ export const Browser = (props) => { // if it isn't the active tab if (tab.id !== activeTabId) { // add idle time for each non-active tab - newIdleTimes[tab.id] = (newIdleTimes[tab.id] || 0) + IDLE_TIME_CALC_INTERVAL; + newIdleTimes[tab.id] = + (newIdleTimes[tab.id] || 0) + IDLE_TIME_CALC_INTERVAL; // if the tab has surpassed the maximum if (newIdleTimes[tab.id] > IDLE_TIME_MAX) { // then "archive" it @@ -257,27 +312,28 @@ export const Browser = (props) => { ], ); - const takeScreenshot = useCallback((url, tabID) => - new Promise((resolve, reject) => { - captureScreen({ - format: 'jpg', - quality: 0.2, - THUMB_WIDTH, - THUMB_HEIGHT, - }).then( - (uri) => { - updateTab(tabID, { - url, - image: uri, - }); - resolve(true); - }, - (error) => { - Logger.error(error, `Error saving tab ${url}`); - reject(error); - }, - ); - }), + const takeScreenshot = useCallback( + (url, tabID) => + new Promise((resolve, reject) => { + captureScreen({ + format: 'jpg', + quality: 0.2, + THUMB_WIDTH, + THUMB_HEIGHT, + }).then( + (uri) => { + updateTab(tabID, { + url, + image: uri, + }); + resolve(true); + }, + (error) => { + Logger.error(error, `Error saving tab ${url}`); + reject(error); + }, + ); + }), [updateTab], ); @@ -362,19 +418,32 @@ export const Browser = (props) => { return null; }; - const renderBrowserTabWindows = useCallback(() => tabs.filter((tab) => !tab.isArchived).map((tab) => ( - - )), [tabs, route.params?.showTabs, newTab, homePageUrl, updateTabInfo, showTabs]); + const renderBrowserTabWindows = useCallback( + () => + tabs + .filter((tab) => !tab.isArchived) + .map((tab) => ( + + )), + [ + tabs, + route.params?.showTabs, + newTab, + homePageUrl, + updateTabInfo, + showTabs, + ], + ); return ( ({ + useAccounts: jest.fn().mockReturnValue({ + evmAccounts: [], + accounts: [], + ensByAccountAddress: {}, + }), +})); const mockTabs = [ { id: 1, url: 'about:blank', image: '', isArchived: false }, @@ -28,6 +37,7 @@ const mockInitialState = { backgroundState: { ...backgroundState, BrowserController: { tabs: mockTabs }, + AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, }, }, security: {}, @@ -42,20 +52,25 @@ const mockInitialState = { browser: { tabs: mockTabs, activeTab: 1, - } + }, }; -jest.mock('../../../core/Engine', () => ({ - context: { - PhishingController: { - maybeUpdateState: jest.fn(), - test: jest.fn((url: string) => { - if (url === 'phishing.com') return { result: true }; - return { result: false }; - }), +jest.mock('../../../core/Engine', () => { + const { MOCK_ACCOUNTS_CONTROLLER_STATE: mockAccountsControllerState } = + jest.requireActual('../../../util/test/accountsControllerTestUtils'); + return { + context: { + PhishingController: { + maybeUpdateState: jest.fn(), + test: jest.fn((url: string) => { + if (url === 'phishing.com') return { result: true }; + return { result: false }; + }), + }, + AccountsController: mockAccountsControllerState, }, - }, -})); + }; +}); jest.mock('react-native/Libraries/Linking/Linking', () => ({ addEventListener: jest.fn(), @@ -109,7 +124,8 @@ describe('Browser', () => { - , { state: { ...mockInitialState } }, + , + { state: { ...mockInitialState } }, ); expect(toJSON()).toMatchSnapshot(); }); @@ -152,7 +168,9 @@ describe('Browser', () => { {() => ( { - + , ); // Check if myFunction was called - expect(navigationSpy).toHaveBeenCalledWith(Routes.MODAL.MAX_BROWSER_TABS_MODAL); + expect(navigationSpy).toHaveBeenCalledWith( + Routes.MODAL.MAX_BROWSER_TABS_MODAL, + ); // Clean up the spy navigationSpy.mockRestore(); }); - it('should mark a tab as archived if it has been idle for too long', async () => { const mockTabsForIdling = [ { id: 1, url: 'about:blank', image: '', isArchived: false }, @@ -206,7 +225,7 @@ describe('Browser', () => { - + , ); // Wrap the timer advancement in act diff --git a/locales/languages/en.json b/locales/languages/en.json index e83b04a9a929..741911c0bccd 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1738,7 +1738,13 @@ "generic": "This website has been blocked from automatically opening an external application" }, "ipfs_gateway_off_title": "IPFS gateway is off", - "ipfs_gateway_off_content": "To see this site, turn on IPFS gateway in Privacy and Security Settings." + "ipfs_gateway_off_content": "To see this site, turn on IPFS gateway in Privacy and Security Settings.", + "toast": { + "solana_dapp_connection_coming_soon": { + "title": "Solana dapp connection coming soon", + "message": "You can connect to Solana dapps in the extension or trade Solana tokens in-app." + } + } }, "backup_alert": { "title": "Protect your wallet",