blob: d8ee0bfc522db470782cd7760f87b84961563d3c [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/webid/account_selection_modal_view.h"
#include <iostream>
#include <memory>
#include <optional>
#include <utility>
#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/image_fetcher/image_decoder_impl.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
#include "chrome/browser/ui/views/extensions/security_dialog_tracker.h"
#include "chrome/browser/ui/views/webid/account_selection_view_base.h"
#include "chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h"
#include "chrome/browser/ui/views/webid/webid_utils.h"
#include "chrome/browser/ui/webid/identity_ui_utils.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/image_fetcher/core/image_fetcher.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/strings/grit/components_strings.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/webid/identity_request_dialog_controller.h"
#include "skia/ext/image_operations.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/ui_base_types.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/progress_bar.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/style/typography.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/window/dialog_delegate.h"
namespace webid {
// The size of the spacing used between most children elements.
constexpr int kBetweenChildSpacing = 4;
// The size of the vertical padding for most elements in the dialog.
constexpr int kVerticalPadding = 8;
// The width of the modal dialog.
constexpr int kDialogWidth = 448;
// The margins of the modal dialog.
constexpr int kDialogMargin = 20;
class BackgroundImageView : public views::ImageView {
METADATA_HEADER(BackgroundImageView, views::ImageView)
public:
explicit BackgroundImageView(base::WeakPtr<content::WebContents> web_contents)
: web_contents_(web_contents) {
constexpr int kBackgroundWidth = 408;
constexpr int kBackgroundHeight = 100;
SetImageSize(gfx::Size(kBackgroundWidth, kBackgroundHeight));
UpdateBackgroundImage();
}
BackgroundImageView(const BackgroundImageView&) = delete;
BackgroundImageView& operator=(const BackgroundImageView&) = delete;
~BackgroundImageView() override = default;
void UpdateBackgroundImage() {
CHECK(web_contents_);
const bool is_dark_mode = color_utils::IsDark(
web_contents_->GetColorProvider().GetColor(ui::kColorDialogBackground));
SetImage(ui::ImageModel::FromResourceId(
is_dark_mode ? IDR_WEBID_MODAL_ICON_BACKGROUND_DARK
: IDR_WEBID_MODAL_ICON_BACKGROUND_LIGHT));
}
void OnThemeChanged() override {
views::ImageView::OnThemeChanged();
UpdateBackgroundImage();
}
private:
// Web contents is used to determine whether to show the light or dark mode
// image.
base::WeakPtr<content::WebContents> web_contents_;
};
BEGIN_METADATA(BackgroundImageView)
END_METADATA
namespace {
std::unique_ptr<views::View> CreateButtonContainer() {
const views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
std::unique_ptr<views::View> button_container =
std::make_unique<views::View>();
constexpr int kButtonRowTopPadding = 24;
button_container->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetMainAxisAlignment(views::LayoutAlignment::kEnd)
.SetIgnoreDefaultMainAxisMargins(true)
.SetDefault(views::kMarginsKey,
gfx::Insets::TLBR(
/*top=*/0, /*left=*/
layout_provider->GetDistanceMetric(
views::DISTANCE_RELATED_BUTTON_HORIZONTAL),
/*bottom=*/0, /*right=*/0))
.SetInteriorMargin(gfx::Insets::TLBR(/*top=*/kButtonRowTopPadding,
/*left=*/kDialogMargin,
/*bottom=*/kDialogMargin,
/*right=*/kDialogMargin));
return button_container;
}
} // namespace
AccountSelectionModalDelegate::AccountSelectionModalDelegate(
std::unique_ptr<AccountSelectionModalView> account_selection_modal_view) {
auto* selection_modal =
SetContentsView(std::move(account_selection_modal_view));
SetModalType(ui::mojom::ModalType::kChild);
SetOwnershipOfNewWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
set_fixed_width(kDialogWidth);
SetShowTitle(false);
SetShowCloseButton(false);
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
SetTitle(selection_modal->dialog_title());
}
AccountSelectionModalDelegate::~AccountSelectionModalDelegate() = default;
views::View* AccountSelectionModalDelegate::GetInitiallyFocusedView() {
if (auto* initially_focused_view =
GetAccountSelectionView()->GetInitiallyFocusedView()) {
return initially_focused_view;
}
return views::DialogDelegate::GetInitiallyFocusedView();
}
views::Widget* AccountSelectionModalDelegate::GetWidget() {
return GetAccountSelectionView()->GetWidget();
}
const views::Widget* AccountSelectionModalDelegate::GetWidget() const {
return const_cast<AccountSelectionModalDelegate*>(this)
->GetAccountSelectionView()
->GetWidget();
}
AccountSelectionModalView*
AccountSelectionModalDelegate::GetAccountSelectionView() {
if (auto* account_selection_modal_view =
views::AsViewClass<AccountSelectionModalView>(GetContentsView())) {
return account_selection_modal_view;
}
NOTREACHED()
<< "Dialog ContentsView isn't of type AccountSelectionModalView!";
}
AccountSelectionModalView::AccountSelectionModalView(
const content::RelyingPartyData& rp_data,
const std::optional<std::u16string>& idp_title,
blink::mojom::RpContext rp_context,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
FedCmAccountSelectionView* owner)
: AccountSelectionViewBase(owner,
std::move(url_loader_factory),
rp_data,
owner->web_contents()
->GetPrimaryMainFrame()
->GetRenderWidgetHost()
->GetDeviceScaleFactor()),
idp_title_(idp_title),
rp_context_(rp_context) {
// Configure the BoxLayoutView
SetOrientation(views::BoxLayout::Orientation::kVertical);
SetBetweenChildSpacing(kBetweenChildSpacing);
header_view_ = AddChildView(CreateHeader());
UpdateTitleAndSubtitle(rp_data);
AddChildView(CreatePlaceholderAccountRow());
AddChildView(CreateButtonRow(/*continue_callback=*/std::nullopt,
/*use_other_account_callback=*/std::nullopt,
/*back_callback=*/std::nullopt));
}
AccountSelectionModalView::~AccountSelectionModalView() = default;
std::unique_ptr<views::View>
AccountSelectionModalView::CreatePlaceholderAccountRow() {
const SkColor kPlaceholderColor =
color_utils::IsDark(owner_->web_contents()->GetColorProvider().GetColor(
ui::kColorDialogBackground))
? gfx::kGoogleGrey800
: gfx::kGoogleGrey200;
std::unique_ptr<views::View> placeholder_account_icon =
std::make_unique<views::View>();
placeholder_account_icon->SetPreferredSize(
gfx::Size(webid::kModalAvatarSize, webid::kModalAvatarSize));
placeholder_account_icon->SizeToPreferredSize();
placeholder_account_icon->SetBackground(views::CreateRoundedRectBackground(
kPlaceholderColor, webid::kModalAvatarSize));
constexpr int kPlaceholderAccountRowPadding = 16;
auto row = std::make_unique<views::View>();
row->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::VH(
/*vertical=*/kPlaceholderAccountRowPadding,
/*horizontal=*/kDialogMargin + kModalHorizontalSpacing),
/*between_child_spacing=*/kModalHorizontalSpacing));
row->AddChildView(std::move(placeholder_account_icon));
constexpr int kPlaceholderVerticalSpacing = 2;
views::View* const text_column =
row->AddChildView(std::make_unique<views::View>());
text_column->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
.SetCrossAxisAlignment(views::LayoutAlignment::kStart)
.SetDefault(views::kMarginsKey,
gfx::Insets::VH(/*vertical=*/kPlaceholderVerticalSpacing,
/*horizontal=*/0));
constexpr int kPlaceholderRadius = 5;
constexpr int kPlaceholderTextHeight = 10;
constexpr int kPlaceholderAccountNameWidth = 79;
constexpr int kPlaceholderAccountEmailWidth = 134;
views::View* placeholder_account_name =
text_column->AddChildView(std::make_unique<views::View>());
placeholder_account_name->SetPreferredSize(
gfx::Size(kPlaceholderAccountNameWidth, kPlaceholderTextHeight));
placeholder_account_name->SizeToPreferredSize();
placeholder_account_name->SetBackground(views::CreateRoundedRectBackground(
kPlaceholderColor, kPlaceholderRadius));
views::View* placeholder_account_email =
text_column->AddChildView(std::make_unique<views::View>());
placeholder_account_email->SetPreferredSize(
gfx::Size(kPlaceholderAccountEmailWidth, kPlaceholderTextHeight));
placeholder_account_email->SizeToPreferredSize();
placeholder_account_email->SetBackground(views::CreateRoundedRectBackground(
kPlaceholderColor, kPlaceholderRadius));
return row;
}
std::unique_ptr<views::View> AccountSelectionModalView::CreateButtonRow(
std::optional<views::Button::PressedCallback> continue_callback,
std::optional<views::Button::PressedCallback> use_other_account_callback,
std::optional<views::Button::PressedCallback> back_callback) {
std::unique_ptr<views::View> button_container = CreateButtonContainer();
std::unique_ptr<views::MdTextButton> cancel_button =
std::make_unique<views::MdTextButton>(
base::BindRepeating(&FedCmAccountSelectionView::OnCloseButtonClicked,
base::Unretained(owner_)),
l10n_util::GetStringUTF16(IDS_CANCEL));
cancel_button_ = cancel_button.get();
// When a continue button is present, the cancel button should be more
// prominent (Tonal) to align with common practice.
cancel_button->SetStyle(continue_callback ? ui::ButtonStyle::kTonal
: ui::ButtonStyle::kDefault);
cancel_button->SetAppearDisabledInInactiveWidget(true);
button_container->AddChildView(std::move(cancel_button));
if (continue_callback) {
std::unique_ptr<views::MdTextButton> continue_button =
std::make_unique<views::MdTextButton>(
std::move(*continue_callback),
l10n_util::GetStringUTF16(IDS_SIGNIN_CONTINUE));
continue_button_ = continue_button.get();
continue_button->SetStyle(ui::ButtonStyle::kProminent);
continue_button->SetAppearDisabledInInactiveWidget(true);
button_container->AddChildView(std::move(continue_button));
}
if (!(use_other_account_callback || back_callback)) {
return button_container;
}
CHECK(!use_other_account_callback || !back_callback);
// Use other account or back button shown on the far left needs to be in its
// own child container because we want it aligned to the start of the button
// row container, whereas the other buttons are aligned to the end of the
// button row container.
std::unique_ptr<views::FlexLayoutView> leftmost_button_container =
std::make_unique<views::FlexLayoutView>();
leftmost_button_container->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kUnbounded));
if (use_other_account_callback) {
std::unique_ptr<views::MdTextButton> use_other_account_button =
std::make_unique<views::MdTextButton>(
std::move(*use_other_account_callback),
l10n_util::GetStringUTF16(IDS_ACCOUNT_SELECTION_USE_OTHER_ACCOUNT));
use_other_account_button_ = use_other_account_button.get();
use_other_account_button->SetStyle(ui::ButtonStyle::kDefault);
use_other_account_button->SetAppearDisabledInInactiveWidget(true);
leftmost_button_container->AddChildView(
std::move(use_other_account_button));
} else {
CHECK(back_callback);
std::unique_ptr<views::MdTextButton> back_button =
std::make_unique<views::MdTextButton>(
std::move(*back_callback),
l10n_util::GetStringUTF16(IDS_ACCOUNT_SELECTION_BACK));
back_button_ = back_button.get();
back_button->SetStyle(ui::ButtonStyle::kDefault);
back_button->SetAppearDisabledInInactiveWidget(true);
leftmost_button_container->AddChildView(std::move(back_button));
}
button_container->AddChildViewAt(std::move(leftmost_button_container), 0);
return button_container;
}
std::unique_ptr<views::View> AccountSelectionModalView::CreateHeader() {
std::unique_ptr<views::View> header = std::make_unique<views::View>();
header->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::TLBR(/*top=*/kDialogMargin, /*left=*/kDialogMargin,
/*bottom=*/kVerticalPadding, /*right=*/kDialogMargin),
/*between_child_spacing=*/kVerticalPadding));
// Add background image and IDP icon container.
header_icon_view_ = header->AddChildView(CreateIconHeaderView());
// Add the title.
title_label_ = header->AddChildView(std::make_unique<views::Label>(
u"", views::style::CONTEXT_DIALOG_TITLE, views::style::STYLE_HEADLINE_4));
SetLabelProperties(title_label_);
return header;
}
std::unique_ptr<views::View>
AccountSelectionModalView::CreateMultipleAccountChooser(
const std::vector<IdentityRequestAccountPtr>& accounts) {
auto scroll_view = std::make_unique<views::ScrollView>();
scroll_view->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
views::View* const content = scroll_view->SetContents(CreateAccountRows(
accounts, /*should_hover=*/true, /*show_separator=*/true,
/*is_request_permission_dialog=*/false));
constexpr float kMaxAccountsToShow = 3.5f;
const int per_account_size =
content->GetPreferredSize().height() / accounts.size();
scroll_view->ClipHeightTo(
0, static_cast<int>(per_account_size * kMaxAccountsToShow));
return scroll_view;
}
std::unique_ptr<views::View> AccountSelectionModalView::CreateAccountRows(
const std::vector<IdentityRequestAccountPtr>& accounts,
bool should_hover,
bool show_separator,
bool is_request_permission_dialog) {
auto content = std::make_unique<views::View>();
content->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::VH(
/*vertical=*/is_request_permission_dialog ? 0 : kVerticalPadding,
/*horizontal=*/kDialogMargin)));
if (show_separator) {
// Add separator before the account rows.
content->AddChildView(std::make_unique<views::Separator>());
}
int num_rows = 0;
constexpr int kAccountRowVerticalPadding = 2;
for (const auto& account : accounts) {
content->AddChildView(CreateAccountRow(
account,
/*clickable_position=*/
should_hover ? std::make_optional<int>(num_rows++) : std::nullopt,
/*should_include_idp=*/false,
/*is_modal_dialog=*/true,
/*additional_vertical_padding=*/
is_request_permission_dialog ? 0 : kAccountRowVerticalPadding));
if (show_separator) {
// Add separator after each account row.
content->AddChildView(std::make_unique<views::Separator>());
}
}
return content;
}
void AccountSelectionModalView::ShowMultiAccountPicker(
const std::vector<IdentityRequestAccountPtr>& accounts,
const std::vector<IdentityProviderDataPtr>& idp_list,
const gfx::Image& rp_icon,
bool show_back_button) {
DCHECK(!show_back_button);
CHECK_EQ(idp_list.size(), 1u);
ShowAccounts(accounts, /*is_single_account_chooser=*/false);
}
void AccountSelectionModalView::ShowAccounts(
const std::vector<IdentityRequestAccountPtr>& accounts,
bool is_single_account_chooser) {
RemoveNonHeaderChildViewsAndUpdateHeaderIfNeeded();
const content::IdentityProviderMetadata& idp_metadata =
accounts[0]->identity_provider->idp_metadata;
// If `brand_decoded_icon` is empty, a globe icon is shown instead.
if (!idp_metadata.brand_decoded_icon.IsEmpty()) {
if (idp_brand_icon_->SetBrandIconImage(idp_metadata.brand_decoded_icon,
/*should_circle_crop=*/true)) {
OnIdpBrandIconSet();
}
} else {
idp_brand_icon_->SetImage(ui::ImageModel::FromVectorIcon(
kWebidGlobeIcon, ui::kColorIconSecondary, kModalIdpIconSize));
}
idp_brand_icon_->SetVisible(/*visible=*/true);
// `combined_icons_` is created in `ShowRequestPermissionDialog` and is only
// meant to be shown there, but it might be present here when the user clicks
// the back button.
MaybeRemoveCombinedIconsView();
// Show the "Choose an account to continue" label.
CHECK(body_label_);
body_label_->SetVisible(/*visible=*/true);
if (is_single_account_chooser) {
CHECK_EQ(accounts.size(), 1u);
account_chooser_ =
AddChildView(CreateAccountRows(accounts,
/*should_hover=*/true,
/*show_separator=*/true,
/*is_request_permission_dialog=*/false));
} else {
account_chooser_ = AddChildView(CreateMultipleAccountChooser(accounts));
}
std::optional<views::Button::PressedCallback> use_other_account_callback =
std::nullopt;
// TODO(crbug.com/324052630): Support add account with multi IDP API.
if (idp_metadata.supports_add_account ||
idp_metadata.has_filtered_out_account) {
use_other_account_callback = base::BindRepeating(
&AccountSelectionModalView::OnUseOtherAccountButtonClicked,
base::Unretained(this), idp_metadata.config_url,
idp_metadata.idp_login_url);
}
AddChildView(CreateButtonRow(/*continue_callback=*/std::nullopt,
std::move(use_other_account_callback),
/*back_callback=*/std::nullopt));
// TODO(crbug.com/324052630): Connect with multi IDP API.
}
void AccountSelectionModalView::ShowVerifyingSheet(
const IdentityRequestAccountPtr& account,
const std::u16string& title) {
// A different type of sheet must have been shown prior to ShowVerifyingSheet.
// This might change if we choose to integrate auto re-authn with button mode.
CHECK(owner_->GetDialogWidget());
SendAccessibilityEvent(GetWidget(),
l10n_util::GetStringUTF16(IDS_VERIFY_SHEET_TITLE));
// Disable account chooser.
CHECK(account_chooser_);
bool is_single_account_chooser = false;
for (const auto& child : account_chooser_->children()) {
// If one of the immediate children is HoverButton, this is a single account
// chooser.
if (child->GetClassName() == "HoverButton") {
is_single_account_chooser = true;
AccountHoverButton* button = static_cast<AccountHoverButton*>(child);
if (button->HasBeenClicked()) {
has_spinner_ = true;
button->ReplaceSecondaryViewWithSpinner();
verifying_focus_view_ = button;
} else {
button->SetEnabled(false);
button->SetDisabledOpacity();
}
}
}
// If no immediate HoverButton child was found, it means that this is a
// multiple account chooser and the HoverButtons are embedded within a
// ScrollView.
if (!is_single_account_chooser) {
views::View* wrapper = account_chooser_->children()[0];
views::View* contents = wrapper->children()[0];
for (const auto& child : contents->children()) {
if (child->GetClassName() == "HoverButton") {
AccountHoverButton* button = static_cast<AccountHoverButton*>(child);
if (button->HasBeenClicked()) {
has_spinner_ = true;
button->ReplaceSecondaryViewWithSpinner();
verifying_focus_view_ = button;
} else {
button->SetEnabled(false);
button->SetDisabledOpacity();
}
}
}
}
if (use_other_account_button_) {
// If there is no spinner, either on any of the account rows or the continue
// button, this verifying sheet must have been triggered as a result of use
// other account so we show the spinner on this button.
if (!has_spinner_) {
verifying_focus_view_ = use_other_account_button_;
ReplaceButtonWithSpinner(use_other_account_button_);
} else {
use_other_account_button_->SetEnabled(false);
}
}
if (continue_button_) {
// If there is no focus view specified at this point, it must be that the
// user clicked on the continue button.
if (!verifying_focus_view_) {
verifying_focus_view_ = continue_button_;
} else {
continue_button_->SetEnabled(false);
}
}
if (back_button_) {
back_button_->SetEnabled(false);
}
has_spinner_ = true;
}
void AccountSelectionModalView::ShowSingleAccountConfirmDialog(
const IdentityRequestAccountPtr& account,
bool show_back_button) {
std::vector<IdentityRequestAccountPtr> accounts = {account};
ShowAccounts(accounts, /*is_single_account_chooser=*/true);
}
void AccountSelectionModalView::ShowFailureDialog(
const std::u16string& idp_for_display,
const content::IdentityProviderMetadata& idp_metadata) {
NOTREACHED()
<< "ShowFailureDialog is only implemented for AccountSelectionBubbleView";
}
void AccountSelectionModalView::ShowErrorDialog(
const std::u16string& idp_for_display,
const content::IdentityProviderMetadata& idp_metadata,
const std::optional<TokenError>& error) {
RemoveNonHeaderChildViewsAndUpdateHeaderIfNeeded();
std::u16string summary_text;
std::u16string description_text;
std::tie(summary_text, description_text) =
GetErrorDialogText(error, idp_for_display);
title_ = summary_text;
title_label_->SetText(title_);
title_label_->SetVisible(true);
if (auto* widget = GetWidget()) {
widget->widget_delegate()->SetTitle(title_);
}
// body_label_ may be invisible if the preceding UI is the disclosure UI. When
// error is triggered directly from the loading UI in case of auto re-authn,
// body_label_ is still present at this moment.
body_label_->SetVisible(/*visible=*/true);
body_label_->SetText(description_text);
std::unique_ptr<views::View> button_container = CreateButtonContainer();
// Add more details button.
if (error && !error->url.is_empty()) {
auto more_details_button = std::make_unique<views::MdTextButton>(
base::BindRepeating(&FedCmAccountSelectionView::OnMoreDetails,
base::Unretained(owner_)),
l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_DIALOG_MORE_DETAILS_BUTTON));
button_container->AddChildView(std::move(more_details_button));
}
// Add got it button.
auto got_it_button = std::make_unique<views::MdTextButton>(
base::BindRepeating(&FedCmAccountSelectionView::OnGotIt,
base::Unretained(owner_)),
l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_DIALOG_GOT_IT_BUTTON));
got_it_button->SetStyle(ui::ButtonStyle::kProminent);
button_container->AddChildView(std::move(got_it_button));
AddChildView(std::move(button_container));
}
void AccountSelectionModalView::OnIdpBrandIconSet() {
header_icon_spinner_->Stop();
header_icon_spinner_->SetVisible(false);
idp_brand_icon_->SetVisible(true);
}
void AccountSelectionModalView::OnCombinedIconsSet() {
header_icon_spinner_->Stop();
header_icon_spinner_->SetVisible(false);
idp_brand_icon_->SetVisible(false);
combined_icons_->SetVisible(true);
}
void AccountSelectionModalView::ShowRequestPermissionDialog(
const IdentityRequestAccountPtr& account) {
RemoveNonHeaderChildViewsAndUpdateHeaderIfNeeded();
const content::IdentityProviderData& idp_data = *account->identity_provider;
const gfx::Image& idp_brand_icon = idp_data.idp_metadata.brand_decoded_icon;
const gfx::Image& rp_brand_icon = idp_data.client_metadata.brand_decoded_icon;
// Show RP icon if and only if both IDP and RP icons are available. The
// combined icons view is only made visible when both IDP and RP icon fetches
// succeed.
if (!idp_brand_icon.IsEmpty() && !rp_brand_icon.IsEmpty()) {
combined_icons_ =
header_icon_view_->AddChildView(CreateCombinedIconsView());
bool idp_icon_set = combined_icons_idp_brand_icon_->SetBrandIconImage(
idp_brand_icon, /*should_circle_crop=*/true);
bool rp_icon_set = combined_icons_rp_brand_icon_->SetBrandIconImage(
rp_brand_icon, /*should_circle_crop=*/true);
if (idp_icon_set && rp_icon_set) {
OnCombinedIconsSet();
}
} else {
// If `idp_brand_icon` is empty, a globe icon is shown instead.
if (!idp_brand_icon.IsEmpty()) {
if (idp_brand_icon_->SetBrandIconImage(idp_brand_icon,
/*should_circle_crop=*/true)) {
OnIdpBrandIconSet();
}
} else {
idp_brand_icon_->SetImage(ui::ImageModel::FromVectorIcon(
kWebidGlobeIcon, ui::kColorIconSecondary, kModalIdpIconSize));
}
idp_brand_icon_->SetVisible(/*visible=*/true);
}
// Hide the "Choose an account to continue" label, but not if we are instead
// showing the iframe text.
CHECK(body_label_);
if (subtitle_.empty()) {
body_label_->SetVisible(/*visible=*/false);
}
std::vector<IdentityRequestAccountPtr> accounts = {account};
account_chooser_ =
AddChildView(CreateAccountRows(accounts,
/*should_hover=*/false,
/*show_separator=*/false,
/*is_request_permission_dialog=*/true));
// It must be that either the account's login state is kSignUp or that fields
// are empty if the account's login state is kSignIn.
CHECK(account->idp_claimed_login_state.value_or(
account->browser_trusted_login_state) ==
Account::LoginState::kSignUp ||
account->fields.empty());
if (!account->fields.empty()) {
// Add disclosure label.
std::unique_ptr<views::StyledLabel> disclosure_label =
CreateDisclosureLabel(account);
disclosure_label->SetDefaultTextStyle(views::style::STYLE_BODY_4);
disclosure_label->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
/*top=*/kVerticalSpacing, /*left=*/0, /*bottom=*/0,
/*right=*/0)));
// Announce immediately if the view is showing.
if (GetWidget()->IsVisible()) {
GetViewAccessibility().AnnounceAlert(disclosure_label->GetText());
} else {
queued_announcement_ = disclosure_label->GetText();
}
account_chooser_->AddChildView(std::move(disclosure_label));
}
AddChildView(CreateButtonRow(
base::BindRepeating(&AccountSelectionModalView::OnContinueButtonClicked,
base::Unretained(this), account),
/*use_other_account_callback=*/std::nullopt,
base::BindRepeating(&FedCmAccountSelectionView::OnBackButtonClicked,
base::Unretained(owner_))));
}
void AccountSelectionModalView::OnContinueButtonClicked(
const IdentityRequestAccountPtr& account,
const ui::Event& event) {
// In the verifying sheet, we do not disable the continue button if it has a
// spinner because otherwise the focus will land on the cancel button.
// Since the button is not disabled, it is possible for the button to be
// clicked again and we would ignore these future clicks.
if (verifying_focus_view_) {
return;
}
owner_->OnAccountSelected(account, event);
has_spinner_ = true;
ReplaceButtonWithSpinner(continue_button_,
ui::kColorButtonForegroundProminent,
ui::kColorButtonBackgroundProminent);
}
void AccountSelectionModalView::OnUseOtherAccountButtonClicked(
const GURL& idp_config_url,
const GURL& idp_login_url,
const ui::Event& event) {
// In the verifying sheet, we do not disable the use other account button if
// it has a spinner because otherwise the focus will land on the cancel
// button. The use other account button has a spinner if the user signs into a
// returning account via the pop-up. Since the button is not disabled, it is
// possible for the button to be clicked again and we would ignore these
// future clicks.
if (verifying_focus_view_) {
return;
}
owner_->OnLoginToIdP(idp_config_url, idp_login_url, event);
}
std::unique_ptr<views::View> AccountSelectionModalView::CreateIconHeaderView() {
// Create background image view.
std::unique_ptr<BackgroundImageView> background_image_view =
std::make_unique<BackgroundImageView>(
owner_->web_contents()->GetWeakPtr());
// Put background image view into a FillLayout container.
std::unique_ptr<views::View> background_container =
std::make_unique<views::View>();
background_container->SetUseDefaultFillLayout(true);
background_container->AddChildView(std::move(background_image_view));
// Put BoxLayout containers into FillLayout container to stack the views. This
// stacks the spinner and icon container on top of the background image.
background_container->AddChildView(CreateSpinnerIconView());
background_container->AddChildView(CreateIdpIconView());
return background_container;
}
std::unique_ptr<views::BoxLayoutView>
AccountSelectionModalView::CreateSpinnerIconView() {
// Put spinner icon into a BoxLayout container so that it can be stacked on
// top of the background.
std::unique_ptr<views::BoxLayoutView> icon_container =
std::make_unique<views::BoxLayoutView>();
icon_container->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
icon_container->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
std::unique_ptr<views::Throbber> header_icon_spinner =
std::make_unique<views::Throbber>();
header_icon_spinner->SetPreferredSize(
gfx::Size(kModalIconSpinnerSize, kModalIconSpinnerSize));
header_icon_spinner->SizeToPreferredSize();
header_icon_spinner->Start();
header_icon_spinner_ =
icon_container->AddChildView(std::move(header_icon_spinner));
return icon_container;
}
std::unique_ptr<views::BoxLayoutView>
AccountSelectionModalView::CreateIdpIconView() {
// Create IDP brand icon image view.
auto idp_brand_icon_image_view =
std::make_unique<BrandIconImageView>(kModalIdpIconSize);
idp_brand_icon_image_view->SetVisible(/*visible=*/false);
// Put IDP icon into a BoxLayout container so that it can be stacked on top of
// the background.
std::unique_ptr<views::BoxLayoutView> icon_container =
std::make_unique<views::BoxLayoutView>();
icon_container->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
icon_container->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
idp_brand_icon_ =
icon_container->AddChildView(std::move(idp_brand_icon_image_view));
return icon_container;
}
std::unique_ptr<views::BoxLayoutView>
AccountSelectionModalView::CreateCombinedIconsView() {
// Create IDP brand icon image view.
auto idp_brand_icon_image_view =
std::make_unique<BrandIconImageView>(kModalCombinedIconSize);
combined_icons_idp_brand_icon_ = idp_brand_icon_image_view.get();
idp_brand_icon_image_view->SetVisible(/*visible=*/true);
// Create arrow icon image view.
std::unique_ptr<views::ImageView> arrow_icon_image_view =
std::make_unique<views::ImageView>();
arrow_icon_image_view->SetImage(ui::ImageModel::FromVectorIcon(
kWebidArrowIcon, ui::kColorIconSecondary, kModalCombinedIconSize));
// Create RP brand icon image view.
auto rp_brand_icon_image_view =
std::make_unique<BrandIconImageView>(kModalCombinedIconSize);
combined_icons_rp_brand_icon_ = rp_brand_icon_image_view.get();
rp_brand_icon_image_view->SetVisible(/*visible=*/true);
// Put IDP icon, arrow icon and RP icon into a BoxLayout container, in that
// order. This is so that they can be stacked on top of the background.
std::unique_ptr<views::BoxLayoutView> icon_container =
std::make_unique<views::BoxLayoutView>();
icon_container->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
icon_container->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
icon_container->SetBetweenChildSpacing(kBetweenChildSpacing);
icon_container->AddChildView(std::move(idp_brand_icon_image_view));
icon_container->AddChildView(std::move(arrow_icon_image_view));
icon_container->AddChildView(std::move(rp_brand_icon_image_view));
icon_container->SetVisible(/*visible=*/false);
return icon_container;
}
void AccountSelectionModalView::ReplaceButtonWithSpinner(
views::MdTextButton* button,
ui::ColorId spinner_color,
ui::ColorId button_color) {
std::unique_ptr<views::Throbber> button_spinner =
std::make_unique<views::Throbber>();
button_spinner->SetPreferredSize(
gfx::Size(kModalButtonSpinnerSize, kModalButtonSpinnerSize));
button_spinner->SizeToPreferredSize();
button_spinner->SetColorId(spinner_color);
button_spinner->Start();
// Spinner is put into a BoxLayoutView so that it can be shown on top of the
// button.
std::unique_ptr<views::BoxLayoutView> spinner_container =
std::make_unique<views::BoxLayoutView>();
spinner_container->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
spinner_container->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
spinner_container->AddChildView(std::move(button_spinner));
// Set button text color to be the same as its background color so that the
// text is not visible and the size of the button doesn't change. Explicitly
// set the vertical border to 0 because otherwise, the spinner cannot fit in
// the button in some OSes.
button->SetUseDefaultFillLayout(true);
button->SetBorder(views::CreateEmptyBorder(
gfx::Insets::VH(0, button->GetBorder()->GetInsets().left())));
button->AddChildView(std::move(spinner_container));
button->SetTextColor(HoverButton::ButtonState::STATE_DISABLED, button_color);
button->SetEnabledTextColors(button_color);
button->SetBgColorIdOverride(button_color);
}
std::string AccountSelectionModalView::GetDialogTitle() const {
return base::UTF16ToUTF8(title_label_->GetText());
}
void AccountSelectionModalView::UpdateTitleAndSubtitle(
const content::RelyingPartyData& rp_data) {
AccountSelectionViewBase::UpdateTitleAndSubtitle(rp_data);
// Don't set a title until we know the strings won't change anymore.
if (rp_data.display_strings_may_change) {
title_label_->SetVisible(false);
return;
}
title_ = GetTitle(rp_data, idp_title_, rp_context_);
subtitle_ = GetSubtitle(rp_data);
title_label_->SetText(title_);
title_label_->SetVisible(true);
if (body_label_) {
body_label_->SetText(subtitle_);
body_label_->SetVisible(true);
}
// Otherwise, we will set the text when we create body_label_.
if (auto* widget = GetWidget()) {
widget->widget_delegate()->SetTitle(title_);
}
}
std::optional<std::string> AccountSelectionModalView::GetDialogSubtitle()
const {
if (subtitle_.empty()) {
return std::nullopt;
}
return base::UTF16ToUTF8(subtitle_);
}
void AccountSelectionModalView::VisibilityChanged(View* starting_from,
bool is_visible) {
if (is_visible && !queued_announcement_.empty()) {
GetViewAccessibility().AnnounceAlert(queued_announcement_);
queued_announcement_ = u"";
}
}
std::u16string AccountSelectionModalView::GetQueuedAnnouncementForTesting() {
return queued_announcement_;
}
views::View* AccountSelectionModalView::GetInitiallyFocusedView() {
// If there is a view that triggered the verifying sheet, focus on the last
// clicked view.
if (verifying_focus_view_) {
return verifying_focus_view_;
}
// If there is a continue button, focus on the continue button.
if (continue_button_) {
return continue_button_;
}
// Return null to indicate to the delegate to use the delegate's super-class.
return nullptr;
}
void AccountSelectionModalView::
RemoveNonHeaderChildViewsAndUpdateHeaderIfNeeded() {
// body_label_ does not apply to the loading modal so it's added to header
// here.
if (!body_label_) {
std::u16string body_text =
subtitle_.empty()
? l10n_util::GetStringUTF16(IDS_ACCOUNT_SELECTION_CHOOSE_AN_ACCOUNT)
: subtitle_;
body_label_ = header_view_->AddChildView(std::make_unique<views::Label>(
body_text, views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_BODY_4));
SetLabelProperties(body_label_);
}
// Make sure not to keep dangling pointers around first. We do not need to
// reset pointers to views in the header.
use_other_account_button_ = nullptr;
back_button_ = nullptr;
continue_button_ = nullptr;
cancel_button_ = nullptr;
account_chooser_ = nullptr;
verifying_focus_view_ = nullptr;
const std::vector<raw_ptr<views::View, VectorExperimental>> child_views =
children();
for (views::View* child_view : child_views) {
if (child_view != header_view_) {
RemoveChildViewT(child_view);
}
}
}
void AccountSelectionModalView::MaybeRemoveCombinedIconsView() {
if (!combined_icons_) {
return;
}
// Make sure not to keep dangling pointers around first.
combined_icons_idp_brand_icon_ = nullptr;
combined_icons_rp_brand_icon_ = nullptr;
combined_icons_->RemoveAllChildViews();
combined_icons_.ClearAndDelete();
}
BEGIN_METADATA(AccountSelectionModalView)
END_METADATA
} // namespace webid