blob: 1602253ee3ab4afacf500edba0433ecad25938b8 [file] [log] [blame]
// Copyright 2015 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/save_card_bubble_views.h"
#include <stddef.h>
#include <memory>
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/views/autofill/dialog_view_ids.h"
#include "chrome/browser/ui/views/autofill/view_util.h"
#include "chrome/browser/ui/views/harmony/chrome_layout_provider.h"
#include "components/autofill/core/browser/autofill_experiments.h"
#include "components/autofill/core/browser/credit_card.h"
#include "components/autofill/core/browser/legal_message_line.h"
#include "components/autofill/core/browser/ui/save_card_bubble_controller.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/blue_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/window/dialog_client_view.h"
namespace autofill {
namespace {
// Fixed width of the bubble, in dip.
const int kBubbleWidth = 395;
// Dimensions of the Google Pay logo.
const int kGooglePayLogoWidth = 57;
const int kGooglePayLogoHeight = 16;
std::unique_ptr<views::StyledLabel> CreateLegalMessageLineLabel(
const LegalMessageLine& line,
views::StyledLabelListener* listener) {
std::unique_ptr<views::StyledLabel> label(
new views::StyledLabel(line.text(), listener));
for (const LegalMessageLine::Link& link : line.links()) {
label->AddStyleRange(link.range,
views::StyledLabel::RangeStyleInfo::CreateForLink());
}
return label;
}
} // namespace
SaveCardBubbleViews::SaveCardBubbleViews(views::View* anchor_view,
const gfx::Point& anchor_point,
content::WebContents* web_contents,
SaveCardBubbleController* controller)
: LocationBarBubbleDelegateView(anchor_view, anchor_point, web_contents),
controller_(controller) {
DCHECK(controller);
mouse_handler_ = std::make_unique<WebContentMouseHandler>(this, web_contents);
chrome::RecordDialogCreation(chrome::DialogIdentifier::SAVE_CARD);
}
void SaveCardBubbleViews::Show(DisplayReason reason) {
ShowForReason(reason);
AssignIdsToDialogClientView();
}
void SaveCardBubbleViews::Hide() {
controller_ = nullptr;
mouse_handler_ = nullptr;
CloseBubble();
}
views::View* SaveCardBubbleViews::CreateExtraView() {
if (GetCurrentFlowStep() != LOCAL_SAVE_ONLY_STEP)
return nullptr;
// Learn More link is only shown on local save bubble.
DCHECK(!learn_more_link_);
learn_more_link_ = new views::Link(l10n_util::GetStringUTF16(IDS_LEARN_MORE));
learn_more_link_->SetUnderline(false);
learn_more_link_->set_listener(this);
learn_more_link_->set_id(DialogViewId::LEARN_MORE_LINK);
return learn_more_link_;
}
views::View* SaveCardBubbleViews::CreateFootnoteView() {
if (controller_->GetLegalMessageLines().empty())
return nullptr;
// Use BoxLayout to provide insets around the label.
footnote_view_ = new View();
footnote_view_->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
footnote_view_->set_id(DialogViewId::FOOTNOTE_VIEW);
// Add a StyledLabel for each line of the legal message.
for (const LegalMessageLine& line : controller_->GetLegalMessageLines()) {
footnote_view_->AddChildView(
CreateLegalMessageLineLabel(line, this).release());
}
// If on the first step of the 2-step upload flow, hide the footer area until
// it's time to actually accept the dialog and ToS.
if (GetCurrentFlowStep() == UPLOAD_SAVE_CVC_FIX_FLOW_STEP_1_OFFER_UPLOAD)
footnote_view_->SetVisible(false);
return footnote_view_;
}
bool SaveCardBubbleViews::Accept() {
DCHECK(initial_step_ || controller_->ShouldRequestCvcFromUser());
if (GetCurrentFlowStep() == UPLOAD_SAVE_CVC_FIX_FLOW_STEP_1_OFFER_UPLOAD) {
// If user accepted upload but more info is needed, swap the content view
// and adjust the layout.
initial_step_ = false;
DCHECK(controller_);
controller_->ContinueToRequestCvcStage();
RemoveAllChildViews(/*delete_children=*/true);
AddChildView(CreateRequestCvcView().release());
GetWidget()->UpdateWindowTitle();
GetWidget()->UpdateWindowIcon();
// Disable the Save button until a valid CVC is entered:
DialogModelChanged();
// Make the legal messaging footer appear:
DCHECK(footnote_view_);
footnote_view_->SetVisible(true);
// Resize the bubble if it's grown larger:
SizeToContents();
return false;
}
// Otherwise, close the bubble as normal.
if (controller_)
controller_->OnSaveButton(cvc_textfield_ ? cvc_textfield_->text()
: base::string16());
return true;
}
bool SaveCardBubbleViews::Cancel() {
if (controller_)
controller_->OnCancelButton();
return true;
}
bool SaveCardBubbleViews::Close() {
// If there is a cancel button (non-Material UI), Cancel is logged as a
// different user action than closing, so override Close() to prevent the
// superclass' implementation from calling Cancel().
//
// Clicking the top-right [X] close button and/or focusing then unfocusing the
// bubble count as a close action only (without calling Cancel), which means
// we can't tell the controller to permanently hide the bubble on close,
// because the user simply dismissed/ignored the bubble; they might want to
// access the bubble again from the location bar icon. Return true to indicate
// that the bubble can be closed.
return true;
}
int SaveCardBubbleViews::GetDialogButtons() const {
// Material UI has no "No thanks" button in favor of an [X].
return ui::MaterialDesignController::IsSecondaryUiMaterial()
? ui::DIALOG_BUTTON_OK
: ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
}
base::string16 SaveCardBubbleViews::GetDialogButtonLabel(
ui::DialogButton button) const {
switch (GetCurrentFlowStep()) {
// Local save has two buttons:
case LOCAL_SAVE_ONLY_STEP:
return l10n_util::GetStringUTF16(
button == ui::DIALOG_BUTTON_OK ? IDS_AUTOFILL_SAVE_CARD_PROMPT_ACCEPT
: IDS_NO_THANKS);
// Upload save has one button but it can say three different things:
case UPLOAD_SAVE_ONLY_STEP:
return l10n_util::GetStringUTF16(
button == ui::DIALOG_BUTTON_OK ? IDS_AUTOFILL_SAVE_CARD_PROMPT_ACCEPT
: IDS_NO_THANKS);
case UPLOAD_SAVE_CVC_FIX_FLOW_STEP_1_OFFER_UPLOAD:
return l10n_util::GetStringUTF16(button == ui::DIALOG_BUTTON_OK
? IDS_AUTOFILL_SAVE_CARD_PROMPT_NEXT
: IDS_NO_THANKS);
case UPLOAD_SAVE_CVC_FIX_FLOW_STEP_2_REQUEST_CVC:
return l10n_util::GetStringUTF16(
button == ui::DIALOG_BUTTON_OK ? IDS_AUTOFILL_SAVE_CARD_PROMPT_CONFIRM
: IDS_NO_THANKS);
default:
NOTREACHED();
return base::string16();
}
}
bool SaveCardBubbleViews::IsDialogButtonEnabled(ui::DialogButton button) const {
if (button == ui::DIALOG_BUTTON_CANCEL)
return true;
DCHECK_EQ(ui::DIALOG_BUTTON_OK, button);
return !cvc_textfield_ ||
controller_->InputCvcIsValid(cvc_textfield_->text());
}
gfx::Size SaveCardBubbleViews::CalculatePreferredSize() const {
if (base::FeatureList::IsEnabled(
features::kAutofillUpstreamUseGooglePayBranding)) {
const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_BUBBLE_PREFERRED_WIDTH) -
margins().width();
return gfx::Size(width, GetHeightForWidth(width));
}
return gfx::Size(kBubbleWidth, GetHeightForWidth(kBubbleWidth));
}
bool SaveCardBubbleViews::ShouldShowCloseButton() const {
// The [X] is shown for Material UI.
return ui::MaterialDesignController::IsSecondaryUiMaterial();
}
base::string16 SaveCardBubbleViews::GetWindowTitle() const {
return controller_ ? controller_->GetWindowTitle() : base::string16();
}
gfx::ImageSkia SaveCardBubbleViews::GetWindowIcon() {
if (base::FeatureList::IsEnabled(
features::kAutofillUpstreamUseGooglePayBranding)) {
return gfx::ImageSkiaOperations::CreateTiledImage(
gfx::CreateVectorIcon(kGooglePayLogoWithVerticalSeparatorIcon,
gfx::kPlaceholderColor),
/*x=*/0, /*y=*/0, kGooglePayLogoWidth, kGooglePayLogoHeight);
}
return gfx::CreateVectorIcon(kGoogleGLogoIcon, 16, gfx::kPlaceholderColor);
}
bool SaveCardBubbleViews::ShouldShowWindowIcon() const {
// We show the window icon (Google "G" or Google Pay logo) in non-local save
// scenarios.
return GetCurrentFlowStep() != LOCAL_SAVE_ONLY_STEP;
}
void SaveCardBubbleViews::WindowClosing() {
if (controller_) {
controller_->OnBubbleClosed();
controller_ = nullptr;
mouse_handler_ = nullptr;
}
}
void SaveCardBubbleViews::LinkClicked(views::Link* source, int event_flags) {
DCHECK_EQ(source, learn_more_link_);
if (controller_)
controller_->OnLearnMoreClicked();
}
void SaveCardBubbleViews::StyledLabelLinkClicked(views::StyledLabel* label,
const gfx::Range& range,
int event_flags) {
if (!controller_)
return;
// Index of |label| within its parent's view hierarchy is the same as the
// legal message line index. DCHECK this assumption to guard against future
// layout changes.
DCHECK_EQ(static_cast<size_t>(label->parent()->child_count()),
controller_->GetLegalMessageLines().size());
const auto& links =
controller_->GetLegalMessageLines()[label->parent()->GetIndexOf(label)]
.links();
for (const LegalMessageLine::Link& link : links) {
if (link.range == range) {
controller_->OnLegalMessageLinkClicked(link.url);
return;
}
}
// |range| was not found.
NOTREACHED();
}
void SaveCardBubbleViews::ContentsChanged(views::Textfield* sender,
const base::string16& new_contents) {
DCHECK_EQ(cvc_textfield_, sender);
DialogModelChanged();
}
views::View* SaveCardBubbleViews::GetFootnoteViewForTesting() {
return footnote_view_;
}
SaveCardBubbleViews::~SaveCardBubbleViews() {}
SaveCardBubbleViews::CurrentFlowStep SaveCardBubbleViews::GetCurrentFlowStep()
const {
// No legal messages means this is not upload save.
if (controller_->GetLegalMessageLines().empty())
return LOCAL_SAVE_ONLY_STEP;
// If we're not requesting CVC, this is the only step on the upload path.
if (!controller_->ShouldRequestCvcFromUser())
return UPLOAD_SAVE_ONLY_STEP;
// Must be on the CVC fix flow on the upload path.
if (initial_step_)
return UPLOAD_SAVE_CVC_FIX_FLOW_STEP_1_OFFER_UPLOAD;
return UPLOAD_SAVE_CVC_FIX_FLOW_STEP_2_REQUEST_CVC;
}
std::unique_ptr<views::View> SaveCardBubbleViews::CreateMainContentView() {
std::unique_ptr<views::View> view = std::make_unique<views::View>();
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
view->set_id(GetCurrentFlowStep() == LOCAL_SAVE_ONLY_STEP
? DialogViewId::MAIN_CONTENT_VIEW_LOCAL
: DialogViewId::MAIN_CONTENT_VIEW_UPLOAD);
// If applicable, add the upload explanation label. Appears above the card
// info.
base::string16 explanation = controller_->GetExplanatoryMessage();
if (!explanation.empty()) {
auto* explanation_label = new views::Label(explanation);
explanation_label->SetMultiLine(true);
explanation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
view->AddChildView(explanation_label);
}
// Add the card type icon, last four digits and expiration date.
auto* description_view = new views::View();
description_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_BUTTON_HORIZONTAL)));
view->AddChildView(description_view);
const CreditCard& card = controller_->GetCard();
auto* card_type_icon = new views::ImageView();
card_type_icon->SetImage(
ui::ResourceBundle::GetSharedInstance()
.GetImageNamed(CreditCard::IconResourceId(card.network()))
.AsImageSkia());
card_type_icon->SetTooltipText(card.NetworkForDisplay());
card_type_icon->SetBorder(
views::CreateSolidBorder(1, SkColorSetA(SK_ColorBLACK, 10)));
description_view->AddChildView(card_type_icon);
description_view->AddChildView(
new views::Label(card.NetworkAndLastFourDigits()));
description_view->AddChildView(
new views::Label(card.AbbreviatedExpirationDateForDisplay()));
return view;
}
std::unique_ptr<views::View> SaveCardBubbleViews::CreateRequestCvcView() {
auto request_cvc_view = std::make_unique<views::View>();
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
request_cvc_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
request_cvc_view->SetBackground(views::CreateThemedSolidBackground(
request_cvc_view.get(), ui::NativeTheme::kColorId_BubbleBackground));
request_cvc_view->set_id(DialogViewId::REQUEST_CVC_VIEW);
const CreditCard& card = controller_->GetCard();
auto* explanation_label = new views::Label(l10n_util::GetStringFUTF16(
IDS_AUTOFILL_SAVE_CARD_PROMPT_ENTER_CVC_EXPLANATION,
card.NetworkAndLastFourDigits()));
explanation_label->SetMultiLine(true);
explanation_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
request_cvc_view->AddChildView(explanation_label);
auto* cvc_entry_view = new views::View();
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_BUTTON_HORIZONTAL));
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
cvc_entry_view->SetLayoutManager(std::move(layout));
DCHECK(!cvc_textfield_);
cvc_textfield_ = CreateCvcTextfield();
cvc_textfield_->set_controller(this);
cvc_textfield_->set_id(DialogViewId::CVC_TEXTFIELD);
cvc_entry_view->AddChildView(cvc_textfield_);
auto* cvc_image = new views::ImageView();
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
cvc_image->SetImage(
rb.GetImageSkiaNamed(controller_->GetCvcImageResourceId()));
cvc_entry_view->AddChildView(cvc_image);
request_cvc_view->AddChildView(cvc_entry_view);
return request_cvc_view;
}
void SaveCardBubbleViews::AssignIdsToDialogClientView() {
auto* ok_button = GetDialogClientView()->ok_button();
if (ok_button)
ok_button->set_id(DialogViewId::OK_BUTTON);
auto* cancel_button = GetDialogClientView()->cancel_button();
if (cancel_button)
cancel_button->set_id(DialogViewId::CANCEL_BUTTON);
}
void SaveCardBubbleViews::Init() {
SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
AddChildView(CreateMainContentView().release());
}
} // namespace autofill