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:
co-authored by
Kevin Smith
parent
4cce300eba
commit
3066b7de24
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+95
@@ -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>
|
||||
)
|
||||
}
|
||||
+18
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+13
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user