Skip to content

Commit 93fba69

Browse files
authored
feat: OmniSearch integration in WebBrowser (#25358)
## **Description** This PR integrates the omni-search feature from the Explore page into the browser URL bar autocomplete, enabling users to search across multiple categories directly from the browser. Jira Ticket: https://consensyssoftware.atlassian.net/browse/MCWP-218 ### What changed **New Omni-Search Integration:** - When users type in the browser URL bar, they now see search results for: - **Sites** - Curated web3 sites matching the query - **Recents** - Recently visited URLs filtered by query - **Favorites** - Bookmarked URLs filtered by query - **Tokens** - Trending tokens with price and 24h change - **Perps** - Perpetual trading markets with leverage info - **Predictions** - Prediction markets from Polymarket **Architecture:** - Reuses the existing `useExploreSearch` hook from TrendingView - Browser-specific section order: Sites → Recents → Favorites → Tokens → Perps → Predictions - Transforms API data (TrendingAsset, PerpsMarketData, PredictMarket) to unified `AutocompleteSearchResult` type - Wraps search content with `PerpsConnectionProvider` and `PerpsStreamProvider` for real-time data **Result Component Enhancements:** - Extended `Result.tsx` to render all new result types (Tokens, Perps, Predictions) - Added swap button for token results that navigates to swap flow - Integrated `TrendingTokenLogo` and `PerpsTokenLogo` components - Shows price and percentage change for tokens and perps **Type System:** - Added discriminated union types: `TokenSearchResult`, `PerpsSearchResult`, `PredictionsSearchResult` - `AutocompleteSearchResult` is now a union of all result types - Type-safe category handling with `UrlAutocompleteCategory` enum **E2E Test Fixes:** The omni-search integration introduced new API calls that caused E2E smoke tests to fail with "unmocked request" errors: - `GET https://token.api.cx.metamask.io/tokens/search?...` - `GET https://token.api.cx.metamask.io/v3/tokens/trending?...` **Fix:** Added `TRENDING_API_MOCKS` to the default mock configuration in `tests/api-mocking/mock-responses/defaults/index.ts`. These mocks already existed in `trending-api-mocks.ts` but weren't loaded into the E2E test harness. **Other Changes:** - Fixed TypeScript error in `DiscoveryTab.tsx` with proper type narrowing for union types - Extended `useExploreSearch` hook to accept custom `sectionsOrder` option - Added comprehensive unit tests achieving 85%+ coverage ### Files Changed (14 files) | File | Description | |------|-------------| | `UrlAutocomplete/index.tsx` | Main integration - omni-search hook, data transformers, search content | | `UrlAutocomplete/Result.tsx` | Extended to render Tokens, Perps, Predictions results | | `UrlAutocomplete/types.ts` | New types for all search result categories | | `UrlAutocomplete/UrlAutocomplete.constants.ts` | New constants for browser search config | | `UrlAutocomplete/index.test.tsx` | Unit tests for omni-search integration | | `UrlAutocomplete/Result.test.tsx` | New unit tests for Result component | | `BrowserTab/BrowserTab.tsx` | Updated to handle new result types in onSelect | | `DiscoveryTab/DiscoveryTab.tsx` | Fixed TypeScript error with type narrowing | | `ExploreSearchResults/ExploreSearchResults.tsx` | Minor updates for shared hook | | `ExploreSearchResults/ExploreSearchResults.test.tsx` | Improved test coverage | | `useExploreSearch.ts` | Added sectionsOrder option for custom ordering | | `useExploreSearch.test.ts` | New unit tests for hook | | `locales/languages/en.json` | Added localization strings for new categories | | `tests/api-mocking/.../defaults/index.ts` | Added `TRENDING_API_MOCKS` import to fix E2E smoke test failures | ## **Changelog** CHANGELOG entry: Added omni-search to browser URL bar - search tokens, perps, and predictions directly from the browser ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Browser URL Bar Omni-Search Scenario: User searches for tokens in browser URL bar Given user is on the browser tab And user taps on the URL bar When user types "eth" Then user sees search results organized by category And user sees Sites section with matching web3 sites And user sees Tokens section with Ethereum and related tokens And each token shows name, symbol, price, and 24h change And each token has a swap button Scenario: User initiates swap from search result Given user has searched for "eth" in the URL bar And user sees Ethereum in the Tokens section When user taps the swap button on Ethereum Then user is navigated to the swap screen And Ethereum is pre-selected as the destination token Scenario: User searches for perps markets Given user is on the browser tab And user taps on the URL bar When user types "btc" Then user sees Perps section with BTC-USD market And market shows name, symbol, leverage, and price Scenario: User searches for prediction markets Given user is on the browser tab And user taps on the URL bar When user types "bitcoin" Then user sees Predictions section with matching markets And each prediction shows title and status (Open/Closed/Resolved) Scenario: User sees empty state with recents and favorites Given user is on the browser tab And user has browser history and bookmarks When user taps on the URL bar without typing Then user sees Recents section with recent URLs And user sees Favorites section with bookmarked URLs Scenario: Basic functionality disabled hides API-dependent sections Given user has disabled basic functionality in settings And user is on the browser tab When user types a search query in the URL bar Then user only sees Recents and Favorites (local data) And user does not see Tokens, Perps, or Predictions sections ``` ## **Screenshots/Recordings** <!-- Add screenshots/recordings showing: 1. Empty state with Recents and Favorites 2. Search results with Sites, Tokens, Perps, Predictions 3. Token result with swap button 4. Loading state with activity indicator --> ### **Before** <!-- Browser URL bar only showed Recents and Favorites --> ### **After** <!-- Browser URL bar now shows omni-search results across all categories --> https://github.com/user-attachments/assets/6bf3cfe8-2e19-42b0-99cf-68f72e8a015c ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands browser URL autocomplete into a multi-source search that now triggers additional API-driven sections and new navigation paths (asset/perps/predictions and swaps). Risk is mainly around UI correctness, section ordering/loading, and navigation/selection behavior rather than security-critical logic. > > **Overview** > Browser URL autocomplete is refactored to use `useExploreSearch` (with a browser-specific section order) and to combine API-driven results (sites/tokens/perps/predictions) with locally filtered recents/favorites; empty state is now explicitly limited to recents/favorites. > > `Result` now renders new result types with appropriate icons and metadata (token price/24h change, perps leverage/price/change, prediction status/image) and adds a token swap action button. Selection handling is updated so token/perps/predictions navigate to their detail screens (and avoid auto-hiding autocomplete), while URL-based results continue to navigate the webview. > > Shared search plumbing is extended by adding an optional `sectionsOrder` to `useExploreSearch`, tests are expanded/updated accordingly, new i18n strings are added for the new sections, and default E2E mocks now include `TRENDING_API_MOCKS` to cover the new token API calls. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d2c682f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent be610d8 commit 93fba69

14 files changed

Lines changed: 2283 additions & 227 deletions

File tree

app/components/UI/UrlAutocomplete/Result.test.tsx

Lines changed: 681 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 203 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,222 @@
11
import React, { memo, useCallback } from 'react';
2-
import { TouchableOpacity, View, Text } from 'react-native';
2+
import { TouchableOpacity, View, Text, Image } from 'react-native';
33
import { useTheme } from '../../../util/theme';
44
import { getHost } from '../../../util/browser';
55
import WebsiteIcon from '../WebsiteIcon';
66
import ButtonIcon from '../../../component-library/components/Buttons/ButtonIcon';
77
import { deleteFavoriteTestId } from '../../../../wdio/screen-objects/testIDs/BrowserScreen/UrlAutocomplete.testIds';
8-
import { IconName } from '../../../component-library/components/Icons/Icon';
9-
import { useDispatch } from 'react-redux';
8+
import {
9+
Box,
10+
Icon,
11+
IconName,
12+
IconSize,
13+
BoxAlignItems,
14+
BoxJustifyContent,
15+
} from '@metamask/design-system-react-native';
16+
import { IconName as ComponentLibraryIconName } from '../../../component-library/components/Icons/Icon';
17+
import { useDispatch, useSelector } from 'react-redux';
1018
import { removeBookmark } from '../../../actions/bookmarks';
1119
import stylesheet from './styles';
12-
import { AutocompleteSearchResult, UrlAutocompleteCategory } from './types';
20+
import {
21+
AutocompleteSearchResult,
22+
TokenSearchResult,
23+
UrlAutocompleteCategory,
24+
PredictionsSearchResult,
25+
} from './types';
26+
import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrapper';
27+
import Badge, {
28+
BadgeVariant,
29+
} from '../../../component-library/components/Badges/Badge';
30+
import { NetworkBadgeSource } from '../AssetOverview/Balance/Balance';
31+
import { selectCurrentCurrency } from '../../../selectors/currencyRateController';
32+
import { addCurrencySymbol } from '../../../util/number';
33+
import PercentageChange from '../../../component-library/components-temp/Price/PercentageChange';
34+
import type { NavigationProp, ParamListBase } from '@react-navigation/native';
35+
import TrendingTokenLogo from '../Trending/components/TrendingTokenLogo';
36+
import PerpsTokenLogo from '../Perps/components/PerpsTokenLogo';
37+
import AppConstants from '../../../core/AppConstants';
1338

1439
interface ResultProps {
1540
result: AutocompleteSearchResult;
1641
onPress: () => void;
42+
onSwapPress: (result: TokenSearchResult) => void;
43+
navigation?: NavigationProp<ParamListBase>;
1744
}
1845

19-
export const Result: React.FC<ResultProps> = memo(({ result, onPress }) => {
20-
const theme = useTheme();
21-
const styles = stylesheet({ theme });
46+
/**
47+
* Render icon for Predictions result
48+
*/
49+
const PredictionsIcon: React.FC<{
50+
result: PredictionsSearchResult;
51+
styles: ReturnType<typeof stylesheet>;
52+
}> = memo(({ result, styles }) => {
53+
if (result.image) {
54+
return (
55+
<Image
56+
source={{ uri: result.image }}
57+
style={styles.bookmarkIco}
58+
resizeMode="cover"
59+
/>
60+
);
61+
}
62+
return (
63+
<Box
64+
style={styles.bookmarkIco}
65+
alignItems={BoxAlignItems.Center}
66+
justifyContent={BoxJustifyContent.Center}
67+
twClassName="bg-background-alternative"
68+
>
69+
<Icon name={IconName.Speedometer} size={IconSize.Md} />
70+
</Box>
71+
);
72+
});
2273

23-
const name = typeof result.name === 'string' || getHost(result.url);
74+
export const Result: React.FC<ResultProps> = memo(
75+
({ result, onPress, onSwapPress }) => {
76+
const theme = useTheme();
77+
const styles = stylesheet({ theme });
2478

25-
const dispatch = useDispatch();
79+
const dispatch = useDispatch();
2680

27-
const onPressRemove = useCallback(() => {
28-
dispatch(removeBookmark(result));
29-
}, [dispatch, result]);
81+
const onPressRemove = useCallback(() => {
82+
dispatch(removeBookmark(result));
83+
}, [dispatch, result]);
3084

31-
return (
32-
<TouchableOpacity style={styles.item} onPress={onPress}>
33-
<View style={styles.itemWrapper}>
34-
<WebsiteIcon
35-
style={styles.bookmarkIco}
36-
url={result.url}
37-
title={name}
38-
textStyle={styles.fallbackTextStyle}
39-
/>
40-
<View style={styles.textContent}>
41-
<Text style={styles.name} numberOfLines={1}>
42-
{result.name}
43-
</Text>
44-
<Text style={styles.url} numberOfLines={1}>
45-
{result.url}
46-
</Text>
85+
const swapsEnabled =
86+
result.category === UrlAutocompleteCategory.Tokens &&
87+
AppConstants.SWAPS.ACTIVE;
88+
89+
const currentCurrency = useSelector(selectCurrentCurrency);
90+
91+
// Determine display name based on category
92+
const getDisplayName = () => {
93+
switch (result.category) {
94+
case UrlAutocompleteCategory.Tokens:
95+
return result.name;
96+
case UrlAutocompleteCategory.Perps:
97+
return result.name;
98+
case UrlAutocompleteCategory.Predictions:
99+
return result.title;
100+
default:
101+
return typeof result.name === 'string'
102+
? result.name
103+
: getHost(result.url);
104+
}
105+
};
106+
107+
// Determine subtitle based on category
108+
const getSubtitle = () => {
109+
switch (result.category) {
110+
case UrlAutocompleteCategory.Tokens:
111+
return result.symbol;
112+
case UrlAutocompleteCategory.Perps:
113+
return `${result.symbol} · ${result.maxLeverage}`;
114+
case UrlAutocompleteCategory.Predictions:
115+
return result.status === 'open' ? 'Open' : result.status;
116+
default:
117+
return result.url;
118+
}
119+
};
120+
121+
// Render the appropriate icon
122+
const renderIcon = () => {
123+
switch (result.category) {
124+
case UrlAutocompleteCategory.Tokens:
125+
return (
126+
<BadgeWrapper
127+
badgeElement={
128+
<Badge
129+
variant={BadgeVariant.Network}
130+
imageSource={NetworkBadgeSource(result.chainId)}
131+
/>
132+
}
133+
>
134+
<TrendingTokenLogo
135+
assetId={result.assetId}
136+
symbol={result.symbol}
137+
size={32}
138+
/>
139+
</BadgeWrapper>
140+
);
141+
case UrlAutocompleteCategory.Perps:
142+
return <PerpsTokenLogo symbol={result.symbol} size={32} />;
143+
case UrlAutocompleteCategory.Predictions:
144+
return <PredictionsIcon result={result} styles={styles} />;
145+
default:
146+
return (
147+
<WebsiteIcon
148+
style={styles.bookmarkIco}
149+
url={result.url}
150+
title={getDisplayName()}
151+
textStyle={styles.fallbackTextStyle}
152+
/>
153+
);
154+
}
155+
};
156+
157+
// Render price/change info for tokens and perps
158+
const renderPriceInfo = () => {
159+
if (result.category === UrlAutocompleteCategory.Tokens) {
160+
return (
161+
<View style={styles.priceContainer}>
162+
<Text style={styles.price}>
163+
{addCurrencySymbol(result.price, currentCurrency, true)}
164+
</Text>
165+
<PercentageChange value={result.percentChange ?? 0} />
166+
</View>
167+
);
168+
}
169+
170+
if (result.category === UrlAutocompleteCategory.Perps) {
171+
// Parse the percentage change from the formatted string
172+
const percentStr = result.change24hPercent.replace(/[+%]/g, '');
173+
const percentValue = parseFloat(percentStr) || 0;
174+
return (
175+
<View style={styles.priceContainer}>
176+
<Text style={styles.price}>{result.price}</Text>
177+
<PercentageChange value={percentValue} />
178+
</View>
179+
);
180+
}
181+
182+
return null;
183+
};
184+
185+
return (
186+
<TouchableOpacity style={styles.item} onPress={onPress}>
187+
<View style={styles.itemWrapper}>
188+
{renderIcon()}
189+
<View style={styles.textContent}>
190+
<Text style={styles.name} numberOfLines={1}>
191+
{getDisplayName()}
192+
</Text>
193+
<Text style={styles.url} numberOfLines={1}>
194+
{getSubtitle()}
195+
</Text>
196+
</View>
197+
{result.category === UrlAutocompleteCategory.Favorites && (
198+
<ButtonIcon
199+
testID={deleteFavoriteTestId(result.url)}
200+
style={styles.resultActionButton}
201+
iconName={ComponentLibraryIconName.Trash}
202+
onPress={onPressRemove}
203+
/>
204+
)}
205+
{renderPriceInfo()}
206+
{result.category === UrlAutocompleteCategory.Tokens && (
207+
<ButtonIcon
208+
style={{
209+
...styles.resultActionButton,
210+
...(swapsEnabled ? {} : styles.hiddenButton),
211+
}}
212+
iconName={ComponentLibraryIconName.SwapHorizontal}
213+
onPress={() => onSwapPress(result)}
214+
disabled={!swapsEnabled}
215+
testID="autocomplete-result-swap-button"
216+
/>
217+
)}
47218
</View>
48-
{result.category === UrlAutocompleteCategory.Favorites && (
49-
<ButtonIcon
50-
testID={deleteFavoriteTestId(result.url)}
51-
style={styles.resultActionButton}
52-
iconName={IconName.Trash}
53-
onPress={onPressRemove}
54-
/>
55-
)}
56-
</View>
57-
</TouchableOpacity>
58-
);
59-
});
219+
</TouchableOpacity>
220+
);
221+
},
222+
);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,51 @@
11
import { UrlAutocompleteCategory } from './types';
2+
import type { SectionId } from '../../Views/TrendingView/sections.config';
23

34
export const MAX_RECENTS = 5;
5+
6+
/**
7+
* Fuse.js options for filtering browser history and bookmarks
8+
* Note: Project uses fuse.js v3.4.4 which has different API than v4+
9+
*/
10+
export const HISTORY_FUSE_OPTIONS = {
11+
shouldSort: true,
12+
threshold: 0.4,
13+
location: 0,
14+
distance: 100,
15+
maxPatternLength: 32,
16+
minMatchCharLength: 1,
17+
keys: [
18+
{ name: 'name', weight: 0.5 },
19+
{ name: 'url', weight: 0.5 },
20+
],
21+
};
22+
23+
/**
24+
* Categories for empty state (no search query)
25+
* Shows Recents and Favorites only
26+
*/
27+
export const EMPTY_STATE_CATEGORIES = [
28+
UrlAutocompleteCategory.Recents,
29+
UrlAutocompleteCategory.Favorites,
30+
];
31+
32+
/**
33+
* @deprecated Use EMPTY_STATE_CATEGORIES for empty state.
34+
* Search is now handled by useExploreSearch with BROWSER_SEARCH_SECTIONS_ORDER.
35+
*/
436
export const ORDERED_CATEGORIES = [
537
UrlAutocompleteCategory.Recents,
638
UrlAutocompleteCategory.Favorites,
739
UrlAutocompleteCategory.Sites,
840
];
41+
42+
/**
43+
* Section order for browser search (Sites first, then other omni-search sections)
44+
* This order is passed to useExploreSearch to display Sites before tokens/perps/predictions
45+
*/
46+
export const BROWSER_SEARCH_SECTIONS_ORDER: SectionId[] = [
47+
'sites',
48+
'tokens',
49+
'perps',
50+
'predictions',
51+
];

0 commit comments

Comments
 (0)