1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/views/autofill/card_unmask_prompt_views.h"
7 #include "base/basictypes.h"
8 #include "base/location.h"
9 #include "base/single_thread_task_runner.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "base/thread_task_runner_handle.h"
12 #include "chrome/browser/ui/autofill/autofill_dialog_types.h"
13 #include "chrome/browser/ui/autofill/create_card_unmask_prompt_view.h"
14 #include "chrome/browser/ui/views/autofill/decorated_textfield.h"
15 #include "chrome/browser/ui/views/autofill/tooltip_icon.h"
16 #include "chrome/grit/generated_resources.h"
17 #include "components/autofill/core/browser/ui/card_unmask_prompt_controller.h"
18 #include "components/constrained_window/constrained_window_views.h"
19 #include "components/web_modal/web_contents_modal_dialog_host.h"
20 #include "components/web_modal/web_contents_modal_dialog_manager.h"
21 #include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
22 #include "grit/theme_resources.h"
23 #include "third_party/skia/include/core/SkColor.h"
24 #include "ui/base/l10n/l10n_util.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/compositor/compositing_recorder.h"
27 #include "ui/gfx/canvas.h"
28 #include "ui/gfx/geometry/safe_integer_conversions.h"
29 #include "ui/gfx/paint_vector_icon.h"
30 #include "ui/gfx/vector_icons_public.h"
31 #include "ui/views/background.h"
32 #include "ui/views/controls/button/checkbox.h"
33 #include "ui/views/controls/combobox/combobox.h"
34 #include "ui/views/controls/image_view.h"
35 #include "ui/views/controls/label.h"
36 #include "ui/views/controls/link.h"
37 #include "ui/views/controls/throbber.h"
38 #include "ui/views/layout/box_layout.h"
39 #include "ui/views/widget/widget.h"
40 #include "ui/views/window/dialog_client_view.h"
44 // The number of pixels of blank space on the outer horizontal edges of the
46 const int kEdgePadding
= 19;
48 SkColor kGreyTextColor
= SkColorSetRGB(0x64, 0x64, 0x64);
50 CardUnmaskPromptView
* CreateCardUnmaskPromptView(
51 CardUnmaskPromptController
* controller
,
52 content::WebContents
* web_contents
) {
53 return new CardUnmaskPromptViews(controller
, web_contents
);
56 CardUnmaskPromptViews::CardUnmaskPromptViews(
57 CardUnmaskPromptController
* controller
,
58 content::WebContents
* web_contents
)
59 : controller_(controller
),
60 web_contents_(web_contents
),
61 main_contents_(nullptr),
62 instructions_(nullptr),
63 permanent_error_label_(nullptr),
66 month_input_(nullptr),
68 new_card_link_(nullptr),
70 error_label_(nullptr),
71 storage_row_(nullptr),
72 storage_checkbox_(nullptr),
73 progress_overlay_(nullptr),
74 progress_throbber_(nullptr),
75 progress_label_(nullptr),
76 overlay_animation_(this),
77 weak_ptr_factory_(this) {
80 CardUnmaskPromptViews::~CardUnmaskPromptViews() {
82 controller_
->OnUnmaskDialogClosed();
85 void CardUnmaskPromptViews::Show() {
86 constrained_window::ShowWebModalDialogViews(this, web_contents_
);
89 void CardUnmaskPromptViews::ControllerGone() {
90 controller_
= nullptr;
94 void CardUnmaskPromptViews::DisableAndWaitForVerification() {
95 SetInputsEnabled(false);
96 progress_overlay_
->SetAlpha(0);
97 progress_overlay_
->SetVisible(true);
98 progress_throbber_
->Start();
99 overlay_animation_
.Show();
100 GetDialogClientView()->UpdateDialogButtons();
104 void CardUnmaskPromptViews::GotVerificationResult(
105 const base::string16
& error_message
,
107 progress_throbber_
->Stop();
108 if (error_message
.empty()) {
109 progress_label_
->SetText(l10n_util::GetStringUTF16(
110 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS
));
111 progress_throbber_
->SetChecked(true);
112 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
113 FROM_HERE
, base::Bind(&CardUnmaskPromptViews::ClosePrompt
,
114 weak_ptr_factory_
.GetWeakPtr()),
115 controller_
->GetSuccessMessageDuration());
117 // TODO(estade): it's somewhat jarring when the error comes back too
119 overlay_animation_
.Reset();
121 storage_row_
->SetAlpha(255);
122 progress_overlay_
->SetVisible(false);
125 SetInputsEnabled(true);
127 if (!controller_
->ShouldRequestExpirationDate()) {
128 // If there is more than one input showing, don't mark anything as
129 // invalid since we don't know the location of the problem.
130 cvc_input_
->SetInvalid(true);
132 // Show a "New card?" link, which when clicked will cause us to ask
133 // for expiration date.
137 // TODO(estade): When do we hide |error_label_|?
138 SetRetriableErrorMessage(error_message
);
140 permanent_error_label_
->SetText(error_message
);
141 permanent_error_label_
->SetVisible(true);
142 SetRetriableErrorMessage(base::string16());
144 GetDialogClientView()->UpdateDialogButtons();
150 void CardUnmaskPromptViews::LinkClicked(views::Link
* source
, int event_flags
) {
151 DCHECK_EQ(source
, new_card_link_
);
152 controller_
->NewCardLinkClicked();
153 for (int i
= 0; i
< input_row_
->child_count(); ++i
)
154 input_row_
->child_at(i
)->SetVisible(true);
156 new_card_link_
->SetVisible(false);
157 input_row_
->InvalidateLayout();
158 cvc_input_
->SetInvalid(false);
159 cvc_input_
->SetText(base::string16());
160 GetDialogClientView()->UpdateDialogButtons();
161 GetWidget()->UpdateWindowTitle();
162 instructions_
->SetText(controller_
->GetInstructionsMessage());
163 SetRetriableErrorMessage(base::string16());
166 void CardUnmaskPromptViews::SetRetriableErrorMessage(
167 const base::string16
& message
) {
168 if (message
.empty()) {
169 error_label_
->SetMultiLine(false);
170 error_label_
->SetText(base::ASCIIToUTF16(" "));
171 error_icon_
->SetVisible(false);
173 error_label_
->SetMultiLine(true);
174 error_label_
->SetText(message
);
175 error_icon_
->SetVisible(true);
178 // Update the dialog's size.
179 if (GetWidget() && web_contents_
) {
180 constrained_window::UpdateWebContentsModalDialogPosition(
182 web_modal::WebContentsModalDialogManager::FromWebContents(web_contents_
)
184 ->GetWebContentsModalDialogHost());
190 void CardUnmaskPromptViews::SetInputsEnabled(bool enabled
) {
191 cvc_input_
->SetEnabled(enabled
);
192 if (storage_checkbox_
)
193 storage_checkbox_
->SetEnabled(enabled
);
194 month_input_
->SetEnabled(enabled
);
195 year_input_
->SetEnabled(enabled
);
198 void CardUnmaskPromptViews::ShowNewCardLink() {
202 new_card_link_
= new views::Link(
203 l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_NEW_CARD_LINK
));
204 new_card_link_
->SetBorder(views::Border::CreateEmptyBorder(0, 7, 0, 0));
205 new_card_link_
->SetUnderline(false);
206 new_card_link_
->set_listener(this);
207 input_row_
->AddChildView(new_card_link_
);
210 views::View
* CardUnmaskPromptViews::GetContentsView() {
215 views::View
* CardUnmaskPromptViews::CreateFootnoteView() {
216 if (!controller_
->CanStoreLocally())
219 // Local storage checkbox and (?) tooltip.
220 storage_row_
= new FadeOutView();
221 views::BoxLayout
* storage_row_layout
= new views::BoxLayout(
222 views::BoxLayout::kHorizontal
, kEdgePadding
, kEdgePadding
, 0);
223 storage_row_
->SetLayoutManager(storage_row_layout
);
224 storage_row_
->SetBorder(
225 views::Border::CreateSolidSidedBorder(1, 0, 0, 0, kSubtleBorderColor
));
226 storage_row_
->set_background(
227 views::Background::CreateSolidBackground(kLightShadingColor
));
229 storage_checkbox_
= new views::Checkbox(l10n_util::GetStringUTF16(
230 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX
));
231 storage_checkbox_
->SetChecked(controller_
->GetStoreLocallyStartState());
232 storage_row_
->AddChildView(storage_checkbox_
);
233 storage_row_layout
->SetFlexForView(storage_checkbox_
, 1);
235 storage_row_
->AddChildView(new TooltipIcon(l10n_util::GetStringUTF16(
236 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP
)));
241 gfx::Size
CardUnmaskPromptViews::GetPreferredSize() const {
242 // Must hardcode a width so the label knows where to wrap.
243 const int kWidth
= 375;
244 return gfx::Size(kWidth
, GetHeightForWidth(kWidth
));
247 void CardUnmaskPromptViews::Layout() {
248 gfx::Rect contents_bounds
= GetContentsBounds();
249 main_contents_
->SetBoundsRect(contents_bounds
);
251 // The progress overlay extends from the top of the input row
252 // to the bottom of the content area.
253 gfx::RectF input_rect
= input_row_
->GetContentsBounds();
254 View::ConvertRectToTarget(input_row_
, this, &input_rect
);
255 input_rect
.set_height(contents_bounds
.height());
256 contents_bounds
.Intersect(gfx::ToNearestRect(input_rect
));
257 progress_overlay_
->SetBoundsRect(contents_bounds
);
260 int CardUnmaskPromptViews::GetHeightForWidth(int width
) const {
263 const gfx::Insets insets
= GetInsets();
264 return main_contents_
->GetHeightForWidth(width
- insets
.width()) +
268 void CardUnmaskPromptViews::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
270 theme
->GetSystemColor(ui::NativeTheme::kColorId_DialogBackground
);
271 progress_overlay_
->set_background(
272 views::Background::CreateSolidBackground(bg_color
));
273 progress_label_
->SetBackgroundColor(bg_color
);
274 progress_label_
->SetEnabledColor(theme
->GetSystemColor(
275 ui::NativeTheme::kColorId_ThrobberSpinningColor
));
278 ui::ModalType
CardUnmaskPromptViews::GetModalType() const {
279 return ui::MODAL_TYPE_CHILD
;
282 base::string16
CardUnmaskPromptViews::GetWindowTitle() const {
283 return controller_
->GetWindowTitle();
286 void CardUnmaskPromptViews::DeleteDelegate() {
290 int CardUnmaskPromptViews::GetDialogButtons() const {
291 return ui::DIALOG_BUTTON_OK
| ui::DIALOG_BUTTON_CANCEL
;
294 base::string16
CardUnmaskPromptViews::GetDialogButtonLabel(
295 ui::DialogButton button
) const {
296 if (button
== ui::DIALOG_BUTTON_OK
)
297 return l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON
);
299 return DialogDelegateView::GetDialogButtonLabel(button
);
302 bool CardUnmaskPromptViews::ShouldDefaultButtonBeBlue() const {
306 bool CardUnmaskPromptViews::IsDialogButtonEnabled(
307 ui::DialogButton button
) const {
308 if (button
== ui::DIALOG_BUTTON_CANCEL
)
311 DCHECK_EQ(ui::DIALOG_BUTTON_OK
, button
);
313 return cvc_input_
->enabled() &&
314 controller_
->InputCvcIsValid(cvc_input_
->text()) &&
315 ExpirationDateIsValid();
318 views::View
* CardUnmaskPromptViews::GetInitiallyFocusedView() {
322 bool CardUnmaskPromptViews::Cancel() {
326 bool CardUnmaskPromptViews::Accept() {
330 controller_
->OnUnmaskResponse(
332 month_input_
->visible()
333 ? month_input_
->GetTextForRow(month_input_
->selected_index())
335 year_input_
->visible()
336 ? year_input_
->GetTextForRow(year_input_
->selected_index())
338 storage_checkbox_
? storage_checkbox_
->checked() : false);
342 void CardUnmaskPromptViews::ContentsChanged(
343 views::Textfield
* sender
,
344 const base::string16
& new_contents
) {
345 if (controller_
->InputCvcIsValid(new_contents
))
346 cvc_input_
->SetInvalid(false);
348 GetDialogClientView()->UpdateDialogButtons();
351 void CardUnmaskPromptViews::OnPerformAction(views::Combobox
* combobox
) {
352 if (ExpirationDateIsValid()) {
353 if (month_input_
->invalid()) {
354 month_input_
->SetInvalid(false);
355 year_input_
->SetInvalid(false);
356 SetRetriableErrorMessage(base::string16());
358 } else if (month_input_
->selected_index() !=
359 month_combobox_model_
.GetDefaultIndex() &&
360 year_input_
->selected_index() !=
361 year_combobox_model_
.GetDefaultIndex()) {
362 month_input_
->SetInvalid(true);
363 year_input_
->SetInvalid(true);
364 SetRetriableErrorMessage(l10n_util::GetStringUTF16(
365 IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE
));
368 GetDialogClientView()->UpdateDialogButtons();
371 void CardUnmaskPromptViews::AnimationProgressed(
372 const gfx::Animation
* animation
) {
373 uint8_t alpha
= static_cast<uint8_t>(animation
->CurrentValueBetween(0, 255));
374 progress_overlay_
->SetAlpha(alpha
);
376 storage_row_
->SetAlpha(255 - alpha
);
379 void CardUnmaskPromptViews::InitIfNecessary() {
383 main_contents_
= new views::View();
384 main_contents_
->SetLayoutManager(
385 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 12));
386 AddChildView(main_contents_
);
388 permanent_error_label_
= new views::Label();
389 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
390 permanent_error_label_
->SetFontList(
391 rb
.GetFontList(ui::ResourceBundle::BoldFont
));
392 permanent_error_label_
->set_background(
393 views::Background::CreateSolidBackground(kWarningColor
));
394 permanent_error_label_
->SetBorder(
395 views::Border::CreateEmptyBorder(12, kEdgePadding
, 12, kEdgePadding
));
396 permanent_error_label_
->SetEnabledColor(SK_ColorWHITE
);
397 permanent_error_label_
->SetAutoColorReadabilityEnabled(false);
398 permanent_error_label_
->SetVisible(false);
399 permanent_error_label_
->SetMultiLine(true);
400 permanent_error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
401 main_contents_
->AddChildView(permanent_error_label_
);
403 views::View
* controls_container
= new views::View();
404 controls_container
->SetLayoutManager(
405 new views::BoxLayout(views::BoxLayout::kVertical
, kEdgePadding
, 0, 0));
406 main_contents_
->AddChildView(controls_container
);
408 instructions_
= new views::Label(controller_
->GetInstructionsMessage());
409 instructions_
->SetEnabledColor(kGreyTextColor
);
410 instructions_
->SetMultiLine(true);
411 instructions_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
412 instructions_
->SetBorder(views::Border::CreateEmptyBorder(0, 0, 16, 0));
413 controls_container
->AddChildView(instructions_
);
415 input_row_
= new views::View();
416 input_row_
->SetLayoutManager(
417 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5));
418 controls_container
->AddChildView(input_row_
);
420 month_input_
= new views::Combobox(&month_combobox_model_
);
421 month_input_
->set_listener(this);
422 input_row_
->AddChildView(month_input_
);
423 views::Label
* separator
= new views::Label(l10n_util::GetStringUTF16(
424 IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR
));
425 separator
->SetEnabledColor(kGreyTextColor
);
426 input_row_
->AddChildView(separator
);
427 year_input_
= new views::Combobox(&year_combobox_model_
);
428 year_input_
->set_listener(this);
429 input_row_
->AddChildView(year_input_
);
430 input_row_
->AddChildView(new views::Label(base::ASCIIToUTF16(" ")));
431 // Hide all of the above as appropriate.
432 if (!controller_
->ShouldRequestExpirationDate()) {
433 for (int i
= 0; i
< input_row_
->child_count(); ++i
)
434 input_row_
->child_at(i
)->SetVisible(false);
437 cvc_input_
= new DecoratedTextfield(
439 l10n_util::GetStringUTF16(IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC
), this);
440 cvc_input_
->set_default_width_in_chars(8);
441 input_row_
->AddChildView(cvc_input_
);
443 views::ImageView
* cvc_image
= new views::ImageView();
444 cvc_image
->SetImage(rb
.GetImageSkiaNamed(controller_
->GetCvcImageRid()));
445 input_row_
->AddChildView(cvc_image
);
447 views::View
* temporary_error
= new views::View();
448 views::BoxLayout
* temporary_error_layout
=
449 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 4);
450 temporary_error
->SetLayoutManager(temporary_error_layout
);
451 temporary_error_layout
->set_cross_axis_alignment(
452 views::BoxLayout::CROSS_AXIS_ALIGNMENT_START
);
453 temporary_error
->SetBorder(views::Border::CreateEmptyBorder(8, 0, 0, 0));
454 controls_container
->AddChildView(temporary_error
);
456 error_icon_
= new views::ImageView();
457 error_icon_
->SetVisible(false);
458 // TODO(estade): revisit this color.
459 error_icon_
->SetImage(gfx::CreateVectorIcon(gfx::VectorIconId::WARNING
, 16,
460 SkColorSetRGB(0xDB, 0x44, 0x37)));
461 temporary_error
->AddChildView(error_icon_
);
463 // Reserve vertical space for the error label, assuming it's one line.
464 error_label_
= new views::Label(base::ASCIIToUTF16(" "));
465 error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
466 error_label_
->SetEnabledColor(kWarningColor
);
467 temporary_error
->AddChildView(error_label_
);
468 temporary_error_layout
->SetFlexForView(error_label_
, 1);
470 progress_overlay_
= new FadeOutView();
471 progress_overlay_
->set_fade_everything(true);
472 views::BoxLayout
* progress_layout
=
473 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5);
474 progress_layout
->set_cross_axis_alignment(
475 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER
);
476 progress_layout
->set_main_axis_alignment(
477 views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER
);
478 progress_overlay_
->SetLayoutManager(progress_layout
);
480 progress_overlay_
->SetVisible(false);
481 AddChildView(progress_overlay_
);
483 progress_throbber_
= new views::Throbber();
484 progress_overlay_
->AddChildView(progress_throbber_
);
486 progress_label_
= new views::Label(l10n_util::GetStringUTF16(
487 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS
));
488 progress_overlay_
->AddChildView(progress_label_
);
491 bool CardUnmaskPromptViews::ExpirationDateIsValid() const {
492 if (!controller_
->ShouldRequestExpirationDate())
495 return controller_
->InputExpirationIsValid(
496 month_input_
->GetTextForRow(month_input_
->selected_index()),
497 year_input_
->GetTextForRow(year_input_
->selected_index()));
500 void CardUnmaskPromptViews::ClosePrompt() {
501 GetWidget()->Close();
504 CardUnmaskPromptViews::FadeOutView::FadeOutView()
505 : fade_everything_(false), alpha_(255) {
507 CardUnmaskPromptViews::FadeOutView::~FadeOutView() {
510 void CardUnmaskPromptViews::FadeOutView::PaintChildren(
511 const ui::PaintContext
& context
) {
512 ui::CompositingRecorder
recorder(context
, alpha_
);
513 views::View::PaintChildren(context
);
516 void CardUnmaskPromptViews::FadeOutView::OnPaint(gfx::Canvas
* canvas
) {
517 if (!fade_everything_
|| alpha_
== 255)
518 return views::View::OnPaint(canvas
);
520 canvas
->SaveLayerAlpha(alpha_
);
521 views::View::OnPaint(canvas
);
525 void CardUnmaskPromptViews::FadeOutView::SetAlpha(uint8_t alpha
) {
530 } // namespace autofill