Skip to content

feat: Start browser tab in token discovery mode #15061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
04e13b1
checkpoint
ziad-saab May 1, 2025
04d7349
checkpoint
ziad-saab May 1, 2025
f909a35
only set the URL text if we had gone to a website
ziad-saab May 2, 2025
de526ea
also process ENS for first URL
ziad-saab May 5, 2025
116d9f9
Merge remote-tracking branch 'origin/main' into zs/token-discovery
ziad-saab May 5, 2025
c5416ad
fix disappearing query when swapping from UrlAutocomplete
ziad-saab May 6, 2025
2d5d862
Merge remote-tracking branch 'origin/main' into zs/token-discovery
ziad-saab May 6, 2025
47b7531
add environment variable flag to switch token discovery on/off
ziad-saab May 6, 2025
cc7c7fe
fix linting and tests
ziad-saab May 6, 2025
7cecc45
fix tests
ziad-saab May 7, 2025
5d28c62
Merge branch 'main' into zs/token-discovery
ziad-saab May 7, 2025
0bd83de
increase test coverage
ziad-saab May 7, 2025
d8261ab
fix linting
ziad-saab May 7, 2025
c08e67c
Merge remote-tracking branch 'origin/main' into zs/token-discovery
ziad-saab May 7, 2025
6b0ca0d
increase test coverage for BrowserTab
ziad-saab May 8, 2025
d438c2d
increase test coverage
ziad-saab May 9, 2025
7ecf656
Merge remote-tracking branch 'origin/main' into zs/token-discovery
ziad-saab May 13, 2025
7b32a5e
remove unused import
ziad-saab May 13, 2025
7d3f718
increase test coverage
ziad-saab May 13, 2025
939ada4
Merge branch 'main' into zs/token-discovery
ziad-saab May 14, 2025
36d5b63
Merge branch 'main' into zs/token-discovery
ziad-saab May 14, 2025
e5d0fba
Merge branch 'main' into zs/token-discovery
ziad-saab May 14, 2025
53ee7a8
Merge branch 'main' into zs/token-discovery
ziad-saab May 14, 2025
49f97ef
Merge branch 'main' into zs/token-discovery
ziad-saab May 16, 2025
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
3 changes: 0 additions & 3 deletions app/components/UI/BrowserUrlBar/BrowserUrlBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ describe('BrowserUrlBar', () => {
onSubmitEditing: jest.fn(),
onCancel: jest.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onChangeText: jest.fn(),
connectedAccounts: ['0x123'],
activeUrl: 'https://example.com',
Expand All @@ -72,7 +71,6 @@ describe('BrowserUrlBar', () => {
onSubmitEditing: jest.fn(),
onCancel: jest.fn(),
onFocus: jest.fn(),
onBlur: jest.fn(),
onChangeText: jest.fn(),
connectedAccounts: ['0x123'],
activeUrl: 'https://example.com',
Expand Down Expand Up @@ -213,6 +211,5 @@ describe('BrowserUrlBar', () => {

fireEvent(urlInput, 'blur');
expect(defaultProps.setIsUrlBarFocused).toHaveBeenCalledWith(false);
expect(defaultProps.onBlur).toHaveBeenCalled();
});
});
2 changes: 0 additions & 2 deletions app/components/UI/BrowserUrlBar/BrowserUrlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ const BrowserUrlBar = forwardRef<BrowserUrlBarRef, BrowserUrlBarProps>(
onSubmitEditing,
onCancel,
onFocus,
onBlur,
onChangeText,
connectedAccounts,
activeUrl,
Expand Down Expand Up @@ -110,7 +109,6 @@ const BrowserUrlBar = forwardRef<BrowserUrlBarRef, BrowserUrlBarProps>(
return;
}
unfocusInput();
onBlur();
};

const onFocusInput = () => {
Expand Down
1 change: 0 additions & 1 deletion app/components/UI/BrowserUrlBar/BrowserUrlBar.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export type BrowserUrlBarProps = {
onSubmitEditing: (text: string) => void;
onCancel: () => void;
onFocus: () => void;
onBlur: () => void;
onChangeText: (text: string) => void;
connectedAccounts: string[];
activeUrl: string;
Expand Down
60 changes: 51 additions & 9 deletions app/components/UI/UrlAutocomplete/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('UrlAutocomplete', () => {

it('should show sites from dapp list', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('uni');
Expand All @@ -128,7 +128,7 @@ describe('UrlAutocomplete', () => {

it('should show sites from bookmarks', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('MyBook');
Expand All @@ -140,7 +140,7 @@ describe('UrlAutocomplete', () => {

it('should show sites from recents/history', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('Goog');
Expand All @@ -152,7 +152,7 @@ describe('UrlAutocomplete', () => {

it('should show history and bookmarks when searching for an empty string', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('');
Expand All @@ -165,7 +165,7 @@ describe('UrlAutocomplete', () => {

it('should not show Recents and Favorites when nothing is found', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('nothing');
Expand All @@ -177,7 +177,7 @@ describe('UrlAutocomplete', () => {

it('should delete a bookmark when pressing the trash icon', async () => {
const ref = React.createRef<UrlAutocompleteRef>();
const { store } = render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
const { store } = render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});
store.dispatch = jest.fn();

act(() => {
Expand All @@ -198,7 +198,7 @@ describe('UrlAutocomplete', () => {
searchTokens: jest.fn(),
});
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('doge');
Expand Down Expand Up @@ -237,7 +237,7 @@ describe('UrlAutocomplete', () => {
searchTokens: jest.fn(),
});
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('dog');
Expand Down Expand Up @@ -266,7 +266,7 @@ describe('UrlAutocomplete', () => {
searchTokens: jest.fn(),
});
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} />, {state: defaultState});
render(<UrlAutocomplete ref={ref} onSelect={noop} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('dog');
Expand All @@ -277,4 +277,46 @@ describe('UrlAutocomplete', () => {
fireEvent.press(swapButton);
expect(mockNavigate).toHaveBeenCalled();
});

it('should call onSelect when a bookmark is selected', async () => {
const onSelect = jest.fn();
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={onSelect} onDismiss={noop} onSwap={noop} />, {state: defaultState});

const result = await screen.findByText('MyBookmark', {includeHiddenElements: true});
fireEvent.press(result);
expect(onSelect).toHaveBeenCalled();
});

it('should call onSelect when a token is selected', async () => {
mockUseTSDReturnValue({
results: [
{
tokenAddress: '0x123',
chainId: '0x1',
name: 'Dogecoin',
symbol: 'DOGE',
usdPrice: 1,
usdPricePercentChange: {
oneDay: 1,
},
},
],
isLoading: false,
reset: jest.fn(),
searchTokens: jest.fn(),
});
const onSelect = jest.fn();
const ref = React.createRef<UrlAutocompleteRef>();
render(<UrlAutocomplete ref={ref} onSelect={onSelect} onDismiss={noop} onSwap={noop} />, {state: defaultState});

act(() => {
ref.current?.search('dog');
jest.runAllTimers();
});

const result = await screen.findByText('Dogecoin', {includeHiddenElements: true});
fireEvent.press(result);
expect(onSelect).toHaveBeenCalled();
});
});
20 changes: 12 additions & 8 deletions app/components/UI/UrlAutocomplete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,13 @@ interface ResultsWithCategory {
const UrlAutocomplete = forwardRef<
UrlAutocompleteRef,
UrlAutocompleteComponentProps
>(({ onSelect, onDismiss }, ref) => {
const [fuseResults, setFuseResults] = useState<FuseSearchResult[]>([]);
>(({ onSelect, onDismiss, onSwap }, ref) => {
const browserHistory = useSelector(selectBrowserHistoryWithType);
const bookmarks = useSelector(selectBrowserBookmarksWithType);
const [fuseResults, setFuseResults] = useState<FuseSearchResult[]>([
...browserHistory,
...bookmarks,
]);
const {searchTokens, results: tokenSearchResults, reset: resetTokenSearch, isLoading: isTokenSearchLoading} = useTokenSearchDiscovery();
const usdConversionRate = useSelector(selectUsdConversionRate);
const tokenResults: TokenSearchResult[] = useMemo(
Expand Down Expand Up @@ -117,8 +122,6 @@ const UrlAutocomplete = forwardRef<
})
), [fuseResults, tokenResults, isTokenSearchLoading]);

const browserHistory = useSelector(selectBrowserHistoryWithType);
const bookmarks = useSelector(selectBrowserBookmarksWithType);
const fuseRef = useRef<Fuse<FuseSearchResult> | null>(null);
const resultsRef = useRef<View | null>(null);
const { styles } = useStyles(styleSheet, {});
Expand Down Expand Up @@ -213,13 +216,12 @@ const UrlAutocomplete = forwardRef<

const goToSwaps = useCallback(async (result: TokenSearchResult) => {
try {
onSwap();
await goToSwapsHook(result);
} catch (error) {
return;
}
hide();
onDismiss();
}, [hide, onDismiss, goToSwapsHook]);
}, [goToSwapsHook, onSwap]);

const renderSectionHeader = useCallback(({section: { category }}: {section: ResultsWithCategory}) => (
<View style={styles.categoryWrapper}>
Expand All @@ -234,7 +236,9 @@ const UrlAutocomplete = forwardRef<
<Result
result={item}
onPress={() => {
hide();
if (item.category !== UrlAutocompleteCategory.Tokens) {
hide();
}
onSelect(item);
}}
onSwapPress={goToSwaps}
Expand Down
5 changes: 5 additions & 0 deletions app/components/UI/UrlAutocomplete/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export type UrlAutocompleteComponentProps = {
* tapping on the background
*/
onDismiss: () => void;
/**
* Callback that is triggered while
* swapping a token
*/
onSwap: () => void;
};

export enum UrlAutocompleteCategory {
Expand Down
28 changes: 14 additions & 14 deletions app/components/Views/Browser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import BrowserTab from '../BrowserTab/BrowserTab';
import URL from 'url-parse';
import { useMetrics } from '../../hooks/useMetrics';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { appendURLParams } from '../../../util/browser';

import { appendURLParams, isTokenDiscoveryBrowserEnabled } from '../../../util/browser';
import {
THUMB_WIDTH,
THUMB_HEIGHT,
Expand Down Expand Up @@ -100,6 +101,18 @@ export const Browser = (props) => {
[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 if (isTokenDiscoveryBrowserEnabled()) {
// When a new tab is created, a new tab is rendered, which automatically sets the url source on the webview
createNewTab(url, linkType);
} else {
createNewTab(url || homePageUrl(), linkType);
}
}, [tabs, navigation, createNewTab, homePageUrl]);

///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
// TODO remove after we release Solana dapp connectivity
useFocusEffect(
Expand Down Expand Up @@ -127,19 +140,6 @@ export const Browser = (props) => {
);
///: END:ONLY_INCLUDE_IF

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);
Expand Down
72 changes: 72 additions & 0 deletions app/components/Views/Browser/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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 { isTokenDiscoveryBrowserEnabled } from '../../../util/browser';
import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils';

jest.mock('../../hooks/useAccounts', () => ({
Expand Down Expand Up @@ -85,6 +86,11 @@ jest.mock('../../../util/phishingDetection', () => ({
getPhishingTestResult: jest.fn().mockReturnValue({ result: false }),
}));

jest.mock('../../../util/browser', () => ({
...jest.requireActual('../../../util/browser'),
isTokenDiscoveryBrowserEnabled: jest.fn().mockReturnValue(false),
}));

const Stack = createStackNavigator();
const mockStore = configureMockStore();

Expand Down Expand Up @@ -130,6 +136,72 @@ describe('Browser', () => {
expect(toJSON()).toMatchSnapshot();
});

it('should create a new homepage tab when rendered with no tabs', () => {
let passedUrl = '';
const mockCreateNewTab = jest.fn((url) => {
passedUrl = url;
});
renderWithProvider(
<Provider store={mockStore(mockInitialState)}>
<NavigationContainer independent>
<Stack.Navigator>
<Stack.Screen name={Routes.BROWSER.VIEW}>
{() => (
<Browser
route={routeMock}
tabs={[]}
activeTab={1}
navigation={mockNavigation}
createNewTab={mockCreateNewTab}
closeAllTabs={jest.fn}
closeTab={jest.fn}
setActiveTab={jest.fn}
updateTab={jest.fn}
/>
)}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
</Provider>,
{ state: { ...mockInitialState } },
);

expect(mockCreateNewTab).toHaveBeenCalled();
expect(passedUrl).toMatch(/^https:\/\//);
});

it('should create a new token discovery tab when rendered with no tabs and token discovery browser is enabled', () => {
jest.mocked(isTokenDiscoveryBrowserEnabled).mockReturnValue(true);
const mockCreateNewTab = jest.fn();
renderWithProvider(
<Provider store={mockStore(mockInitialState)}>
<NavigationContainer independent>
<Stack.Navigator>
<Stack.Screen name={Routes.BROWSER.VIEW}>
{() => (
<Browser
route={routeMock}
tabs={[]}
activeTab={1}
navigation={mockNavigation}
createNewTab={mockCreateNewTab}
closeAllTabs={jest.fn}
closeTab={jest.fn}
setActiveTab={jest.fn}
updateTab={jest.fn}
/>
)}
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
</Provider>,
{ state: { ...mockInitialState } },
);

expect(mockCreateNewTab).toHaveBeenCalledWith(undefined, undefined);
jest.mocked(isTokenDiscoveryBrowserEnabled).mockReturnValue(false);
});

it('should call navigate when route param `newTabUrl` and `timestamp` are added', () => {
// Render the component with an initial prop value
const { rerender } = renderWithProvider(
Expand Down
Loading
Loading