Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 123 additions & 54 deletions app/components/Views/Browser/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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],
);

Expand Down Expand Up @@ -362,19 +418,32 @@ export const Browser = (props) => {
return null;
};

const renderBrowserTabWindows = useCallback(() => tabs.filter((tab) => !tab.isArchived).map((tab) => (
<BrowserTab
id={tab.id}
key={`tab_${tab.id}`}
initialUrl={tab.url}
linkType={tab.linkType}
updateTabInfo={updateTabInfo}
showTabs={showTabs}
newTab={newTab}
isInTabsView={route.params?.showTabs}
homePageUrl={homePageUrl()}
/>
)), [tabs, route.params?.showTabs, newTab, homePageUrl, updateTabInfo, showTabs]);
const renderBrowserTabWindows = useCallback(
() =>
tabs
.filter((tab) => !tab.isArchived)
.map((tab) => (
<BrowserTab
id={tab.id}
key={`tab_${tab.id}`}
initialUrl={tab.url}
linkType={tab.linkType}
updateTabInfo={updateTabInfo}
showTabs={showTabs}
newTab={newTab}
isInTabsView={route.params?.showTabs}
homePageUrl={homePageUrl()}
/>
)),
[
tabs,
route.params?.showTabs,
newTab,
homePageUrl,
updateTabInfo,
showTabs,
],
);

return (
<View
Expand Down
53 changes: 36 additions & 17 deletions app/components/Views/Browser/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ThemeContext, mockTheme } from '../../../util/theme';
import { act } from '@testing-library/react';
import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils';

jest.mock('../../hooks/useAccounts', () => ({
useAccounts: jest.fn().mockReturnValue({
evmAccounts: [],
accounts: [],
ensByAccountAddress: {},
}),
}));

const mockTabs = [
{ id: 1, url: 'about:blank', image: '', isArchived: false },
Expand All @@ -28,6 +37,7 @@ const mockInitialState = {
backgroundState: {
...backgroundState,
BrowserController: { tabs: mockTabs },
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
},
},
security: {},
Expand All @@ -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(),
Expand Down Expand Up @@ -109,7 +124,8 @@ describe('Browser', () => {
</Stack.Navigator>
</NavigationContainer>
</ThemeContext.Provider>
</Provider>, { state: { ...mockInitialState } },
</Provider>,
{ state: { ...mockInitialState } },
);
expect(toJSON()).toMatchSnapshot();
});
Expand Down Expand Up @@ -152,7 +168,9 @@ describe('Browser', () => {
<Stack.Screen name={Routes.BROWSER.VIEW}>
{() => (
<Browser
route={{ params: { newTabUrl: 'about:blank', timestamp: '987' } }}
route={{
params: { newTabUrl: 'about:blank', timestamp: '987' },
}}
tabs={mockTabs}
activeTab={1}
navigation={mockNavigation}
Expand All @@ -166,16 +184,17 @@ describe('Browser', () => {
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
</Provider>
</Provider>,
);
// 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 },
Expand Down Expand Up @@ -206,7 +225,7 @@ describe('Browser', () => {
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
</Provider>
</Provider>,
);

// Wrap the timer advancement in act
Expand Down
8 changes: 7 additions & 1 deletion locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading