Skip to content

Commit bf03a94

Browse files
feat: switch default search engine to Brave Search (#26356)
## **Description** The MetaMask mobile in-app browser currently defaults to Google search, which shows ads that can lead users to fake/scam websites. This PR switches the default search engine to Brave Search, providing a privacy-focused, ad-free search experience that better protects users. **Changes:** - Updated `DEFAULT_SEARCH_ENGINE` constant from `'Google'` to `'Brave'` - Added Brave Search URL (`https://search.brave.com/search?q=`) and refactored the if/else chain to a module-scoped lookup map (`SEARCH_ENGINE_URLS`) for cleaner extensibility and to avoid re-allocation on every call - Added `'Brave Search'` option to the search engine picker in General Settings - Added Redux persist migration (121) to automatically switch all existing users to Brave, following the same pattern as migration 58 - Updated `processUrlForBrowser` default parameter to use `AppConstants.DEFAULT_SEARCH_ENGINE` instead of hardcoded `'Google'` - Updated `SitesSearchFooter` to use the shared `SEARCH_ENGINE_URLS` map so the "Search for ... on {engine}" label and URL correctly reflect the selected engine (previously hardcoded to Google/DuckDuckGo only) - Fallback for unknown search engine values now uses `AppConstants.DEFAULT_SEARCH_ENGINE` (Brave) instead of hardcoded Google ## **Changelog** CHANGELOG entry: Changed the default search engine to Brave Search for a privacy-focused, ad-free browsing experience ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Brave Search as default search engine Scenario: New user searches in the in-app browser Given the user has freshly installed the app When user types a search keyword in the browser URL bar Then the search results are served by Brave Search (search.brave.com) Scenario: Existing user is migrated to Brave Search Given the user previously had Google or DuckDuckGo as their search engine When the user updates the app and launches it Then the search engine setting is automatically changed to Brave And searches in the in-app browser use Brave Search Scenario: Search footer label reflects selected engine Given the user is typing a keyword in the browser URL bar When the autocomplete dropdown appears Then the footer shows "Search for {keyword} on Brave" (not Google) And tapping it navigates to search.brave.com Scenario: User can still select other search engines Given the user is on Settings > General When user opens the Search Engine picker Then Google, DuckDuckGo, and Brave Search are all available options And the user can switch between them And the search footer label updates to match the selected engine ``` ## **Screenshots/Recordings** ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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 - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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** > Changes core browser URL processing and the default search engine, plus a state migration that mutates persisted user settings; mistakes could send users to wrong destinations or override preferences unexpectedly. > > **Overview** > **Switches the in-app browser default search engine from Google to Brave.** This updates `AppConstants.DEFAULT_SEARCH_ENGINE`, adds Brave to the General Settings search engine picker, and centralizes engine base URLs in `SEARCH_ENGINE_URLS`. > > Search URL generation is refactored to use the shared lookup (with fallback to the default) in both `processUrlForBrowser` and `SitesSearchFooter`, and the footer testID/text are renamed from Google-specific to a generic `trending-search-footer-search-link`. > > Adds migration `126` to move persisted `settings.searchEngine` from `Google` to `Brave` for existing users, and updates unit/integration/e2e tests and selectors to match the new engine + testIDs. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 471a8dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e393675 commit bf03a94

13 files changed

Lines changed: 230 additions & 52 deletions

File tree

app/components/UI/Sites/components/SitesSearchFooter/SitesSearchFooter.test.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('SitesSearchFooter', () => {
2828
} as unknown as jest.Mocked<NavigationProp<ParamListBase>>;
2929

3030
(useNavigation as jest.Mock).mockReturnValue(mockNavigation);
31-
(useSelector as jest.Mock).mockReturnValue('Google');
31+
(useSelector as jest.Mock).mockReturnValue('Brave');
3232
dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(1234567890);
3333
});
3434

