diff --git a/.storybook/main.ts b/.storybook/main.ts index c201a222ea00..1d022664c8e9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -22,7 +22,11 @@ const slashStoriesIndexer: Indexer = { const config: StorybookConfig = { stories: process.env.STORYBOOK_STORYPATH ? [`../${process.env.STORYBOOK_STORYPATH}`] - : ['../components/**/stories/*.tsx', '../components/**/*.stories.tsx'], + : [ + '../components/**/stories/*.tsx', + '../components/**/*.stories.tsx', + '../browser/resources/**/stories/*.tsx' + ], typescript: { check: false, reactDocgen: false, diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 06e4dff3521c..c74a47a6517d 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -535,6 +535,13 @@ const flags_ui::FeatureEntry::FeatureVariation kZCashFeatureVariations[] = { kOsDesktop, \ FEATURE_VALUE_TYPE(features::kBraveNtpSearchWidget), \ }, \ + { \ + "brave-use-updated-ntp", \ + "Use the updated New Tab Page", \ + "Uses an updated version of the New Tab Page", \ + kOsDesktop, \ + FEATURE_VALUE_TYPE(features::kUseUpdatedNTP), \ + }, \ { \ "brave-adblock-cname-uncloaking", \ "Enable CNAME uncloaking", \ diff --git a/browser/brave_browser_features.cc b/browser/brave_browser_features.cc index 85c244faad08..8e3ca0b1dd30 100644 --- a/browser/brave_browser_features.cc +++ b/browser/brave_browser_features.cc @@ -9,6 +9,10 @@ namespace features { +BASE_FEATURE(kUseUpdatedNTP, + "BraveUseUpdatedNewTabPage", + base::FEATURE_DISABLED_BY_DEFAULT); + // Cleanup Session Cookies on browser restart if Session Restore is enabled. BASE_FEATURE(kBraveCleanupSessionCookiesOnSessionRestore, "BraveCleanupSessionCookiesOnSessionRestore", diff --git a/browser/brave_browser_features.h b/browser/brave_browser_features.h index ad30895a251e..5d06cd360b4f 100644 --- a/browser/brave_browser_features.h +++ b/browser/brave_browser_features.h @@ -13,6 +13,7 @@ namespace features { +BASE_DECLARE_FEATURE(kUseUpdatedNTP); BASE_DECLARE_FEATURE(kBraveCleanupSessionCookiesOnSessionRestore); BASE_DECLARE_FEATURE(kBraveCopyCleanLinkByDefault); BASE_DECLARE_FEATURE(kBraveCopyCleanLinkFromJs); diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 38cc978efdb4..262bb23199a9 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -90,6 +90,7 @@ #include "brave/components/decentralized_dns/content/decentralized_dns_navigation_throttle.h" #include "brave/components/google_sign_in_permission/google_sign_in_permission_throttle.h" #include "brave/components/google_sign_in_permission/google_sign_in_permission_util.h" +#include "brave/components/ntp_background_images/browser/mojom/ntp_background_images.mojom.h" #include "brave/components/playlist/common/buildflags/buildflags.h" #include "brave/components/playlist/common/features.h" #include "brave/components/request_otr/common/buildflags/buildflags.h" @@ -234,6 +235,7 @@ using extensions::ChromeContentBrowserClientExtensionsPart; #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/new_tab/new_tab_shows_navigation_throttle.h" #include "brave/browser/ui/geolocation/brave_geolocation_permission_tab_helper.h" +#include "brave/browser/ui/webui/brave_new_tab_page_refresh/brave_new_tab_page_ui.h" #include "brave/browser/ui/webui/brave_news_internals/brave_news_internals_ui.h" #include "brave/browser/ui/webui/brave_rewards/rewards_page_top_ui.h" #include "brave/browser/ui/webui/brave_rewards/rewards_panel_ui.h" @@ -666,14 +668,23 @@ void BraveContentBrowserClient::RegisterWebUIInterfaceBrokers( .Add() .Add(); + auto ntp_refresh_registration = + registry.ForWebUI() + .Add() + .Add() + .Add< + ntp_background_images::mojom::SponsoredRichMediaAdEventHandler>(); + #if BUILDFLAG(ENABLE_BRAVE_VPN) if (brave_vpn::IsBraveVPNFeatureEnabled()) { ntp_registration.Add(); + ntp_refresh_registration.Add(); } #endif if (base::FeatureList::IsEnabled(features::kBraveNtpSearchWidget)) { ntp_registration.Add(); + ntp_refresh_registration.Add(); } if (base::FeatureList::IsEnabled( diff --git a/browser/resources/brave_new_tab_page_refresh/BUILD.gn b/browser/resources/brave_new_tab_page_refresh/BUILD.gn new file mode 100644 index 000000000000..e98d8dcbb2e0 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/BUILD.gn @@ -0,0 +1,32 @@ +# Copyright (c) 2025 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("//brave/components/common/typescript.gni") +import("//mojo/public/tools/bindings/mojom.gni") + +assert(!is_android) + +transpile_web_ui("resources") { + entry_points = [ [ + "brave_new_tab_page", + rebase_path("brave_new_tab_page.tsx"), + ] ] + resource_name = "brave_new_tab_page" + output_module = true + deps = [ + "//brave/browser/ui/webui/brave_new_tab_page_refresh:mojom_js", + "//brave/components/brave_ads/core/mojom:mojom_js", + "//brave/components/brave_rewards/core/mojom:webui_js", + "//brave/components/brave_vpn/common/mojom:mojom_js", + "//brave/components/ntp_background_images/browser/mojom:mojom_js", + ] +} + +pack_web_resources("generated_resources") { + resource_name = "brave_new_tab_page" + output_dir = + "$root_gen_dir/brave/browser/resources/brave_new_tab_page_refresh" + deps = [ ":resources" ] +} diff --git a/browser/resources/brave_new_tab_page_refresh/assets/favorites_active.svg b/browser/resources/brave_new_tab_page_refresh/assets/favorites_active.svg new file mode 100644 index 000000000000..52232b6c9cd2 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/favorites_active.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/favorites_active_dark.svg b/browser/resources/brave_new_tab_page_refresh/assets/favorites_active_dark.svg new file mode 100644 index 000000000000..9a7d067763a5 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/favorites_active_dark.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive.svg b/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive.svg new file mode 100644 index 000000000000..b8013a6b7fb9 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive_dark.svg b/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive_dark.svg new file mode 100644 index 000000000000..1b97ecad6758 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/favorites_inactive_dark.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active.svg b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active.svg new file mode 100644 index 000000000000..1390115434f0 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active_dark.svg b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active_dark.svg new file mode 100644 index 000000000000..ee6a933aba6a --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_active_dark.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive.svg b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive.svg new file mode 100644 index 000000000000..8f7c89a505e5 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive_dark.svg b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive_dark.svg new file mode 100644 index 000000000000..8b64a2d17311 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/frequently_visited_inactive_dark.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/guardian_vpn_logo.svg b/browser/resources/brave_new_tab_page_refresh/assets/guardian_vpn_logo.svg new file mode 100644 index 000000000000..0558e74de79a --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/guardian_vpn_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/rewards_bat_coin.svg b/browser/resources/brave_new_tab_page_refresh/assets/rewards_bat_coin.svg new file mode 100644 index 000000000000..fc7e80963f8b --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/rewards_bat_coin.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/talk_graphic.svg b/browser/resources/brave_new_tab_page_refresh/assets/talk_graphic.svg new file mode 100644 index 000000000000..d8ce336182f8 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/talk_graphic.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_connected.svg b/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_connected.svg new file mode 100644 index 000000000000..80a553b7d6f4 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_connected.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_disconnected.svg b/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_disconnected.svg new file mode 100644 index 000000000000..749ceed5d497 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/assets/vpn_shield_disconnected.svg @@ -0,0 +1,4 @@ + + + + diff --git a/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.html b/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.html new file mode 100644 index 000000000000..c3dc7bd690fb --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.html @@ -0,0 +1,22 @@ + + + + + + New Tab + + + + + + + + + +
+ + diff --git a/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.tsx b/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.tsx new file mode 100644 index 000000000000..4b2055eaf887 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/brave_new_tab_page.tsx @@ -0,0 +1,56 @@ +/* Copyright (c) 2025 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 { createRoot } from 'react-dom/client' +import { setIconBasePath } from '@brave/leo/react/icon' + +import { LocaleContext } from './components/context/locale_context' +import { NewTabContext } from './components/context/new_tab_context' +import { createNewTabModel } from './webui/webui_new_tab_model' +import { SearchContext } from './components/context/search_context' +import { createSearchModel } from './webui/webui_search_model' +import { TopSitesContext } from './components/context/top_sites_context' +import { createTopSitesModel } from './webui/webui_top_sites_model' +import { RewardsContext } from './components/context/rewards_context' +import { createRewardsModel } from './webui/webui_rewards_model' +import { VPNContext } from './components/context/vpn_context' +import { createVPNModel } from './webui/webui_vpn_model' +import { createLocale } from './webui/webui_locale' +import { App } from './components/app' + +setIconBasePath('chrome://resources/brave-icons') + +const newTabModel = createNewTabModel() +const searchModel = createSearchModel() +const topSitesModel = createTopSitesModel() +const rewardsModel = createRewardsModel() +const vpnModel = createVPNModel() + +Object.assign(self, { + [Symbol.for('ntpInternals')]: { + newTabModel, + searchModel, + topSitesModel, + rewardsModel, + vpnModel + } +}) + +createRoot(document.getElementById('root')!).render( + + + + + + + + + + + + + +) diff --git a/browser/resources/brave_new_tab_page_refresh/components/app.style.ts b/browser/resources/brave_new_tab_page_refresh/components/app.style.ts new file mode 100644 index 000000000000..aa28f233d86f --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/components/app.style.ts @@ -0,0 +1,225 @@ +/* Copyright (c) 2025 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 { color, font } from '@brave/leo/tokens/css/variables' +import { scoped, global } from '../lib/scoped_css' + +export const style = scoped.css` + + & { + --search-transition-duration: 120ms; + } + + .top-controls { + position: absolute; + inset-block-start: 4px; + inset-inline-end: 4px; + min-height: 24px; + display: flex; + gap: 8px; + align-items: center; + z-index: 2; + } + + .settings { + --leo-icon-size: 20px; + + opacity: 0.5; + color: #fff; + filter: drop-shadow(0px 1px 4px rgba(0, 0, 0, 0.60)); + + &:hover { + opacity: 0.7; + cursor: pointer; + } + } + + .clock { + font: ${font.large.semibold}; + color: #fff; + opacity: .8; + } + + .allow-background-pointer-events { + /* This element will allow pointer events to target the background. */ + pointer-events: none; + + /* But children will not (unless the explicitly allow it). */ + > :not(.allow-background-pointer-events) { + pointer-events: auto; + } + + /* And not when a popover is open. When a popover is open, pointer events + on a background iframe will not "light-dismiss" the popover. */ + :scope:has(:popover-open) & { + pointer-events: auto; + } + } + + main { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + gap: 16px; + padding: 16px 24px; + + > * { + transition: + opacity var(--search-transition-duration), + transform var(--search-transition-duration); + + .search-box-expanded & { + opacity: 0; + transform: scale(0.9); + } + } + } + + .topsites-container { + padding: 16px 0; + align-self: stretch; + display: flex; + gap: 16px; + } + + .searchbox-container { + align-self: stretch; + + .search-box-expanded & { + opacity: 1; + transform: none; + } + } + + .spacer { + flex: 1 1 auto; + align-self: stretch; + } + + .widget-container { + align-self: stretch; + flex: 0 0 120px; + display: flex; + justify-content: center; + align-items: stretch; + gap: 16px; + } + + &.widget-position-top { + .widget-container { + order: 1; + margin-top: 16px; + margin-bottom: 18px; + } + + .searchbox-container { + order: 2; + } + + .spacer { + order: 3; + } + + .background-caption-container { + order: 4; + } + + .topsites-container { + order: 5; + } + } + +` + +global.css` + @scope (${style.selector}) { + + & { + font: ${font.default.regular}; + color: ${color.text.primary}; + interpolate-size: allow-keywords; + } + + button { + margin: 0; + padding: 0; + background: 0; + border: none; + text-align: unset; + width: unset; + font: inherit; + cursor: pointer; + + &:disabled { + cursor: default; + } + } + + h2 { + font: ${font.heading.h2}; + margin: 0; + } + + h3 { + font: ${font.heading.h3}; + margin: 0; + } + + h4 { + font: ${font.heading.h4}; + margin: 0; + } + + p { + margin: 0; + } + + dialog, [popover] { + border: none; + color: inherit; + margin: 0; + padding: 0; + background: none; + + &::backdrop { + background-color: transparent; + } + } + + .popover-menu { + padding: 4px; + border-radius: 8px; + border: solid 1px ${color.divider.subtle}; + background: ${color.container.background}; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + gap: 4px; + min-width: 180px; + + .divider { + height: 1px; + background: ${color.divider.subtle}; + } + + button { + --leo-icon-size: 20px; + + padding: 8px 24px 8px 8px; + border-radius: 4px; + display: flex; + align-items: center; + gap: 16px; + + &:hover, &.highlight { + background: ${color.container.highlight}; + } + } + } + } +` diff --git a/browser/resources/brave_new_tab_page_refresh/components/app.tsx b/browser/resources/brave_new_tab_page_refresh/components/app.tsx new file mode 100644 index 000000000000..21c3ae8cd022 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/components/app.tsx @@ -0,0 +1,72 @@ +/* Copyright (c) 2025 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 Icon from '@brave/leo/react/icon' + +import { useNewTabState } from './context/new_tab_context' +import { SearchBox } from './search/search_box' +import { Background } from './background' +import { BackgroundCaption } from './background_caption' +import { SettingsModal, SettingsView } from './settings/settings_modal' +import { TopSites } from './top_sites/top_sites' +import { Clock } from './clock' +import { StatsWidget } from './widgets/stats_widget' +import { ProductWidgetStack } from './widgets/product_widget_stack' + +import { style } from './app.style' + +export function App() { + const widgetPosition = useNewTabState((state) => state.widgetPosition) + + const [settingsView, setSettingsView] = + React.useState(null) + + return ( +
+ +
+ + +
+
+
+ +
+
+ setSettingsView('search')} + /> +
+
+
+ +
+
+ + +
+
+ setSettingsView(null)} + /> +
+ ) +} diff --git a/browser/resources/brave_new_tab_page_refresh/components/background.style.ts b/browser/resources/brave_new_tab_page_refresh/components/background.style.ts new file mode 100644 index 000000000000..4d7310613ef3 --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/components/background.style.ts @@ -0,0 +1,54 @@ +/* Copyright (c) 2025 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 { scoped } from '../lib/scoped_css' + +export const style = scoped.css` + + & { + position: fixed; + inset: 0; + z-index: 0; + display: flex; + animation-name: fade-in; + animation-timing-function: ease-in-out; + animation-duration: 350ms; + animation-delay: 0s; + animation-fill-mode: both; + + > * { + flex: 1 1 auto; + } + } + + .image-background { + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + background-image: + linear-gradient( + rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0) 80%, + rgba(0, 0, 0, 0.6) 100%), + var(--ntp-background); + } + + .color-background { + background: var(--ntp-background); + } + + iframe { + border: none; + + &.loading { + opacity: 1; + } + } + + @keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + +` diff --git a/browser/resources/brave_new_tab_page_refresh/components/background.tsx b/browser/resources/brave_new_tab_page_refresh/components/background.tsx new file mode 100644 index 000000000000..f8466fa9466e --- /dev/null +++ b/browser/resources/brave_new_tab_page_refresh/components/background.tsx @@ -0,0 +1,172 @@ +/* Copyright (c) 2025 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 { SponsoredRichMediaEventType, SponsoredImageBackground } from '../models/new_tab_model' +import { useNewTabState, useNewTabModel } from './context/new_tab_context' +import { openLink } from './link' +import { loadImage } from '../lib/image_loader' + +import { style } from './background.style' + +export function Background() { + const currentBackground = useNewTabState((state) => state.currentBackground) + + function renderBackground() { + if (!currentBackground) { + return + } + + switch (currentBackground.type) { + case 'brave': + case 'custom': + case 'sponsored-image': + return + case 'sponsored-rich-media': + return + case 'solid': + case 'gradient': + return + } + } + + return ( +
+ {renderBackground()} +
+ ) +} + +function ColorBackground(props: { colorValue: string }) { + React.useEffect(() => { + setBackgroundVariable(props.colorValue) + }, [props.colorValue]) + + return
+} + +function setBackgroundVariable(value: string) { + if (value) { + document.body.style.setProperty('--ntp-background', value) + } else { + document.body.style.removeProperty('--ntp-background') + } +} + +function ImageBackground(props: { url: string }) { + // In order to avoid a "flash-of-unloaded-image", load the image in the + // background and only update the background CSS variable when the image has + // finished loading. + React.useEffect(() => { + loadImage(props.url).then((loaded) => { + if (loaded) { + setBackgroundVariable(`url(${CSS.escape(props.url)})`) + } + }) + }, [props.url]) + + return
+} + +function SponsoredRichMediaBackground( + props: { background: SponsoredImageBackground } +) { + const newTabModel = useNewTabModel() + + const [sponsoredRichMediaBaseUrl] = useNewTabState((state) => [ + state.sponsoredRichMediaBaseUrl + ]) + + return ( + { + const eventType = getRichMediaEventType(data) + if (eventType) { + newTabModel.notifySponsoredRichMediaEvent(eventType) + } + if (eventType === 'click') { + const url = props.background.logo?.destinationUrl + if (url) { + openLink(url) + } + } + }} + /> + ) +} + +function getRichMediaEventType(data: any): SponsoredRichMediaEventType | null { + if (!data || data.type !== 'richMediaEvent') { + return null + } + const value = String(data.value || '') + switch (value) { + case 'click': + case 'interaction': + case 'mediaPlay': + case 'media25': + case 'media100': + return value + } + return null +} + +interface IframeBackgroundProps { + url: string + expectedOrigin: string + onMessage: (data: unknown) => void +} + +function IframeBackground(props: IframeBackgroundProps) { + const iframeRef = React.useRef(null) + const [contentLoaded, setContentLoaded] = React.useState(false) + + React.useEffect(() => { + function listener(event: MessageEvent) { + if (!event.origin || event.origin !== props.expectedOrigin) { + return + } + if (!event.source || event.source !== iframeRef.current?.contentWindow) { + return + } + props.onMessage(event.data) + } + + window.addEventListener('message', listener) + return () => { window.removeEventListener('message', listener) } + }, [props.expectedOrigin, props.onMessage]) + + return ( +