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/views/background.h"
30 #include "ui/views/controls/button/checkbox.h"
31 #include "ui/views/controls/combobox/combobox.h"
32 #include "ui/views/controls/image_view.h"
33 #include "ui/views/controls/label.h"
34 #include "ui/views/controls/link.h"
35 #include "ui/views/controls/throbber.h"
36 #include "ui/views/layout/box_layout.h"
37 #include "ui/views/widget/widget.h"
38 #include "ui/views/window/dialog_client_view.h"
42 // The number of pixels of blank space on the outer horizontal edges of the
44 const int kEdgePadding
= 19;
46 SkColor kGreyTextColor
= SkColorSetRGB(0x64, 0x64, 0x64);
48 CardUnmaskPromptView
* CreateCardUnmaskPromptView(
49 CardUnmaskPromptController
* controller
,
50 content::WebContents
* web_contents
) {
51 return new CardUnmaskPromptViews(controller
, web_contents
);
54 CardUnmaskPromptViews::CardUnmaskPromptViews(
55 CardUnmaskPromptController
* controller
,
56 content::WebContents
* web_contents
)
57 : controller_(controller
),
58 web_contents_(web_contents
),
59 main_contents_(nullptr),
60 instructions_(nullptr),
61 permanent_error_label_(nullptr),
64 month_input_(nullptr),
66 new_card_link_(nullptr),
68 error_label_(nullptr),
69 storage_row_(nullptr),
70 storage_checkbox_(nullptr),
71 progress_overlay_(nullptr),
72 progress_throbber_(nullptr),
73 progress_label_(nullptr),
74 overlay_animation_(this),
75 weak_ptr_factory_(this) {
78 CardUnmaskPromptViews::~CardUnmaskPromptViews() {
80 controller_
->OnUnmaskDialogClosed();
83 void CardUnmaskPromptViews::Show() {
84 constrained_window::ShowWebModalDialogViews(this, web_contents_
);
87 void CardUnmaskPromptViews::ControllerGone() {
88 controller_
= nullptr;
92 void CardUnmaskPromptViews::DisableAndWaitForVerification() {
93 SetInputsEnabled(false);
94 progress_overlay_
->SetAlpha(0);
95 progress_overlay_
->SetVisible(true);
96 progress_throbber_
->Start();
97 overlay_animation_
.Show();
98 GetDialogClientView()->UpdateDialogButtons();
102 void CardUnmaskPromptViews::GotVerificationResult(
103 const base::string16
& error_message
,
105 progress_throbber_
->Stop();
106 if (error_message
.empty()) {
107 progress_label_
->SetText(l10n_util::GetStringUTF16(
108 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_SUCCESS
));
109 progress_throbber_
->SetChecked(true);
110 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
111 FROM_HERE
, base::Bind(&CardUnmaskPromptViews::ClosePrompt
,
112 weak_ptr_factory_
.GetWeakPtr()),
113 controller_
->GetSuccessMessageDuration());
115 // TODO(estade): it's somewhat jarring when the error comes back too
117 overlay_animation_
.Reset();
119 storage_row_
->SetAlpha(255);
120 progress_overlay_
->SetVisible(false);
123 SetInputsEnabled(true);
125 if (!controller_
->ShouldRequestExpirationDate()) {
126 // If there is more than one input showing, don't mark anything as
127 // invalid since we don't know the location of the problem.
128 cvc_input_
->SetInvalid(true);
130 // Show a "New card?" link, which when clicked will cause us to ask
131 // for expiration date.
135 // TODO(estade): When do we hide |error_label_|?
136 SetRetriableErrorMessage(error_message
);
138 permanent_error_label_
->SetText(error_message
);
139 permanent_error_label_
->SetVisible(true);
140 SetRetriableErrorMessage(base::string16());
142 GetDialogClientView()->UpdateDialogButtons();
148 void CardUnmaskPromptViews::LinkClicked(views::Link
* source
, int event_flags
) {
149 DCHECK_EQ(source
, new_card_link_
);
150 controller_
->NewCardLinkClicked();
151 for (int i
= 0; i
< input_row_
->child_count(); ++i
)
152 input_row_
->child_at(i
)->SetVisible(true);
154 new_card_link_
->SetVisible(false);
155 input_row_
->InvalidateLayout();
156 cvc_input_
->SetInvalid(false);
157 cvc_input_
->SetText(base::string16());
158 GetDialogClientView()->UpdateDialogButtons();
159 GetWidget()->UpdateWindowTitle();
160 instructions_
->SetText(controller_
->GetInstructionsMessage());
161 SetRetriableErrorMessage(base::string16());
164 void CardUnmaskPromptViews::SetRetriableErrorMessage(
165 const base::string16
& message
) {
166 if (message
.empty()) {
167 error_label_
->SetMultiLine(false);
168 error_label_
->SetText(base::ASCIIToUTF16(" "));
169 error_icon_
->SetVisible(false);
171 error_label_
->SetMultiLine(true);
172 error_label_
->SetText(message
);
173 error_icon_
->SetVisible(true);
176 // Update the dialog's size.
177 if (GetWidget() && web_contents_
) {
178 constrained_window::UpdateWebContentsModalDialogPosition(
180 web_modal::WebContentsModalDialogManager::FromWebContents(web_contents_
)
182 ->GetWebContentsModalDialogHost());
188 void CardUnmaskPromptViews::SetInputsEnabled(bool enabled
) {
189 cvc_input_
->SetEnabled(enabled
);
190 if (storage_checkbox_
)
191 storage_checkbox_
->SetEnabled(enabled
);
192 month_input_
->SetEnabled(enabled
);
193 year_input_
->SetEnabled(enabled
);
196 void CardUnmaskPromptViews::ShowNewCardLink() {
200 new_card_link_
= new views::Link(
201 l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_NEW_CARD_LINK
));
202 new_card_link_
->SetBorder(views::Border::CreateEmptyBorder(0, 7, 0, 0));
203 new_card_link_
->SetUnderline(false);
204 new_card_link_
->set_listener(this);
205 input_row_
->AddChildView(new_card_link_
);
208 views::View
* CardUnmaskPromptViews::GetContentsView() {
213 views::View
* CardUnmaskPromptViews::CreateFootnoteView() {
214 if (!controller_
->CanStoreLocally())
217 // Local storage checkbox and (?) tooltip.
218 storage_row_
= new FadeOutView();
219 views::BoxLayout
* storage_row_layout
= new views::BoxLayout(
220 views::BoxLayout::kHorizontal
, kEdgePadding
, kEdgePadding
, 0);
221 storage_row_
->SetLayoutManager(storage_row_layout
);
222 storage_row_
->SetBorder(
223 views::Border::CreateSolidSidedBorder(1, 0, 0, 0, kSubtleBorderColor
));
224 storage_row_
->set_background(
225 views::Background::CreateSolidBackground(kLightShadingColor
));
227 storage_checkbox_
= new views::Checkbox(l10n_util::GetStringUTF16(
228 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_CHECKBOX
));
229 storage_checkbox_
->SetChecked(controller_
->GetStoreLocallyStartState());
230 storage_row_
->AddChildView(storage_checkbox_
);
231 storage_row_layout
->SetFlexForView(storage_checkbox_
, 1);
233 storage_row_
->AddChildView(new TooltipIcon(l10n_util::GetStringUTF16(
234 IDS_AUTOFILL_CARD_UNMASK_PROMPT_STORAGE_TOOLTIP
)));
239 gfx::Size
CardUnmaskPromptViews::GetPreferredSize() const {
240 // Must hardcode a width so the label knows where to wrap.
241 const int kWidth
= 375;
242 return gfx::Size(kWidth
, GetHeightForWidth(kWidth
));
245 void CardUnmaskPromptViews::Layout() {
246 gfx::Rect contents_bounds
= GetContentsBounds();
247 main_contents_
->SetBoundsRect(contents_bounds
);
249 // The progress overlay extends from the top of the input row
250 // to the bottom of the content area.
251 gfx::RectF input_rect
= input_row_
->GetContentsBounds();
252 View::ConvertRectToTarget(input_row_
, this, &input_rect
);
253 input_rect
.set_height(contents_bounds
.height());
254 contents_bounds
.Intersect(gfx::ToNearestRect(input_rect
));
255 progress_overlay_
->SetBoundsRect(contents_bounds
);
258 int CardUnmaskPromptViews::GetHeightForWidth(int width
) const {
261 const gfx::Insets insets
= GetInsets();
262 return main_contents_
->GetHeightForWidth(width
- insets
.width()) +
266 void CardUnmaskPromptViews::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
268 theme
->GetSystemColor(ui::NativeTheme::kColorId_DialogBackground
);
269 progress_overlay_
->set_background(
270 views::Background::CreateSolidBackground(bg_color
));
271 progress_label_
->SetBackgroundColor(bg_color
);
272 progress_label_
->SetEnabledColor(theme
->GetSystemColor(
273 ui::NativeTheme::kColorId_ThrobberSpinningColor
));
276 ui::ModalType
CardUnmaskPromptViews::GetModalType() const {
277 return ui::MODAL_TYPE_CHILD
;
280 base::string16
CardUnmaskPromptViews::GetWindowTitle() const {
281 return controller_
->GetWindowTitle();
284 void CardUnmaskPromptViews::DeleteDelegate() {
288 int CardUnmaskPromptViews::GetDialogButtons() const {
289 return ui::DIALOG_BUTTON_OK
| ui::DIALOG_BUTTON_CANCEL
;
292 base::string16
CardUnmaskPromptViews::GetDialogButtonLabel(
293 ui::DialogButton button
) const {
294 if (button
== ui::DIALOG_BUTTON_OK
)
295 return l10n_util::GetStringUTF16(IDS_AUTOFILL_CARD_UNMASK_CONFIRM_BUTTON
);
297 return DialogDelegateView::GetDialogButtonLabel(button
);
300 bool CardUnmaskPromptViews::ShouldDefaultButtonBeBlue() const {
304 bool CardUnmaskPromptViews::IsDialogButtonEnabled(
305 ui::DialogButton button
) const {
306 if (button
== ui::DIALOG_BUTTON_CANCEL
)
309 DCHECK_EQ(ui::DIALOG_BUTTON_OK
, button
);
311 return cvc_input_
->enabled() &&
312 controller_
->InputCvcIsValid(cvc_input_
->text()) &&
313 ExpirationDateIsValid();
316 views::View
* CardUnmaskPromptViews::GetInitiallyFocusedView() {
320 bool CardUnmaskPromptViews::Cancel() {
324 bool CardUnmaskPromptViews::Accept() {
328 controller_
->OnUnmaskResponse(
330 month_input_
->visible()
331 ? month_input_
->GetTextForRow(month_input_
->selected_index())
333 year_input_
->visible()
334 ? year_input_
->GetTextForRow(year_input_
->selected_index())
336 storage_checkbox_
? storage_checkbox_
->checked() : false);
340 void CardUnmaskPromptViews::ContentsChanged(
341 views::Textfield
* sender
,
342 const base::string16
& new_contents
) {
343 if (controller_
->InputCvcIsValid(new_contents
))
344 cvc_input_
->SetInvalid(false);
346 GetDialogClientView()->UpdateDialogButtons();
349 void CardUnmaskPromptViews::OnPerformAction(views::Combobox
* combobox
) {
350 if (ExpirationDateIsValid()) {
351 if (month_input_
->invalid()) {
352 month_input_
->SetInvalid(false);
353 year_input_
->SetInvalid(false);
354 SetRetriableErrorMessage(base::string16());
356 } else if (month_input_
->selected_index() !=
357 month_combobox_model_
.GetDefaultIndex() &&
358 year_input_
->selected_index() !=
359 year_combobox_model_
.GetDefaultIndex()) {
360 month_input_
->SetInvalid(true);
361 year_input_
->SetInvalid(true);
362 SetRetriableErrorMessage(l10n_util::GetStringUTF16(
363 IDS_AUTOFILL_CARD_UNMASK_INVALID_EXPIRATION_DATE
));
366 GetDialogClientView()->UpdateDialogButtons();
369 void CardUnmaskPromptViews::AnimationProgressed(
370 const gfx::Animation
* animation
) {
371 uint8_t alpha
= static_cast<uint8_t>(animation
->CurrentValueBetween(0, 255));
372 progress_overlay_
->SetAlpha(alpha
);
374 storage_row_
->SetAlpha(255 - alpha
);
377 void CardUnmaskPromptViews::InitIfNecessary() {
381 main_contents_
= new views::View();
382 main_contents_
->SetLayoutManager(
383 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 12));
384 AddChildView(main_contents_
);
386 permanent_error_label_
= new views::Label();
387 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
388 permanent_error_label_
->SetFontList(
389 rb
.GetFontList(ui::ResourceBundle::BoldFont
));
390 permanent_error_label_
->set_background(
391 views::Background::CreateSolidBackground(kWarningColor
));
392 permanent_error_label_
->SetBorder(
393 views::Border::CreateEmptyBorder(12, kEdgePadding
, 12, kEdgePadding
));
394 permanent_error_label_
->SetEnabledColor(SK_ColorWHITE
);
395 permanent_error_label_
->SetAutoColorReadabilityEnabled(false);
396 permanent_error_label_
->SetVisible(false);
397 permanent_error_label_
->SetMultiLine(true);
398 permanent_error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
399 main_contents_
->AddChildView(permanent_error_label_
);
401 views::View
* controls_container
= new views::View();
402 controls_container
->SetLayoutManager(
403 new views::BoxLayout(views::BoxLayout::kVertical
, kEdgePadding
, 0, 0));
404 main_contents_
->AddChildView(controls_container
);
406 instructions_
= new views::Label(controller_
->GetInstructionsMessage());
407 instructions_
->SetEnabledColor(kGreyTextColor
);
408 instructions_
->SetMultiLine(true);
409 instructions_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
410 instructions_
->SetBorder(views::Border::CreateEmptyBorder(0, 0, 16, 0));
411 controls_container
->AddChildView(instructions_
);
413 input_row_
= new views::View();
414 input_row_
->SetLayoutManager(
415 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5));
416 controls_container
->AddChildView(input_row_
);
418 month_input_
= new views::Combobox(&month_combobox_model_
);
419 month_input_
->set_listener(this);
420 input_row_
->AddChildView(month_input_
);
421 views::Label
* separator
= new views::Label(l10n_util::GetStringUTF16(
422 IDS_AUTOFILL_CARD_UNMASK_EXPIRATION_DATE_SEPARATOR
));
423 separator
->SetEnabledColor(kGreyTextColor
);
424 input_row_
->AddChildView(separator
);
425 year_input_
= new views::Combobox(&year_combobox_model_
);
426 year_input_
->set_listener(this);
427 input_row_
->AddChildView(year_input_
);
428 input_row_
->AddChildView(new views::Label(base::ASCIIToUTF16(" ")));
429 // Hide all of the above as appropriate.
430 if (!controller_
->ShouldRequestExpirationDate()) {
431 for (int i
= 0; i
< input_row_
->child_count(); ++i
)
432 input_row_
->child_at(i
)->SetVisible(false);
435 cvc_input_
= new DecoratedTextfield(
437 l10n_util::GetStringUTF16(IDS_AUTOFILL_DIALOG_PLACEHOLDER_CVC
), this);
438 cvc_input_
->set_default_width_in_chars(8);
439 input_row_
->AddChildView(cvc_input_
);
441 views::ImageView
* cvc_image
= new views::ImageView();
442 cvc_image
->SetImage(rb
.GetImageSkiaNamed(controller_
->GetCvcImageRid()));
443 input_row_
->AddChildView(cvc_image
);
445 views::View
* temporary_error
= new views::View();
446 views::BoxLayout
* temporary_error_layout
=
447 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 4);
448 temporary_error
->SetLayoutManager(temporary_error_layout
);
449 temporary_error_layout
->set_cross_axis_alignment(
450 views::BoxLayout::CROSS_AXIS_ALIGNMENT_START
);
451 temporary_error
->SetBorder(views::Border::CreateEmptyBorder(8, 0, 0, 0));
452 controls_container
->AddChildView(temporary_error
);
454 error_icon_
= new views::ImageView();
455 error_icon_
->SetVisible(false);
456 error_icon_
->SetImage(ui::ResourceBundle::GetSharedInstance()
457 .GetImageNamed(IDR_ALERT_RED
)
459 temporary_error
->AddChildView(error_icon_
);
461 // Reserve vertical space for the error label, assuming it's one line.
462 error_label_
= new views::Label(base::ASCIIToUTF16(" "));
463 error_label_
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
464 error_label_
->SetEnabledColor(kWarningColor
);
465 temporary_error
->AddChildView(error_label_
);
466 temporary_error_layout
->SetFlexForView(error_label_
, 1);
468 progress_overlay_
= new FadeOutView();
469 progress_overlay_
->set_fade_everything(true);
470 views::BoxLayout
* progress_layout
=
471 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 5);
472 progress_layout
->set_cross_axis_alignment(
473 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER
);
474 progress_layout
->set_main_axis_alignment(
475 views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER
);
476 progress_overlay_
->SetLayoutManager(progress_layout
);
478 progress_overlay_
->SetVisible(false);
479 AddChildView(progress_overlay_
);
481 progress_throbber_
= new views::Throbber();
482 progress_overlay_
->AddChildView(progress_throbber_
);
484 progress_label_
= new views::Label(l10n_util::GetStringUTF16(
485 IDS_AUTOFILL_CARD_UNMASK_VERIFICATION_IN_PROGRESS
));
486 progress_overlay_
->AddChildView(progress_label_
);
489 bool CardUnmaskPromptViews::ExpirationDateIsValid() const {
490 if (!controller_
->ShouldRequestExpirationDate())
493 return controller_
->InputExpirationIsValid(
494 month_input_
->GetTextForRow(month_input_
->selected_index()),
495 year_input_
->GetTextForRow(year_input_
->selected_index()));
498 void CardUnmaskPromptViews::ClosePrompt() {
499 GetWidget()->Close();
502 CardUnmaskPromptViews::FadeOutView::FadeOutView()
503 : fade_everything_(false), alpha_(255) {
505 CardUnmaskPromptViews::FadeOutView::~FadeOutView() {
508 void CardUnmaskPromptViews::FadeOutView::PaintChildren(
509 const ui::PaintContext
& context
) {
510 ui::CompositingRecorder
recorder(context
, alpha_
);
511 views::View::PaintChildren(context
);
514 void CardUnmaskPromptViews::FadeOutView::OnPaint(gfx::Canvas
* canvas
) {
515 if (!fade_everything_
|| alpha_
== 255)
516 return views::View::OnPaint(canvas
);
518 canvas
->SaveLayerAlpha(alpha_
);
519 views::View::OnPaint(canvas
);
523 void CardUnmaskPromptViews::FadeOutView::SetAlpha(uint8_t alpha
) {
528 } // namespace autofill