diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 25932600d476..5c9c41d69dc9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -105,6 +105,9 @@ net/proxy_resolution/ @brave/tor-reviewers chromium_src/**/extensions/**/*_features.json @brave/sec-team chromium_src/tools/json_schema_compiler/ @brave/sec-team +# Browser Commands (available via Brave Education content) +browser/ui/webui/browser_command/ @brave/sec-team + # Brave theme browser/themes @simonhong diff --git a/browser/about_flags.cc b/browser/about_flags.cc index 8a8cf03f0ec1..f545f0e36f1f 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -20,6 +20,7 @@ #include "brave/components/brave_ads/core/public/ad_units/notification_ad/notification_ad_feature.h" #include "brave/components/brave_ads/core/public/ads_feature.h" #include "brave/components/brave_component_updater/browser/features.h" +#include "brave/components/brave_education/common/features.h" #include "brave/components/brave_news/common/features.h" #include "brave/components/brave_rewards/common/buildflags/buildflags.h" #include "brave/components/brave_rewards/common/features.h" @@ -442,6 +443,19 @@ FEATURE_VALUE_TYPE(kExtensionsManifestV2), \ })) +#if !BUILDFLAG(IS_ANDROID) +#define BRAVE_EDUCATION_FEATURE_ENTRIES \ + EXPAND_FEATURE_ENTRIES({ \ + "brave-show-getting-started-page", \ + "Show getting started pages", \ + "Show a getting started page after completing the Welcome UX.", \ + kOsDesktop, \ + FEATURE_VALUE_TYPE(brave_education::features::kShowGettingStartedPage), \ + }) +#else +#define BRAVE_EDUCATION_FEATURE_ENTRIES +#endif + // Keep the last item empty. #define LAST_BRAVE_FEATURE_ENTRIES_ITEM @@ -981,6 +995,7 @@ BRAVE_MIDDLE_CLICK_AUTOSCROLL_FEATURE_ENTRY \ BRAVE_EXTENSIONS_MANIFEST_V2 \ BRAVE_WORKAROUND_NEW_WINDOW_FLASH \ + BRAVE_EDUCATION_FEATURE_ENTRIES \ LAST_BRAVE_FEATURE_ENTRIES_ITEM // Keep it as the last item. namespace flags_ui { namespace { diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index a37795802975..a087a21e5d2c 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -222,6 +222,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_education/brave_education_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" @@ -826,6 +827,9 @@ void BraveContentBrowserClient::RegisterBrowserInterfaceBindersForFrame( content::RegisterWebUIControllerInterfaceBinder< commands::mojom::CommandsService, BraveSettingsUI>(map); } + content::RegisterWebUIControllerInterfaceBinder< + brave_education::mojom::EducationPageHandlerFactory, + brave_education::BraveEducationUI>(map); #endif #if BUILDFLAG(ENABLE_AI_CHAT) diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 08f53da439b8..568caa93e830 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -246,6 +246,12 @@ source_set("ui") { "toolbar/brave_bookmark_sub_menu_model.cc", "toolbar/brave_bookmark_sub_menu_model.h", "toolbar/brave_recent_tabs_sub_menu_model.h", + "webui/brave_education/brave_education_ui.cc", + "webui/brave_education/brave_education_ui.h", + "webui/brave_education/education_page_handler.cc", + "webui/brave_education/education_page_handler.h", + "webui/brave_education/getting_started_helper.cc", + "webui/brave_education/getting_started_helper.h", "webui/brave_rewards/rewards_page_top_ui.cc", "webui/brave_rewards/rewards_page_top_ui.h", "webui/brave_rewards/rewards_panel_handler.cc", @@ -268,6 +274,8 @@ source_set("ui") { "webui/brave_shields/shields_panel_handler.h", "webui/brave_shields/shields_panel_ui.cc", "webui/brave_shields/shields_panel_ui.h", + "webui/browser_command/brave_browser_command_handler.cc", + "webui/browser_command/brave_browser_command_handler.h", "webui/navigation_bar_data_provider.cc", "webui/navigation_bar_data_provider.h", "webui/new_tab_page/brave_new_tab_message_handler.cc", @@ -331,6 +339,9 @@ source_set("ui") { "//base", "//brave/browser/profiles:util", "//brave/common", + "//brave/components/brave_education/common", + "//brave/components/brave_education/common:mojom", + "//brave/components/brave_education/resources:generated_resources", "//brave/components/constants", "//brave/components/misc_metrics", "//brave/components/sidebar/browser", diff --git a/browser/ui/webui/brave_education/brave_education_ui.cc b/browser/ui/webui/brave_education/brave_education_ui.cc new file mode 100644 index 000000000000..c18dea41a244 --- /dev/null +++ b/browser/ui/webui/brave_education/brave_education_ui.cc @@ -0,0 +1,106 @@ +// 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/ui/webui/brave_education/brave_education_ui.h" + +#include +#include +#include +#include + +#include "brave/browser/ui/webui/brave_education/education_page_handler.h" +#include "brave/browser/ui/webui/brave_webui_source.h" +#include "brave/browser/ui/webui/browser_command/brave_browser_command_handler.h" +#include "brave/components/brave_education/common/education_content_urls.h" +#include "brave/components/brave_education/resources/grit/brave_education_generated_map.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "chrome/grit/branded_strings.h" +#include "components/grit/brave_components_resources.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "services/network/public/mojom/content_security_policy.mojom.h" +#include "ui/base/webui/web_ui_util.h" + +namespace brave_education { + +namespace { + +std::vector GetSupportedCommands( + const GURL& webui_url) { + using browser_command::mojom::Command; + + auto content_type = EducationContentTypeFromBrowserURL(webui_url.spec()); + if (!content_type) { + return {}; + } + + switch (*content_type) { + case EducationContentType::kGettingStarted: + return {Command::kOpenRewardsOnboarding, Command::kOpenWalletOnboarding, + Command::kOpenVPNOnboarding, Command::kOpenAIChat}; + } +} + +} // namespace + +BraveEducationUI::BraveEducationUI(content::WebUI* web_ui, + const std::string& host_name) + : ui::MojoWebUIController(web_ui) { + content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( + Profile::FromWebUI(web_ui), host_name); + + webui::SetupWebUIDataSource( + source, + base::make_span(kBraveEducationGenerated, kBraveEducationGeneratedSize), + IDR_BRAVE_EDUCATION_HTML); + + AddBackgroundColorToSource(source, web_ui->GetWebContents()); + + static constexpr webui::LocalizedString kStrings[] = { + {"headerText", IDS_WELCOME_HEADER}}; + source->AddLocalizedStrings(kStrings); + + // Allow embedding of iframe from brave.com. + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ChildSrc, + "child-src chrome://webui-test https://brave.com/;"); +} + +BraveEducationUI::~BraveEducationUI() = default; + +void BraveEducationUI::BindInterface( + mojo::PendingReceiver pending_receiver) { + if (page_factory_receiver_.is_bound()) { + page_factory_receiver_.reset(); + } + page_factory_receiver_.Bind(std::move(pending_receiver)); +} + +void BraveEducationUI::BindInterface( + mojo::PendingReceiver pending_receiver) { + if (command_handler_factory_receiver_.is_bound()) { + command_handler_factory_receiver_.reset(); + } + command_handler_factory_receiver_.Bind(std::move(pending_receiver)); +} + +void BraveEducationUI::CreatePageHandler( + mojo::PendingReceiver handler) { + page_handler_ = std::make_unique(std::move(handler)); +} + +void BraveEducationUI::CreateBrowserCommandHandler( + mojo::PendingReceiver + pending_handler) { + command_handler_ = std::make_unique( + std::move(pending_handler), Profile::FromWebUI(web_ui()), + GetSupportedCommands(web_ui()->GetWebContents()->GetVisibleURL())); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(BraveEducationUI) + +} // namespace brave_education diff --git a/browser/ui/webui/brave_education/brave_education_ui.h b/browser/ui/webui/brave_education/brave_education_ui.h new file mode 100644 index 000000000000..b8b284c4b746 --- /dev/null +++ b/browser/ui/webui/brave_education/brave_education_ui.h @@ -0,0 +1,64 @@ +// 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_UI_WEBUI_BRAVE_EDUCATION_BRAVE_EDUCATION_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_BRAVE_EDUCATION_UI_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "brave/components/brave_education/common/education_page.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "ui/webui/mojo_web_ui_controller.h" +#include "ui/webui/resources/js/browser_command/browser_command.mojom.h" + +namespace content { +class WebUI; +} + +namespace brave_education { + +// The Web UI controller for the Brave product education page, which displays +// production education website content in an iframe. +class BraveEducationUI : public ui::MojoWebUIController, + public mojom::EducationPageHandlerFactory, + public browser_command::mojom::CommandHandlerFactory { + public: + BraveEducationUI(content::WebUI* web_ui, const std::string& host_name); + ~BraveEducationUI() override; + + BraveEducationUI(const BraveEducationUI&) = delete; + BraveEducationUI& operator=(const BraveEducationUI&) = delete; + + void BindInterface( + mojo::PendingReceiver pending_receiver); + + void BindInterface( + mojo::PendingReceiver pending_receiver); + + // mojom::EducationPageHandlerFactory: + void CreatePageHandler( + mojo::PendingReceiver handler) override; + + // browser_command::mojom::CommandHandlerFactory: + void CreateBrowserCommandHandler( + mojo::PendingReceiver handler) + override; + + private: + mojo::Receiver page_factory_receiver_{this}; + mojo::Receiver command_handler_factory_receiver_{this}; + std::unique_ptr page_handler_; + std::unique_ptr command_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +} // namespace brave_education + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_BRAVE_EDUCATION_UI_H_ diff --git a/browser/ui/webui/brave_education/brave_education_ui_browsertest.cc b/browser/ui/webui/brave_education/brave_education_ui_browsertest.cc new file mode 100644 index 000000000000..61c9bb129333 --- /dev/null +++ b/browser/ui/webui/brave_education/brave_education_ui_browsertest.cc @@ -0,0 +1,152 @@ +/* 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 +#include +#include + +#include "base/memory/weak_ptr.h" +#include "brave/components/ai_chat/core/common/buildflags/buildflags.h" +#include "brave/components/brave_education/common/education_content_urls.h" +#include "brave/components/brave_vpn/common/buildflags/buildflags.h" +#include "brave/components/constants/webui_url_constants.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/webui_url_constants.h" +#include "chrome/test/base/chrome_test_utils.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" + +namespace brave_education { + +class BraveEducationUIBrowserTest : public InProcessBrowserTest { + protected: + void SetUpOnMainThread() override { CreateAndAddWebUITestDataSource(); } + + void NavigateToEducationPage(EducationContentType content_type) { + auto webui_url = GetEducationContentBrowserURL(content_type); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), webui_url)); + auto* web_contents = chrome_test_utils::GetActiveWebContents(this); + ASSERT_TRUE(WaitForLoadStop(web_contents)); + } + + void PostMessageFromIFrame(std::string_view message_data) { + message_data_ = message_data; + auto* web_contents = chrome_test_utils::GetActiveWebContents(this); + ASSERT_TRUE(content::ExecJs(web_contents, R"( + const iframe = document.getElementById('content') + iframe.src = "chrome://webui-test/" + )")); + } + + private: + void CreateAndAddWebUITestDataSource() { + auto* web_contents = chrome_test_utils::GetActiveWebContents(this); + + content::WebUIDataSource* source = content::WebUIDataSource::CreateAndAdd( + web_contents->GetBrowserContext(), chrome::kChromeUIWebUITestHost); + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameAncestors, + "frame-ancestors chrome://* 'self';"); + + source->SetRequestFilter( + base::BindRepeating([](const std::string&) { return true; }), + base::BindRepeating( + &BraveEducationUIBrowserTest::HandleWebUITestRequest, + weak_factory_.GetWeakPtr())); + } + + void HandleWebUITestRequest( + const std::string& path, + content::WebUIDataSource::GotDataCallback callback) { + if (path == "post-message.js") { + std::string js = "window.parent.postMessage(" + message_data_ + ", '*')"; + std::move(callback).Run( + base::MakeRefCounted(std::move(js))); + return; + } + + static const char kHTML[] = R"( + + + + + Hello world! + + + )"; + + std::move(callback).Run( + base::MakeRefCounted(std::move(kHTML))); + } + + std::string message_data_; + base::WeakPtrFactory weak_factory_{this}; +}; + +IN_PROC_BROWSER_TEST_F(BraveEducationUIBrowserTest, OpenWalletOnboarding) { + NavigateToEducationPage(EducationContentType::kGettingStarted); + + content::WebContentsAddedObserver added_observer; + + PostMessageFromIFrame(R"( + {messageType: 'browser-command', + command: 'open-wallet-onboarding'})"); + + auto* new_web_contents = added_observer.GetWebContents(); + EXPECT_EQ(new_web_contents->GetVisibleURL(), GURL(kBraveUIWalletPageURL)); +} + +IN_PROC_BROWSER_TEST_F(BraveEducationUIBrowserTest, OpenRewardsOnboarding) { + NavigateToEducationPage(EducationContentType::kGettingStarted); + + content::WebContentsAddedObserver added_observer; + + PostMessageFromIFrame(R"( + {messageType: 'browser-command', + command: 'open-rewards-onboarding'})"); + + auto* new_web_contents = added_observer.GetWebContents(); + EXPECT_EQ(new_web_contents->GetVisibleURL(), GURL(kBraveRewardsPanelURL)); +} + +#if BUILDFLAG(ENABLE_BRAVE_VPN) + +IN_PROC_BROWSER_TEST_F(BraveEducationUIBrowserTest, OpenVPNOnboarding) { + NavigateToEducationPage(EducationContentType::kGettingStarted); + + content::WebContentsAddedObserver added_observer; + + PostMessageFromIFrame(R"( + {messageType: 'browser-command', + command: 'open-vpn-onboarding'})"); + + auto* new_web_contents = added_observer.GetWebContents(); + EXPECT_EQ(new_web_contents->GetVisibleURL(), GURL(kVPNPanelURL)); +} + +#endif // BUILDFLAG(ENABLE_BRAVE_VPN) + +#if BUILDFLAG(ENABLE_AI_CHAT) + +IN_PROC_BROWSER_TEST_F(BraveEducationUIBrowserTest, OpenAPIChat) { + NavigateToEducationPage(EducationContentType::kGettingStarted); + + content::WebContentsAddedObserver added_observer; + + PostMessageFromIFrame(R"( + {messageType: 'browser-command', + command: 'open-ai-chat'})"); + + auto* new_web_contents = added_observer.GetWebContents(); + EXPECT_EQ(new_web_contents->GetVisibleURL(), GURL(kChatUIURL)); +} + +#endif // BUILDFLAG(ENABLE_AI_CHAT) + +} // namespace brave_education diff --git a/browser/ui/webui/brave_education/education_page_handler.cc b/browser/ui/webui/brave_education/education_page_handler.cc new file mode 100644 index 000000000000..7564403f2138 --- /dev/null +++ b/browser/ui/webui/brave_education/education_page_handler.cc @@ -0,0 +1,30 @@ +/* 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/ui/webui/brave_education/education_page_handler.h" + +#include + +#include "brave/components/brave_education/common/education_content_urls.h" + +namespace brave_education { + +EducationPageHandler::EducationPageHandler( + mojo::PendingReceiver receiver) + : receiver_(this, std::move(receiver)) {} + +EducationPageHandler::~EducationPageHandler() = default; + +void EducationPageHandler::GetServerUrl(const std::string& webui_url, + GetServerUrlCallback callback) { + if (auto content_type = EducationContentTypeFromBrowserURL(webui_url)) { + auto server_url = GetEducationContentServerURL(*content_type).spec(); + std::move(callback).Run(server_url); + return; + } + std::move(callback).Run(""); +} + +} // namespace brave_education diff --git a/browser/ui/webui/brave_education/education_page_handler.h b/browser/ui/webui/brave_education/education_page_handler.h new file mode 100644 index 000000000000..e300c0af8998 --- /dev/null +++ b/browser/ui/webui/brave_education/education_page_handler.h @@ -0,0 +1,34 @@ +/* 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_UI_WEBUI_BRAVE_EDUCATION_EDUCATION_PAGE_HANDLER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_EDUCATION_PAGE_HANDLER_H_ + +#include + +#include "brave/components/brave_education/common/education_page.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" + +namespace brave_education { + +class EducationPageHandler : public mojom::EducationPageHandler { + public: + explicit EducationPageHandler( + mojo::PendingReceiver receiver); + + ~EducationPageHandler() override; + + // mojom::EducationPageHandler: + void GetServerUrl(const std::string& webui_url, + GetServerUrlCallback callback) override; + + private: + mojo::Receiver receiver_; +}; + +} // namespace brave_education + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_EDUCATION_PAGE_HANDLER_H_ diff --git a/browser/ui/webui/brave_education/getting_started_helper.cc b/browser/ui/webui/brave_education/getting_started_helper.cc new file mode 100644 index 000000000000..0377a5c02fa0 --- /dev/null +++ b/browser/ui/webui/brave_education/getting_started_helper.cc @@ -0,0 +1,136 @@ +/* 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/ui/webui/brave_education/getting_started_helper.h" + +#include + +#include "brave/components/brave_education/common/features.h" +#include "chrome/browser/net/system_network_context_manager.h" +#include "chrome/browser/profiles/profile.h" +#include "content/public/browser/reduce_accept_language_controller_delegate.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/mojom/url_response_head.mojom.h" + +namespace brave_education { + +namespace { + +constexpr int64_t kMaxDownloadBytes = 1024 * 1024; + +constexpr auto kTrafficAnnotation = net::DefineNetworkTrafficAnnotation( + "brave_education_getting_started_helper", + R"( + semantics { + sender: "Brave Education" + description: "Attempts to fetch the content for the Brave Education + getting-started page to ensure that it loads successfully." + trigger: + "Completing the Brave Welcome UX flow." + data: + "No data sent, other than URL of the getting-started page. " + "Data does not contain PII." + destination: BRAVE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: + "None" + } + )"); + +std::optional GetEducationContentType() { + if (base::FeatureList::IsEnabled(features::kShowGettingStartedPage)) { + return EducationContentType::kGettingStarted; + } + return std::nullopt; +} + +} // namespace + +GettingStartedHelper::GettingStartedHelper(Profile* profile) + : profile_(profile) {} + +GettingStartedHelper::~GettingStartedHelper() = default; + +void GettingStartedHelper::GetEducationURL(GetEducationURLCallback callback) { + // Add the callback to our list of pending callbacks. + url_callbacks_.push_back(std::move(callback)); + + // If we are currently waiting on a URL, then exit. All callbacks in the + // pending list will be executed when the website check is completed. + if (url_loader_) { + return; + } + + auto content_type = GetEducationContentType(); + if (!content_type) { + RunCallbacks(std::nullopt); + return; + } + + // Attempt to fetch the content URL in the background. + auto url_loader_factory = profile_->GetURLLoaderFactory(); + auto request = std::make_unique(); + request->url = GetEducationContentServerURL(*content_type); + request->referrer_policy = net::ReferrerPolicy::NO_REFERRER; + request->credentials_mode = network::mojom::CredentialsMode::kOmit; + + if (auto* delegate = profile_->GetReduceAcceptLanguageControllerDelegate()) { + auto languages = delegate->GetUserAcceptLanguages(); + if (!languages.empty()) { + request->headers.SetHeader(request->headers.kAcceptLanguage, + languages.front()); + } + } + + url_loader_ = + network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation); + + url_loader_->DownloadToString( + url_loader_factory.get(), + base::BindOnce(&GettingStartedHelper::OnURLResponse, + base::Unretained(this), *content_type), + kMaxDownloadBytes); +} + +void GettingStartedHelper::OnURLResponse(EducationContentType content_type, + std::optional body) { + if (URLLoadedWithSuccess() && body) { + RunCallbacks(GetEducationContentBrowserURL(content_type)); + } else { + RunCallbacks(std::nullopt); + } +} + +bool GettingStartedHelper::URLLoadedWithSuccess() { + if (!url_loader_) { + return false; + } + if (url_loader_->NetError() != net::OK) { + return false; + } + if (!url_loader_->ResponseInfo()) { + return false; + } + auto& headers = url_loader_->ResponseInfo()->headers; + if (!headers) { + return false; + } + int response_code = headers->response_code(); + return response_code >= 200 && response_code <= 302; +} + +void GettingStartedHelper::RunCallbacks(std::optional webui_url) { + url_loader_.reset(); + std::list callbacks = std::move(url_callbacks_); + for (auto& callback : callbacks) { + std::move(callback).Run(webui_url); + } +} + +} // namespace brave_education diff --git a/browser/ui/webui/brave_education/getting_started_helper.h b/browser/ui/webui/brave_education/getting_started_helper.h new file mode 100644 index 000000000000..acc2242caf91 --- /dev/null +++ b/browser/ui/webui/brave_education/getting_started_helper.h @@ -0,0 +1,59 @@ +/* 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_UI_WEBUI_BRAVE_EDUCATION_GETTING_STARTED_HELPER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_GETTING_STARTED_HELPER_H_ + +#include +#include +#include +#include + +#include "base/functional/callback.h" +#include "base/memory/raw_ptr.h" +#include "brave/components/brave_education/common/education_content_urls.h" +#include "url/gurl.h" + +class Profile; + +namespace network { +class SimpleURLLoader; +} + +namespace brave_education { + +// A helper for determining the "getting started" WebUI URL for a given profile. +class GettingStartedHelper { + public: + explicit GettingStartedHelper(Profile* profile); + ~GettingStartedHelper(); + + GettingStartedHelper(const GettingStartedHelper&) = delete; + GettingStartedHelper& operator=(const GettingStartedHelper&) = delete; + + using GetEducationURLCallback = base::OnceCallback)>; + + // Asynchronously returns a "getting started" education WebUI URL. Returns + // `std::nullopt` if a "getting started" URL is not available (e.g. if + // the network is not available or the web server is not returning a valid + // response). + void GetEducationURL(GetEducationURLCallback callback); + + private: + void OnURLResponse(EducationContentType content_type, + std::optional body); + + bool URLLoadedWithSuccess(); + + void RunCallbacks(std::optional webui_url); + + raw_ptr profile_; + std::unique_ptr url_loader_; + std::list url_callbacks_; +}; + +} // namespace brave_education + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_EDUCATION_GETTING_STARTED_HELPER_H_ diff --git a/browser/ui/webui/brave_education/getting_started_helper_unittest.cc b/browser/ui/webui/brave_education/getting_started_helper_unittest.cc new file mode 100644 index 000000000000..c816cd0623b2 --- /dev/null +++ b/browser/ui/webui/brave_education/getting_started_helper_unittest.cc @@ -0,0 +1,104 @@ +/* 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/ui/webui/brave_education/getting_started_helper.h" + +#include + +#include "base/test/scoped_feature_list.h" +#include "base/test/test_future.h" +#include "brave/components/brave_education/common/education_content_urls.h" +#include "brave/components/brave_education/common/features.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "net/http/http_status_code.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace brave_education { + +class GettingStartedHelperTest : public testing::Test { + protected: + void AddSuccessResponse() { + auto url = + GetEducationContentServerURL(EducationContentType::kGettingStarted); + test_url_loader_factory_.AddResponse(url.spec(), "success"); + } + + void AddErrorResponse() { + auto url = + GetEducationContentServerURL(EducationContentType::kGettingStarted); + test_url_loader_factory_.AddResponse(url.spec(), "error", + net::HTTP_NOT_FOUND); + } + + std::unique_ptr BuildProfile() { + TestingProfile::Builder builder; + builder.SetSharedURLLoaderFactory( + test_url_loader_factory_.GetSafeWeakWrapper()); + return builder.Build(); + } + + content::BrowserTaskEnvironment task_environment_; + network::TestURLLoaderFactory test_url_loader_factory_; +}; + +TEST_F(GettingStartedHelperTest, WithDefaultContentType) { + base::test::ScopedFeatureList feature_list; + feature_list.InitAndEnableFeature(features::kShowGettingStartedPage); + + AddSuccessResponse(); + + auto profile = BuildProfile(); + + base::test::TestFuture> future; + GettingStartedHelper helper(profile.get()); + helper.GetEducationURL(future.GetCallback()); + ASSERT_TRUE(future.Wait()); + + auto result = future.Get<0>(); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result.value(), GetEducationContentBrowserURL( + EducationContentType::kGettingStarted)); +} + +TEST_F(GettingStartedHelperTest, FeatureDisabled) { + base::test::ScopedFeatureList feature_list; + feature_list.InitAndDisableFeature(features::kShowGettingStartedPage); + + AddSuccessResponse(); + + auto profile = BuildProfile(); + + base::test::TestFuture> future; + GettingStartedHelper helper(profile.get()); + helper.GetEducationURL(future.GetCallback()); + ASSERT_TRUE(future.Wait()); + + auto result = future.Get<0>(); + ASSERT_FALSE(result.has_value()); +} + +TEST_F(GettingStartedHelperTest, BadResponse) { + base::test::ScopedFeatureList feature_list; + feature_list.InitAndEnableFeature(features::kShowGettingStartedPage); + + AddErrorResponse(); + + auto profile = BuildProfile(); + + base::test::TestFuture> future; + GettingStartedHelper helper(profile.get()); + helper.GetEducationURL(future.GetCallback()); + ASSERT_TRUE(future.Wait()); + + auto result = future.Get<0>(); + ASSERT_FALSE(result.has_value()); +} + +} // namespace brave_education diff --git a/browser/ui/webui/brave_web_ui_controller_factory.cc b/browser/ui/webui/brave_web_ui_controller_factory.cc index dd7e8c39c815..d05cfaad718b 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -39,6 +39,7 @@ #if !BUILDFLAG(IS_ANDROID) #include "brave/browser/brave_wallet/brave_wallet_context_utils.h" +#include "brave/browser/ui/webui/brave_education/brave_education_ui.h" #include "brave/browser/ui/webui/brave_news_internals/brave_news_internals_ui.h" #include "brave/browser/ui/webui/brave_settings_ui.h" #include "brave/browser/ui/webui/brave_wallet/wallet_page_ui.h" @@ -140,6 +141,8 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { return new BravePrivateNewTabUI(web_ui, url.host()); } return new BraveNewTabUI(web_ui, url.host()); + } else if (host == kBraveGettingStartedHost) { + return new brave_education::BraveEducationUI(web_ui, url.host()); #endif // !BUILDFLAG(IS_ANDROID) #if BUILDFLAG(ENABLE_TOR) } else if (host == kTorInternalsHost) { @@ -192,6 +195,7 @@ WebUIFactoryFunction GetWebUIFactoryFunction(WebUI* web_ui, ((url.host_piece() == kWelcomeHost || url.host_piece() == chrome::kChromeUIWelcomeURL) && !profile->IsGuestSession()) || + url.host_piece() == kBraveGettingStartedHost || #endif // BUILDFLAG(IS_ANDROID) #if BUILDFLAG(ENABLE_TOR) url.host_piece() == kTorInternalsHost || diff --git a/browser/ui/webui/browser_command/brave_browser_command_handler.cc b/browser/ui/webui/browser_command/brave_browser_command_handler.cc new file mode 100644 index 000000000000..c3aef84c57df --- /dev/null +++ b/browser/ui/webui/browser_command/brave_browser_command_handler.cc @@ -0,0 +1,165 @@ +/* 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/ui/webui/browser_command/brave_browser_command_handler.h" + +#include "brave/app/brave_command_ids.h" +#include "brave/browser/brave_rewards/rewards_service_factory.h" +#include "brave/browser/brave_wallet/brave_wallet_service_factory.h" +#include "brave/browser/ui/brave_rewards/rewards_panel_coordinator.h" +#include "brave/browser/ui/browser_commands.h" +#include "brave/components/ai_chat/core/common/buildflags/buildflags.h" +#include "brave/components/brave_vpn/common/buildflags/buildflags.h" +#include "brave/components/constants/webui_url_constants.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/command_updater.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "components/prefs/pref_service.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/web_contents_observer.h" + +#if BUILDFLAG(ENABLE_AI_CHAT) +#include "brave/browser/ai_chat/ai_chat_utils.h" +#endif + +#if BUILDFLAG(ENABLE_BRAVE_VPN) +#include "brave/browser/brave_vpn/vpn_utils.h" +#endif + +namespace { + +using browser_command::mojom::Command; +using browser_command::mojom::CommandHandler; + +bool CanShowWalletOnboarding(Profile* profile) { + return brave_wallet::BraveWalletServiceFactory::GetServiceForContext(profile); +} + +bool CanShowRewardsOnboarding(Profile* profile) { + return brave_rewards::RewardsServiceFactory::GetForProfile(profile); +} + +bool CanShowVPNBubble(Profile* profile) { +#if BUILDFLAG(ENABLE_BRAVE_VPN) + return brave_vpn::IsAllowedForContext(profile); +#else + return false; +#endif +} + +bool CanShowAIChat(Profile* profile) { +#if BUILDFLAG(ENABLE_AI_CHAT) + return ai_chat::IsAllowedForContext(profile); +#else + return false; +#endif +} + +class Delegate : public BraveBrowserCommandHandler::BrowserDelegate { + public: + explicit Delegate(Profile* profile) : profile_(profile) {} + ~Delegate() override = default; + + void OpenURL(const GURL& url, WindowOpenDisposition disposition) override { + NavigateParams params(profile_, url, ui::PAGE_TRANSITION_LINK); + params.disposition = disposition; + Navigate(¶ms); + } + + void OpenRewardsPanel() override { + auto* panel_coordinator = + brave_rewards::RewardsPanelCoordinator::FromBrowser(GetBrowser()); + if (panel_coordinator) { + panel_coordinator->OpenRewardsPanel(); + } + } + + void OpenVPNPanel() override { +#if BUILDFLAG(ENABLE_BRAVE_VPN) + brave::ShowBraveVPNBubble(GetBrowser()); +#endif + } + + void ExecuteBrowserCommand(int command_id) override { + chrome::ExecuteCommand(GetBrowser(), command_id); + } + + private: + Browser* GetBrowser() { return chrome::FindBrowserWithProfile(profile_); } + + raw_ptr profile_; +}; + +} // namespace + +BraveBrowserCommandHandler::BraveBrowserCommandHandler( + mojo::PendingReceiver pending_command_handler, + Profile* profile, + std::vector supported_commands) + : BrowserCommandHandler(std::move(pending_command_handler), + profile, + std::move(supported_commands)), + delegate_(std::make_unique(profile)), + profile_(profile) { + CHECK(profile_); +} + +BraveBrowserCommandHandler::~BraveBrowserCommandHandler() = default; + +void BraveBrowserCommandHandler::CanExecuteCommand( + Command command, + CanExecuteCommandCallback callback) { + if (CanExecute(command)) { + std::move(callback).Run(true); + } else { + BrowserCommandHandler::CanExecuteCommand(command, std::move(callback)); + } +} + +void BraveBrowserCommandHandler::ExecuteCommandWithDisposition( + int id, + WindowOpenDisposition disposition) { + Command command = static_cast(id); + if (!CanExecute(command)) { + return; + } + switch (static_cast(id)) { + case Command::kOpenWalletOnboarding: + delegate_->OpenURL(GURL(kBraveUIWalletURL), disposition); + break; + case Command::kOpenRewardsOnboarding: + delegate_->OpenRewardsPanel(); + break; + case Command::kOpenVPNOnboarding: + delegate_->OpenVPNPanel(); + break; + case Command::kOpenAIChat: + delegate_->ExecuteBrowserCommand(IDC_TOGGLE_AI_CHAT); + break; + default: + break; + } +} + +bool BraveBrowserCommandHandler::CanExecute(Command command) { + if (!GetCommandUpdater()->SupportsCommand(static_cast(command))) { + return false; + } + switch (command) { + case Command::kOpenWalletOnboarding: + return CanShowWalletOnboarding(profile_); + case Command::kOpenRewardsOnboarding: + return CanShowRewardsOnboarding(profile_); + case Command::kOpenVPNOnboarding: + return CanShowVPNBubble(profile_); + case Command::kOpenAIChat: + return CanShowAIChat(profile_); + default: + return false; + } +} diff --git a/browser/ui/webui/browser_command/brave_browser_command_handler.h b/browser/ui/webui/browser_command/brave_browser_command_handler.h new file mode 100644 index 000000000000..9d65a725002a --- /dev/null +++ b/browser/ui/webui/browser_command/brave_browser_command_handler.h @@ -0,0 +1,65 @@ +/* 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_UI_WEBUI_BROWSER_COMMAND_BRAVE_BROWSER_COMMAND_HANDLER_H_ +#define BRAVE_BROWSER_UI_WEBUI_BROWSER_COMMAND_BRAVE_BROWSER_COMMAND_HANDLER_H_ + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "chrome/browser/ui/webui/browser_command/browser_command_handler.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "ui/base/window_open_disposition.h" +#include "url/gurl.h" + +class Profile; + +// A handler for commands that are dispatched from web content (typically +// embedded in an iframe). `BraveBrowserCommandHandler` adds support for +// Brave-specific commands. +class BraveBrowserCommandHandler : public BrowserCommandHandler { + public: + BraveBrowserCommandHandler( + mojo::PendingReceiver + pending_command_handler, + Profile* profile, + std::vector supported_commands); + + ~BraveBrowserCommandHandler() override; + + // mojom::CommandHandler: + void CanExecuteCommand(browser_command::mojom::Command command, + CanExecuteCommandCallback callback) override; + + // CommandUpdaterDelegate: + void ExecuteCommandWithDisposition( + int command_id, + WindowOpenDisposition disposition) override; + + class BrowserDelegate { + public: + virtual ~BrowserDelegate() = default; + + virtual void OpenURL(const GURL& url, + WindowOpenDisposition disposition) = 0; + virtual void OpenRewardsPanel() = 0; + virtual void OpenVPNPanel() = 0; + virtual void ExecuteBrowserCommand(int command_id) = 0; + }; + + void SetBrowserDelegateForTesting(std::unique_ptr delegate) { + delegate_ = std::move(delegate); + } + + private: + bool CanExecute(browser_command::mojom::Command command); + + std::unique_ptr delegate_; + raw_ptr profile_; +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_BROWSER_COMMAND_BRAVE_BROWSER_COMMAND_HANDLER_H_ diff --git a/browser/ui/webui/browser_command/brave_browser_command_handler_unitttest.cc b/browser/ui/webui/browser_command/brave_browser_command_handler_unitttest.cc new file mode 100644 index 000000000000..bf65acf4b1fa --- /dev/null +++ b/browser/ui/webui/browser_command/brave_browser_command_handler_unitttest.cc @@ -0,0 +1,170 @@ +/* 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 +#include +#include + +#include "base/strings/string_number_conversions.h" +#include "base/test/bind.h" +#include "base/test/test_future.h" +#include "brave/browser/ui/webui/browser_command/brave_browser_command_handler.h" +#include "brave/components/ai_chat/core/common/buildflags/buildflags.h" +#include "brave/components/brave_vpn/common/buildflags/buildflags.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/browser_task_environment.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "testing/gtest/include/gtest/gtest.h" + +using browser_command::mojom::ClickInfo; +using browser_command::mojom::Command; +using browser_command::mojom::CommandHandler; + +class TestBrowserDelegate : public BraveBrowserCommandHandler::BrowserDelegate { + public: + using AddActionCallback = base::RepeatingCallback; + + explicit TestBrowserDelegate(AddActionCallback add_action) + : add_action_(std::move(add_action)) {} + + ~TestBrowserDelegate() override = default; + + void OpenURL(const GURL& url, WindowOpenDisposition disposition) override { + add_action_.Run("open-url: " + url.spec()); + } + + void OpenRewardsPanel() override { add_action_.Run("open-rewards-panel"); } + void OpenVPNPanel() override { add_action_.Run("open-vpn-panel"); } + + void ExecuteBrowserCommand(int command_id) override { + add_action_.Run("execute-command: " + base::NumberToString(command_id)); + } + + private: + AddActionCallback add_action_; +}; + +class BraveBrowserCommandHandlerTest : public testing::Test { + protected: + mojo::Remote& CreateHandler( + Profile* profile, + std::vector supported_commands) { + command_handler_ = std::make_unique( + remote_.BindNewPipeAndPassReceiver(), profile, + std::move(supported_commands)); + + command_handler_->SetBrowserDelegateForTesting( + std::make_unique( + base::BindLambdaForTesting([this](std::string action) { + actions_.push_back(std::move(action)); + }))); + + return remote_; + } + + const std::vector& actions() { return actions_; } + + private: + content::BrowserTaskEnvironment task_environment_; + ScopedTestingLocalState local_state{TestingBrowserProcess::GetGlobal()}; + mojo::Remote remote_; + std::unique_ptr command_handler_; + std::vector actions_; +}; + +TEST_F(BraveBrowserCommandHandlerTest, OnlySupportedCommandsAreExecuted) { + TestingProfile::Builder builder; + auto profile = builder.Build(); + auto& handler = CreateHandler(profile.get(), {}); + + base::test::TestFuture future; + handler->ExecuteCommand(Command::kOpenRewardsOnboarding, ClickInfo::New(), + future.GetCallback()); + + ASSERT_FALSE(future.Get<0>()); + ASSERT_TRUE(actions().empty()); +} + +TEST_F(BraveBrowserCommandHandlerTest, BasicCommandsExecuted) { + TestingProfile::Builder builder; + auto profile = builder.Build(); + auto& handler = CreateHandler( + profile.get(), + {Command::kOpenWalletOnboarding, Command::kOpenRewardsOnboarding}); + + base::test::TestFuture future; + + handler->ExecuteCommand(Command::kOpenWalletOnboarding, ClickInfo::New(), + base::DoNothing()); + handler->ExecuteCommand(Command::kOpenRewardsOnboarding, ClickInfo::New(), + future.GetCallback()); + + ASSERT_TRUE(future.Get<0>()); + EXPECT_EQ(actions()[0], "open-url: chrome://wallet/"); + EXPECT_EQ(actions()[1], "open-rewards-panel"); +} + +TEST_F(BraveBrowserCommandHandlerTest, VPNCommandsExecuted) { + TestingProfile::Builder builder; + auto profile = builder.Build(); + auto& handler = CreateHandler(profile.get(), {Command::kOpenVPNOnboarding}); + base::test::TestFuture future; + + handler->ExecuteCommand(Command::kOpenVPNOnboarding, ClickInfo::New(), + future.GetCallback()); + + ASSERT_TRUE(future.Get<0>()); +#if BUILDFLAG(ENABLE_BRAVE_VPN) + EXPECT_EQ(actions()[0], "open-vpn-panel"); +#else + EXPECT_TRUE(actions().empty()); +#endif +} + +TEST_F(BraveBrowserCommandHandlerTest, ChatCommandsExecuted) { + TestingProfile::Builder builder; + auto profile = builder.Build(); + auto& handler = CreateHandler(profile.get(), {Command::kOpenAIChat}); + base::test::TestFuture future; + + handler->ExecuteCommand(Command::kOpenAIChat, ClickInfo::New(), + future.GetCallback()); + + ASSERT_TRUE(future.Get<0>()); +#if BUILDFLAG(ENABLE_AI_CHAT) + EXPECT_EQ(actions()[0], "execute-command: 56007"); +#else + EXPECT_TRUE(actions().empty()); +#endif +} + +TEST_F(BraveBrowserCommandHandlerTest, OffTheRecordProfile) { + TestingProfile::Builder builder; + auto profile = builder.Build(); + auto* otr_profile = profile->GetOffTheRecordProfile( + Profile::OTRProfileID::CreateUniqueForTesting(), + /*create_if_needed=*/true); + + auto& handler = CreateHandler( + otr_profile, + {Command::kOpenWalletOnboarding, Command::kOpenRewardsOnboarding, + Command::kOpenVPNOnboarding, Command::kOpenAIChat}); + + base::test::TestFuture future; + + handler->ExecuteCommand(Command::kOpenWalletOnboarding, ClickInfo::New(), + base::DoNothing()); + handler->ExecuteCommand(Command::kOpenRewardsOnboarding, ClickInfo::New(), + base::DoNothing()); + handler->ExecuteCommand(Command::kOpenVPNOnboarding, ClickInfo::New(), + base::DoNothing()); + handler->ExecuteCommand(Command::kOpenAIChat, ClickInfo::New(), + future.GetCallback()); + + ASSERT_TRUE(future.Get<0>()); + EXPECT_TRUE(actions().empty()); +} diff --git a/browser/ui/webui/welcome_page/welcome_dom_handler.cc b/browser/ui/webui/welcome_page/welcome_dom_handler.cc index 11f13039ef92..88997ed1a092 100644 --- a/browser/ui/webui/welcome_page/welcome_dom_handler.cc +++ b/browser/ui/webui/welcome_page/welcome_dom_handler.cc @@ -61,7 +61,8 @@ bool IsChromeDev(const std::u16string& browser_name) { } // namespace -WelcomeDOMHandler::WelcomeDOMHandler(Profile* profile) : profile_(profile) { +WelcomeDOMHandler::WelcomeDOMHandler(Profile* profile) + : profile_(profile), getting_started_helper_(profile) { base::MakeRefCounted( GURL("https://brave.com")) ->StartCheckIsDefaultAndGetDefaultClientName( @@ -104,6 +105,10 @@ void WelcomeDOMHandler::RegisterMessages() { "enableWebDiscovery", base::BindRepeating(&WelcomeDOMHandler::HandleEnableWebDiscovery, base::Unretained(this))); + web_ui()->RegisterMessageCallback( + "getWelcomeCompleteURL", + base::BindRepeating(&WelcomeDOMHandler::HandleGetWelcomeCompleteURL, + base::Unretained(this))); } void WelcomeDOMHandler::HandleImportNowRequested( @@ -173,6 +178,31 @@ void WelcomeDOMHandler::HandleEnableWebDiscovery( profile_->GetPrefs()->SetBoolean(kWebDiscoveryEnabled, true); } +void WelcomeDOMHandler::HandleGetWelcomeCompleteURL( + const base::Value::List& args) { + CHECK_EQ(1U, args.size()); + const auto& callback_id = args[0].GetString(); + getting_started_helper_.GetEducationURL( + base::BindOnce(&WelcomeDOMHandler::OnEducationURL, + weak_ptr_factory_.GetWeakPtr(), callback_id)); +} + +void WelcomeDOMHandler::OnEducationURL(const std::string& callback_id, + std::optional url) { + if (!IsJavascriptAllowed()) { + return; + } + + // If there is no "getting started" education URL to show the user (perhaps + // because they are not currently active, or because of a network issue), then + // send the user to the new tab page. + if (!url.has_value()) { + url = GURL(chrome::kChromeUINewTabURL); + } + + ResolveJavascriptCallback(base::Value(callback_id), base::Value(url->spec())); +} + void WelcomeDOMHandler::SetLocalStateBooleanEnabled( const std::string& path, const base::Value::List& args) { diff --git a/browser/ui/webui/welcome_page/welcome_dom_handler.h b/browser/ui/webui/welcome_page/welcome_dom_handler.h index ba0c3109fdc4..4e87c778ead8 100644 --- a/browser/ui/webui/welcome_page/welcome_dom_handler.h +++ b/browser/ui/webui/welcome_page/welcome_dom_handler.h @@ -6,13 +6,16 @@ #ifndef BRAVE_BROWSER_UI_WEBUI_WELCOME_PAGE_WELCOME_DOM_HANDLER_H_ #define BRAVE_BROWSER_UI_WEBUI_WELCOME_PAGE_WELCOME_DOM_HANDLER_H_ +#include #include #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" #include "base/values.h" +#include "brave/browser/ui/webui/brave_education/getting_started_helper.h" #include "chrome/browser/shell_integration.h" #include "content/public/browser/web_ui_message_handler.h" +#include "url/gurl.h" class Profile; class Browser; @@ -40,12 +43,16 @@ class WelcomeDOMHandler : public content::WebUIMessageHandler { void HandleOpenSettingsPage(const base::Value::List& args); void HandleSetMetricsReportingEnabled(const base::Value::List& args); void HandleEnableWebDiscovery(const base::Value::List& args); + void HandleGetWelcomeCompleteURL(const base::Value::List& args); + + void OnEducationURL(const std::string& callback_id, std::optional url); Browser* GetBrowser(); size_t last_onboarding_phase_ = 0; std::u16string default_browser_name_; raw_ptr profile_ = nullptr; + brave_education::GettingStartedHelper getting_started_helper_; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/chromium_src/chrome/browser/chrome_browser_interface_binders.cc b/chromium_src/chrome/browser/chrome_browser_interface_binders.cc new file mode 100644 index 000000000000..fe5da80e08e7 --- /dev/null +++ b/chromium_src/chrome/browser/chrome_browser_interface_binders.cc @@ -0,0 +1,24 @@ +/* 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 "build/build_config.h" + +#if !BUILDFLAG(IS_ANDROID) + +#include "brave/browser/ui/webui/brave_education/brave_education_ui.h" +#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h" +#include "chrome/browser/ui/webui/whats_new/whats_new_ui.h" + +// Add `BraveEducationUI` to the registered list of WebUIs that can execute +// browser commands via the `browser_command::mojom::BrowserCommandHandler` +// interface. +#define CommandHandlerFactory \ + CommandHandlerFactory, brave_education::BraveEducationUI + +#endif // !BUILDFLAG(IS_ANDROID) + +#include "src/chrome/browser/chrome_browser_interface_binders.cc" + +#undef CommandHandlerFactory diff --git a/chromium_src/chrome/browser/ui/browser_navigator.cc b/chromium_src/chrome/browser/ui/browser_navigator.cc index d5001e4f99f9..d69c7d7668e2 100644 --- a/chromium_src/chrome/browser/ui/browser_navigator.cc +++ b/chromium_src/chrome/browser/ui/browser_navigator.cc @@ -37,7 +37,7 @@ bool IsURLAllowedInIncognitoBraveImpl( if (host == kRewardsPageHost || host == chrome::kChromeUISyncInternalsHost || host == chrome::kChromeUISyncHost || host == kAdblockHost || - host == kWelcomeHost) { + host == kWelcomeHost || host == kBraveGettingStartedHost) { return false; } diff --git a/chromium_src/chrome/browser/ui/webui/browser_command/browser_command_handler.cc b/chromium_src/chrome/browser/ui/webui/browser_command/browser_command_handler.cc new file mode 100644 index 000000000000..b930e14dd68f --- /dev/null +++ b/chromium_src/chrome/browser/ui/webui/browser_command/browser_command_handler.cc @@ -0,0 +1,17 @@ +/* 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 "base/notreached.h" + +// The switch statement in `BrowserCommandHandler::CanExecuteCommand` does not +// include a default clause. As we are adding values to the enumeration being +// handled, we must add a default clause in order to avoid a compile error. +#define BRAVE_CAN_EXECUTE_COMMAND \ + default: \ + NOTREACHED_NORETURN(); + +#include "src/chrome/browser/ui/webui/browser_command/browser_command_handler.cc" + +#undef BRAVE_CAN_EXECUTE_COMMAND diff --git a/chromium_src/ui/webui/resources/js/browser_command/browser_command.mojom b/chromium_src/ui/webui/resources/js/browser_command/browser_command.mojom new file mode 100644 index 000000000000..38cfb8115656 --- /dev/null +++ b/chromium_src/ui/webui/resources/js/browser_command/browser_command.mojom @@ -0,0 +1,14 @@ +// 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/. + +module browser_command.mojom; + +[BraveExtend] +enum Command { + kOpenRewardsOnboarding = 500, + kOpenWalletOnboarding, + kOpenVPNOnboarding, + kOpenAIChat +}; diff --git a/components/brave_education/common/BUILD.gn b/components/brave_education/common/BUILD.gn new file mode 100644 index 000000000000..523a7c7cbb47 --- /dev/null +++ b/components/brave_education/common/BUILD.gn @@ -0,0 +1,25 @@ +# 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/. + +import("//mojo/public/tools/bindings/mojom.gni") + +source_set("common") { + sources = [ + "education_content_urls.cc", + "education_content_urls.h", + "features.cc", + "features.h", + ] + + deps = [ + "//base", + "//url", + ] +} + +mojom("mojom") { + sources = [ "education_page.mojom" ] + public_deps = [ "//mojo/public/mojom/base" ] +} diff --git a/components/brave_education/common/education_content_urls.cc b/components/brave_education/common/education_content_urls.cc new file mode 100644 index 000000000000..622ffc22e800 --- /dev/null +++ b/components/brave_education/common/education_content_urls.cc @@ -0,0 +1,57 @@ +/* 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/components/brave_education/common/education_content_urls.h" + +#include +#include + +#include "base/containers/fixed_flat_map.h" + +namespace brave_education { + +namespace { + +constexpr std::string_view TypeToBrowserURL(EducationContentType content_type) { + switch (content_type) { + case EducationContentType::kGettingStarted: + return "chrome://getting-started/"; + } +} + +constexpr std::string_view TypeToServerURL(EducationContentType content_type) { + switch (content_type) { + case EducationContentType::kGettingStarted: + return "https://brave.com/get-started/"; + } +} + +consteval auto MapEntry(EducationContentType content_type) { + return std::make_pair(TypeToBrowserURL(content_type), content_type); +} + +constexpr auto kBrowserURLMap = + base::MakeFixedFlatMap({MapEntry(EducationContentType::kGettingStarted)}); + +} // namespace + +GURL GetEducationContentBrowserURL(EducationContentType content_type) { + return GURL(TypeToBrowserURL(content_type)); +} + +GURL GetEducationContentServerURL(EducationContentType content_type) { + return GURL(TypeToServerURL(content_type)); +} + +std::optional EducationContentTypeFromBrowserURL( + std::string_view browser_url) { + auto iter = kBrowserURLMap.find(browser_url); + if (iter == kBrowserURLMap.end()) { + return std::nullopt; + } + return iter->second; +} + +} // namespace brave_education diff --git a/components/brave_education/common/education_content_urls.h b/components/brave_education/common/education_content_urls.h new file mode 100644 index 000000000000..f6d3bc49f0d1 --- /dev/null +++ b/components/brave_education/common/education_content_urls.h @@ -0,0 +1,33 @@ +/* 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_COMPONENTS_BRAVE_EDUCATION_COMMON_EDUCATION_CONTENT_URLS_H_ +#define BRAVE_COMPONENTS_BRAVE_EDUCATION_COMMON_EDUCATION_CONTENT_URLS_H_ + +#include +#include + +#include "url/gurl.h" + +namespace brave_education { + +enum class EducationContentType { kGettingStarted }; + +// Returns a WebUI URL for displaying the specified education content type. +GURL GetEducationContentBrowserURL(EducationContentType content_type); + +// Returns a website URL that will be loaded into an iframe for the specified +// education content type. +GURL GetEducationContentServerURL(EducationContentType content_type); + +// Returns the education content type that corresponds to the specified WebUI +// URL. A WebUI can use this function to determine which content type to show +// for the current URL. +std::optional EducationContentTypeFromBrowserURL( + std::string_view browser_url); + +} // namespace brave_education + +#endif // BRAVE_COMPONENTS_BRAVE_EDUCATION_COMMON_EDUCATION_CONTENT_URLS_H_ diff --git a/components/brave_education/common/education_page.mojom b/components/brave_education/common/education_page.mojom new file mode 100644 index 000000000000..257c9caed93d --- /dev/null +++ b/components/brave_education/common/education_page.mojom @@ -0,0 +1,20 @@ +// 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/. + +module brave_education.mojom; + +// Initializes messaging between the browser and the page. +interface EducationPageHandlerFactory { + CreatePageHandler(pending_receiver receiver); +}; + +// Browser-side handler for requests from the WebUI page. +interface EducationPageHandler { + + // Returns the education content server URL that corresponds to the specified + // WebUI URL. + GetServerUrl(string webui_url) => (string server_url); + +}; diff --git a/components/brave_education/common/features.cc b/components/brave_education/common/features.cc new file mode 100644 index 000000000000..60525453e96e --- /dev/null +++ b/components/brave_education/common/features.cc @@ -0,0 +1,14 @@ +/* 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/components/brave_education/common/features.h" + +namespace brave_education::features { + +BASE_FEATURE(kShowGettingStartedPage, + "BraveShowGettingStartedPage", + base::FEATURE_DISABLED_BY_DEFAULT); + +} // namespace brave_education::features diff --git a/components/brave_education/common/features.h b/components/brave_education/common/features.h new file mode 100644 index 000000000000..db1be10d7602 --- /dev/null +++ b/components/brave_education/common/features.h @@ -0,0 +1,17 @@ +/* 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_COMPONENTS_BRAVE_EDUCATION_COMMON_FEATURES_H_ +#define BRAVE_COMPONENTS_BRAVE_EDUCATION_COMMON_FEATURES_H_ + +#include "base/feature_list.h" + +namespace brave_education::features { + +BASE_DECLARE_FEATURE(kShowGettingStartedPage); + +} // namespace brave_education::features + +#endif // BRAVE_COMPONENTS_BRAVE_EDUCATION_COMMON_FEATURES_H_ diff --git a/components/brave_education/resources/BUILD.gn b/components/brave_education/resources/BUILD.gn new file mode 100644 index 000000000000..44fadef46d33 --- /dev/null +++ b/components/brave_education/resources/BUILD.gn @@ -0,0 +1,23 @@ +# 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/. + +import("//brave/components/common/typescript.gni") +import("//mojo/public/tools/bindings/mojom.gni") + +transpile_web_ui("brave_education") { + entry_points = [ [ + "brave_education", + rebase_path("brave_education.ts"), + ] ] + resource_name = "brave_education" + output_module = true + deps = [ "//brave/components/brave_education/common:mojom_js" ] +} + +pack_web_resources("generated_resources") { + resource_name = "brave_education" + output_dir = "$root_gen_dir/brave/components/brave_education/resources" + deps = [ ":brave_education" ] +} diff --git a/components/brave_education/resources/brave_education.html b/components/brave_education/resources/brave_education.html new file mode 100644 index 000000000000..e75f4975b02e --- /dev/null +++ b/components/brave_education/resources/brave_education.html @@ -0,0 +1,40 @@ + + + + + + $i18n{headerText} + + + + + + + + + diff --git a/components/brave_education/resources/brave_education.ts b/components/brave_education/resources/brave_education.ts new file mode 100644 index 000000000000..49c9da7a1a29 --- /dev/null +++ b/components/brave_education/resources/brave_education.ts @@ -0,0 +1,63 @@ +/* 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/. */ + +import { BrowserCommandProxy } from 'chrome://resources/js/browser_command/browser_command_proxy.js'; +import { EducationPageProxy } from './education_page_proxy'; +import { readCommandMessage } from './message_data_reader' + +function pageReady() { + return new Promise((resolve) => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => resolve()) + } else { + resolve() + } + }) +} + +async function handleMessage(data: any) { + // Read the message provided by the website. If the message is invalid, then + // an error will be thrown and will appear on the console for debugging. + const message = readCommandMessage(data) + + // Check to make sure that the command can be executed for the current + // profile. + const { handler } = BrowserCommandProxy.getInstance() + const { canExecute } = await handler.canExecuteCommand(message.command) + if (!canExecute) { + console.warn(`Command "${ message.command }" cannot be executed`) + return + } + + // Execute the command. + handler.executeCommand(message.command, message.clickInfo); +} + +async function main() { + await pageReady() + + // Get the website URL that corresponds to this webUI URL. If there is no + // match, then do not load anything into the iframe and leave the page empty. + const { handler } = EducationPageProxy.getInstance() + const { serverUrl } = await handler.getServerUrl(location.href) + if (!serverUrl) { + console.warn('No matching server URL') + return + } + + // Load the education content into the iframe. + const frame = document.getElementById('content') as HTMLIFrameElement + frame.src = serverUrl + + // Attach a postMessage handler for messages originating from the iframe. + window.addEventListener('message', (event) => { + if (event.origin === 'https://brave.com' || + event.origin === 'chrome://webui-test') { + handleMessage(event.data) + } + }) +} + +main() diff --git a/components/brave_education/resources/brave_education_resources.grdp b/components/brave_education/resources/brave_education_resources.grdp new file mode 100644 index 000000000000..94b248fe86c9 --- /dev/null +++ b/components/brave_education/resources/brave_education_resources.grdp @@ -0,0 +1,4 @@ + + + + diff --git a/components/brave_education/resources/education_page_proxy.ts b/components/brave_education/resources/education_page_proxy.ts new file mode 100644 index 000000000000..e728c996b9d1 --- /dev/null +++ b/components/brave_education/resources/education_page_proxy.ts @@ -0,0 +1,26 @@ +/* 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/. */ + +import * as mojom from 'gen/brave/components/brave_education/common/education_page.mojom.m.js' + +let instance: EducationPageProxy | null = null + +export class EducationPageProxy { + handler: mojom.EducationPageHandlerRemote + + constructor(handler: mojom.EducationPageHandlerRemote) { + this.handler = handler + } + + static getInstance(): EducationPageProxy { + if (!instance) { + const handler = new mojom.EducationPageHandlerRemote() + mojom.EducationPageHandlerFactory.getRemote().createPageHandler( + handler.$.bindNewPipeAndPassReceiver()) + instance = new EducationPageProxy(handler) + } + return instance + } +} diff --git a/components/brave_education/resources/message_data_reader.test.ts b/components/brave_education/resources/message_data_reader.test.ts new file mode 100644 index 000000000000..35e8aabefe16 --- /dev/null +++ b/components/brave_education/resources/message_data_reader.test.ts @@ -0,0 +1,69 @@ +/* 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/. */ + +import { readCommandMessage } from './message_data_reader'; + +describe('readCommandMessage', () => { + const openRewardsOnboardingCommandId = 500 + + it('throws if arg is invalid', () => { + expect(() => readCommandMessage(undefined)).toThrow() + expect(() => readCommandMessage(null)).toThrow() + expect(() => readCommandMessage('str')).toThrow() + expect(() => readCommandMessage(0)).toThrow() + }) + + it('throws if messageType is invalid', () => { + expect(() => readCommandMessage({})).toThrow() + expect(() => readCommandMessage({ + messageType: 'bowser-command', + command: 'open-rewards-onboarding' + })).toThrow() + }) + + it('throws if command is unrecognized', () => { + expect(() => readCommandMessage({ + messageType: 'browser-command', + command: 'open-shield-settings' + })).toThrow() + }) + + it('returns default click info', () => { + expect(readCommandMessage({ + messageType: 'browser-command', + command: 'open-rewards-onboarding' + })).toEqual({ + command: openRewardsOnboardingCommandId, + clickInfo: { + middleButton: false, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false + } + }) + }) + + it('returns correctly mapped data', () => { + expect(readCommandMessage({ + messageType: 'browser-command', + command: 'open-rewards-onboarding', + middleButton: true, + altKey: true, + ctrlKey: true, + metaKey: true, + shiftKey: true + })).toEqual({ + command: openRewardsOnboardingCommandId, + clickInfo: { + middleButton: true, + altKey: true, + ctrlKey: true, + metaKey: true, + shiftKey: true + } + }) + }) +}) diff --git a/components/brave_education/resources/message_data_reader.ts b/components/brave_education/resources/message_data_reader.ts new file mode 100644 index 000000000000..deee6d56541d --- /dev/null +++ b/components/brave_education/resources/message_data_reader.ts @@ -0,0 +1,52 @@ +/* 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/. */ + +import { Command, ClickInfo } from 'chrome://resources/js/browser_command.mojom-webui.js'; + +const commandNames = new Map(Object.entries({ + 'open-rewards-onboarding': Command.kOpenRewardsOnboarding, + 'open-wallet-onboarding': Command.kOpenWalletOnboarding, + 'open-vpn-onboarding': Command.kOpenVPNOnboarding, + 'open-ai-chat': Command.kOpenAIChat +})) + +interface CommandMessage { + command: Command + clickInfo: ClickInfo +} + +function parseClickInfo (clickInfo: any): ClickInfo { + if (!clickInfo || typeof clickInfo !== 'object') { + clickInfo = {} + } + + return { + middleButton: Boolean(clickInfo.middleButton), + altKey: Boolean(clickInfo.altKey), + ctrlKey: Boolean(clickInfo.ctrlKey), + metaKey: Boolean(clickInfo.metaKey), + shiftKey: Boolean(clickInfo.shiftKey) + } +} + +// Reads and validates a message posted from an education page iframe and +// returns a `CommandMessage` that can be dispatched to the browser. If the +// message is invalid, an error is thrown. +export function readCommandMessage (data: any): CommandMessage { + if (!data || typeof data !== 'object') { + data = {} + } + + if (data.messageType !== 'browser-command') { + throw new Error(`Invalid messageType "${ data.messageType }"`) + } + + const command = commandNames.get(String(data.command || '')) + if (command === undefined) { + throw new Error(`Unrecognized command "${ data.command }"`) + } + + return { command, clickInfo: parseClickInfo(data) } +} diff --git a/components/brave_welcome_ui/api/welcome_browser_proxy.ts b/components/brave_welcome_ui/api/welcome_browser_proxy.ts index 637e71d9c77f..ca1c8d51c44c 100644 --- a/components/brave_welcome_ui/api/welcome_browser_proxy.ts +++ b/components/brave_welcome_ui/api/welcome_browser_proxy.ts @@ -35,6 +35,7 @@ export interface WelcomeBrowserProxy { openSettingsPage: () => void enableWebDiscovery: () => void getDefaultBrowser: () => Promise + getWelcomeCompleteURL: () => Promise } export { DefaultBrowserBrowserProxyImpl, ImportDataBrowserProxyImpl } @@ -64,6 +65,10 @@ export class WelcomeBrowserProxyImpl implements WelcomeBrowserProxy { return sendWithPromise('getDefaultBrowser') } + getWelcomeCompleteURL (): Promise { + return sendWithPromise('getWelcomeCompleteURL') + } + static getInstance (): WelcomeBrowserProxy { return instance || (instance = new WelcomeBrowserProxyImpl()) } diff --git a/components/brave_welcome_ui/components/help-improve/index.tsx b/components/brave_welcome_ui/components/help-improve/index.tsx index 259f036f2414..4e9dac52f7ec 100644 --- a/components/brave_welcome_ui/components/help-improve/index.tsx +++ b/components/brave_welcome_ui/components/help-improve/index.tsx @@ -35,6 +35,13 @@ function InputCheckbox (props: InputCheckboxProps) { function HelpImprove () { const [isMetricsReportingEnabled, setMetricsReportingEnabled] = React.useState(true) const [isP3AEnabled, setP3AEnabled] = React.useState(true) + const [completeURLPromise, setCompleteURLPromise] = + React.useState(Promise.resolve('')) + + React.useEffect(() => { + setCompleteURLPromise( + WelcomeBrowserProxyImpl.getInstance().getWelcomeCompleteURL()) + }, []) const handleP3AChange = () => { setP3AEnabled(!isP3AEnabled) @@ -48,7 +55,9 @@ function HelpImprove () { WelcomeBrowserProxyImpl.getInstance().setP3AEnabled(isP3AEnabled) WelcomeBrowserProxyImpl.getInstance().setMetricsReportingEnabled(isMetricsReportingEnabled) WelcomeBrowserProxyImpl.getInstance().recordP3A(P3APhase.Finished) - window.open('chrome://newtab', '_self') + completeURLPromise.then((url) => { + window.open(url || 'chrome://newtab', '_self', 'noopener') + }) } const handleOpenSettingsPage = () => { diff --git a/components/constants/webui_url_constants.h b/components/constants/webui_url_constants.h index def8886f112c..2e8ec89af4db 100644 --- a/components/constants/webui_url_constants.h +++ b/components/constants/webui_url_constants.h @@ -80,6 +80,7 @@ inline constexpr char kSpeedreaderPanelHost[] = "brave-speedreader.top-chrome"; inline constexpr char kShortcutsURL[] = "chrome://settings/system/shortcuts"; inline constexpr char kChatUIURL[] = "chrome-untrusted://chat/"; inline constexpr char kChatUIHost[] = "chat"; +inline constexpr char kBraveGettingStartedHost[] = "getting-started"; inline constexpr char kRewriterUIURL[] = "chrome://rewriter/"; inline constexpr char kRewriterUIHost[] = "rewriter"; diff --git a/components/resources/BUILD.gn b/components/resources/BUILD.gn index 452e53e516c1..649ead59a70f 100644 --- a/components/resources/BUILD.gn +++ b/components/resources/BUILD.gn @@ -78,6 +78,7 @@ repack("resources") { if (!is_android && !is_ios) { deps += [ + "//brave/components/brave_education/resources:generated_resources", "//brave/components/brave_new_tab_ui:generated_resources", "//brave/components/brave_news/browser/resources:generated_resources", "//brave/components/brave_news/browser/resources:generated_resources", @@ -90,6 +91,7 @@ repack("resources") { ] sources += [ + "$root_gen_dir/brave/components/brave_education/resources/brave_education_generated.pak", "$root_gen_dir/brave/components/brave_new_tab/resources/brave_new_tab_generated.pak", "$root_gen_dir/brave/components/brave_news/browser/resources/brave_news_internals_generated.pak", "$root_gen_dir/brave/components/brave_private_new_tab/resources/page/brave_private_new_tab_generated.pak", diff --git a/components/resources/brave_components_resources.grd b/components/resources/brave_components_resources.grd index 0c749cd8d9b1..5534c830e9ae 100644 --- a/components/resources/brave_components_resources.grd +++ b/components/resources/brave_components_resources.grd @@ -103,6 +103,7 @@ + diff --git a/jest.config.js b/jest.config.js index abef19f6ab2b..19aaf958d153 100644 --- a/jest.config.js +++ b/jest.config.js @@ -113,6 +113,9 @@ module.exports = { 'chrome://resources\\/(.*)': getBuildOutputPathList( 'gen/ui/webui/resources/tsc/$1' ), + '//resources\\/mojo\\/mojo\\/(.*)': getBuildOutputPathList( + 'gen/mojo/$1' + ), 'chrome://interstitials\\/(.*)': getBuildOutputPathList( 'gen/components/security_interstitials/core/$1' ), diff --git a/patches/chrome-browser-ui-webui-browser_command-browser_command_handler.cc.patch b/patches/chrome-browser-ui-webui-browser_command-browser_command_handler.cc.patch new file mode 100644 index 000000000000..b97dcfad15a9 --- /dev/null +++ b/patches/chrome-browser-ui-webui-browser_command-browser_command_handler.cc.patch @@ -0,0 +1,12 @@ +diff --git a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc +index 5c0a4196548de2c85c2a2db7f88ef9a696d633f6..878b704fff6a36b62fccfe6d993cfdf7e7277987 100644 +--- a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc ++++ b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc +@@ -128,6 +128,7 @@ void BrowserCommandHandler::CanExecuteCommand( + case Command::kOpenPaymentsSettings: + can_execute = true; + break; ++ BRAVE_CAN_EXECUTE_COMMAND + } + std::move(callback).Run(can_execute); + } diff --git a/resources/resource_ids.spec b/resources/resource_ids.spec index 8c0c471b2330..e8d307eef4b3 100644 --- a/resources/resource_ids.spec +++ b/resources/resource_ids.spec @@ -221,4 +221,8 @@ "META": {"sizes": {"includes": [50]}}, "includes": [64430], }, + "<(SHARED_INTERMEDIATE_DIR)/brave/web-ui-brave_education/brave_education.grd": { + "META": {"sizes": {"includes": [10]}}, + "includes": [64480], + }, } diff --git a/test/BUILD.gn b/test/BUILD.gn index c3ea3f2e27db..4f4e474a353e 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -181,6 +181,7 @@ test("brave_unit_tests") { "//brave/chromium_src/components/flags_ui:unit_tests", "//brave/chromium_src/components/privacy_sandbox:unit_tests", "//brave/chromium_src/net/base:unit_tests", + "//brave/components/ai_chat/core/common/buildflags", "//brave/components/api_request_helper:api_request_helper_unit_tests", "//brave/components/brave_adaptive_captcha/test:brave_adaptive_captcha_unit_tests", "//brave/components/brave_ads/browser:test_support", @@ -407,7 +408,9 @@ test("brave_unit_tests") { "//brave/browser/ui/views/bookmarks/brave_bookmark_context_menu_unittest.cc", "//brave/browser/ui/views/frame/brave_contents_layout_manager_unittest.cc", "//brave/browser/ui/views/wallet_bubble_focus_observer_unittest.cc", + "//brave/browser/ui/webui/brave_education/getting_started_helper_unittest.cc", "//brave/browser/ui/webui/brave_wallet/wallet_common_ui_unittest.cc", + "//brave/browser/ui/webui/browser_command/brave_browser_command_handler_unitttest.cc", "//brave/browser/ui/webui/settings/brave_wallet_handler_unittest.cc", "//brave/chromium_src/chrome/browser/devtools/url_constants_unittest.cc", "//brave/chromium_src/chrome/browser/profiles/profile_avatar_icon_util_unittest.cc", @@ -445,8 +448,10 @@ test("brave_unit_tests") { "//brave/browser/ui/views/tabs:unit_tests", "//brave/browser/ui/webui/settings:unittests", "//brave/browser/ui/whats_new:unit_test", + "//brave/components/brave_education/common", "//brave/components/brave_news/common:unit_tests", "//brave/components/brave_shields/core/common:mojom", + "//brave/components/brave_vpn/common/buildflags", "//brave/components/brave_wallet/browser:pref_names", "//brave/components/brave_wallet/browser:utils", "//brave/components/brave_wallet/common:test_support", @@ -1146,6 +1151,7 @@ test("brave_browser_tests") { "//brave/browser/renderer_context_menu/test/render_view_context_menu_browsertest.cc", "//brave/browser/ssl/certificate_transparency_browsertest.cc", "//brave/browser/ui/views/toolbar/wallet_button_notification_source_browsertest.cc", + "//brave/browser/ui/webui/brave_education/brave_education_ui_browsertest.cc", ] deps += [ "//brave/app/theme:brave_theme_resources_grit", @@ -1156,6 +1162,7 @@ test("brave_browser_tests") { "//brave/browser/ui/geolocation:browser_tests", "//brave/browser/ui/whats_new:browser_test", "//brave/browser/url_sanitizer:browser_tests", + "//brave/components/brave_education/common", "//brave/components/brave_wallet/browser:test_support", "//chrome/browser/apps/app_service:app_service", "//chrome/browser/apps/app_service:constants",