diff --git a/app/brave_generated_resources.grd b/app/brave_generated_resources.grd index d5416f7954a9..69c8fc35ebae 100644 --- a/app/brave_generated_resources.grd +++ b/app/brave_generated_resources.grd @@ -664,6 +664,12 @@ Or change later at $2brave://settings/ext You don't need to click to earn, but do click if you're interested! + + Thanks for supporting Brave Search by clicking a private ad. Unlike Big Tech, we measure ad performance anonymously to preserve your privacy. + + + Learn more / opt out choices + diff --git a/browser/brave_ads/creatives/search_result_ad/BUILD.gn b/browser/brave_ads/creatives/search_result_ad/BUILD.gn index 3af8bdcc55f1..febf7f4f6edb 100644 --- a/browser/brave_ads/creatives/search_result_ad/BUILD.gn +++ b/browser/brave_ads/creatives/search_result_ad/BUILD.gn @@ -5,25 +5,37 @@ source_set("search_result_ad") { sources = [ + "creative_search_result_ad_clicked_infobar_delegate.cc", + "creative_search_result_ad_clicked_infobar_delegate.h", "creative_search_result_ad_tab_helper.cc", "creative_search_result_ad_tab_helper.h", ] public_deps = [ "//base", + "//brave/components/brave_ads/core/mojom", + "//components/infobars/core", "//content/public/browser", ] deps = [ + "//brave/app:brave_generated_resources_grit_grit", "//brave/browser/brave_ads", "//brave/components/brave_ads/browser", "//brave/components/brave_ads/content/browser", "//brave/components/brave_ads/core", "//brave/components/brave_rewards/common", "//brave/components/brave_search/common", + "//brave/components/vector_icons", + "//chrome/browser:browser_public_dependencies", "//chrome/browser/profiles:profile", "//chrome/common", + "//components/infobars/content", + "//components/infobars/core", "//components/prefs", + "//components/strings:components_strings_grit", + "//components/vector_icons", + "//ui/base", "//url", ] } diff --git a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.cc b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.cc new file mode 100644 index 000000000000..59f9f1c689a0 --- /dev/null +++ b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.cc @@ -0,0 +1,99 @@ +/* Copyright (c) 2024 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/. */ + +#include "brave/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.h" + +#include + +#include "brave/components/brave_ads/core/public/prefs/pref_names.h" +#include "brave/grit/brave_generated_resources.h" +#include "chrome/browser/infobars/confirm_infobar_creator.h" +#include "components/infobars/content/content_infobar_manager.h" +#include "components/infobars/core/infobar.h" +#include "components/prefs/pref_service.h" +#include "components/strings/grit/components_strings.h" +#include "components/vector_icons/vector_icons.h" +#include "content/public/browser/web_contents.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/models/image_model.h" +#include "url/gurl.h" + +namespace brave_ads { + +namespace { + +constexpr int kIconSize = 20; + +constexpr char kLearnMoreUrl[] = + "https://support.brave.com/hc/en-us/articles/360026361072-Brave-Ads-FAQ"; + +} // namespace + +// static +void CreativeSearchResultAdClickedInfoBarDelegate::Create( + content::WebContents* web_contents, + PrefService* prefs) { + CHECK(web_contents); + CHECK(prefs); + + if (!prefs->GetBoolean(prefs::kShouldShowSearchResultAdClickedInfoBar)) { + return; + } + prefs->SetBoolean(prefs::kShouldShowSearchResultAdClickedInfoBar, false); + + infobars::ContentInfoBarManager* infobar_manager = + infobars::ContentInfoBarManager::FromWebContents(web_contents); + if (!infobar_manager) { + return; + } + infobar_manager->AddInfoBar( + CreateConfirmInfoBar(std::unique_ptr( + new CreativeSearchResultAdClickedInfoBarDelegate()))); +} + +CreativeSearchResultAdClickedInfoBarDelegate:: + CreativeSearchResultAdClickedInfoBarDelegate() = default; + +CreativeSearchResultAdClickedInfoBarDelegate:: + ~CreativeSearchResultAdClickedInfoBarDelegate() = default; + +infobars::InfoBarDelegate::InfoBarIdentifier +CreativeSearchResultAdClickedInfoBarDelegate::GetIdentifier() const { + return SEARCH_RESULT_AD_CLICKED_INFOBAR_DELEGATE; +} + +ui::ImageModel CreativeSearchResultAdClickedInfoBarDelegate::GetIcon() const { + return ui::ImageModel::FromVectorIcon(vector_icons::kProductIcon, + ui::kColorIcon, kIconSize); +} + +std::u16string CreativeSearchResultAdClickedInfoBarDelegate::GetMessageText() + const { + return l10n_util::GetStringUTF16( + IDS_BRAVE_ADS_SEARCH_RESULT_AD_CLICKED_INFOBAR_MESSAGE); +} + +int CreativeSearchResultAdClickedInfoBarDelegate::GetButtons() const { + return BUTTON_NONE; +} + +std::u16string CreativeSearchResultAdClickedInfoBarDelegate::GetLinkText() + const { + return l10n_util::GetStringUTF16( + IDS_BRAVE_ADS_SEARCH_RESULT_AD_LEARN_MORE_OPT_OUT_CHOICES_LABEL); +} + +GURL CreativeSearchResultAdClickedInfoBarDelegate::GetLinkURL() const { + return GURL(kLearnMoreUrl); +} + +bool CreativeSearchResultAdClickedInfoBarDelegate::LinkClicked( + WindowOpenDisposition disposition) { + ConfirmInfoBarDelegate::LinkClicked(disposition); + // Return true to immediately close the infobar. + return true; +} + +} // namespace brave_ads diff --git a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.h b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.h new file mode 100644 index 000000000000..6ccb4f865840 --- /dev/null +++ b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.h @@ -0,0 +1,45 @@ +/* Copyright (c) 2024 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/. */ + +#ifndef BRAVE_BROWSER_BRAVE_ADS_CREATIVES_SEARCH_RESULT_AD_CREATIVE_SEARCH_RESULT_AD_CLICKED_INFOBAR_DELEGATE_H_ +#define BRAVE_BROWSER_BRAVE_ADS_CREATIVES_SEARCH_RESULT_AD_CREATIVE_SEARCH_RESULT_AD_CLICKED_INFOBAR_DELEGATE_H_ + +#include "components/infobars/core/confirm_infobar_delegate.h" + +class PrefService; + +namespace content { +class WebContents; +} // namespace content + +namespace brave_ads { + +class CreativeSearchResultAdClickedInfoBarDelegate + : public ConfirmInfoBarDelegate { + public: + CreativeSearchResultAdClickedInfoBarDelegate(); + ~CreativeSearchResultAdClickedInfoBarDelegate() override; + + CreativeSearchResultAdClickedInfoBarDelegate( + const CreativeSearchResultAdClickedInfoBarDelegate&) = delete; + CreativeSearchResultAdClickedInfoBarDelegate& operator=( + const CreativeSearchResultAdClickedInfoBarDelegate&) = delete; + + static void Create(content::WebContents* web_contents, PrefService* prefs); + + private: + // ConfirmInfoBarDelegate: + infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override; + ui::ImageModel GetIcon() const override; + std::u16string GetMessageText() const override; + int GetButtons() const override; + std::u16string GetLinkText() const override; + GURL GetLinkURL() const override; + bool LinkClicked(WindowOpenDisposition disposition) override; +}; + +} // namespace brave_ads + +#endif // BRAVE_BROWSER_BRAVE_ADS_CREATIVES_SEARCH_RESULT_AD_CREATIVE_SEARCH_RESULT_AD_CLICKED_INFOBAR_DELEGATE_H_ diff --git a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.cc b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.cc index f84f5bcd5030..b9c7613f2c61 100644 --- a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.cc +++ b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.cc @@ -14,6 +14,7 @@ #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "brave/browser/brave_ads/ads_service_factory.h" +#include "brave/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_clicked_infobar_delegate.h" #include "brave/components/brave_ads/browser/ads_service.h" #include "brave/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.h" #include "brave/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_url_placement_id_extractor.h" @@ -98,9 +99,7 @@ bool CreativeSearchResultAdTabHelper::ShouldHandleCreativeAdEvents() const { // If the feature is enabled, we should only trigger creative ad events when // the user has joined Brave Rewards. - const Profile* const profile = - Profile::FromBrowserContext(web_contents()->GetBrowserContext()); - return profile->GetPrefs()->GetBoolean(brave_rewards::prefs::kEnabled); + return GetPrefs()->GetBoolean(brave_rewards::prefs::kEnabled); } /////////////////////////////////////////////////////////////////////////////// @@ -117,6 +116,22 @@ AdsService* CreativeSearchResultAdTabHelper::GetAdsService() const { return AdsServiceFactory::GetForProfile(profile); } +PrefService* CreativeSearchResultAdTabHelper::GetPrefs() const { + Profile* const profile = + Profile::FromBrowserContext(web_contents()->GetBrowserContext()); + return profile->GetPrefs(); +} + +void CreativeSearchResultAdTabHelper:: + MaybeTriggerCreativeAdClickedEventCallback(const bool success) { + if (!success) { + return; + } + + CreativeSearchResultAdClickedInfoBarDelegate::Create(web_contents(), + GetPrefs()); +} + void CreativeSearchResultAdTabHelper::MaybeCreateCreativeSearchResultAdHandler( content::NavigationHandle* const navigation_handle) { CHECK(navigation_handle); @@ -234,7 +249,10 @@ void CreativeSearchResultAdTabHelper::MaybeHandleCreativeAdClickedEventCallback( ads_service->TriggerSearchResultAdEvent( std::move(creative_search_result_ad), - mojom::SearchResultAdEventType::kClicked, base::DoNothing()); + mojom::SearchResultAdEventType::kClicked, + base::BindOnce(&CreativeSearchResultAdTabHelper:: + MaybeTriggerCreativeAdClickedEventCallback, + weak_factory_.GetWeakPtr())); } void CreativeSearchResultAdTabHelper::DidStartNavigation( diff --git a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.h b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.h index 068253a24b2b..ff888d0b08c5 100644 --- a/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.h +++ b/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.h @@ -11,10 +11,12 @@ #include "base/memory/weak_ptr.h" #include "brave/components/brave_ads/core/mojom/brave_ads.mojom-forward.h" +#include "brave/components/brave_ads/core/public/ads_callback.h" #include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_user_data.h" class GURL; +class PrefService; namespace brave_ads { @@ -39,11 +41,18 @@ class CreativeSearchResultAdTabHelper bool ShouldHandleCreativeAdEvents() const; + void MaybeTriggerCreativeAdClickedEvent(const GURL& url, + TriggerAdEventCallback callback); + private: friend class content::WebContentsUserData; AdsService* GetAdsService() const; + PrefService* GetPrefs() const; + + void MaybeTriggerCreativeAdClickedEventCallback(bool success); + void MaybeCreateCreativeSearchResultAdHandler( content::NavigationHandle* navigation_handle); diff --git a/browser/brave_ads/sources.gni b/browser/brave_ads/sources.gni new file mode 100644 index 000000000000..a6f0de4792f3 --- /dev/null +++ b/browser/brave_ads/sources.gni @@ -0,0 +1,111 @@ +# Copyright (c) 2021 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/. + +brave_browser_brave_ads_sources = [ + "//brave/browser/brave_ads/ad_units/notification_ad/notification_ad_platform_bridge.h", + "//brave/browser/brave_ads/ads_service_factory.cc", + "//brave/browser/brave_ads/ads_service_factory.h", + "//brave/browser/brave_ads/analytics/p3a/brave_stats_helper.cc", + "//brave/browser/brave_ads/analytics/p3a/brave_stats_helper.h", + "//brave/browser/brave_ads/application_state/background_helper/background_helper.cc", + "//brave/browser/brave_ads/application_state/background_helper/background_helper.h", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_holder.cc", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_holder.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper.cc", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl.cc", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl.h", + "//brave/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.cc", + "//brave/browser/brave_ads/creatives/search_result_ad/creative_search_result_ad_tab_helper.h", + "//brave/browser/brave_ads/tabs/ads_tab_helper.cc", + "//brave/browser/brave_ads/tabs/ads_tab_helper.h", +] + +brave_browser_brave_ads_deps = [ + "//base", + "//brave/browser/brave_ads/device_id", + "//brave/browser/brave_ads/services", + "//brave/browser/brave_rewards:util", + "//brave/browser/ui/brave_ads", + "//brave/components/brave_ads/browser", + "//brave/components/brave_ads/content/browser", + "//brave/components/brave_ads/core", + "//chrome/browser/notifications", + "//chrome/browser/profiles", + "//chrome/browser/profiles:profile", + "//chrome/browser/ui", + "//components/history/core/browser", + "//components/keyed_service/content", + "//components/sessions", + "//content/public/browser", + "//ui/base", +] + +if (!is_android) { + brave_browser_brave_ads_sources += [ "//brave/browser/brave_ads/ad_units/notification_ad/notification_ad_platform_bridge.cc" ] +} + +if (is_android) { + brave_browser_brave_ads_sources += [ + "//brave/browser/brave_ads/ad_units/notification_ad/notification_ad_platform_bridge_android.cc", + "//brave/browser/brave_ads/android/brave_ads_native_helper.cc", + "//brave/browser/brave_ads/android/brave_ads_native_helper.h", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_android.cc", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_android.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_android.cc", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_android.h", + ] + + brave_browser_brave_ads_deps += [ + "//brave/browser/brave_ads/android:jni_headers", + "//brave/build/android:jni_headers", + "//chrome/android:jni_headers", + ] +} + +if (is_linux) { + brave_browser_brave_ads_sources += [ + "//brave/browser/brave_ads/application_state/background_helper/background_helper_linux.cc", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_linux.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_linux.cc", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_linux.h", + ] + + brave_browser_brave_ads_deps += [ + "//chrome/common", + "//ui/aura", + "//ui/base/x", + "//ui/gfx/x", + ] +} + +if (is_mac) { + brave_browser_brave_ads_sources += [ + "//brave/browser/brave_ads/application_state/background_helper/background_helper_mac.h", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_mac.mm", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_mac.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_mac.mm", + ] + + brave_browser_brave_ads_deps += [ "//chrome/common" ] +} + +if (is_win) { + brave_browser_brave_ads_sources += [ + "//brave/browser/brave_ads/application_state/background_helper/background_helper_win.cc", + "//brave/browser/brave_ads/application_state/background_helper/background_helper_win.h", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_win.cc", + "//brave/browser/brave_ads/application_state/notification_helper/notification_helper_impl_win.h", + ] + + brave_browser_brave_ads_deps += [ + "//brave/browser/brave_ads/tooltips/", + "//brave/components/brave_adaptive_captcha:brave_adaptive_captcha", + "//chrome/common", + "//chrome/installer/util:with_no_strings", + "//ui/gfx", + "//ui/views", + ] +} diff --git a/browser/sources.gni b/browser/sources.gni index bfc253936934..9f04741d3f74 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -584,6 +584,7 @@ brave_chrome_browser_ui_allow_circular_includes_from += brave_chrome_browser_allow_circular_includes_from = [ "//brave/browser/ui", "//brave/browser/brave_ads:impl", + "//brave/browser/brave_ads/creatives/search_result_ad", ] # https://github.com/brave/brave-browser/issues/41418 diff --git a/chromium_src/components/infobars/core/infobar_delegate.h b/chromium_src/components/infobars/core/infobar_delegate.h index 91848314bc83..8d829c8da85e 100644 --- a/chromium_src/components/infobars/core/infobar_delegate.h +++ b/chromium_src/components/infobars/core/infobar_delegate.h @@ -17,7 +17,8 @@ WEB_DISCOVERY_INFOBAR_DELEGATE = 506, \ BRAVE_SYNC_ACCOUNT_DELETED_INFOBAR = 507, \ BRAVE_REQUEST_OTR_INFOBAR_DELEGATE = 508, \ - DEV_CHANNEL_DEPRECATION_INFOBAR_DELEGATE = 509, + DEV_CHANNEL_DEPRECATION_INFOBAR_DELEGATE = 509, \ + SEARCH_RESULT_AD_CLICKED_INFOBAR_DELEGATE = 510, // Deprecated: // WAYBACK_MACHINE_INFOBAR_DELEGATE = 502 diff --git a/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.cc b/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.cc index ebfdbb0deb73..c3364294cb7c 100644 --- a/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.cc +++ b/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.cc @@ -5,6 +5,7 @@ #include "brave/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler.h" +#include #include #include "base/functional/bind.h" diff --git a/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler_unittest.cc b/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler_unittest.cc index eae2b09fdca6..ce9c5cbd2ed0 100644 --- a/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler_unittest.cc +++ b/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_handler_unittest.cc @@ -17,6 +17,7 @@ #include "brave/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_mojom_web_page_entities_test_util.h" #include "brave/components/brave_ads/content/browser/creatives/search_result_ad/creative_search_result_ad_test_constants.h" #include "brave/components/brave_ads/core/mojom/brave_ads.mojom.h" +#include "brave/components/brave_ads/core/public/ads_callback.h" #include "brave/components/brave_ads/core/public/ads_feature.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -132,7 +133,7 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, } TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - DoNotTriggerViewedOrClickedAdEventsForInvalidWebPage) { + DoNotTriggerViewedAdEventForInvalidWebPage) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( @@ -148,18 +149,12 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, ::testing::_)) .Times(0); - EXPECT_CALL( - ads_service_mock_, - TriggerSearchResultAdEvent( - ::testing::_, mojom::SearchResultAdEventType::kClicked, ::testing::_)) - .Times(0); - SimulateMaybeExtractCreativeAdPlacementIdsFromWebPageCallback( creative_search_result_ad_handler.get(), blink::mojom::WebPagePtr()); } TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - DoNotTriggerViewedOrClickedAdEventsForEmptyWebPage) { + DoNotTriggerViewedAdEventForEmptyWebPage) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( @@ -175,18 +170,12 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, ::testing::_)) .Times(0); - EXPECT_CALL( - ads_service_mock_, - TriggerSearchResultAdEvent( - ::testing::_, mojom::SearchResultAdEventType::kClicked, ::testing::_)) - .Times(0); - SimulateMaybeExtractCreativeAdPlacementIdsFromWebPageCallback( creative_search_result_ad_handler.get(), blink::mojom::WebPage::New()); } TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - DoNotTriggerViewedOrClickedAdEventsForInvalidCreativeAd) { + DoNotTriggerViewedAdEventForInvalidCreativeAd) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( @@ -202,12 +191,6 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, ::testing::_)) .Times(0); - EXPECT_CALL( - ads_service_mock_, - TriggerSearchResultAdEvent( - ::testing::_, mojom::SearchResultAdEventType::kClicked, ::testing::_)) - .Times(0); - SimulateMaybeExtractCreativeAdPlacementIdsFromWebPageCallback( creative_search_result_ad_handler.get(), test::CreativeSearchResultAdMojomWebPage( @@ -215,7 +198,7 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, } TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - DoNotTriggerViewedOrClickedAdEventsForInvalidCreativeSetConversion) { + DoNotTriggerViewedAdEventForInvalidCreativeSetConversion) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( @@ -242,8 +225,7 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, kCreativeSetConversionUrlPatternPropertyName})); } -TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - TriggerViewedAndClickedAdEvents) { +TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, TriggerAdViewedEvent) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( @@ -281,7 +263,7 @@ TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, } TEST_F(BraveAdsCreativeSearchResultAdHandlerTest, - TriggerViewedAndClickedAdEventsWithUnreservedCharactersInPlacementId) { + TriggerViewedAdEventWithUnreservedCharactersInPlacementId) { // Arrange const auto creative_search_result_ad_handler = CreativeSearchResultAdHandler::MaybeCreate( diff --git a/components/brave_ads/core/internal/prefs/pref_registry.cc b/components/brave_ads/core/internal/prefs/pref_registry.cc index 32d08ed5195f..d74e4a25072c 100644 --- a/components/brave_ads/core/internal/prefs/pref_registry.cc +++ b/components/brave_ads/core/internal/prefs/pref_registry.cc @@ -62,6 +62,9 @@ void RegisterProfilePrefs(PrefRegistrySimple* const registry) { 0.0); registry->RegisterBooleanPref(prefs::kNotificationAdDidFallbackToCustom, false); + + registry->RegisterBooleanPref(prefs::kShouldShowSearchResultAdClickedInfoBar, + true); } } // namespace brave_ads diff --git a/components/brave_ads/core/public/prefs/pref_names.h b/components/brave_ads/core/public/prefs/pref_names.h index 79dbcb530cf0..a6d276c5c6eb 100644 --- a/components/brave_ads/core/public/prefs/pref_names.h +++ b/components/brave_ads/core/public/prefs/pref_names.h @@ -66,6 +66,9 @@ inline constexpr char kNotificationAdDidFallbackToCustom[] = inline constexpr char kShouldShowOnboardingNotification[] = "brave.brave_ads.should_show_my_first_ad_notification"; +inline constexpr char kShouldShowSearchResultAdClickedInfoBar[] = + "brave.brave_ads.should_show_search_result_ad_clicked_infobar"; + // Brave stats prefs. inline constexpr char kEnabledForLastProfile[] = "brave.brave_ads.enabled_last_profile"; diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift index 5e276da8659d..578e28a40c9c 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+TabManagerDelegate.swift @@ -211,6 +211,10 @@ extension BrowserViewController: TabManagerDelegate { self.downloadToast = downloadToast } + if let searchResultAdClickedInfoBar = toast as? SearchResultAdClickedInfoBar { + self.searchResultAdClickedInfoBar = searchResultAdClickedInfoBar + } + // If BVC isnt visible hold on to this toast until viewDidAppear if view.window == nil { pendingToast = toast @@ -241,6 +245,13 @@ extension BrowserViewController: TabManagerDelegate { ) } + func hideToastsOnNavigationStartIfNeeded(_ tabManager: TabManager) { + if tabManager.selectedTab?.braveSearchResultAdManager == nil { + searchResultAdClickedInfoBar?.dismiss(false) + searchResultAdClickedInfoBar = nil + } + } + func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) { guard let toast = toast, !privateBrowsingManager.isPrivateBrowsing else { return diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift index 38099bbdca7b..dc2a43e4043c 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BVC+WKNavigationDelegate.swift @@ -97,6 +97,8 @@ extension BrowserViewController: WKNavigationDelegate { } } + hideToastsOnNavigationStartIfNeeded(tabManager) + resetRedirectChain(webView) // Append source URL to redirect chain @@ -427,9 +429,20 @@ extension BrowserViewController: WKNavigationDelegate { ) { // Ensure the webView is not a link preview popup. if self.presentedViewController == nil { + let showSearchResultAdClickedPrivacyNotice = + rewards.ads.shouldShowSearchResultAdClickedInfoBar() BraveSearchResultAdManager.maybeTriggerSearchResultAdClickedEvent( requestURL, - rewards: rewards + rewards: rewards, + completion: { [weak self] success in + guard let self, success, showSearchResultAdClickedPrivacyNotice else { + return + } + let searchResultClickedInfobar = SearchResultAdClickedInfoBar( + tabManager: self.tabManager + ) + self.show(toast: searchResultClickedInfobar, duration: nil) + } ) } } else { diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift index b3d63eb01cbc..d90def8afddb 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController.swift @@ -210,6 +210,8 @@ public class BrowserViewController: UIViewController { var downloadToast: DownloadToast? /// A toast which is active and not yet dismissed var activeButtonToast: Toast? + /// An infobar displaying a privacy notice when a search result ad is clicked + var searchResultAdClickedInfoBar: SearchResultAdClickedInfoBar? /// A boolean to determine If AddToListActivity should be added var addToPlayListActivityItem: (enabled: Bool, item: PlaylistInfo?)? /// A boolean to determine if OpenInPlaylistActivity should be shown diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift index bad8b0f955d2..dc2aabe01179 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Search/BraveSearchResultAdManager.swift @@ -36,14 +36,18 @@ class BraveSearchResultAdManager: NSObject { ) } - static func maybeTriggerSearchResultAdClickedEvent(_ url: URL, rewards: BraveRewards) { + static func maybeTriggerSearchResultAdClickedEvent( + _ url: URL, + rewards: BraveRewards, + completion: @escaping ((Bool) -> Void) + ) { guard let placementId = getPlacementID(url) else { return } rewards.ads.triggerSearchResultAdClickedEvent( placementId, - completion: { _ in } + completion: completion ) } diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/SearchResultAdClickedInfoBar.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/SearchResultAdClickedInfoBar.swift new file mode 100644 index 000000000000..6466348e7e7d --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/SearchResultAdClickedInfoBar.swift @@ -0,0 +1,139 @@ +// 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 DesignSystem +import Foundation +import Shared +import SnapKit +import SwiftUI +import UIKit + +private struct SearchResultAdClickedInfoBarUX { + static let toastHeight: CGFloat = 100.0 + static let toastPadding: CGFloat = 10.0 + static let toastCloseButtonWidth: CGFloat = 20.0 + static let toastLabelFont = UIFont.preferredFont(forTextStyle: .subheadline) + static let toastBackgroundColor = UIColor(braveSystemName: .schemesOnPrimaryFixed) + static let learnMoreUrl = "https://support.brave.com/hc/en-us/articles/360026361072-Brave-Ads-FAQ" +} + +class SearchResultAdClickedInfoBar: Toast, UITextViewDelegate { + let tabManager: TabManager + + init(tabManager: TabManager) { + self.tabManager = tabManager + + super.init(frame: .zero) + + self.tapDismissalMode = .outsideTap + + self.clipsToBounds = true + + toastView.backgroundColor = SearchResultAdClickedInfoBarUX.toastBackgroundColor + + let toastContent = createToastContent( + Strings.searchResultAdClickedInfoBarTitle + " " + ) + toastView.addSubview(toastContent) + toastContent.snp.makeConstraints { make in + make.centerX.equalTo(toastView) + make.centerY.equalTo(toastView) + make.edges.equalTo(toastView).inset(SearchResultAdClickedInfoBarUX.toastPadding) + } + + addSubview(toastView) + toastView.snp.makeConstraints { make in + make.left.right.height.equalTo(self) + self.animationConstraint = make.top.equalTo(self).offset(SimpleToastUX.toastHeight).constraint + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func createToastContent( + _ labelText: String + ) -> UIView { + let horizontalStackView = UIStackView() + horizontalStackView.axis = .horizontal + horizontalStackView.alignment = .center + horizontalStackView.spacing = SearchResultAdClickedInfoBarUX.toastPadding + + let label = UITextView() + label.textAlignment = .left + label.textColor = .white + label.font = SearchResultAdClickedInfoBarUX.toastLabelFont + label.backgroundColor = SearchResultAdClickedInfoBarUX.toastBackgroundColor + label.isEditable = false + label.isScrollEnabled = false + label.isSelectable = true + label.delegate = self + + let learnMoreOptOutChoicesText = Strings.searchResultAdClickedLearnMoreOptOutChoicesLabel + let attributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: UIColor.white, + .font: SearchResultAdClickedInfoBarUX.toastLabelFont, + ] + + let linkAttributes: [NSAttributedString.Key: Any] = [ + .font: SearchResultAdClickedInfoBarUX.toastLabelFont, + .foregroundColor: UIColor.white, + .underlineStyle: 1, + ] + label.linkTextAttributes = linkAttributes + + let nsLabelAttributedString = NSMutableAttributedString( + string: labelText, + attributes: attributes + ) + let nsLinkAttributedString = NSMutableAttributedString( + string: learnMoreOptOutChoicesText, + attributes: linkAttributes + ) + + if let url = URL(string: SearchResultAdClickedInfoBarUX.learnMoreUrl) { + let linkTextRange = NSRange(location: 0, length: learnMoreOptOutChoicesText.count) + nsLinkAttributedString.addAttribute(.link, value: url, range: linkTextRange) + nsLabelAttributedString.append(nsLinkAttributedString) + label.isUserInteractionEnabled = true + } + label.attributedText = nsLabelAttributedString + horizontalStackView.addArrangedSubview(label) + + let button = UIButton() + button.setImage(UIImage(braveSystemNamed: "leo.close")!, for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.imageView?.tintColor = .white + button.imageView?.preferredSymbolConfiguration = .init( + font: .preferredFont(for: .title3, weight: .regular), + scale: .small + ) + button.snp.makeConstraints { + $0.width.equalTo(SearchResultAdClickedInfoBarUX.toastCloseButtonWidth) + } + button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside) + horizontalStackView.addArrangedSubview(button) + + return horizontalStackView + } + + func textView( + _ textView: UITextView, + shouldInteractWith url: URL, + in characterRange: NSRange, + interaction: UITextItemInteraction + ) -> Bool { + self.tabManager.addTabAndSelect( + URLRequest(url: URL(string: SearchResultAdClickedInfoBarUX.learnMoreUrl)!), + isPrivate: false + ) + dismiss(true) + return false + } + + @objc func buttonPressed(_ gestureRecognizer: UIGestureRecognizer) { + dismiss(true) + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toast.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toast.swift index 61f376d17586..c4fd1266cc79 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toast.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toast.swift @@ -14,6 +14,8 @@ class Toast: UIView { var displayState = State.dismissed + var tapDismissalMode: TapDismissalMode = .anyTap + lazy var gestureRecognizer: UITapGestureRecognizer = { let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) gestureRecognizer.cancelsTouchesInView = false @@ -97,6 +99,14 @@ class Toast: UIView { } @objc func handleTap(_ gestureRecognizer: UIGestureRecognizer) { + if tapDismissalMode == .outsideTap { + let location = gestureRecognizer.location(in: self) + // Check if the tap was inside the toast view + if self.point(inside: location, with: nil) { + return + } + } + dismiss(false) } @@ -106,4 +116,9 @@ class Toast: UIView { case pendingDismiss case dismissed } + + enum TapDismissalMode { + case outsideTap + case anyTap + } } diff --git a/ios/brave-ios/Sources/Shared/SharedStrings.swift b/ios/brave-ios/Sources/Shared/SharedStrings.swift index 7e720d1ff293..796a92becaf4 100644 --- a/ios/brave-ios/Sources/Shared/SharedStrings.swift +++ b/ios/brave-ios/Sources/Shared/SharedStrings.swift @@ -337,3 +337,20 @@ extension Strings { comment: "Question shown to user when tapping a link that opens the App Store app" ) } + +// Search result ad clicked InfoBar title and learn more / opt out choices link. +extension Strings { + public static let searchResultAdClickedInfoBarTitle = NSLocalizedString( + "SearchResultAdClickedInfoBarTitle", + bundle: .module, + value: "Thanks for supporting Brave Search by clicking a private ad. Unlike Big Tech, we measure ad performance anonymously to preserve your privacy.", + comment: "The text label of creative search result ad infobar message." + ) + + public static let searchResultAdClickedLearnMoreOptOutChoicesLabel = NSLocalizedString( + "SearchResultAdClickedLearnMoreOptOutChoicesLabel", + bundle: .module, + value: "Learn more / opt out choices", + comment: "The text label of creative search result ad learn more / opt out choices link." + ) +} diff --git a/ios/browser/api/ads/brave_ads.h b/ios/browser/api/ads/brave_ads.h index 7d3a094e3ae6..57ba65af4be5 100644 --- a/ios/browser/api/ads/brave_ads.h +++ b/ios/browser/api/ads/brave_ads.h @@ -70,6 +70,11 @@ OBJC_EXPORT /// Returns `true` if the user opted-in to search result ads. - (BOOL)isOptedInToSearchResultAds; +/// Returns `true` if the privacy notice infobar should be displayed when a user +/// clicks on a search result ad. This should be called before calling +/// `triggerSearchResultAdEvent` for the click. +- (BOOL)shouldShowSearchResultAdClickedInfoBar; + /// Used to notify the ads service that the user has opted-in/opted-out to /// Brave News. - (void)notifyBraveNewsIsEnabledPreferenceDidChange:(BOOL)isEnabled; diff --git a/ios/browser/api/ads/brave_ads.mm b/ios/browser/api/ads/brave_ads.mm index 3cbb627f440d..cb833ab54da3 100644 --- a/ios/browser/api/ads/brave_ads.mm +++ b/ios/browser/api/ads/brave_ads.mm @@ -209,6 +209,11 @@ - (BOOL)isOptedInToSearchResultAds { brave_ads::prefs::kOptedInToSearchResultAds); } +- (BOOL)shouldShowSearchResultAdClickedInfoBar { + return self.profilePrefService->GetBoolean( + brave_ads::prefs::kShouldShowSearchResultAdClickedInfoBar); +} + - (void)notifyBraveNewsIsEnabledPreferenceDidChange:(BOOL)isEnabled { [self setProfilePref:brave_news::prefs::kBraveNewsOptedIn value:base::Value(isEnabled)]; @@ -1663,10 +1668,19 @@ - (void)triggerSearchResultAdEvent: return completion(/*success=*/false); } + const auto mojom_event_type = + static_cast(eventType); adsService->TriggerSearchResultAdEvent( - searchResultAd.cppObjPtr, - static_cast(eventType), - base::BindOnce(completion)); + searchResultAd.cppObjPtr, mojom_event_type, + base::BindOnce(^(const bool success) { + if (success && + mojom_event_type == + brave_ads::mojom::SearchResultAdEventType::kClicked) { + self.profilePrefService->SetBoolean( + brave_ads::prefs::kShouldShowSearchResultAdClickedInfoBar, false); + } + completion(success); + })); } - (void)purgeOrphanedAdEventsForType:(BraveAdsAdType)adType