diff --git a/browser/brave_app_controller_mac.mm b/browser/brave_app_controller_mac.mm index b47f6c89627..8d1c161e23c 100644 --- a/browser/brave_app_controller_mac.mm +++ b/browser/brave_app_controller_mac.mm @@ -12,6 +12,7 @@ #include "brave/app/brave_command_ids.h" #include "brave/browser/brave_browser_features.h" #include "brave/browser/ui/browser_commands.h" +#include "brave/components/brave_origin/buildflags/buildflags.h" #include "brave/components/tor/buildflags/buildflags.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/browser_process.h" @@ -23,6 +24,10 @@ #include "components/prefs/pref_member.h" #include "ui/base/l10n/l10n_util_mac.h" +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) +#include "brave/browser/ui/views/brave_origin/brave_origin_startup_view.h" +#endif + #if BUILDFLAG(ENABLE_TOR) #include "brave/browser/tor/tor_profile_service_factory.h" #include "brave/components/tor/pref_names.h" @@ -73,8 +78,9 @@ class TorPrefObserver : public BooleanPrefMember { } // namespace @interface AppController (Brave) -// Expose method in chrome/..app_controller_mac.mm +// Expose methods in chrome/..app_controller_mac.mm - (BOOL)canOpenNewBrowser; +- (void)commandDispatchUsingKeyModifiers:(id)sender; @end @interface BraveAppController () { @@ -91,6 +97,43 @@ class TorPrefObserver : public BooleanPrefMember { @end @implementation BraveAppController + +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) +- (BOOL)canOpenNewBrowser { + if (BraveOriginStartupView::IsShowing()) { + return NO; + } + return [super canOpenNewBrowser]; +} + +- (void)commandDispatch:(id)sender { + if (BraveOriginStartupView::IsShowing()) { + // Allow Quit through so the user can exit from the startup dialog. + if ([sender tag] == IDC_EXIT) { + BraveOriginStartupView::Hide(); + } + return; + } + [super commandDispatch:sender]; +} + +- (void)commandDispatchUsingKeyModifiers:(id)sender { + if (BraveOriginStartupView::IsShowing()) { + return; + } + [super commandDispatchUsingKeyModifiers:sender]; +} + +- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication + hasVisibleWindows:(BOOL)hasVisibleWindows { + if (BraveOriginStartupView::IsShowing()) { + return NO; + } + return [super applicationShouldHandleReopen:theApplication + hasVisibleWindows:hasVisibleWindows]; +} +#endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + - (void)mainMenuCreated { [super mainMenuCreated]; diff --git a/browser/resources/brave_origin_startup/brave_origin_startup.html b/browser/resources/brave_origin_startup/brave_origin_startup.html index 4091abef1c8..edc476f72ff 100644 --- a/browser/resources/brave_origin_startup/brave_origin_startup.html +++ b/browser/resources/brave_origin_startup/brave_origin_startup.html @@ -2,8 +2,8 @@ - $i18n{braveOriginStartupTitle} - + $i18n{BRAVE_ORIGIN_STARTUP_TITLE} + diff --git a/browser/sources.gni b/browser/sources.gni index 1d2053a8c44..8b6a358a6c2 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -387,8 +387,10 @@ if (enable_brave_education) { } if (is_brave_origin_branded) { - brave_chrome_browser_deps += - [ "//brave/browser/ui/webui/brave_origin_startup" ] + brave_chrome_browser_deps += [ + "//brave/browser/ui/views/brave_origin", + "//brave/browser/ui/webui/brave_origin_startup", + ] } if (enable_ai_chat) { diff --git a/browser/ui/views/brave_origin/BUILD.gn b/browser/ui/views/brave_origin/BUILD.gn index a076c988e17..f37be2ccee9 100644 --- a/browser/ui/views/brave_origin/BUILD.gn +++ b/browser/ui/views/brave_origin/BUILD.gn @@ -33,3 +33,45 @@ source_set("brave_origin") { "//url", ] } + +source_set("browser_tests") { + testonly = true + + defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ] + + sources = [ "brave_origin_startup_view_browsertest.cc" ] + + deps = [ + ":brave_origin", + "//base", + "//brave/components/brave_origin:pref_names", + "//brave/components/skus/browser", + "//chrome/browser", + "//chrome/browser:browser_process", + "//chrome/browser/profiles:profile", + "//chrome/browser/ui", + "//chrome/browser/ui/startup", + "//chrome/test:test_support_ui", + "//components/prefs", + "//content/public/browser", + "//content/test:test_support", + "//testing/gtest", + "//ui/views", + "//url", + ] +} + +source_set("unit_tests") { + testonly = true + + sources = [ "brave_origin_startup_view_unittest.cc" ] + + deps = [ + ":brave_origin", + "//base", + "//brave/components/brave_origin:pref_names", + "//brave/components/skus/browser", + "//components/prefs:test_support", + "//testing/gtest", + ] +} diff --git a/browser/ui/views/brave_origin/brave_origin_startup_view.cc b/browser/ui/views/brave_origin/brave_origin_startup_view.cc index e904d0a6d9b..a75e950348d 100644 --- a/browser/ui/views/brave_origin/brave_origin_startup_view.cc +++ b/browser/ui/views/brave_origin/brave_origin_startup_view.cc @@ -7,6 +7,7 @@ #include +#include "base/command_line.h" #include "base/functional/bind.h" #include "base/json/json_reader.h" #include "base/strings/strcat.h" @@ -25,6 +26,7 @@ #include "content/public/browser/render_frame_host.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui.h" +#include "content/public/common/content_switches.h" #include "ui/gfx/geometry/size.h" #include "ui/views/controls/webview/webview.h" #include "ui/views/widget/widget.h" @@ -34,9 +36,10 @@ namespace { constexpr int kDialogWidth = 500; -constexpr int kDialogHeight = 600; +constexpr int kDialogHeight = 450; BraveOriginStartupView* g_startup_view = nullptr; +std::optional g_should_show_dialog_override; bool HasOriginSkuCredentials(PrefService* local_state) { const auto& skus_state = local_state->GetDict(skus::prefs::kSkusState); @@ -65,23 +68,33 @@ bool HasOriginSkuCredentials(PrefService* local_state) { // static bool BraveOriginStartupView::ShouldShowDialog(PrefService* local_state) { + if (g_should_show_dialog_override.has_value()) { + return *g_should_show_dialog_override; + } + + // Skip the dialog when running under test infrastructure. + if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType)) { + return false; + } + return !local_state->GetBoolean(brave_origin::kOriginPurchaseValidated) || !HasOriginSkuCredentials(local_state); } // static -void BraveOriginStartupView::Show( - base::OnceClosure on_complete, - OpenExternalCallback open_external, - base::RepeatingClosure attempt_exit, - CreateProfilesCallback create_system_profile, - CreateProfilesCallback create_default_profile) { +void BraveOriginStartupView::SetShouldShowDialogForTesting( // IN-TEST + std::optional override) { + g_should_show_dialog_override = override; +} + +// static +void BraveOriginStartupView::Show(base::OnceClosure on_complete, + std::unique_ptr delegate) { if (g_startup_view) { return; } - g_startup_view = new BraveOriginStartupView( - std::move(on_complete), std::move(open_external), std::move(attempt_exit), - std::move(create_system_profile), std::move(create_default_profile)); + g_startup_view = + new BraveOriginStartupView(std::move(on_complete), std::move(delegate)); g_startup_view->Display(); } @@ -97,20 +110,21 @@ bool BraveOriginStartupView::IsShowing() { return g_startup_view != nullptr; } +// static +void BraveOriginStartupView::ValidateForTesting() { // IN-TEST + if (g_startup_view && g_startup_view->GetWidget()) { + g_startup_view->CloseAndProceed(); + } +} + BraveOriginStartupView::BraveOriginStartupView( base::OnceClosure on_complete, - OpenExternalCallback open_external, - base::RepeatingClosure attempt_exit, - CreateProfilesCallback create_system_profile, - CreateProfilesCallback create_default_profile) + std::unique_ptr delegate) : keep_alive_( std::make_unique(KeepAliveOrigin::USER_MANAGER_VIEW, KeepAliveRestartOption::DISABLED)), - on_complete_(std::move(on_complete)), - open_external_(std::move(open_external)), - attempt_exit_(std::move(attempt_exit)), - create_system_profile_(std::move(create_system_profile)), - create_default_profile_(std::move(create_default_profile)) { + delegate_(std::move(delegate)), + on_complete_(std::move(on_complete)) { SetHasWindowSizeControls(false); SetTitle(IDS_BRAVE_ORIGIN_STARTUP_TITLE); } @@ -124,10 +138,10 @@ BraveOriginStartupView::~BraveOriginStartupView() { void BraveOriginStartupView::Display() { // Load both the system profile (for WebUI) and the default user profile // (for SKU service, which requires a regular profile). - create_system_profile_.Run( + delegate_->CreateSystemProfile( base::BindOnce(&BraveOriginStartupView::OnSystemProfileCreated, weak_ptr_factory_.GetWeakPtr())); - create_default_profile_.Run( + delegate_->CreateDefaultProfile( base::BindOnce(&BraveOriginStartupView::OnDefaultProfileCreated, weak_ptr_factory_.GetWeakPtr())); } @@ -239,8 +253,8 @@ void BraveOriginStartupView::OpenBuyWindow() { "account", brave_domains::ServicesEnvironment::STAGING), "/?intent=checkout&product=origin"}); GURL gurl(url); - if (gurl.is_valid() && open_external_) { - open_external_.Run(gurl); + if (gurl.is_valid() && delegate_) { + delegate_->OpenExternal(gurl); } } @@ -303,8 +317,14 @@ void BraveOriginStartupView::WindowClosing() { std::unique_ptr) {}, std::move(keep_alive_), std::move(profile_keep_alive_))); - if (!validated_) { - task_runner->PostTask(FROM_HERE, attempt_exit_); + if (!validated_ && delegate_) { + // Move delegate_ into the closure so it outlives `this` (which is deleted + // in WidgetIsZombie right after WindowClosing returns). + task_runner->PostTask( + FROM_HERE, + base::BindOnce( + [](std::unique_ptr delegate) { delegate->AttemptExit(); }, + std::move(delegate_))); } } diff --git a/browser/ui/views/brave_origin/brave_origin_startup_view.h b/browser/ui/views/brave_origin/brave_origin_startup_view.h index 1b7d7358319..49dd701e2bd 100644 --- a/browser/ui/views/brave_origin/brave_origin_startup_view.h +++ b/browser/ui/views/brave_origin/brave_origin_startup_view.h @@ -11,6 +11,7 @@ static_assert(BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED)); #include +#include #include "base/functional/callback.h" #include "base/memory/raw_ptr.h" @@ -42,39 +43,56 @@ class BraveOriginStartupView : public views::WidgetDelegate, public content::WebContentsDelegate, public content::WebContentsObserver { public: + using ProfileCallback = base::OnceCallback; + + // Delegate interface for external dependencies. Implemented by the browser + // startup code and easily mockable in tests. + class Delegate { + public: + virtual ~Delegate() = default; + + // Opens |url| in the user's default external browser. + virtual void OpenExternal(const GURL& url) = 0; + + // Exits the browser process. + virtual void AttemptExit() = 0; + + // Asynchronously creates the system profile and passes it to |callback|. + virtual void CreateSystemProfile(ProfileCallback callback) = 0; + + // Asynchronously creates the default user profile and passes it to + // |callback|. + virtual void CreateDefaultProfile(ProfileCallback callback) = 0; + }; + // Returns true if the startup dialog should be shown (purchase not yet - // validated or SKU credentials missing). + // validated or SKU credentials missing). Also returns false when running + // under test infrastructure (--test-type flag). static bool ShouldShowDialog(PrefService* local_state); + // Override ShouldShowDialog() result for testing. Pass std::nullopt to + // remove the override and restore normal behavior. + static void SetShouldShowDialogForTesting(std::optional override); + // Shows the startup dialog. |on_complete| is called when the user has // been validated and the dialog closes, to continue the startup flow. - // |open_external| is called to open URLs in the default browser. - // |attempt_exit| is called to terminate the browser when the dialog is - // closed without validation. - using OpenExternalCallback = base::RepeatingCallback; - using ProfileCallback = base::OnceCallback; - using CreateProfilesCallback = base::RepeatingCallback; - - // |on_complete| runs when the user validates; |open_external| opens URLs - // externally; |attempt_exit| exits the browser; |create_system_profile| and - // |create_default_profile| load the required profiles asynchronously. + // |delegate| provides external operations (opening URLs, exiting, creating + // profiles). static void Show(base::OnceClosure on_complete, - OpenExternalCallback open_external, - base::RepeatingClosure attempt_exit, - CreateProfilesCallback create_system_profile, - CreateProfilesCallback create_default_profile); + std::unique_ptr delegate); static void Hide(); static bool IsShowing(); + // Simulates a successful validation for testing. Calls CloseAndProceed() on + // the current instance, triggering the on_complete callback. + static void ValidateForTesting(); // IN-TEST + BraveOriginStartupView(const BraveOriginStartupView&) = delete; BraveOriginStartupView& operator=(const BraveOriginStartupView&) = delete; private: BraveOriginStartupView(base::OnceClosure on_complete, - OpenExternalCallback open_external, - base::RepeatingClosure attempt_exit, - CreateProfilesCallback create_system_profile, - CreateProfilesCallback create_default_profile); + std::unique_ptr delegate); ~BraveOriginStartupView() override; void Display(); @@ -120,15 +138,12 @@ class BraveOriginStartupView : public views::WidgetDelegate, std::unique_ptr keep_alive_; std::unique_ptr profile_keep_alive_; std::unique_ptr web_view_; + std::unique_ptr delegate_; int profiles_loaded_count_ = 0; bool validated_ = false; base::OnceClosure on_complete_; - OpenExternalCallback open_external_; - base::RepeatingClosure attempt_exit_; - CreateProfilesCallback create_system_profile_; - CreateProfilesCallback create_default_profile_; views::UnhandledKeyboardEventHandler unhandled_keyboard_event_handler_; diff --git a/browser/ui/views/brave_origin/brave_origin_startup_view_browsertest.cc b/browser/ui/views/brave_origin/brave_origin_startup_view_browsertest.cc new file mode 100644 index 00000000000..6e3ef40abf2 --- /dev/null +++ b/browser/ui/views/brave_origin/brave_origin_startup_view_browsertest.cc @@ -0,0 +1,279 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/views/brave_origin/brave_origin_startup_view.h" + +#include + +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/test/bind.h" +#include "base/test/run_until.h" +#include "brave/components/brave_origin/pref_names.h" +#include "brave/components/skus/browser/pref_names.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/startup/startup_browser_creator.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "components/prefs/pref_service.h" +#include "content/public/test/browser_test.h" + +namespace { + +// Test delegate that uses real profile infrastructure but records calls to +// OpenExternal and AttemptExit via shared flags (the delegate is moved into +// an async closure during WindowClosing, so raw pointers to it can dangle). +class TestDelegate : public BraveOriginStartupView::Delegate { + public: + explicit TestDelegate(bool* attempt_exit_flag) + : attempt_exit_flag_(attempt_exit_flag) {} + + void OpenExternal(const GURL& url) override {} + + void AttemptExit() override { + if (attempt_exit_flag_) { + *attempt_exit_flag_ = true; + } + } + + void CreateSystemProfile( + BraveOriginStartupView::ProfileCallback callback) override { + g_browser_process->profile_manager()->CreateProfileAsync( + ProfileManager::GetSystemProfilePath(), std::move(callback)); + } + + void CreateDefaultProfile( + BraveOriginStartupView::ProfileCallback callback) override { + g_browser_process->profile_manager()->CreateProfileAsync( + g_browser_process->profile_manager()->GetLastUsedProfileDir(), + std::move(callback)); + } + + private: + raw_ptr attempt_exit_flag_ = nullptr; +}; + +} // namespace + +class BraveOriginStartupViewBrowserTest : public InProcessBrowserTest { + public: + void SetUp() override { + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + InProcessBrowserTest::SetUp(); + } + + void TearDown() override { + InProcessBrowserTest::TearDown(); + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + } + + void TearDownOnMainThread() override { + if (BraveOriginStartupView::IsShowing()) { + HideAndWaitForClose(); + } + InProcessBrowserTest::TearDownOnMainThread(); + } + + // Hide() is a no-op until the widget exists (async profile creation may + // still be in progress). Retry Hide() inside RunUntil so that pending tasks + // are processed between attempts, and the widget is closed as soon as it + // becomes available. + void HideAndWaitForClose() { + ASSERT_TRUE(base::test::RunUntil([] { + BraveOriginStartupView::Hide(); + return !BraveOriginStartupView::IsShowing(); + })); + } +}; + +// Verifies the dialog can be shown and IsShowing() reflects its state. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, ShowAndHide) { + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + + bool on_complete_called = false; + BraveOriginStartupView::Show( + base::BindLambdaForTesting([&]() { on_complete_called = true; }), + std::make_unique(nullptr)); + + EXPECT_TRUE(BraveOriginStartupView::IsShowing()); + + HideAndWaitForClose(); + + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + // Closing without validation should not call on_complete. + EXPECT_FALSE(on_complete_called); +} + +// Verifies that calling Show() twice does not create a second dialog. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, ShowTwiceIsNoOp) { + BraveOriginStartupView::Show(base::DoNothing(), + std::make_unique(nullptr)); + EXPECT_TRUE(BraveOriginStartupView::IsShowing()); + + // Second Show should be a no-op. + BraveOriginStartupView::Show(base::DoNothing(), + std::make_unique(nullptr)); + EXPECT_TRUE(BraveOriginStartupView::IsShowing()); + + HideAndWaitForClose(); + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); +} + +// Verifies that Hide() is safe to call when dialog is not showing. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + HideWhenNotShowingIsNoOp) { + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + BraveOriginStartupView::Hide(); // Should not crash. + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); +} + +// Verifies that the testing override controls ShouldShowDialog(). +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + ShouldShowDialogTestingOverride) { + PrefService* local_state = g_browser_process->local_state(); + + // Default in tests: --test-type flag makes it return false. + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(local_state)); + + // Override to force show. + BraveOriginStartupView::SetShouldShowDialogForTesting(true); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(local_state)); + + // Override to force hide. + BraveOriginStartupView::SetShouldShowDialogForTesting(false); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(local_state)); + + // Reset override. + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(local_state)); +} + +// Verifies closing without validation calls AttemptExit on the delegate. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + CloseWithoutValidationCallsAttemptExit) { + bool attempt_exit_called = false; + + BraveOriginStartupView::Show( + base::DoNothing(), std::make_unique(&attempt_exit_called)); + EXPECT_FALSE(attempt_exit_called); + + HideAndWaitForClose(); + + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + EXPECT_TRUE(attempt_exit_called); +} + +// Verifies that when the purchase is validated and SKU credentials exist, +// the dialog does not appear and the browser window opens normally. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + PaidUserSkipsDialogAndGetsBrowser) { + PrefService* local_state = g_browser_process->local_state(); + + // Simulate a paid user: set validated pref and add SKU credentials. + local_state->SetBoolean(brave_origin::kOriginPurchaseValidated, true); + base::DictValue skus_state; + skus_state.Set("production", R"({ + "credentials": { + "items": { + "origin": "some-credential-value" + } + } + })"); + local_state->SetDict(skus::prefs::kSkusState, std::move(skus_state)); + + // With valid purchase state, ShouldShowDialog returns false (even without + // the --test-type override). + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(local_state)); + + // The dialog is not showing, and we have a browser window (provided by the + // test framework via the normal startup path). + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + EXPECT_TRUE(browser() != nullptr); +} + +// -------------------------------------------------------------------------- +// Integration tests for the StartupBrowserCreator::Start override. +// These call Start() directly to exercise the real startup interception logic +// in chromium_src/chrome/browser/ui/startup/startup_browser_creator.cc. +// -------------------------------------------------------------------------- + +// Verifies that StartupBrowserCreator::Start() shows the dialog when +// ShouldShowDialog is true, and that closing without validation does not crash. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + StartOverrideShowsDialogOnClose) { + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + + BraveOriginStartupView::SetShouldShowDialogForTesting(true); + + // Call the real Start() override. It should intercept and show the dialog. + StartupBrowserCreator browser_creator; + StartupProfileInfo profile_info{browser()->profile(), + StartupProfileMode::kBrowserWindow}; + bool result = browser_creator.Start(*base::CommandLine::ForCurrentProcess(), + base::FilePath(), profile_info, + {browser()->profile()}); + EXPECT_TRUE(result); + EXPECT_TRUE(BraveOriginStartupView::IsShowing()); + + // Close without validation — should not crash. + HideAndWaitForClose(); + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); +} + +// Verifies that StartupBrowserCreator::Start() shows the dialog and that +// simulating validation fires the on_complete callback (which calls +// Start_ChromiumImpl), opening a new browser window without crashing. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + StartOverrideShowsDialogAndProceeds) { + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + + size_t browser_count_before = chrome::GetTotalBrowserCount(); + + BraveOriginStartupView::SetShouldShowDialogForTesting(true); + + StartupBrowserCreator browser_creator; + StartupProfileInfo profile_info{browser()->profile(), + StartupProfileMode::kBrowserWindow}; + bool result = browser_creator.Start(*base::CommandLine::ForCurrentProcess(), + base::FilePath(), profile_info, + {browser()->profile()}); + EXPECT_TRUE(result); + EXPECT_TRUE(BraveOriginStartupView::IsShowing()); + + // ValidateForTesting() is a no-op until the widget is ready (async profile + // creation). Retry inside RunUntil so pending tasks are processed between + // attempts. + ASSERT_TRUE(base::test::RunUntil([] { + BraveOriginStartupView::ValidateForTesting(); + return !BraveOriginStartupView::IsShowing(); + })); + + // A new browser window should have been created by Start_ChromiumImpl. + EXPECT_GT(chrome::GetTotalBrowserCount(), browser_count_before); +} + +// Verifies that StartupBrowserCreator::Start() proceeds normally (no dialog) +// when ShouldShowDialog is false. +IN_PROC_BROWSER_TEST_F(BraveOriginStartupViewBrowserTest, + StartOverrideSkipsDialogWhenNotNeeded) { + BraveOriginStartupView::SetShouldShowDialogForTesting(false); + + size_t browser_count_before = chrome::GetTotalBrowserCount(); + + StartupBrowserCreator browser_creator; + StartupProfileInfo profile_info{browser()->profile(), + StartupProfileMode::kBrowserWindow}; + bool result = browser_creator.Start(*base::CommandLine::ForCurrentProcess(), + base::FilePath(), profile_info, + {browser()->profile()}); + EXPECT_TRUE(result); + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); + + // Start_ChromiumImpl should have run directly, creating a new browser. + EXPECT_GT(chrome::GetTotalBrowserCount(), browser_count_before); +} diff --git a/browser/ui/views/brave_origin/brave_origin_startup_view_unittest.cc b/browser/ui/views/brave_origin/brave_origin_startup_view_unittest.cc new file mode 100644 index 00000000000..005cb7e1f75 --- /dev/null +++ b/browser/ui/views/brave_origin/brave_origin_startup_view_unittest.cc @@ -0,0 +1,167 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/views/brave_origin/brave_origin_startup_view.h" + +#include +#include +#include +#include + +#include "base/values.h" +#include "brave/components/brave_origin/pref_names.h" +#include "brave/components/skus/browser/pref_names.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gtest/include/gtest/gtest.h" + +class BraveOriginStartupViewTest : public testing::Test { + public: + void SetUp() override { + local_state_.registry()->RegisterBooleanPref( + brave_origin::kOriginPurchaseValidated, false); + local_state_.registry()->RegisterDictionaryPref(skus::prefs::kSkusState); + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + } + + void TearDown() override { + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + } + + // Builds a JSON string representing an SKU state entry with credentials. + // If |has_items| is true, the credentials dict contains a non-empty "items" + // dict; otherwise it is empty. + std::string BuildSkuStateJson(bool has_items) { + if (has_items) { + return R"({ + "credentials": { + "items": { + "origin": "some-credential-value" + } + } + })"; + } + return R"({ + "credentials": { + "items": {} + } + })"; + } + + void SetSkuCredentials(const std::string& env_key, const std::string& json) { + base::DictValue skus_state; + skus_state.Set(env_key, json); + local_state_.SetDict(skus::prefs::kSkusState, std::move(skus_state)); + } + + protected: + TestingPrefServiceSimple local_state_; +}; + +// --- ShouldShowDialog tests --- + +TEST_F(BraveOriginStartupViewTest, + ShouldShowDialogWhenPurchaseNotValidatedAndNoCredentials) { + // Default state: pref is false, no SKU credentials -> should show. + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldNotShowDialogWhenPurchaseValidatedAndHasCredentials) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", BuildSkuStateJson(/*has_items=*/true)); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldShowDialogWhenPurchaseValidatedButNoCredentials) { + // Pref validated but SKU credentials missing -> should show. + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldShowDialogWhenNotValidatedButHasCredentials) { + // Has credentials but pref not set -> should show. + SetSkuCredentials("production", BuildSkuStateJson(/*has_items=*/true)); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldNotShowDialogWhenValidatedAndMultipleEnvCredentials) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + base::DictValue skus_state; + skus_state.Set("staging", BuildSkuStateJson(/*has_items=*/false)); + skus_state.Set("production", BuildSkuStateJson(/*has_items=*/true)); + local_state_.SetDict(skus::prefs::kSkusState, std::move(skus_state)); + + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldShowDialogWhenCredentialsHaveEmptyItems) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", BuildSkuStateJson(/*has_items=*/false)); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, ShouldShowDialogWhenSkuStateHasInvalidJson) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", "not-valid-json{{{"); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, + ShouldShowDialogWhenSkuStateHasNonStringValue) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + base::DictValue skus_state; + skus_state.Set("production", 42); // Not a string. + local_state_.SetDict(skus::prefs::kSkusState, std::move(skus_state)); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, ShouldShowDialogWhenCredentialsDictMissing) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", R"({"some_other_key": "value"})"); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, ShouldShowDialogWhenItemsDictMissing) { + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", R"({"credentials": {"not_items": "value"}})"); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +// --- SetShouldShowDialogForTesting tests --- + +TEST_F(BraveOriginStartupViewTest, TestOverrideForceShow) { + // Even with valid state, the override forces show. + local_state_.SetBoolean(brave_origin::kOriginPurchaseValidated, true); + SetSkuCredentials("production", BuildSkuStateJson(/*has_items=*/true)); + + BraveOriginStartupView::SetShouldShowDialogForTesting(true); + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, TestOverrideForceHide) { + // Even with invalid state, the override forces hide. + BraveOriginStartupView::SetShouldShowDialogForTesting(false); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +TEST_F(BraveOriginStartupViewTest, TestOverrideResetsToNormal) { + BraveOriginStartupView::SetShouldShowDialogForTesting(false); + EXPECT_FALSE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); + + BraveOriginStartupView::SetShouldShowDialogForTesting(std::nullopt); + // Back to normal: default state should show. + EXPECT_TRUE(BraveOriginStartupView::ShouldShowDialog(&local_state_)); +} + +// --- IsShowing tests (without needing a widget) --- + +TEST_F(BraveOriginStartupViewTest, IsShowingReturnsFalseByDefault) { + EXPECT_FALSE(BraveOriginStartupView::IsShowing()); +} diff --git a/browser/ui/webui/brave_origin_startup/brave_origin_startup_ui.cc b/browser/ui/webui/brave_origin_startup/brave_origin_startup_ui.cc index 5ec671b9c50..e3e25e57e26 100644 --- a/browser/ui/webui/brave_origin_startup/brave_origin_startup_ui.cc +++ b/browser/ui/webui/brave_origin_startup/brave_origin_startup_ui.cc @@ -9,11 +9,12 @@ #include "brave/browser/resources/brave_origin_startup/grit/brave_origin_startup_generated_map.h" #include "brave/browser/resources/brave_origin_startup/grit/brave_origin_startup_static_resources.h" +#include "brave/browser/resources/brave_origin_startup/grit/brave_origin_startup_static_resources_map.h" #include "brave/browser/skus/skus_service_factory.h" #include "brave/browser/ui/webui/brave_origin_startup/brave_origin_startup_handler.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/profiles/profile.h" -#include "components/grit/brave_components_strings.h" +#include "components/grit/brave_components_webui_strings.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui.h" @@ -28,28 +29,9 @@ void CreateAndAddDataSource(content::WebUI* web_ui, Profile* profile) { webui::SetupWebUIDataSource( source, kBraveOriginStartupGenerated, IDR_BRAVE_ORIGIN_STARTUP_STATIC_BRAVE_ORIGIN_STARTUP_HTML); + source->AddResourcePaths(kBraveOriginStartupStaticResources); - static constexpr webui::LocalizedString kStrings[] = { - {"braveOriginStartupTitle", IDS_BRAVE_ORIGIN_STARTUP_TITLE}, - {"braveOriginStartupDescription", IDS_BRAVE_ORIGIN_STARTUP_DESCRIPTION}, - {"braveOriginStartupDescription2", IDS_BRAVE_ORIGIN_STARTUP_DESCRIPTION2}, - {"braveOriginStartupRestoreButton", - IDS_BRAVE_ORIGIN_STARTUP_RESTORE_BUTTON}, - {"braveOriginStartupBuyButton", IDS_BRAVE_ORIGIN_STARTUP_BUY_BUTTON}, - {"braveOriginStartupRestoreTitle", - IDS_BRAVE_ORIGIN_STARTUP_RESTORE_TITLE}, - {"braveOriginStartupRestoreDescription", - IDS_BRAVE_ORIGIN_STARTUP_RESTORE_DESCRIPTION}, - {"braveOriginStartupPurchaseIdLabel", - IDS_BRAVE_ORIGIN_STARTUP_PURCHASE_ID_LABEL}, - {"braveOriginStartupPurchaseIdPlaceholder", - IDS_BRAVE_ORIGIN_STARTUP_PURCHASE_ID_PLACEHOLDER}, - {"braveOriginStartupVerifyButton", - IDS_BRAVE_ORIGIN_STARTUP_VERIFY_BUTTON}, - {"braveOriginStartupVerifyingMessage", - IDS_BRAVE_ORIGIN_STARTUP_VERIFYING_MESSAGE}, - }; - source->AddLocalizedStrings(kStrings); + source->AddLocalizedStrings(webui::kBraveOriginStartupStrings); } } // namespace diff --git a/browser/ui/webui/brave_settings_ui.cc b/browser/ui/webui/brave_settings_ui.cc index 7b624ae0cae..cc234edb53d 100644 --- a/browser/ui/webui/brave_settings_ui.cc +++ b/browser/ui/webui/brave_settings_ui.cc @@ -286,6 +286,7 @@ void BraveSettingsUI::AddResources(content::WebUIDataSource* html_source, brave_origin::IsBraveOriginEnabled()); html_source->AddBoolean("isBraveOriginBrandedBuild", BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED)); + // STAGING for unofficial builds; official builds always resolve to prod. html_source->AddString( "braveOriginBuyUrl", base::StrCat( diff --git a/chromium_src/chrome/browser/ui/startup/DEPS b/chromium_src/chrome/browser/ui/startup/DEPS index 64c5c3fce6b..5ab4051bbbf 100644 --- a/chromium_src/chrome/browser/ui/startup/DEPS +++ b/chromium_src/chrome/browser/ui/startup/DEPS @@ -1,3 +1,5 @@ include_rules = [ + "+brave/browser/ui/views/brave_origin", + "+brave/components/brave_origin/buildflags", "+brave/components/tor/buildflags", ] diff --git a/chromium_src/chrome/browser/ui/startup/startup_browser_creator.cc b/chromium_src/chrome/browser/ui/startup/startup_browser_creator.cc index 05e8aee8d91..899949a4f95 100644 --- a/chromium_src/chrome/browser/ui/startup/startup_browser_creator.cc +++ b/chromium_src/chrome/browser/ui/startup/startup_browser_creator.cc @@ -7,6 +7,7 @@ #include "base/command_line.h" #include "base/logging.h" +#include "brave/components/brave_origin/buildflags/buildflags.h" #include "brave/components/constants/brave_switches.h" #include "brave/components/tor/buildflags/buildflags.h" #include "chrome/browser/ui/startup/startup_browser_creator_impl.h" @@ -15,6 +16,15 @@ #include "brave/browser/tor/tor_profile_manager.h" #endif +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) +#include "brave/browser/ui/views/brave_origin/brave_origin_startup_view.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/lifetime/application_lifetime.h" +#include "chrome/browser/platform_util.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "components/prefs/pref_service.h" +#endif + #ifdef LaunchModeRecorder static_assert(false, "Replace the use of OldLaunchModeRecorder with " @@ -85,5 +95,95 @@ void BraveStartupBrowserCreatorImpl::Launch( } #define StartupBrowserCreatorImpl BraveStartupBrowserCreatorImpl +#define Start(...) Start_ChromiumImpl(__VA_ARGS__) +#define ProcessCommandLineAlreadyRunning(...) \ + ProcessCommandLineAlreadyRunning_ChromiumImpl(__VA_ARGS__) #include +#undef ProcessCommandLineAlreadyRunning +#undef Start #undef StartupBrowserCreatorImpl + +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) +namespace { + +// Concrete delegate that wires up the startup dialog to real browser services. +class StartupDialogDelegate : public BraveOriginStartupView::Delegate { + public: + void OpenExternal(const GURL& url) override { + platform_util::OpenExternal(url); + } + + void AttemptExit() override { chrome::AttemptExit(); } + + void CreateSystemProfile( + BraveOriginStartupView::ProfileCallback callback) override { + g_browser_process->profile_manager()->CreateProfileAsync( + ProfileManager::GetSystemProfilePath(), std::move(callback)); + } + + void CreateDefaultProfile( + BraveOriginStartupView::ProfileCallback callback) override { + g_browser_process->profile_manager()->CreateProfileAsync( + g_browser_process->profile_manager()->GetLastUsedProfileDir(), + std::move(callback)); + } +}; + +} // namespace +#endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + +// For Brave Origin branded builds, intercept Start() to show a purchase +// validation dialog before any browser window or profile picker opens. Start() +// is only called externally from chrome_browser_main.cc, so the #define above +// only renames the definition (not external callers). When the dialog closes +// with a successful validation, the callback invokes Start_ChromiumImpl() to +// continue the normal startup flow. +bool StartupBrowserCreator::Start(const base::CommandLine& cmd_line, + const base::FilePath& cur_dir, + StartupProfileInfo profile_info, + const Profiles& last_opened_profiles) { +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + if (BraveOriginStartupView::ShouldShowDialog( + g_browser_process->local_state())) { + // Capture first_run_tabs_ by value because `this` (the + // StartupBrowserCreator) is destroyed by chrome_browser_main.cc + // (browser_creator_.reset()) right after Start() returns. + BraveOriginStartupView::Show( + base::BindOnce( + [](std::vector first_run_tabs, + const base::CommandLine& cmd_line, const base::FilePath& cur_dir, + StartupProfileInfo profile_info, + const Profiles& last_opened_profiles) { + StartupBrowserCreator browser_creator; + browser_creator.AddFirstRunTabs(first_run_tabs); + browser_creator.Start_ChromiumImpl(cmd_line, cur_dir, + std::move(profile_info), + last_opened_profiles); + }, + std::move(first_run_tabs_), cmd_line, cur_dir, + std::move(profile_info), last_opened_profiles), + std::make_unique()); + return true; + } +#endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + return Start_ChromiumImpl(cmd_line, cur_dir, std::move(profile_info), + last_opened_profiles); +} + +// For Brave Origin branded builds, block second-launch attempts while the +// startup dialog is showing. Without this, a second launch goes through +// ProcessCommandLineAlreadyRunning (not Start), bypassing the dialog guard +// and opening a browser window directly. +// static +void StartupBrowserCreator::ProcessCommandLineAlreadyRunning( + const base::CommandLine& command_line, + const base::FilePath& cur_dir, + const StartupProfilePathInfo& profile_path_info) { +#if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + if (BraveOriginStartupView::IsShowing()) { + return; + } +#endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) + ProcessCommandLineAlreadyRunning_ChromiumImpl(command_line, cur_dir, + profile_path_info); +} diff --git a/chromium_src/chrome/browser/ui/startup/startup_browser_creator.h b/chromium_src/chrome/browser/ui/startup/startup_browser_creator.h new file mode 100644 index 00000000000..64d5f44c554 --- /dev/null +++ b/chromium_src/chrome/browser/ui/startup/startup_browser_creator.h @@ -0,0 +1,25 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_STARTUP_STARTUP_BROWSER_CREATOR_H_ +#define BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_STARTUP_STARTUP_BROWSER_CREATOR_H_ + +// Adds _ChromiumImpl method declarations alongside the originals, so the Brave +// Origin startup dialog can intercept the startup flow in the .cc override. +// Start() is only called from chrome_browser_main.cc (a different translation +// unit) so the #define in the .cc does not affect its external callers. +#define Start(...) \ + Start_ChromiumImpl(__VA_ARGS__); \ + bool Start(__VA_ARGS__) + +#define ProcessCommandLineAlreadyRunning(...) \ + ProcessCommandLineAlreadyRunning_ChromiumImpl(__VA_ARGS__); \ + static void ProcessCommandLineAlreadyRunning(__VA_ARGS__) + +#include // IWYU pragma: export +#undef ProcessCommandLineAlreadyRunning +#undef Start + +#endif // BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_STARTUP_STARTUP_BROWSER_CREATOR_H_ diff --git a/test/BUILD.gn b/test/BUILD.gn index db8edfb3b7b..827b7d6b85f 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -191,7 +191,10 @@ test("brave_unit_tests") { ] if (is_brave_origin_branded) { - deps += [ "//brave/browser/ui/webui/brave_origin_startup:unit_tests" ] + deps += [ + "//brave/browser/ui/views/brave_origin:unit_tests", + "//brave/browser/ui/webui/brave_origin_startup:unit_tests", + ] } if (enable_brave_news) { @@ -1183,6 +1186,10 @@ test("brave_browser_tests") { sources += [ "//brave/browser/ui/views/crash_report_permission_ask_dialog_browsertest.cc" ] } + if (is_brave_origin_branded) { + deps += [ "//brave/browser/ui/views/brave_origin:browser_tests" ] + } + if (enable_brave_wayback_machine) { sources += [ "//brave/components/brave_wayback_machine/brave_wayback_machine_browsertest.cc" ] deps += [ "//brave/components/brave_wayback_machine" ]