// 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/thread_task_runner_handle.h" #include "chrome/browser/ui/autofill/autofill_dialog_types.h" #include "chrome/browser/ui/autofill/create_card_unmask_prompt_view.h" #include "chrome/browser/ui/views/autofill/decorated_textfield.h" #include "chrome/browser/ui/views/autofill/tooltip_icon.h" #include "chrome/grit/generated_resources.h" #include "components/autofill/core/browser/ui/card_unmask_prompt_controller.h" #include "components/constrained_window/constrained_window_views.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 "grit/components_strings.h" #include "grit/theme_resources.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/compositor/compositing_recorder.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/geometry/safe_integer_conversions.h" #include "ui/gfx/paint_vector_icon.h" #include "ui/gfx/vector_icons_public.h" #include "ui/views/background.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/throbber.h" #include "ui/views/layout/box_layout.h" #include "ui/views/widget/widget.h" #include "ui/views/window/dialog_client_view.h" namespace autofill { // The number of pixels of blank space on the outer horizontal edges of the // dialog. const int kEdgePadding = 19; SkColor kGreyTextColor = SkColorSetRGB(0x64, 0x64, 0x64); CardUnmaskPromptView* CreateCardUnmaskPromptView( CardUnmaskPromptController* controller, content::WebContents* web_contents) { return new CardUnmaskPromptViews(controller, web_contents); } CardUnmaskPromptViews::CardUnmaskPromptViews( CardUnmaskPromptController* controller, content::WebContents* web_contents) : controller_(controller), web_contents_(web_contents), main_contents_(nullptr), instructions_(nullptr), permanent_error_label_(nullptr), input_row_(nullptr), cvc_input_(nullptr), month_input_(nullptr), year_input_(nullptr), new_card_link_(nullptr), error_icon_(nullptr), error_label_(nullptr), storage_row_(nullptr), storage_checkbox_(nullptr), progress_overlay_(nullptr), progress_throbber_(nullptr), progress_label_(nullptr), overlay_animation_(this), weak_ptr_factory_(this) { } 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); progress_overlay_->SetAlpha(0); progress_overlay_->SetVisible(true); progress_throbber_->Start(); overlay_animation_.Show(); GetDialogClientView()->UpdateDialogButtons(); 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::Bind(&CardUnmaskPromptViews::ClosePrompt, weak_ptr_factory_.GetWeakPtr()), controller_->GetSuccessMessageDuration()); } else { // TODO(estade): it's somewhat jarring when the error comes back too // quickly. overlay_animation_.Reset(); if (storage_row_) storage_row_->SetAlpha(255); progress_overlay_->SetVisible(false); 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()); } GetDialogClientView()->UpdateDialogButtons(); } 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()); GetDialogClientView()->UpdateDialogButtons(); GetWidget()->UpdateWindowTitle(); instructions_->SetText(controller_->GetInstructionsMessage()); SetRetriableErrorMessage(base::string16()); } void CardUnmaskPromptViews::SetRetriableErrorMessage( const base::string16& message) { if (message.empty()) { error_label_->SetMultiLine(false); error_label_->SetText(base::ASCIIToUTF16(" ")); error_icon_->SetVisible(false); } else { error_label_->SetMultiLine(true); error_label_->SetText(message); error_icon_->SetVisible(true); } // 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::Border::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 FadeOutView(); views::BoxLayout* storage_row_layout = new views::BoxLayout( views::BoxLayout::kHorizontal, kEdgePadding, kEdgePadding, 0); storage_row_->SetLayoutManager(storage_row_layout); storage_row_->SetBorder( views::Border::CreateSolidSidedBorder(1, 0, 0, 0, kSubtleBorderColor)); storage_row_->set_background( views::Background::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); storage_row_->AddChildView(new TooltipIcon(l10n_util::GetStringUTF16( IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP))); return storage_row_; } gfx::Size CardUnmaskPromptViews::GetPreferredSize() const { // Must hardcode a width so the label knows where to wrap. const int kWidth = 375; return gfx::Size(kWidth, GetHeightForWidth(kWidth)); } void CardUnmaskPromptViews::Layout() { gfx::Rect contents_bounds = GetContentsBounds(); main_contents_->SetBoundsRect(contents_bounds); // The progress overlay extends from the top of the input row // to the bottom of the content area. gfx::RectF input_rect = gfx::RectF(input_row_->GetContentsBounds()); View::ConvertRectToTarget(input_row_, this, &input_rect); input_rect.set_height(contents_bounds.height()); contents_bounds.Intersect(gfx::ToNearestRect(input_rect)); progress_overlay_->SetBoundsRect(contents_bounds); } int CardUnmaskPromptViews::GetHeightForWidth(int width) const { if (!has_children()) return 0; const gfx::Insets insets = GetInsets(); return main_contents_->GetHeightForWidth(width - insets.width()) + insets.height(); } void CardUnmaskPromptViews::OnNativeThemeChanged(const ui::NativeTheme* theme) { SkColor bg_color = theme->GetSystemColor(ui::NativeTheme::kColorId_DialogBackground); progress_overlay_->set_background( views::Background::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 l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON); return DialogDelegateView::GetDialogButtonLabel(button); } bool CardUnmaskPromptViews::ShouldDefaultButtonBeBlue() const { return true; } 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::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); GetDialogClientView()->UpdateDialogButtons(); } 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)); } GetDialogClientView()->UpdateDialogButtons(); } void CardUnmaskPromptViews::AnimationProgressed( const gfx::Animation* animation) { uint8_t alpha = static_cast<uint8_t>(animation->CurrentValueBetween(0, 255)); progress_overlay_->SetAlpha(alpha); if (storage_row_) storage_row_->SetAlpha(255 - alpha); } void CardUnmaskPromptViews::InitIfNecessary() { if (has_children()) return; main_contents_ = new views::View(); main_contents_->SetLayoutManager( new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 12)); AddChildView(main_contents_); permanent_error_label_ = new views::Label(); ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); permanent_error_label_->SetFontList( rb.GetFontList(ui::ResourceBundle::BoldFont)); permanent_error_label_->set_background( views::Background::CreateSolidBackground(kWarningColor)); permanent_error_label_->SetBorder( views::Border::CreateEmptyBorder(12, kEdgePadding, 12, kEdgePadding)); 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); main_contents_->AddChildView(permanent_error_label_); views::View* controls_container = new views::View(); controls_container->SetLayoutManager( new views::BoxLayout(views::BoxLayout::kVertical, kEdgePadding, 0, 0)); main_contents_->AddChildView(controls_container); instructions_ = new views::Label(controller_->GetInstructionsMessage()); instructions_->SetEnabledColor(kGreyTextColor); instructions_->SetMultiLine(true); instructions_->SetHorizontalAlignment(gfx::ALIGN_LEFT); instructions_->SetBorder(views::Border::CreateEmptyBorder(0, 0, 16, 0)); controls_container->AddChildView(instructions_); input_row_ = new views::View(); input_row_->SetLayoutManager( new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 5)); controls_container->AddChildView(input_row_); month_input_ = new views::Combobox(&month_combobox_model_); month_input_->set_listener(this); input_row_->AddChildView(month_input_); views::Label* separator = new views::Label(l10n_util::GetStringUTF16( IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR)); separator->SetEnabledColor(kGreyTextColor); input_row_->AddChildView(separator); year_input_ = new views::Combobox(&year_combobox_model_); year_input_->set_listener(this); input_row_->AddChildView(year_input_); input_row_->AddChildView(new views::Label(base::ASCIIToUTF16(" "))); // Hide all of the above as appropriate. if (!controller_->ShouldRequestExpirationDate()) { for (int i = 0; i < input_row_->child_count(); ++i) input_row_->child_at(i)->SetVisible(false); } cvc_input_ = new DecoratedTextfield( base::string16(), l10n_util::GetStringUTF16(IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC), this); cvc_input_->set_default_width_in_chars(8); input_row_->AddChildView(cvc_input_); views::ImageView* cvc_image = new views::ImageView(); cvc_image->SetImage(rb.GetImageSkiaNamed(controller_->GetCvcImageRid())); input_row_->AddChildView(cvc_image); views::View* temporary_error = new views::View(); views::BoxLayout* temporary_error_layout = new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 4); temporary_error->SetLayoutManager(temporary_error_layout); temporary_error_layout->set_cross_axis_alignment( views::BoxLayout::CROSS_AXIS_ALIGNMENT_START); temporary_error->SetBorder(views::Border::CreateEmptyBorder(8, 0, 0, 0)); controls_container->AddChildView(temporary_error); error_icon_ = new views::ImageView(); error_icon_->SetVisible(false); error_icon_->SetImage(gfx::CreateVectorIcon(gfx::VectorIconId::WARNING, 16, gfx::kGoogleRed700)); temporary_error->AddChildView(error_icon_); // Reserve vertical space for the error label, assuming it's one line. error_label_ = new views::Label(base::ASCIIToUTF16(" ")); error_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); error_label_->SetEnabledColor(kWarningColor); temporary_error->AddChildView(error_label_); temporary_error_layout->SetFlexForView(error_label_, 1); progress_overlay_ = new FadeOutView(); progress_overlay_->set_fade_everything(true); views::BoxLayout* progress_layout = new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 5); 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); AddChildView(progress_overlay_); 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_); } 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(); } CardUnmaskPromptViews::FadeOutView::FadeOutView() : fade_everything_(false), alpha_(255) { } CardUnmaskPromptViews::FadeOutView::~FadeOutView() { } void CardUnmaskPromptViews::FadeOutView::PaintChildren( const ui::PaintContext& context) { const bool kLcdTextRequiresOpaqueLayer = true; ui::CompositingRecorder recorder(context, size(), alpha_, kLcdTextRequiresOpaqueLayer); views::View::PaintChildren(context); } void CardUnmaskPromptViews::FadeOutView::OnPaint(gfx::Canvas* canvas) { if (!fade_everything_ || alpha_ == 255) return views::View::OnPaint(canvas); canvas->SaveLayerAlpha(alpha_); views::View::OnPaint(canvas); canvas->Restore(); } void CardUnmaskPromptViews::FadeOutView::SetAlpha(uint8_t alpha) { alpha_ = alpha; SchedulePaint(); } } // namespace autofill