blob: d59a15ded73b2f0a5d60ca31b7321c1407246dac [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/signin_intercept_first_run_experience_dialog.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/check_op.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/signin/profile_customization_synced_theme_waiter.h"
#include "chrome/browser/ui/webui/signin/login_ui_service.h"
#include "chrome/browser/ui/webui/signin/login_ui_service_factory.h"
#include "chrome/browser/ui/webui/signin/profile_customization_ui.h"
#include "chrome/browser/ui/webui/signin/turn_sync_on_helper.h"
#include "chrome/common/webui_url_constants.h"
#include "components/signin/public/base/signin_metrics.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
namespace {
void RecordDialogEvent(
SigninInterceptFirstRunExperienceDialog::DialogEvent event) {
base::UmaHistogramEnumeration("Signin.Intercept.FRE.Event", event);
}
} // namespace
// Delegate class for TurnSyncOnHelper. Determines what will be the next
// step for the first run based on Sync availabitily.
class SigninInterceptFirstRunExperienceDialog::InterceptTurnSyncOnHelperDelegate
: public TurnSyncOnHelper::Delegate,
public LoginUIService::Observer {
public:
explicit InterceptTurnSyncOnHelperDelegate(
base::WeakPtr<SigninInterceptFirstRunExperienceDialog> dialog);
~InterceptTurnSyncOnHelperDelegate() override;
// TurnSyncOnHelper::Delegate:
void ShowLoginError(const SigninUIError& error) override;
void ShowMergeSyncDataConfirmation(
const std::string& previous_email,
const std::string& new_email,
signin::SigninChoiceCallback callback) override;
void ShowEnterpriseAccountConfirmation(
const AccountInfo& account_info,
signin::SigninChoiceCallback callback) override;
void ShowSyncConfirmation(
base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
callback) override;
void ShowSyncDisabledConfirmation(
bool is_managed_account,
base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
callback) override;
void ShowSyncSettings() override;
void SwitchToProfile(Profile* new_profile) override;
// LoginUIService::Observer:
void OnSyncConfirmationUIClosed(
LoginUIService::SyncConfirmationUIClosedResult result) override;
private:
const base::WeakPtr<SigninInterceptFirstRunExperienceDialog> dialog_;
// Store `browser_` separately as it may outlive `dialog_`.
const base::WeakPtr<Browser> browser_;
base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
sync_confirmation_callback_;
base::ScopedObservation<LoginUIService, LoginUIService::Observer>
scoped_login_ui_service_observation_{this};
};
SigninInterceptFirstRunExperienceDialog::InterceptTurnSyncOnHelperDelegate::
InterceptTurnSyncOnHelperDelegate(
base::WeakPtr<SigninInterceptFirstRunExperienceDialog> dialog)
: dialog_(std::move(dialog)), browser_(dialog_->browser_->AsWeakPtr()) {}
SigninInterceptFirstRunExperienceDialog::InterceptTurnSyncOnHelperDelegate::
~InterceptTurnSyncOnHelperDelegate() = default;
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowLoginError(
const SigninUIError& error) {
// Do not display the sync error since the user hasn't asked for sync
// explicitly. Skip to the next step.
if (dialog_)
dialog_->DoNextStep(Step::kTurnOnSync, Step::kProfileCustomization);
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowMergeSyncDataConfirmation(
const std::string& previous_email,
const std::string& new_email,
signin::SigninChoiceCallback callback) {
NOTREACHED() << "Sign-in intercept shouldn't create a profile for an "
"account known to Chrome";
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowEnterpriseAccountConfirmation(
const AccountInfo& account_info,
signin::SigninChoiceCallback callback) {
// This is a brand new profile. Skip the enterprise confirmation.
// TODO(crbug.com/1282157): Do not show the sync promo if either
// - PromotionalTabsEnabled policy is set to False, or
// - the user went through the Profile Separation dialog.
std::move(callback).Run(signin::SIGNIN_CHOICE_CONTINUE);
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowSyncConfirmation(
base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
callback) {
if (!dialog_) {
std::move(callback).Run(LoginUIService::ABORT_SYNC);
return;
}
scoped_login_ui_service_observation_.Observe(
LoginUIServiceFactory::GetForProfile(browser_->profile()));
DCHECK(!sync_confirmation_callback_);
sync_confirmation_callback_ = std::move(callback);
dialog_->DoNextStep(Step::kTurnOnSync, Step::kSyncConfirmation);
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowSyncDisabledConfirmation(
bool is_managed_account,
base::OnceCallback<void(LoginUIService::SyncConfirmationUIClosedResult)>
callback) {
// Abort the sync flow and proceed to profile customization.
if (dialog_) {
dialog_->DoNextStep(Step::kTurnOnSync, Step::kProfileCustomization);
}
// `SYNC_WITH_DEFAULT_SETTINGS` for the sync disable confirmation means "stay
// signed in". See https://crbug.com/1141341.
std::move(callback).Run(LoginUIService::SYNC_WITH_DEFAULT_SETTINGS);
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::ShowSyncSettings() {
// Dialog's step is updated in OnSyncConfirmationUIClosed(). This
// function only needs to open the Sync Settings.
if (browser_) {
chrome::ShowSettingsSubPage(browser_.get(), chrome::kSyncSetupSubPage);
}
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::SwitchToProfile(Profile* new_profile) {
NOTREACHED() << "Sign-in intercept shouldn't create a new profile for an "
"account known to Chrome";
}
void SigninInterceptFirstRunExperienceDialog::
InterceptTurnSyncOnHelperDelegate::OnSyncConfirmationUIClosed(
LoginUIService::SyncConfirmationUIClosedResult result) {
scoped_login_ui_service_observation_.Reset();
Step next_step;
switch (result) {
case LoginUIService::SYNC_WITH_DEFAULT_SETTINGS:
RecordDialogEvent(DialogEvent::kSyncConfirmationClickConfirm);
next_step = Step::kWaitForSyncedTheme;
break;
case LoginUIService::ABORT_SYNC:
RecordDialogEvent(DialogEvent::kSyncConfirmationClickCancel);
next_step = Step::kProfileCustomization;
break;
case LoginUIService::CONFIGURE_SYNC_FIRST:
RecordDialogEvent(DialogEvent::kSyncConfirmationClickSettings);
[[fallthrough]];
case LoginUIService::UI_CLOSED:
next_step = Step::kProfileSwitchIPHAndCloseModal;
break;
}
// This may delete `dialog_`.
if (dialog_)
dialog_->DoNextStep(Step::kSyncConfirmation, next_step);
if (result == LoginUIService::UI_CLOSED) {
// Sync must be aborted if the user didn't interact explicitly with the
// dialog.
result = LoginUIService::ABORT_SYNC;
}
DCHECK(sync_confirmation_callback_);
std::move(sync_confirmation_callback_).Run(result);
// `this` may now be deleted.
}
SigninInterceptFirstRunExperienceDialog::
SigninInterceptFirstRunExperienceDialog(Browser* browser,
const CoreAccountId& account_id,
bool is_forced_intercept,
base::OnceClosure on_close_callback)
: SigninModalDialog(std::move(on_close_callback)),
browser_(browser),
account_id_(account_id),
is_forced_intercept_(is_forced_intercept) {}
void SigninInterceptFirstRunExperienceDialog::Show() {
RecordDialogEvent(DialogEvent::kStart);
// Don't show the sync promo to the users who went through the forced
// interception.
Step first_step =
is_forced_intercept_ ? Step::kProfileCustomization : Step::kTurnOnSync;
DoNextStep(Step::kStart, first_step);
}
SigninInterceptFirstRunExperienceDialog::
~SigninInterceptFirstRunExperienceDialog() = default;
void SigninInterceptFirstRunExperienceDialog::CloseModalDialog() {
if (dialog_delegate_) {
// Delegate will notify `this` when modal signin is closed.
dialog_delegate_->CloseModalSignin();
} else {
// No dialog is displayed yet, so close `this` directly.
OnModalDialogClosed();
}
}
void SigninInterceptFirstRunExperienceDialog::ResizeNativeView(int height) {
DCHECK(dialog_delegate_);
dialog_delegate_->ResizeNativeView(height);
}
content::WebContents*
SigninInterceptFirstRunExperienceDialog::GetModalDialogWebContentsForTesting() {
return dialog_delegate_ ? dialog_delegate_->GetWebContents() : nullptr;
}
void SigninInterceptFirstRunExperienceDialog::OnModalDialogClosed() {
DCHECK(!dialog_delegate_ ||
dialog_delegate_observation_.IsObservingSource(dialog_delegate_));
dialog_delegate_observation_.Reset();
dialog_delegate_ = nullptr;
NotifyModalDialogClosed();
}
void SigninInterceptFirstRunExperienceDialog::DoNextStep(
Step expected_current_step,
Step step) {
DCHECK_EQ(expected_current_step, current_step_);
// Going to a previous step is not allowed.
DCHECK_GT(step, current_step_);
current_step_ = step;
switch (step) {
case Step::kStart:
NOTREACHED();
return;
case Step::kTurnOnSync:
DoTurnOnSync();
return;
case Step::kSyncConfirmation:
DoSyncConfirmation();
return;
case Step::kWaitForSyncedTheme:
DoWaitForSyncedTheme();
return;
case Step::kProfileCustomization:
DoProfileCustomization();
return;
case Step::kProfileSwitchIPHAndCloseModal:
DoProfileSwitchIPHAndCloseModal();
return;
}
}
void SigninInterceptFirstRunExperienceDialog::DoTurnOnSync() {
const signin_metrics::AccessPoint access_point = signin_metrics::AccessPoint::
ACCESS_POINT_SIGNIN_INTERCEPT_FIRST_RUN_EXPERIENCE;
const signin_metrics::PromoAction promo_action =
signin_metrics::PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO;
signin_metrics::LogSigninAccessPointStarted(access_point, promo_action);
signin_metrics::RecordSigninUserActionForAccessPoint(access_point);
// TurnSyncOnHelper deletes itself once done.
new TurnSyncOnHelper(browser_->profile(), access_point, promo_action,
signin_metrics::Reason::kSigninPrimaryAccount,
account_id_,
TurnSyncOnHelper::SigninAbortedMode::KEEP_ACCOUNT,
std::make_unique<InterceptTurnSyncOnHelperDelegate>(
weak_ptr_factory_.GetWeakPtr()),
base::OnceClosure());
}
void SigninInterceptFirstRunExperienceDialog::DoSyncConfirmation() {
RecordDialogEvent(DialogEvent::kShowSyncConfirmation);
SetDialogDelegate(
SigninViewControllerDelegate::CreateSyncConfirmationDelegate(browser_));
PreloadProfileCustomizationUI();
}
void SigninInterceptFirstRunExperienceDialog::DoWaitForSyncedTheme() {
synced_theme_waiter_ =
std::make_unique<ProfileCustomizationSyncedThemeWaiter>(
SyncServiceFactory::GetForProfile(browser_->profile()),
ThemeServiceFactory::GetForProfile(browser_->profile()),
base::BindOnce(
&SigninInterceptFirstRunExperienceDialog::OnSyncedThemeReady,
// Unretained() is fine because `this` owns `synced_theme_waiter_`
base::Unretained(this)));
synced_theme_waiter_->Run();
}
void SigninInterceptFirstRunExperienceDialog::DoProfileCustomization() {
// Don't show the customization bubble if a valid policy theme is set.
if (ThemeServiceFactory::GetForProfile(browser_->profile())
->UsingPolicyTheme()) {
// Show the profile switch IPH that is normally shown after the
// customization bubble.
DoNextStep(Step::kProfileCustomization,
Step::kProfileSwitchIPHAndCloseModal);
return;
}
RecordDialogEvent(DialogEvent::kShowProfileCustomization);
if (!dialog_delegate_) {
// Modal dialog doesn't exist yet, create a new one.
SetDialogDelegate(
SigninViewControllerDelegate::CreateProfileCustomizationDelegate(
browser_));
return;
}
DCHECK(profile_customization_preloaded_contents_);
dialog_delegate_->SetWebContents(
profile_customization_preloaded_contents_.get());
dialog_delegate_->ResizeNativeView(ProfileCustomizationUI::kPreferredHeight);
}
void SigninInterceptFirstRunExperienceDialog::
DoProfileSwitchIPHAndCloseModal() {
browser_->window()->MaybeShowProfileSwitchIPH();
CloseModalDialog();
}
void SigninInterceptFirstRunExperienceDialog::SetDialogDelegate(
SigninViewControllerDelegate* delegate) {
DCHECK(!dialog_delegate_);
DCHECK(!dialog_delegate_observation_.IsObserving());
dialog_delegate_ = delegate;
dialog_delegate_observation_.Observe(dialog_delegate_);
}
void SigninInterceptFirstRunExperienceDialog::PreloadProfileCustomizationUI() {
profile_customization_preloaded_contents_ =
content::WebContents::Create(content::WebContents::CreateParams(
browser_->profile(),
content::SiteInstance::Create(browser_->profile())));
profile_customization_preloaded_contents_->GetController().LoadURL(
GURL(chrome::kChromeUIProfileCustomizationURL), content::Referrer(),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string());
ProfileCustomizationUI* web_ui =
profile_customization_preloaded_contents_->GetWebUI()
->GetController()
->GetAs<ProfileCustomizationUI>();
DCHECK(web_ui);
web_ui->Initialize(
base::BindOnce(&SigninInterceptFirstRunExperienceDialog::
ProfileCustomizationCloseOnCompletion,
// Unretained is fine because `this` owns the web contents.
base::Unretained(this)));
}
void SigninInterceptFirstRunExperienceDialog::OnSyncedThemeReady(
ProfileCustomizationSyncedThemeWaiter::Outcome outcome) {
synced_theme_waiter_.reset();
Step next_step;
switch (outcome) {
case ProfileCustomizationSyncedThemeWaiter::Outcome::kSyncSuccess:
case ProfileCustomizationSyncedThemeWaiter::Outcome::kSyncCannotStart:
next_step = Step::kProfileCustomization;
break;
case ProfileCustomizationSyncedThemeWaiter::Outcome::
kSyncPassphraseRequired:
case ProfileCustomizationSyncedThemeWaiter::Outcome::kTimeout:
next_step = Step::kProfileSwitchIPHAndCloseModal;
break;
}
DoNextStep(Step::kWaitForSyncedTheme, next_step);
}
void SigninInterceptFirstRunExperienceDialog::
ProfileCustomizationCloseOnCompletion(
ProfileCustomizationHandler::CustomizationResult customization_result) {
switch (customization_result) {
case ProfileCustomizationHandler::CustomizationResult::kDone:
RecordDialogEvent(DialogEvent::kProfileCustomizationClickDone);
break;
case ProfileCustomizationHandler::CustomizationResult::kSkip:
RecordDialogEvent(DialogEvent::kProfileCustomizationClickSkip);
break;
}
DoNextStep(Step::kProfileCustomization, Step::kProfileSwitchIPHAndCloseModal);
}