@@ -41,17 +41,17 @@ describe('SitesSearchFooter', () => {
4141
it('returns null when searchQuery is empty', () => {
4242
const { queryByTestId } = render(<SitesSearchFooter searchQuery="" />);
4343

44-
expect(queryByTestId('trending-search-footer-google-link')).toBeNull();
44+
expect(queryByTestId('trending-search-footer-search-link')).toBeNull();
4545
expect(queryByTestId('trending-search-footer-url-link')).toBeNull();
4646
});
4747

48-
it('renders Google search link when query is provided', () => {
48+
it('renders search engine link when query is provided', () => {
4949
const { getByTestId } = render(
5050
<SitesSearchFooter searchQuery="ethereum" />,
5151
);
5252

5353
expect(
54-
getByTestId('trending-search-footer-google-link'),
54+
getByTestId('trending-search-footer-search-link'),
5555
).toBeOnTheScreen();
5656
});
5757

@@ -62,7 +62,7 @@ describe('SitesSearchFooter', () => {
6262

6363
expect(getByTestId('trending-search-footer-url-link')).toBeOnTheScreen();
6464
expect(
65-
getByTestId('trending-search-footer-google-link'),
65+
getByTestId('trending-search-footer-search-link'),
6666
).toBeOnTheScreen();
6767
});
6868

@@ -73,7 +73,7 @@ describe('SitesSearchFooter', () => {
7373

7474
expect(queryByTestId('trending-search-footer-url-link')).toBeNull();
7575
expect(
76-
getByTestId('trending-search-footer-google-link'),
76+
getByTestId('trending-search-footer-search-link'),
7777
).toBeOnTheScreen();
7878
});
7979
});
@@ -161,26 +161,26 @@ describe('SitesSearchFooter', () => {
161161
expect(mockNavigation.navigate).toHaveBeenCalledTimes(1);
162162
});
163163

164-
it('navigates to Google search when Google link is pressed', () => {
164+
it('navigates to Brave search when search link is pressed', () => {
165165
const { getByTestId } = render(
166166
<SitesSearchFooter searchQuery="ethereum" />,
167167
);
168168

169-
fireEvent.press(getByTestId('trending-search-footer-google-link'));
169+
fireEvent.press(getByTestId('trending-search-footer-search-link'));
170170

171-
assertBrowserNavigation('https://www.google.com/search?q=ethereum');
171+
assertBrowserNavigation('https://search.brave.com/search?q=ethereum');
172172
expect(mockNavigation.navigate).toHaveBeenCalledTimes(1);
173173
});
174174

175-
it('encodes special characters in Google search query', () => {
175+
it('encodes special characters in Brave search query', () => {
176176
const { getByTestId } = render(
177177
<SitesSearchFooter searchQuery="ethereum & bitcoin" />,
178178
);
179179

180-
fireEvent.press(getByTestId('trending-search-footer-google-link'));
180+
fireEvent.press(getByTestId('trending-search-footer-search-link'));
181181

182182
assertBrowserNavigation(
183-
'https://www.google.com/search?q=ethereum%20%26%20bitcoin',
183+
'https://search.brave.com/search?q=ethereum%20%26%20bitcoin',
184184
);
185185
});
186186

@@ -191,7 +191,7 @@ describe('SitesSearchFooter', () => {
191191
<SitesSearchFooter searchQuery="ethereum" />,
192192
);
193193

194-
fireEvent.press(getByTestId('trending-search-footer-google-link'));
194+
fireEvent.press(getByTestId('trending-search-footer-search-link'));
195195

196196
assertBrowserNavigation('https://duckduckgo.com/?q=ethereum');
197197
expect(mockNavigation.navigate).toHaveBeenCalledTimes(1);
@@ -204,7 +204,7 @@ describe('SitesSearchFooter', () => {
204204
<SitesSearchFooter searchQuery="ethereum & bitcoin" />,
205205
);
206206

207-
fireEvent.press(getByTestId('trending-search-footer-google-link'));
207+
fireEvent.press(getByTestId('trending-search-footer-search-link'));
208208

209209
assertBrowserNavigation(
210210
'https://duckduckgo.com/?q=ethereum%20%26%20bitcoin',
@@ -213,13 +213,13 @@ describe('SitesSearchFooter', () => {
213213
});
214214

215215
describe('text display', () => {
216-
it('displays search query in Google search link', () => {
216+
it('displays search query in Brave search link', () => {
217217
const { getByText } = render(
218218
<SitesSearchFooter searchQuery="ethereum" />,
219219
);
220220

221221
expect(getByText('ethereum')).toBeOnTheScreen();
222-
expect(getByText(/on Google/)).toBeOnTheScreen();
222+
expect(getByText(/on Brave/)).toBeOnTheScreen();
223223
});
224224

225225
it('displays search query in DuckDuckGo search link when DuckDuckGo is selected', () => {
@@ -250,7 +250,7 @@ describe('SitesSearchFooter', () => {
250250

251251
// Component trims or handles spaces, but doesn't return null
252252
expect(
253-
queryByTestId('trending-search-footer-google-link'),
253+
queryByTestId('trending-search-footer-search-link'),
254254
).toBeOnTheScreen();
255255
});
256256

@@ -276,7 +276,7 @@ describe('SitesSearchFooter', () => {
276276
);
277277

278278
expect(
279-
getByTestId('trending-search-footer-google-link'),
279+
getByTestId('trending-search-footer-search-link'),
280280
).toBeOnTheScreen();
281281
});
282282

@@ -286,7 +286,7 @@ describe('SitesSearchFooter', () => {
286286
);
287287

288288
expect(
289-
getByTestId('trending-search-footer-google-link'),
289+
getByTestId('trending-search-footer-search-link'),
290290
).toBeOnTheScreen();
291291
expect(getByText('ethereum 🚀')).toBeOnTheScreen();
292292
});

app/components/UI/Sites/components/SitesSearchFooter/SitesSearchFooter.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { useNavigation } from '@react-navigation/native';
1414
import { useSelector } from 'react-redux';
1515
import Routes from '../../../../../constants/navigation/Routes';
1616
import { selectSearchEngine } from '../../../../../reducers/browser/selectors';
17+
import { SEARCH_ENGINE_URLS, SearchEngine } from '../../../../../util/browser';
18+
import AppConstants from '../../../../../core/AppConstants';
1719

1820
export interface SitesSearchFooterProps {
1921
searchQuery: string;
@@ -72,13 +74,16 @@ const SitesSearchFooter: React.FC<SitesSearchFooterProps> = ({
7274

7375
const isUrl = looksLikeUrl(searchQuery.toLowerCase());
7476

77+
const engineKey = searchEngine ?? AppConstants.DEFAULT_SEARCH_ENGINE;
78+
const resolvedEngine: SearchEngine = SEARCH_ENGINE_URLS[
79+
engineKey as SearchEngine
80+
]
81+
? (engineKey as SearchEngine)
82+
: AppConstants.DEFAULT_SEARCH_ENGINE;
7583
const searchUrl =
76-
searchEngine === 'DuckDuckGo'
77-
? `https://duckduckgo.com/?q=${encodeURIComponent(searchQuery)}`
78-
: `https://www.google.com/search?q=${encodeURIComponent(searchQuery)}`;
84+
SEARCH_ENGINE_URLS[resolvedEngine] + encodeURIComponent(searchQuery);
7985

80-
const searchEngineLabel =
81-
searchEngine === 'DuckDuckGo' ? 'DuckDuckGo' : 'Google';
86+
const searchEngineLabel = resolvedEngine;
8287

8388
return (
8489
<Box style={containerStyle}>
@@ -110,7 +115,7 @@ const SitesSearchFooter: React.FC<SitesSearchFooterProps> = ({
110115
<TouchableOpacity
111116
style={tw.style('flex-row items-center py-4')}
112117
onPress={() => handlePress(searchUrl)}
113-
testID="trending-search-footer-google-link"
118+
testID="trending-search-footer-search-link"
114119
>
115120
<Box twClassName="flex-1 flex-row items-center">
116121
<Text

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,10 +577,10 @@ describe('UrlAutocomplete', () => {
577577

578578
// Assert
579579
expect(
580-
await screen.findByTestId('trending-search-footer-google-link', {
580+
await screen.findByTestId('trending-search-footer-search-link', {
581581
includeHiddenElements: true,
582582
}),
583-
).toHaveTextContent('Search for "MetaMask Test Dapp" on Google');
583+
).toHaveTextContent('Search for "MetaMask Test Dapp" on Brave');
584584
});
585585

586586
it('transforms and displays token search results', async () => {

app/components/Views/Settings/GeneralSettings/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ class Settings extends PureComponent {
259259
this.searchEngineOptions = [
260260
{ value: 'Google', label: 'Google', key: 'Google' },
261261
{ value: 'DuckDuckGo', label: 'DuckDuckGo', key: 'DuckDuckGo' },
262+
{ value: 'Brave', label: 'Brave', key: 'Brave' },
262263
];
263264
this.primaryCurrencyOptions = [
264265
{

app/core/AppConstants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default {
1616
IS_DEV: process.env?.NODE_ENV === DEVELOPMENT,
1717
METAMASK_BUILD_TYPE: process.env.METAMASK_BUILD_TYPE,
1818
DEFAULT_LOCK_TIMEOUT: 30000,
19-
DEFAULT_SEARCH_ENGINE: 'Google',
19+
DEFAULT_SEARCH_ENGINE: 'Brave',
2020
TX_CHECK_BACKGROUND_FREQUENCY: 30000,
2121
IPFS_OVERRIDE_PARAM: 'mm_override',
2222
IPFS_DEFAULT_GATEWAY_URL: 'https://dweb.link/ipfs/',

app/store/migrations/126.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import migrate from './126';
2+
import { merge } from 'lodash';
3+
import { captureException } from '@sentry/react-native';
4+
import initialRootState from '../../util/test/initial-root-state';
5+
import mockedEngine from '../../core/__mocks__/MockedEngine';
6+
7+
jest.mock('@sentry/react-native', () => ({
8+
captureException: jest.fn(),
9+
}));
10+
const mockedCaptureException = jest.mocked(captureException);
11+
12+
jest.mock('../../core/Engine', () => ({
13+
init: () => mockedEngine.init(),
14+
}));
15+
16+
describe('Migration #126 - Update default search engine to Brave', () => {
17+
beforeEach(() => {
18+
jest.restoreAllMocks();
19+
jest.resetAllMocks();
20+
});
21+
22+
const invalidStates = [
23+
{
24+
state: null,
25+
errorMessage: "FATAL ERROR: Migration 126: Invalid state error: 'object'",
26+
scenario: 'state is invalid',
27+
},
28+
{
29+
state: merge({}, initialRootState, {
30+
engine: null,
31+
}),
32+
errorMessage:
33+
"FATAL ERROR: Migration 126: Invalid engine state error: 'object'",
34+
scenario: 'engine state is invalid',
35+
},
36+
{
37+
state: merge({}, initialRootState, {
38+
engine: {
39+
backgroundState: null,
40+
},
41+
}),
42+
errorMessage:
43+
"FATAL ERROR: Migration 126: Invalid engine backgroundState error: 'object'",
44+
scenario: 'backgroundState is invalid',
45+
},
46+
{
47+
state: merge({}, initialRootState, {
48+
engine: {
49+
backgroundState: {},
50+
},
51+
settings: null,
52+
}),
53+
errorMessage:
54+
"FATAL ERROR: Migration 126: Invalid Settings state error: 'object'",
55+
scenario: 'Settings object is invalid',
56+
},
57+
];
58+
59+
for (const { errorMessage, scenario, state } of invalidStates) {
60+
it(`captures exception if ${scenario}`, async () => {
61+
const newState = await migrate(state);
62+
63+
expect(newState).toStrictEqual(state);
64+
expect(mockedCaptureException).toHaveBeenCalledWith(expect.any(Error));
65+
expect(mockedCaptureException.mock.calls[0][0].message).toBe(
66+
errorMessage,
67+
);
68+
});
69+
}
70+
71+
it('updates the search engine from Google to Brave', async () => {
72+
const oldState = {
73+
engine: {
74+
backgroundState: {},
75+
},
76+
settings: {
77+
searchEngine: 'Google',
78+
},
79+
};
80+
81+
const expectedState = {
82+
engine: {
83+
backgroundState: {},
84+
},
85+
settings: {
86+
searchEngine: 'Brave',
87+
},
88+
};
89+
90+
const migratedState = await migrate(oldState);
91+
expect(migratedState).toStrictEqual(expectedState);
92+
});
93+
94+
it('does not change the search engine if it is already DuckDuckGo', async () => {
95+
const oldState = {
96+
engine: {
97+
backgroundState: {},
98+
},
99+
settings: {
100+
searchEngine: 'DuckDuckGo',
101+
},
102+
};
103+
104+
const migratedState = await migrate(oldState);
105+
expect(migratedState).toStrictEqual(oldState);
106+
});
107+
108+
it('does not change the search engine if it is already Brave', async () => {
109+
const oldState = {
110+
engine: {
111+
backgroundState: {},
112+
},
113+
settings: {
114+
searchEngine: 'Brave',
115+
},
116+
};
117+
118+
const migratedState = await migrate(oldState);
119+
expect(migratedState).toStrictEqual(oldState);
120+
});
121+
});

app/store/migrations/126.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { captureException } from '@sentry/react-native';
2+
import { isObject } from '@metamask/utils';
3+
import { ensureValidState } from './util';
4+
5+
/**
6+
* Migration 126: Change default search engine to Brave
7+
*
8+
* All existing users will be migrated to 'Brave' for a privacy-focused,
9+
* ad-free search experience.
10+
*
11+
* @param state - The persisted Redux state
12+
* @returns The migrated Redux state
13+
*/
14+
export default function migrate(state: unknown) {
15+
if (!ensureValidState(state, 126)) {
16+
return state;
17+
}
18+
19+
if (!isObject(state.settings)) {
20+
captureException(
21+
new Error(
22+
`FATAL ERROR: Migration 126: Invalid Settings state error: '${typeof state.settings}'`,
23+
),
24+
);
25+
return state;
26+
}
27+
28+
if (state.settings.searchEngine === 'Google') {
29+
state.settings.searchEngine = 'Brave';
30+
}
31+
32+
return state;
33+
}

app/store/migrations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import migration122 from './122';
126126
import migration123 from './123';
127127
import migration124 from './124';
128128
import migration125 from './125';
129+
import migration126 from './126';
129130

130131
// Add migrations above this line
131132
import { ControllerStorage } from '../persistConfig';
@@ -271,6 +272,7 @@ export const migrationList: MigrationsList = {
271272
123: migration123,
272273
124: migration124,
273274
125: migration125,
275+
126: migration126,
274276
};
275277

276278
// Enable both synchronous and asynchronous migrations

0 commit comments

Comments
 (0)