blob: 96895fd2c9f8894b6f639d8ab4d28209afe1c4a5 [file] [log] [blame]
// Copyright 2014 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/card_unmask_prompt_views.h"
#include "base/location.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/ui/autofill/create_card_unmask_prompt_view.h"
#include "chrome/browser/ui/browser_dialogs.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/ui/card_unmask_prompt_controller.h"
#include "components/constrained_window/constrained_window_views.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/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/geometry/safe_integer_conversions.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/bubble/tooltip_icon.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/throbber.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
namespace autofill {
namespace {
SkColor kGreyTextColor = SkColorSetRGB(0x64, 0x64, 0x64);
SkColor const kWarningColor = gfx::kGoogleRed700;
SkColor const kLightShadingColor = SkColorSetARGB(7, 0, 0, 0);
SkColor const kSubtleBorderColor = SkColorSetARGB(10, 0, 0, 0);
} // namespace
CardUnmaskPromptViews::CardUnmaskPromptViews(
CardUnmaskPromptController* controller,
content::WebContents* web_contents)
: controller_(controller),
web_contents_(web_contents),
weak_ptr_factory_(this) {
chrome::RecordDialogCreation(chrome::DialogIdentifier::CARD_UNMASK);
}
CardUnmaskPromptViews::~CardUnmaskPromptViews() {
if (controller_)
controller_->OnUnmaskDialogClosed();
}
void CardUnmaskPromptViews::Show() {
constrained_window::ShowWebModalDialogViews(this, web_contents_);
}
void CardUnmaskPromptViews::ControllerGone() {
controller_ = nullptr;
ClosePrompt();
}
void CardUnmaskPromptViews::DisableAndWaitForVerification() {
SetInputsEnabled(false);
controls_container_->SetVisible(false);
progress_overlay_->SetVisible(true);
progress_throbber_->Start();
DialogModelChanged();
Layout();
}
void CardUnmaskPromptViews::GotVerificationResult(
const base::string16& error_message,
bool allow_retry) {
progress_throbber_->Stop();
if (error_message.empty()) {
progress_label_->SetText(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS));
progress_throbber_->SetChecked(true);
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CardUnmaskPromptViews::ClosePrompt,
weak_ptr_factory_.GetWeakPtr()),
controller_->GetSuccessMessageDuration());
} else {
progress_overlay_->SetVisible(false);
controls_container_->SetVisible(true);
if (allow_retry) {
SetInputsEnabled(true);
if (!controller_->ShouldRequestExpirationDate()) {
// If there is more than one input showing, don't mark anything as
// invalid since we don't know the location of the problem.
cvc_input_->SetInvalid(true);
// Show a "New card?" link, which when clicked will cause us to ask
// for expiration date.
ShowNewCardLink();
}
// TODO(estade): When do we hide |error_label_|?
SetRetriableErrorMessage(error_message);
} else {
permanent_error_label_->SetText(error_message);
permanent_error_label_->SetVisible(true);
SetRetriableErrorMessage(base::string16());
}
DialogModelChanged();
}
Layout();
}
void CardUnmaskPromptViews::LinkClicked(views::Link* source, int event_flags) {
DCHECK_EQ(source, new_card_link_);
controller_->NewCardLinkClicked();
for (int i = 0; i < input_row_->child_count(); ++i)
input_row_->child_at(i)->SetVisible(true);
new_card_link_->SetVisible(false);
input_row_->InvalidateLayout();
cvc_input_->SetInvalid(false);
cvc_input_->SetText(base::string16());
DialogModelChanged();
GetWidget()->UpdateWindowTitle();
instructions_->SetText(controller_->GetInstructionsMessage());
SetRetriableErrorMessage(base::string16());
}
void CardUnmaskPromptViews::SetRetriableErrorMessage(
const base::string16& message) {
error_label_->SetMultiLine(!message.empty());
error_label_->SetText(message);
error_icon_->SetVisible(!message.empty());
// Update the dialog's size.
if (GetWidget() && web_contents_) {
constrained_window::UpdateWebContentsModalDialogPosition(
GetWidget(),
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents_)
->delegate()
->GetWebContentsModalDialogHost());
}
Layout();
}
void CardUnmaskPromptViews::SetInputsEnabled(bool enabled) {
cvc_input_->SetEnabled(enabled);
if (storage_checkbox_)
storage_checkbox_->SetEnabled(enabled);
month_input_->SetEnabled(enabled);
year_input_->SetEnabled(enabled);
}
void CardUnmaskPromptViews::ShowNewCardLink() {
if (new_card_link_)
return;
new_card_link_ = new views::Link(
l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_NEW_CARD_LINK));
new_card_link_->SetBorder(views::CreateEmptyBorder(0, 7, 0, 0));
new_card_link_->SetUnderline(false);
new_card_link_->set_listener(this);
input_row_->AddChildView(new_card_link_);
}
views::View* CardUnmaskPromptViews::GetContentsView() {
InitIfNecessary();
return this;
}
views::View* CardUnmaskPromptViews::CreateFootnoteView() {
if (!controller_->CanStoreLocally())
return nullptr;
// Local storage checkbox and (?) tooltip.
storage_row_ = new views::View();
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
views::BoxLayout* storage_row_layout = new views::BoxLayout(
views::BoxLayout::kHorizontal,
provider->GetInsetsMetric(views::INSETS_DIALOG_SUBSECTION));
storage_row_->SetLayoutManager(storage_row_layout);
storage_row_->SetBorder(
views::CreateSolidSidedBorder(1, 0, 0, 0, kSubtleBorderColor));
storage_row_->SetBackground(views::CreateSolidBackground(kLightShadingColor));
storage_checkbox_ = new views::Checkbox(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX));
storage_checkbox_->SetChecked(controller_->GetStoreLocallyStartState());
storage_row_->AddChildView(storage_checkbox_);
storage_row_layout->SetFlexForView(storage_checkbox_, 1);
views::TooltipIcon* icon = new views::TooltipIcon(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP));
const int kTooltipWidth = 233;
icon->set_bubble_width(kTooltipWidth);
storage_row_->AddChildView(icon);
return storage_row_;
}
gfx::Size CardUnmaskPromptViews::CalculatePreferredSize() const {
const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
return gfx::Size(width, GetHeightForWidth(width));
}
void CardUnmaskPromptViews::OnNativeThemeChanged(const ui::NativeTheme* theme) {
SkColor bg_color =
theme->GetSystemColor(ui::NativeTheme::kColorId_DialogBackground);
progress_overlay_->SetBackground(views::CreateSolidBackground(bg_color));
progress_label_->SetBackgroundColor(bg_color);
progress_label_->SetEnabledColor(theme->GetSystemColor(
ui::NativeTheme::kColorId_ThrobberSpinningColor));
}
ui::ModalType CardUnmaskPromptViews::GetModalType() const {
return ui::MODAL_TYPE_CHILD;
}
base::string16 CardUnmaskPromptViews::GetWindowTitle() const {
return controller_->GetWindowTitle();
}
void CardUnmaskPromptViews::DeleteDelegate() {
delete this;
}
base::string16 CardUnmaskPromptViews::GetDialogButtonLabel(
ui::DialogButton button) const {
if (button == ui::DIALOG_BUTTON_OK)
return controller_->GetOkButtonLabel();
return DialogDelegateView::GetDialogButtonLabel(button);
}
bool CardUnmaskPromptViews::IsDialogButtonEnabled(
ui::DialogButton button) const {
if (button == ui::DIALOG_BUTTON_CANCEL)
return true;
DCHECK_EQ(ui::DIALOG_BUTTON_OK, button);
return cvc_input_->enabled() &&
controller_->InputCvcIsValid(cvc_input_->text()) &&
ExpirationDateIsValid();
}
views::View* CardUnmaskPromptViews::GetInitiallyFocusedView() {
return cvc_input_;
}
bool CardUnmaskPromptViews::ShouldShowCloseButton() const {
// Material UI has no [X] in the corner of this dialog.
return !ui::MaterialDesignController::IsSecondaryUiMaterial();
}
bool CardUnmaskPromptViews::Cancel() {
return true;
}
bool CardUnmaskPromptViews::Accept() {
if (!controller_)
return true;
controller_->OnUnmaskResponse(
cvc_input_->text(),
month_input_->visible()
? month_input_->GetTextForRow(month_input_->selected_index())
: base::string16(),
year_input_->visible()
? year_input_->GetTextForRow(year_input_->selected_index())
: base::string16(),
storage_checkbox_ ? storage_checkbox_->checked() : false);
return false;
}
void CardUnmaskPromptViews::ContentsChanged(
views::Textfield* sender,
const base::string16& new_contents) {
if (controller_->InputCvcIsValid(new_contents))
cvc_input_->SetInvalid(false);
DialogModelChanged();
}
void CardUnmaskPromptViews::OnPerformAction(views::Combobox* combobox) {
if (ExpirationDateIsValid()) {
if (month_input_->invalid()) {
month_input_->SetInvalid(false);
year_input_->SetInvalid(false);
SetRetriableErrorMessage(base::string16());
}
} else if (month_input_->selected_index() !=
month_combobox_model_.GetDefaultIndex() &&
year_input_->selected_index() !=
year_combobox_model_.GetDefaultIndex()) {
month_input_->SetInvalid(true);
year_input_->SetInvalid(true);
SetRetriableErrorMessage(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE));
}
DialogModelChanged();
}
void CardUnmaskPromptViews::InitIfNecessary() {
if (has_children())
return;
ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
// The main content view is a box layout with two things in it: the permanent
// error label layout, and |main_contents|.
SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kVertical, gfx::Insets()));
// This is a big red-background section at the top of the dialog in case there
// is a permanent error. It is not in an inset layout because the red
// background needs the full width.
permanent_error_label_ = new views::Label();
permanent_error_label_->SetFontList(
rb.GetFontList(ui::ResourceBundle::BoldFont));
permanent_error_label_->SetBorder(views::CreateEmptyBorder(
provider->GetInsetsMetric(views::INSETS_DIALOG_SUBSECTION)));
permanent_error_label_->SetBackground(
views::CreateSolidBackground(kWarningColor));
permanent_error_label_->SetEnabledColor(SK_ColorWHITE);
permanent_error_label_->SetAutoColorReadabilityEnabled(false);
permanent_error_label_->SetVisible(false);
permanent_error_label_->SetMultiLine(true);
permanent_error_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
AddChildView(permanent_error_label_);
// The |main_contents| layout is a FillLayout that will contain the progress
// overlay on top of the actual contents in |controls_container|
// (instructions, input fields).
views::View* main_contents = new views::View();
main_contents->SetLayoutManager(new views::FillLayout());
// Inset the whole main section.
main_contents->SetBorder(views::CreateEmptyBorder(
provider->GetInsetsMetric(views::INSETS_DIALOG)));
AddChildView(main_contents);
controls_container_ = new views::View();
controls_container_->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kVertical, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
main_contents->AddChildView(controls_container_);
// Instruction text of the dialog.
instructions_ = new views::Label(controller_->GetInstructionsMessage());
instructions_->SetEnabledColor(kGreyTextColor);
instructions_->SetMultiLine(true);
instructions_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
controls_container_->AddChildView(instructions_);
// Input row, containing month/year dropdowns if needed and the CVC field.
input_row_ = new views::View();
input_row_->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kHorizontal, gfx::Insets(),
provider->GetDistanceMetric(DISTANCE_RELATED_CONTROL_HORIZONTAL_SMALL)));
// Add the month and year comboboxes if the expiration date is needed.
month_input_ = new views::Combobox(&month_combobox_model_);
month_input_->set_listener(this);
input_row_->AddChildView(month_input_);
year_input_ = new views::Combobox(&year_combobox_model_);
year_input_->set_listener(this);
input_row_->AddChildView(year_input_);
if (!controller_->ShouldRequestExpirationDate()) {
month_input_->SetVisible(false);
year_input_->SetVisible(false);
}
cvc_input_ = CreateCvcTextfield();
cvc_input_->set_controller(this);
input_row_->AddChildView(cvc_input_);
views::ImageView* cvc_image = new views::ImageView();
cvc_image->SetImage(rb.GetImageSkiaNamed(controller_->GetCvcImageRid()));
cvc_image->SetTooltipText(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_CVC_IMAGE_DESCRIPTION));
input_row_->AddChildView(cvc_image);
controls_container_->AddChildView(input_row_);
// Temporary error view, just below the input field(s).
views::View* temporary_error = new views::View();
views::BoxLayout* temporary_error_layout = new views::BoxLayout(
views::BoxLayout::kHorizontal, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_LABEL_HORIZONTAL));
temporary_error->SetLayoutManager(temporary_error_layout);
temporary_error_layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_START);
error_icon_ = new views::ImageView();
error_icon_->SetVisible(false);
error_icon_->SetImage(
gfx::CreateVectorIcon(vector_icons::kWarningIcon, 16, kWarningColor));
temporary_error->AddChildView(error_icon_);
error_label_ = new views::Label();
error_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
error_label_->SetEnabledColor(kWarningColor);
temporary_error->AddChildView(error_label_);
temporary_error_layout->SetFlexForView(error_label_, 1);
controls_container_->AddChildView(temporary_error);
// On top of the main contents, we add the progress overlay and hide it.
progress_overlay_ = new views::View();
views::BoxLayout* progress_layout = new views::BoxLayout(
views::BoxLayout::kHorizontal, gfx::Insets(),
provider->GetDistanceMetric(views::DISTANCE_RELATED_LABEL_HORIZONTAL));
progress_layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
progress_layout->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
progress_overlay_->SetLayoutManager(progress_layout);
progress_overlay_->SetVisible(false);
progress_throbber_ = new views::Throbber();
progress_overlay_->AddChildView(progress_throbber_);
progress_label_ = new views::Label(l10n_util::GetStringUTF16(
IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS));
progress_overlay_->AddChildView(progress_label_);
main_contents->AddChildView(progress_overlay_);
}
bool CardUnmaskPromptViews::ExpirationDateIsValid() const {
if (!controller_->ShouldRequestExpirationDate())
return true;
return controller_->InputExpirationIsValid(
month_input_->GetTextForRow(month_input_->selected_index()),
year_input_->GetTextForRow(year_input_->selected_index()));
}
void CardUnmaskPromptViews::ClosePrompt() {
GetWidget()->Close();
}
} // namespace autofill