[go: up one dir, main page]

blob: 151d003f6869990f6f93c313828dec4c63f869d4 [file] [log] [blame]
// Copyright 2018 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/views/autofill/payments/local_card_migration_dialog_view.h"
#include <memory>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/i18n/message_formatter.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "build/branding_buildflags.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/autofill/payments/local_card_migration_dialog_factory.h"
#include "chrome/browser/ui/autofill/payments/local_card_migration_dialog_state.h"
#include "chrome/browser/ui/autofill/payments/payments_ui_constants.h"
#include "chrome/browser/ui/views/autofill/payments/migratable_card_view.h"
#include "chrome/browser/ui/views/autofill/payments/payments_view_util.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "components/autofill/core/browser/payments/local_card_migration_manager.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/grit/components_scaled_resources.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "components/web_modal/web_contents_modal_dialog_host.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.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/style/typography.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace autofill {
namespace {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
class AutofillMigrationHeaderView : public views::ImageView {
public:
METADATA_HEADER(AutofillMigrationHeaderView);
AutofillMigrationHeaderView() {
constexpr int kImageBorderBottom = 8;
SetBorder(views::CreateEmptyBorder(0, 0, kImageBorderBottom, 0));
SetAccessibleName(l10n_util::GetStringUTF16(
IDS_AUTOFILL_GOOGLE_PAY_LOGO_ACCESSIBLE_NAME));
}
// views::Label:
void OnThemeChanged() override {
ImageView::OnThemeChanged();
SetImage(ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
GetNativeTheme()->ShouldUseDarkColors()
? IDR_AUTOFILL_MIGRATION_DIALOG_HEADER_DARK
: IDR_AUTOFILL_MIGRATION_DIALOG_HEADER));
}
};
BEGIN_METADATA(AutofillMigrationHeaderView, views::ImageView)
END_METADATA
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Create the view containing the |tip_message| shown to the user.
class TipTextContainer : public views::View {
public:
METADATA_HEADER(TipTextContainer);
explicit TipTextContainer(const std::u16string& tip_message) {
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
// Set up the tip text container with inset, background and a solid border.
const gfx::Insets container_insets =
provider->GetInsetsMetric(views::INSETS_DIALOG_SUBSECTION);
const int container_child_space =
provider->GetDistanceMetric(views::DISTANCE_RELATED_LABEL_HORIZONTAL);
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets(container_insets), container_child_space));
constexpr int kTipImageSize = 16;
auto* lightbulb_outline_image = AddChildView(
std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
vector_icons::kLightbulbOutlineIcon, ui::kColorAlertMediumSeverity,
kTipImageSize)));
lightbulb_outline_image->SetVerticalAlignment(
views::ImageView::Alignment::kLeading);
tip_ = AddChildView(std::make_unique<views::Label>(
tip_message, CONTEXT_DIALOG_BODY_TEXT_SMALL,
views::style::STYLE_SECONDARY));
tip_->SetMultiLine(true);
tip_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
tip_->SizeToFit(provider->GetDistanceMetric(
DISTANCE_LARGE_MODAL_DIALOG_PREFERRED_WIDTH) -
kMigrationDialogInsets.width() - container_insets.width() -
kTipImageSize - container_child_space);
}
// views::Label:
void OnThemeChanged() override {
View::OnThemeChanged();
const bool should_use_dark_colors = GetNativeTheme()->ShouldUseDarkColors();
// TODO(tluk): We should not be using hard coded color constants and
// switching colors based on the dark mode flag. We should instead
// systematize these into color ids and simply call GetColor() for these
// ids.
SetBackground(views::CreateSolidBackground(
should_use_dark_colors ? gfx::kGoogleGrey800 : gfx::kGoogleGrey050));
// Do not add the border if it is not using dark colors.
constexpr int kTipValuePromptBorderThickness = 1;
SetBorder(should_use_dark_colors
? views::NullBorder()
: views::CreateSolidBorder(kTipValuePromptBorderThickness,
gfx::kGoogleGrey100));
// If it is in dark mode, set the font color to GG200 since it is on a
// lighter shade of grey background.
tip_->SetEnabledColor(
should_use_dark_colors
? gfx::kGoogleGrey200
: GetColorProvider()->GetColor(ui::kColorLabelForegroundSecondary));
}
private:
views::Label* tip_ = nullptr;
};
BEGIN_METADATA(TipTextContainer, views::View)
END_METADATA
// Create the title label container for the migration dialogs. The title
// text depends on the |view_state| of the dialog.
std::unique_ptr<views::Label> CreateTitle(
LocalCardMigrationDialogState view_state,
int card_list_size) {
int message_id;
switch (view_state) {
case LocalCardMigrationDialogState::kOffered:
message_id = IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_TITLE_OFFER;
break;
case LocalCardMigrationDialogState::kFinished:
message_id = IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_TITLE_DONE;
break;
case LocalCardMigrationDialogState::kActionRequired:
message_id = IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_TITLE_FIX;
break;
}
auto title = std::make_unique<views::Label>(
l10n_util::GetPluralStringFUTF16(message_id, card_list_size));
constexpr int kMigrationDialogTitleFontSize = 8;
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
constexpr int kMigrationDialogTitleMarginTop = 0;
#else
constexpr int kMigrationDialogTitleMarginTop = 12;
#endif
title->SetBorder(views::CreateEmptyBorder(
/*top=*/kMigrationDialogTitleMarginTop,
/*left=*/kMigrationDialogInsets.left(), /*bottom=*/0,
/*right=*/kMigrationDialogInsets.right()));
title->SetFontList(gfx::FontList().Derive(kMigrationDialogTitleFontSize,
gfx::Font::NORMAL,
gfx::Font::Weight::NORMAL));
constexpr int kMigrationDialogTitleLineHeight = 20;
title->SetMultiLine(true);
title->SetLineHeight(kMigrationDialogTitleLineHeight);
return title;
}
// Create the explanation text label with |user_email| for the migration
// dialogs. The text content depends on the |view_state| of the dialog and the
// |card_list_size|.
std::unique_ptr<views::Label> CreateExplanationText(
LocalCardMigrationDialogState view_state,
int card_list_size,
const std::u16string& user_email) {
auto explanation_text = std::make_unique<views::Label>(
std::u16string(), views::style::CONTEXT_DIALOG_BODY_TEXT,
views::style::STYLE_SECONDARY);
switch (view_state) {
case LocalCardMigrationDialogState::kOffered:
DCHECK(!user_email.empty());
explanation_text->SetText(
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringFUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_MESSAGE_OFFER,
user_email),
card_list_size));
break;
case LocalCardMigrationDialogState::kFinished:
explanation_text->SetText(l10n_util::GetPluralStringFUTF16(
card_list_size == 0
? IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_MESSAGE_INVALID_CARD_REMOVED
: IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_MESSAGE_DONE,
card_list_size));
break;
case LocalCardMigrationDialogState::kActionRequired:
explanation_text->SetText(l10n_util::GetStringUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_MESSAGE_FIX));
break;
}
explanation_text->SetMultiLine(true);
explanation_text->SetHorizontalAlignment(gfx::ALIGN_LEFT);
return explanation_text;
}
// Create the scroll view of cards in |migratable_credit_cards|, and each
// row in the scroll view is a MigratableCardView. |dialog_view|
// will be notified whenever the checkbox or the trash can button
// (if any) in any row is clicked. The content and the layout of the
// scroll view depends on |should_show_checkbox|.
std::unique_ptr<views::ScrollView> CreateCardList(
const std::vector<MigratableCreditCard>& migratable_credit_cards,
LocalCardMigrationDialogView* dialog_view,
bool should_show_checkbox) {
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
auto card_list_view = std::make_unique<views::View>();
constexpr int kCardListSmallVerticalDistance = 8;
auto* card_list_view_layout =
card_list_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
should_show_checkbox
? kCardListSmallVerticalDistance
: provider->GetDistanceMetric(
views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
card_list_view_layout->set_main_axis_alignment(
views::BoxLayout::MainAxisAlignment::kStart);
for (const auto& migratable_credit_card : migratable_credit_cards) {
card_list_view->AddChildView(std::make_unique<MigratableCardView>(
migratable_credit_card, dialog_view, should_show_checkbox));
}
auto card_list_scroll_view = std::make_unique<views::ScrollView>();
card_list_scroll_view->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kDisabled);
card_list_scroll_view->SetContents(std::move(card_list_view));
card_list_scroll_view->SetDrawOverflowIndicator(false);
constexpr int kCardListScrollViewHeight = 140;
card_list_scroll_view->ClipHeightTo(0, kCardListScrollViewHeight);
return card_list_scroll_view;
}
// Create the feedback main content view composed of
// title, explanation text, card list, and the tip (if present).
std::unique_ptr<views::View> CreateFeedbackContentView(
LocalCardMigrationDialogController* controller,
LocalCardMigrationDialogView* dialog_view) {
DCHECK(controller->GetViewState() != LocalCardMigrationDialogState::kOffered);
auto feedback_view = std::make_unique<views::View>();
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
feedback_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
feedback_view->SetBorder(views::CreateEmptyBorder(kMigrationDialogInsets));
LocalCardMigrationDialogState view_state = controller->GetViewState();
const std::vector<MigratableCreditCard>& card_list =
controller->GetCardList();
const int card_list_size = card_list.size();
feedback_view->AddChildView(
CreateExplanationText(view_state, card_list_size, std::u16string()));
if (card_list_size > 0) {
feedback_view->AddChildView(CreateCardList(card_list, dialog_view, false));
// If there are no more than two cards in the finished dialog, show the tip.
constexpr int kShowTipMessageCardNumberLimit = 2;
if (view_state == LocalCardMigrationDialogState::kFinished &&
card_list_size <= kShowTipMessageCardNumberLimit) {
feedback_view->AddChildView(
std::make_unique<TipTextContainer>(controller->GetTipMessage()));
}
}
return feedback_view;
}
} // namespace
// A view composed of the main contents for local card migration dialog
// including title, explanatory message, migratable credit card list,
// horizontal separator, and legal message. It is used by
// LocalCardMigrationDialogView class when it offers the user the
// option to upload all browser-saved credit cards.
class LocalCardMigrationOfferView : public views::View {
public:
METADATA_HEADER(LocalCardMigrationOfferView);
LocalCardMigrationOfferView(LocalCardMigrationDialogController* controller,
LocalCardMigrationDialogView* dialog_view)
: controller_(controller) {
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
kMigrationDialogMainContainerChildSpacing));
auto* contents_container = AddChildView(std::make_unique<views::View>());
contents_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
provider->GetDistanceMetric(
views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
// Don't set bottom since there is a legal message view in the offer dialog.
contents_container->SetBorder(views::CreateEmptyBorder(
0, kMigrationDialogInsets.left(), 0, kMigrationDialogInsets.right()));
const std::vector<MigratableCreditCard>& card_list =
controller->GetCardList();
int card_list_size = card_list.size();
contents_container->AddChildView(
CreateExplanationText(controller_->GetViewState(), card_list_size,
base::UTF8ToUTF16(controller_->GetUserEmail())));
auto* scroll_view = contents_container->AddChildView(
CreateCardList(card_list, dialog_view, card_list_size != 1));
card_list_view_ = scroll_view->contents();
AddChildView(std::make_unique<views::Separator>());
legal_message_container_ = AddChildView(std::make_unique<LegalMessageView>(
controller->GetLegalMessageLines(),
base::BindRepeating(
&LocalCardMigrationDialogController::OnLegalMessageLinkClicked,
base::Unretained(controller_))));
legal_message_container_->SetBorder(
views::CreateEmptyBorder(kMigrationDialogInsets));
}
LocalCardMigrationOfferView(const LocalCardMigrationOfferView&) = delete;
LocalCardMigrationOfferView& operator=(const LocalCardMigrationOfferView&) =
delete;
~LocalCardMigrationOfferView() override = default;
const std::vector<std::string> GetSelectedCardGuids() const {
std::vector<std::string> selected_cards;
for (views::View* child : card_list_view_->children()) {
DCHECK(views::IsViewClass<MigratableCardView>(child));
auto* card = static_cast<MigratableCardView*>(child);
if (card->GetSelected())
selected_cards.push_back(card->GetGuid());
}
return selected_cards;
}
private:
friend class LocalCardMigrationDialogView;
LocalCardMigrationDialogController* controller_;
views::View* card_list_view_ = nullptr;
// The view that contains legal message and handles legal message links
// clicking.
LegalMessageView* legal_message_container_ = nullptr;
};
BEGIN_METADATA(LocalCardMigrationOfferView, views::View)
ADD_READONLY_PROPERTY_METADATA(std::vector<std::string>, SelectedCardGuids)
END_METADATA
LocalCardMigrationDialogView::LocalCardMigrationDialogView(
LocalCardMigrationDialogController* controller,
content::WebContents* web_contents)
: controller_(controller), web_contents_(web_contents->GetWeakPtr()) {
SetButtons(controller_->AllCardsInvalid()
? ui::DIALOG_BUTTON_OK
: ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL);
SetButtonLabel(ui::DIALOG_BUTTON_OK, GetOkButtonLabel());
SetButtonLabel(ui::DIALOG_BUTTON_CANCEL, GetCancelButtonLabel());
SetCancelCallback(
base::BindOnce(&LocalCardMigrationDialogView::OnDialogCancelled,
base::Unretained(this)));
SetAcceptCallback(base::BindOnce(
&LocalCardMigrationDialogView::OnDialogAccepted, base::Unretained(this)));
RegisterWindowClosingCallback(base::BindOnce(
&LocalCardMigrationDialogView::OnWindowClosing, base::Unretained(this)));
// This should be a modal dialog blocking the browser since we don't want
// users to lose progress in the migration workflow until they are done.
SetModalType(ui::MODAL_TYPE_WINDOW);
set_close_on_deactivate(false);
set_margins(gfx::Insets());
set_fixed_width(ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_LARGE_MODAL_DIALOG_PREFERRED_WIDTH));
SetShowCloseButton(false);
}
LocalCardMigrationDialogView::~LocalCardMigrationDialogView() {}
void LocalCardMigrationDialogView::ShowDialog() {
if (!web_contents_) {
// If web_contents does not exist, delete this because at this step this
// View is not owned by any class.
delete this;
return;
}
ConstructView();
constrained_window::CreateBrowserModalDialogViews(
this, web_contents_->GetTopLevelNativeWindow())
->Show();
}
void LocalCardMigrationDialogView::CloseDialog() {
controller_ = nullptr;
GetWidget()->Close();
}
void LocalCardMigrationDialogView::OnDialogAccepted() {
switch (controller_->GetViewState()) {
case LocalCardMigrationDialogState::kOffered:
DCHECK(offer_view_);
controller_->OnSaveButtonClicked(offer_view_->GetSelectedCardGuids());
break;
case LocalCardMigrationDialogState::kFinished:
case LocalCardMigrationDialogState::kActionRequired:
controller_->OnDoneButtonClicked();
break;
}
}
void LocalCardMigrationDialogView::OnDialogCancelled() {
switch (controller_->GetViewState()) {
case LocalCardMigrationDialogState::kOffered:
controller_->OnCancelButtonClicked();
break;
case LocalCardMigrationDialogState::kFinished:
case LocalCardMigrationDialogState::kActionRequired:
controller_->OnViewCardsButtonClicked();
break;
}
}
void LocalCardMigrationDialogView::OnWindowClosing() {
if (controller_) {
controller_->OnDialogClosed();
controller_ = nullptr;
}
}
bool LocalCardMigrationDialogView::GetEnableOkButton() const {
if (controller_->GetViewState() == LocalCardMigrationDialogState::kOffered) {
DCHECK(offer_view_) << "This method can't be called before ConstructView";
return !offer_view_->GetSelectedCardGuids().empty();
}
return true;
}
void LocalCardMigrationDialogView::DeleteCard(const std::string& guid) {
controller_->DeleteCard(guid);
ConstructView();
UpdateLayout();
}
void LocalCardMigrationDialogView::OnCardCheckboxToggled() {
SetButtonEnabled(ui::DIALOG_BUTTON_OK, GetEnableOkButton());
}
// TODO(crbug.com/913571): Figure out a way to avoid two consecutive layouts.
void LocalCardMigrationDialogView::UpdateLayout() {
Layout();
// Since the dialog does not have anchor view or arrow, cannot use
// SizeToContents() for now. TODO(crbug.com/867194): Try to fix the
// BubbleDialogDelegateView::GetBubbleBounds() when there is no anchor
// view or arrow.
GetWidget()->SetSize(GetWidget()->non_client_view()->GetPreferredSize());
}
void LocalCardMigrationDialogView::ConstructView() {
DCHECK(controller_->GetViewState() !=
LocalCardMigrationDialogState::kOffered ||
children().empty());
RemoveAllChildViews();
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(),
kMigrationDialogMainContainerChildSpacing));
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
AddChildView(std::make_unique<AutofillMigrationHeaderView>());
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)
LocalCardMigrationDialogState view_state = controller_->GetViewState();
AddChildView(CreateTitle(view_state, controller_->GetCardList().size()));
if (view_state == LocalCardMigrationDialogState::kOffered) {
offer_view_ = AddChildView(
std::make_unique<LocalCardMigrationOfferView>(controller_, this));
offer_view_->SetID(DialogViewId::MAIN_CONTENT_VIEW_MIGRATION_OFFER_DIALOG);
card_list_view_ = offer_view_->card_list_view_;
SetButtonEnabled(ui::DIALOG_BUTTON_OK, GetEnableOkButton());
} else {
AddChildView(CreateFeedbackContentView(controller_, this));
}
}
std::u16string LocalCardMigrationDialogView::GetOkButtonLabel() const {
switch (controller_->GetViewState()) {
case LocalCardMigrationDialogState::kOffered:
return l10n_util::GetStringUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_BUTTON_LABEL_SAVE);
case LocalCardMigrationDialogState::kFinished:
case LocalCardMigrationDialogState::kActionRequired:
return l10n_util::GetStringUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_BUTTON_LABEL_DONE);
}
}
std::u16string LocalCardMigrationDialogView::GetCancelButtonLabel() const {
switch (controller_->GetViewState()) {
case LocalCardMigrationDialogState::kOffered:
return l10n_util::GetStringUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_BUTTON_LABEL_CANCEL);
case LocalCardMigrationDialogState::kFinished:
case LocalCardMigrationDialogState::kActionRequired:
return l10n_util::GetStringUTF16(
IDS_AUTOFILL_LOCAL_CARD_MIGRATION_DIALOG_BUTTON_LABEL_VIEW_CARDS);
}
}
LocalCardMigrationDialog* CreateLocalCardMigrationDialogView(
LocalCardMigrationDialogController* controller,
content::WebContents* web_contents) {
return new LocalCardMigrationDialogView(controller, web_contents);
}
BEGIN_METADATA(LocalCardMigrationDialogView, views::BubbleDialogDelegateView)
ADD_READONLY_PROPERTY_METADATA(bool, EnableOkButton)
ADD_READONLY_PROPERTY_METADATA(std::u16string, OkButtonLabel)
ADD_READONLY_PROPERTY_METADATA(std::u16string, CancelButtonLabel)
END_METADATA
} // namespace autofill