Allow new tab takeovers to prompt users to set Brave Search as default (#35113)

Rich media new tab takeover creatives can now trigger a one-click flow
to set Brave Search as the user's default search engine. The creative
sends a message to the new tab page which shows a confirmation dialog,
and if the user confirms, Brave Search is set as the default without the
user needing to visit settings.

Co-authored-by: Kevin Smith <zenparsing@gmail.com>
This commit is contained in:
Terry Mancey
2026-04-03 15:26:47 +01:00
committed by GitHub
co-authored by Kevin Smith
parent 4cce300eba
commit 3066b7de24
13 changed files with 174 additions and 35 deletions
@@ -45,6 +45,12 @@ export const style = scoped.css`
border: none;
}
leo-dialog {
--leo-dialog-padding: 24px;
position: fixed;
}
@keyframes background-fade-in {
from { opacity: 0; }
to { opacity: 1; }
@@ -5,16 +5,10 @@
import * as React from 'react'
import { SponsoredImageBackground } from '../../state/background_store'
import { loadImage } from '../../lib/image_loader'
import { IframeBackground, IframeBackgroundHandle } from './iframe_background'
import {
useSafeAreaReporter,
useRichMediaMessageHandler,
} from './rich_media_capability_provider'
import { RichMediaBackground } from './rich_media_background'
import {
useBackgroundState,
useCurrentBackground,
useBackgroundActions,
} from '../../context/background_context'
@@ -44,7 +38,7 @@ export function Background() {
/>
)
case 'sponsored-rich-media':
return <SponsoredRichMediaBackground background={currentBackground} />
return <RichMediaBackground background={currentBackground} />
case 'color':
return <ColorBackground colorValue={currentBackground.cssValue} />
}
@@ -102,28 +96,3 @@ function ImageBackground(props: ImageBackgroundProps) {
/>
)
}
function SponsoredRichMediaBackground(props: {
background: SponsoredImageBackground
}) {
const sponsoredRichMediaBaseUrl = useBackgroundState(
(s) => s.sponsoredRichMediaBaseUrl,
)
const [frameHandle, setFrameHandle] = React.useState<IframeBackgroundHandle>()
const messageHandler = useRichMediaMessageHandler(frameHandle, {
destinationUrl: props.background.logo?.destinationUrl,
})
useSafeAreaReporter(frameHandle)
return (
<IframeBackground
url={props.background.imageUrl}
expectedOrigin={new URL(sponsoredRichMediaBaseUrl).origin}
onReady={setFrameHandle}
onMessage={messageHandler}
/>
)
}
@@ -0,0 +1,95 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
import * as React from 'react'
import Dialog from '@brave/leo/react/dialog'
import Button from '@brave/leo/react/button'
import { SponsoredImageBackground } from '../../state/background_store'
import { useBackgroundState } from '../../context/background_context'
import { useSearchActions } from '../../context/search_context'
import { IframeBackground, IframeBackgroundHandle } from './iframe_background'
import {
useRichMediaMessageHandler,
useSafeAreaReporter,
} from './rich_media_capability_provider'
import { getString } from '../../lib/strings'
interface Props {
background: SponsoredImageBackground
}
export function RichMediaBackground(props: Props) {
const sponsoredRichMediaBaseUrl = useBackgroundState(
(s) => s.sponsoredRichMediaBaseUrl,
)
const [showMakeDefaultDialog, setShowMakeDefaultDialog] =
React.useState(false)
const openMakeBraveSearchDefaultDialog = React.useCallback(() => {
setShowMakeDefaultDialog(true)
}, [])
const [frameHandle, setFrameHandle] = React.useState<IframeBackgroundHandle>()
const messageHandler = useRichMediaMessageHandler(frameHandle, {
destinationUrl: props.background.logo?.destinationUrl,
onMakeBraveSearchDefault: openMakeBraveSearchDefaultDialog,
})
useSafeAreaReporter(frameHandle)
return (
<>
<IframeBackground
url={props.background.imageUrl}
expectedOrigin={new URL(sponsoredRichMediaBaseUrl).origin}
onReady={setFrameHandle}
onMessage={messageHandler}
/>
<MakeBraveSearchDefaultModal
isOpen={showMakeDefaultDialog}
onClose={() => setShowMakeDefaultDialog(false)}
/>
</>
)
}
interface MakeBraveSearchDefaultModalProps {
isOpen: boolean
onClose: () => void
}
function MakeBraveSearchDefaultModal(props: MakeBraveSearchDefaultModalProps) {
const searchActions = useSearchActions()
return (
<Dialog
isOpen={props.isOpen}
showClose
onClose={props.onClose}
>
<h3 slot='title'>{getString(S.NEW_TAB_MAKE_SEARCH_DEFAULT_TITLE)}</h3>
<p>{getString(S.NEW_TAB_MAKE_SEARCH_DEFAULT_TEXT)}</p>
<div slot='actions'>
<Button
onClick={() => {
searchActions.setDefaultSearchEngineAsBraveSearch()
props.onClose()
}}
>
{getString(S.NEW_TAB_MAKE_SEARCH_DEFAULT_CONFIRM_LABEL)}
</Button>
<Button
kind='outline'
onClick={props.onClose}
>
{getString(S.NEW_TAB_CANCEL_BUTTON_LABEL)}
</Button>
</div>
</Dialog>
)
}
@@ -30,6 +30,15 @@ describe('readIncomingMessage', () => {
})
})
describe('valid richMediaMakeBraveSearchDefault messages', () => {
test('returns correct message for richMediaMakeBraveSearchDefault', () => {
const result = readIncomingMessage({
type: 'richMediaMakeBraveSearchDefault',
})
expect(result).toEqual({ type: 'richMediaMakeBraveSearchDefault' })
})
})
describe('invalid message types', () => {
test('returns null for unknown message type', () => {
const result = readIncomingMessage({
@@ -168,6 +177,7 @@ describe('dispatchIncomingMessage', () => {
openBraveSearch: jest.fn(),
queryBraveSearchAutocomplete: jest.fn(),
hideBraveSearchBox: jest.fn(),
makeBraveSearchDefault: jest.fn(),
}
})
@@ -244,4 +254,12 @@ describe('dispatchIncomingMessage', () => {
expect(mockCapabilities.hideBraveSearchBox).toHaveBeenCalled()
})
})
describe('richMediaMakeBraveSearchDefault message', () => {
it('should call makeBraveSearchDefault', () => {
dispatch({ type: 'richMediaMakeBraveSearchDefault' })
expect(mockCapabilities.makeBraveSearchDefault).toHaveBeenCalled()
})
})
})
@@ -35,6 +35,7 @@ export type RichMediaIncomingMessage =
| SearchAutocompleteMessage
| OpenSearchMessage
| HideBraveSearchBoxMessage
| MakeBraveSearchDefaultMessage
interface AdEventMessage {
type: 'richMediaEvent'
@@ -55,6 +56,10 @@ interface HideBraveSearchBoxMessage {
type: 'richMediaHideBraveSearchBox'
}
interface MakeBraveSearchDefaultMessage {
type: 'richMediaMakeBraveSearchDefault'
}
// The interface through which messages can be sent to a rich media background
// frame.
export interface RichMediaFrameHandle {
@@ -69,6 +74,7 @@ export interface RichMediaCapabilities {
openBraveSearch: (pathAndQuery: string) => void
queryBraveSearchAutocomplete: (query: string) => void
hideBraveSearchBox: () => void
makeBraveSearchDefault: () => void
}
// Takes a raw incoming rich media message and executes the appropriate
@@ -108,6 +114,9 @@ export function readIncomingMessage(
case 'richMediaHideBraveSearchBox': {
return { type }
}
case 'richMediaMakeBraveSearchDefault': {
return { type }
}
}
return null
}
@@ -155,5 +164,9 @@ export function dispatchIncomingMessage(
capabilities.hideBraveSearchBox()
break
}
case 'richMediaMakeBraveSearchDefault': {
capabilities.makeBraveSearchDefault()
break
}
}
}
@@ -22,6 +22,7 @@ import {
interface MessageHandlerOptions {
destinationUrl?: string
onMakeBraveSearchDefault: () => void
}
// Returns a callback that will handle messages received from the rich media
@@ -30,7 +31,7 @@ export function useRichMediaMessageHandler(
frameHandle: RichMediaFrameHandle | undefined,
options: MessageHandlerOptions,
) {
const { destinationUrl } = options
const { destinationUrl, onMakeBraveSearchDefault } = options
const actions = useBackgroundActions()
const searchActions = useSearchActions()
const queryAutocomplete = useBraveSearchAutocomplete(frameHandle)
@@ -55,9 +56,12 @@ export function useRichMediaMessageHandler(
hideBraveSearchBox() {
searchActions.setSearchBoxSuppressed(true)
},
makeBraveSearchDefault() {
onMakeBraveSearchDefault()
},
})
},
[destinationUrl, frameHandle],
[destinationUrl, onMakeBraveSearchDefault, frameHandle],
)
}
@@ -242,6 +242,10 @@ export function createSearchStore() {
newTabProxy.handler.openURLFromSearch(url, event)
},
setDefaultSearchEngineAsBraveSearch() {
newTabProxy.handler.setDefaultSearchEngineAsBraveSearch()
},
reportSearchBoxHidden() {
newTabProxy.handler.reportSearchBoxHidden()
},
@@ -78,6 +78,7 @@ export function defaultSearchStore(): SearchStore {
stopAutocomplete() {},
openSearch(query, engine, event) {},
openUrlFromSearch(url, event) {},
setDefaultSearchEngineAsBraveSearch() {},
reportSearchBoxHidden() {},
reportSearchEngineUsage(engine) {},
reportSearchResultUsage(engine) {},
@@ -107,6 +108,7 @@ export interface SearchActions {
stopAutocomplete: () => void
openSearch: (query: string, engine: string, event: ClickEvent) => void
openUrlFromSearch: (url: string, event: ClickEvent) => void
setDefaultSearchEngineAsBraveSearch: () => void
reportSearchBoxHidden: () => void
reportSearchEngineUsage: (engine: string) => void
reportSearchResultUsage: (engine: string) => void
@@ -72,6 +72,7 @@ source_set("brave_new_tab_page_refresh") {
"//brave/components/brave_search_conversion",
"//brave/components/brave_talk/buildflags",
"//brave/components/brave_vpn/common/buildflags",
"//brave/components/constants",
"//brave/components/misc_metrics",
"//brave/components/ntp_background_images/browser",
"//brave/components/ntp_background_images/common",
@@ -166,6 +166,9 @@ interface NewTabPageHandler {
// Opens a URL from the search box.
OpenURLFromSearch(string url, EventDetails details) => ();
// Sets Brave Search as the user's default search engine.
SetDefaultSearchEngineAsBraveSearch() => ();
// Records search box usage metrics.
ReportSearchBoxHidden() => ();
ReportSearchEngineUsage(int64 engine_prepopulate_id) => ();
@@ -20,6 +20,7 @@
#include "brave/components/brave_search_conversion/pref_names.h"
#include "brave/components/brave_talk/buildflags/buildflags.h"
#include "brave/components/constants/pref_names.h"
#include "brave/components/constants/url_constants.h"
#include "brave/components/misc_metrics/brave_search_metrics.h"
#include "brave/components/misc_metrics/navigation_source_metrics.h"
#include "brave/components/misc_metrics/new_tab_metrics.h"
@@ -312,6 +313,15 @@ void NewTabPageHandler::OpenURLFromSearch(const std::string& url,
std::move(callback).Run();
}
void NewTabPageHandler::SetDefaultSearchEngineAsBraveSearch(
SetDefaultSearchEngineAsBraveSearchCallback callback) {
if (auto* template_url =
template_url_service_->GetTemplateURLForHost(kBraveSearchHost)) {
template_url_service_->SetUserSelectedDefaultSearchProvider(template_url);
}
std::move(callback).Run();
}
void NewTabPageHandler::ReportSearchBoxHidden(
ReportSearchBoxHiddenCallback callback) {
new_tab_metrics_->ReportNTPSearchDefaultEngine(std::nullopt);
@@ -121,6 +121,8 @@ class NewTabPageHandler : public mojom::NewTabPageHandler {
void OpenURLFromSearch(const std::string& url,
mojom::EventDetailsPtr details,
OpenURLFromSearchCallback callback) override;
void SetDefaultSearchEngineAsBraveSearch(
SetDefaultSearchEngineAsBraveSearchCallback callback) override;
void ReportSearchBoxHidden(ReportSearchBoxHiddenCallback callback) override;
void ReportSearchEngineUsage(
int64_t engine_prepopulate_id,
@@ -85,6 +85,18 @@
Hide VPN card
</message>
<message name="IDS_NEW_TAB_MAKE_SEARCH_DEFAULT_CONFIRM_LABEL" desc="Label for the confirm button on the Make Brave Search Default dialog" formatter_data="webui=BraveNewTabPage">
Set as default
</message>
<message name="IDS_NEW_TAB_MAKE_SEARCH_DEFAULT_TEXT" desc="Body text of the Make Brave Search Default dialog" formatter_data="webui=BraveNewTabPage">
Brave will be used as the search engine in the address bar for normal windows.
</message>
<message name="IDS_NEW_TAB_MAKE_SEARCH_DEFAULT_TITLE" desc="Title of the Make Brave Search Default dialog" formatter_data="webui=BraveNewTabPage">
Set Brave as the default search engine?
</message>
<message name="IDS_NEW_TAB_NEWS_CUSTOMIZE_BUTTON_LABEL" desc="Label for button that opens the News settings modal" formatter_data="webui=BraveNewTabPage">
Customize News
</message>