| // Copyright 2016 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/payments/payment_request_dialog_view.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser_dialogs.h" |
| #include "chrome/browser/ui/views/payments/contact_info_editor_view_controller.h" |
| #include "chrome/browser/ui/views/payments/credit_card_editor_view_controller.h" |
| #include "chrome/browser/ui/views/payments/cvc_unmask_view_controller.h" |
| #include "chrome/browser/ui/views/payments/error_message_view_controller.h" |
| #include "chrome/browser/ui/views/payments/order_summary_view_controller.h" |
| #include "chrome/browser/ui/views/payments/payment_handler_web_flow_view_controller.h" |
| #include "chrome/browser/ui/views/payments/payment_method_view_controller.h" |
| #include "chrome/browser/ui/views/payments/payment_request_views_util.h" |
| #include "chrome/browser/ui/views/payments/payment_sheet_view_controller.h" |
| #include "chrome/browser/ui/views/payments/profile_list_view_controller.h" |
| #include "chrome/browser/ui/views/payments/shipping_address_editor_view_controller.h" |
| #include "chrome/browser/ui/views/payments/shipping_option_view_controller.h" |
| #include "components/autofill/core/browser/autofill_profile.h" |
| #include "components/autofill/core/browser/credit_card.h" |
| #include "components/constrained_window/constrained_window_views.h" |
| #include "components/payments/content/payment_request.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/grid_layout.h" |
| |
| namespace chrome { |
| |
| payments::PaymentRequestDialog* CreatePaymentRequestDialog( |
| payments::PaymentRequest* request) { |
| return new payments::PaymentRequestDialogView(request, |
| /* no observer */ nullptr); |
| } |
| |
| } // namespace chrome |
| |
| namespace payments { |
| |
| namespace { |
| |
| // This function creates an instance of a PaymentRequestSheetController |
| // subclass of concrete type |Controller|, passing it non-owned pointers to |
| // |dialog| and the |request| that initiated that dialog. |map| should be owned |
| // by |dialog|. |
| std::unique_ptr<views::View> CreateViewAndInstallController( |
| std::unique_ptr<PaymentRequestSheetController> controller, |
| payments::ControllerMap* map) { |
| std::unique_ptr<views::View> view = controller->CreateView(); |
| (*map)[view.get()] = std::move(controller); |
| return view; |
| } |
| |
| } // namespace |
| |
| PaymentRequestDialogView::PaymentRequestDialogView( |
| PaymentRequest* request, |
| PaymentRequestDialogView::ObserverForTest* observer) |
| : request_(request), observer_for_testing_(observer), being_closed_(false) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| request->spec()->AddObserver(this); |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| view_stack_ = std::make_unique<ViewStack>(); |
| view_stack_->set_owned_by_client(); |
| AddChildView(view_stack_.get()); |
| |
| SetupSpinnerOverlay(); |
| |
| if (!request->state()->IsInitialized()) { |
| request->state()->AddInitializationObserver(this); |
| ++number_of_initialization_tasks_; |
| } |
| |
| if (number_of_initialization_tasks_ > 0) { |
| ShowProcessingSpinner(); |
| } else if (observer_for_testing_) { |
| // When testing, signal that the processing spinner events have passed, even |
| // though the UI does not need to show it. |
| observer_for_testing_->OnProcessingSpinnerShown(); |
| observer_for_testing_->OnProcessingSpinnerHidden(); |
| } |
| |
| ShowInitialPaymentSheet(); |
| |
| chrome::RecordDialogCreation(chrome::DialogIdentifier::PAYMENT_REQUEST); |
| } |
| |
| PaymentRequestDialogView::~PaymentRequestDialogView() {} |
| |
| void PaymentRequestDialogView::RequestFocus() { |
| view_stack_->RequestFocus(); |
| } |
| |
| ui::ModalType PaymentRequestDialogView::GetModalType() const { |
| return ui::MODAL_TYPE_CHILD; |
| } |
| |
| views::View* PaymentRequestDialogView::GetInitiallyFocusedView() { |
| return view_stack_.get(); |
| } |
| |
| bool PaymentRequestDialogView::Cancel() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // Called when the widget is about to close. We send a message to the |
| // PaymentRequest object to signal user cancellation. |
| // |
| // The order of destruction is important here. First destroy all the views |
| // because they may have pointers/delegates to their controllers. Then destroy |
| // all the controllers, because they may have pointers to PaymentRequestSpec/ |
| // PaymentRequestState. Then send the signal to PaymentRequest to destroy. |
| being_closed_ = true; |
| view_stack_.reset(); |
| controller_map_.clear(); |
| request_->UserCancelled(); |
| return true; |
| } |
| |
| bool PaymentRequestDialogView::ShouldShowCloseButton() const { |
| // Don't show the normal close button on the dialog. This is because the |
| // typical dialog header doesn't allow displaying anything other that the |
| // title and the close button. This is insufficient for the PaymentRequest |
| // dialog, which must sometimes show the back arrow next to the title. |
| // Moreover, the title (and back arrow) should animate with the view they're |
| // attached to. |
| return false; |
| } |
| |
| int PaymentRequestDialogView::GetDialogButtons() const { |
| // The buttons should animate along with the different dialog sheets since |
| // each sheet presents a different set of buttons. Because of this, hide the |
| // usual dialog buttons. |
| return ui::DIALOG_BUTTON_NONE; |
| } |
| |
| void PaymentRequestDialogView::ShowDialog() { |
| constrained_window::ShowWebModalDialogViews(this, request_->web_contents()); |
| } |
| |
| void PaymentRequestDialogView::CloseDialog() { |
| // This calls PaymentRequestDialogView::Cancel() before closing. |
| // ViewHierarchyChanged() also gets called after Cancel(). |
| GetWidget()->Close(); |
| } |
| |
| void PaymentRequestDialogView::ShowErrorMessage() { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<ErrorMessageViewController>( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ false); |
| HideProcessingSpinner(); |
| |
| if (observer_for_testing_) |
| observer_for_testing_->OnErrorMessageShown(); |
| } |
| |
| void PaymentRequestDialogView::ShowProcessingSpinner() { |
| throbber_.Start(); |
| throbber_overlay_.SetVisible(true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnProcessingSpinnerShown(); |
| } |
| |
| bool PaymentRequestDialogView::IsInteractive() const { |
| return !throbber_overlay_.visible(); |
| } |
| |
| void PaymentRequestDialogView::ShowPaymentHandlerScreen( |
| const GURL& url, |
| PaymentHandlerOpenWindowCallback callback) { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| std::make_unique<PaymentHandlerWebFlowViewController>( |
| request_->spec(), request_->state(), this, |
| request_->web_contents(), GetProfile(), url, std::move(callback)), |
| &controller_map_), |
| /* animate = */ !request_->skipped_payment_request_ui()); |
| HideProcessingSpinner(); |
| } |
| |
| void PaymentRequestDialogView::RetryDialog() { |
| HideProcessingSpinner(); |
| GoBackToPaymentSheet(false /* animate */); |
| |
| if (request_->spec()->has_shipping_address_error()) { |
| autofill::AutofillProfile* profile = |
| request_->state()->invalid_shipping_profile(); |
| ShowShippingAddressEditor( |
| BackNavigationType::kOneStep, |
| /*on_edited=*/ |
| base::BindOnce(&PaymentRequestState::SetSelectedShippingProfile, |
| base::Unretained(request_->state()), profile), |
| /*on_added=*/ |
| base::OnceCallback<void(const autofill::AutofillProfile&)>(), profile); |
| } |
| |
| if (request_->spec()->has_payer_error()) { |
| autofill::AutofillProfile* profile = |
| request_->state()->invalid_contact_profile(); |
| ShowContactInfoEditor( |
| BackNavigationType::kOneStep, |
| /*on_edited=*/ |
| base::BindOnce(&PaymentRequestState::SetSelectedContactProfile, |
| base::Unretained(request_->state()), profile), |
| /*on_added=*/ |
| base::OnceCallback<void(const autofill::AutofillProfile&)>(), profile); |
| } |
| } |
| |
| void PaymentRequestDialogView::OnStartUpdating( |
| PaymentRequestSpec::UpdateReason reason) { |
| ShowProcessingSpinner(); |
| } |
| |
| void PaymentRequestDialogView::OnSpecUpdated() { |
| if (request_->spec()->current_update_reason() != |
| PaymentRequestSpec::UpdateReason::NONE) { |
| HideProcessingSpinner(); |
| } |
| |
| if (observer_for_testing_) |
| observer_for_testing_->OnSpecDoneUpdating(); |
| } |
| |
| void PaymentRequestDialogView::OnInitialized( |
| InitializationTask* initialization_task) { |
| initialization_task->RemoveInitializationObserver(this); |
| if (--number_of_initialization_tasks_ > 0) |
| return; |
| |
| HideProcessingSpinner(); |
| |
| if (request_->state()->are_requested_methods_supported()) { |
| request_->RecordDialogShownEventInJourneyLogger(); |
| if (observer_for_testing_) |
| observer_for_testing_->OnDialogOpened(); |
| } |
| } |
| |
| void PaymentRequestDialogView::Pay() { |
| request_->Pay(); |
| } |
| |
| void PaymentRequestDialogView::GoBack() { |
| // If payment request UI is skipped when calling PaymentRequest.show, then |
| // abort payment request when back button is clicked. This only happens for |
| // service worker based payment handler under circumstance. |
| if (request_->skipped_payment_request_ui()) { |
| CloseDialog(); |
| return; |
| } |
| |
| view_stack_->Pop(); |
| |
| if (observer_for_testing_) |
| observer_for_testing_->OnBackNavigation(); |
| } |
| |
| void PaymentRequestDialogView::GoBackToPaymentSheet(bool animate) { |
| // This assumes that the Payment Sheet is the first view in the stack. Thus if |
| // there is only one view, we are already showing the payment sheet. |
| if (view_stack_->size() > 1) |
| view_stack_->PopMany(view_stack_->size() - 1, animate); |
| |
| if (observer_for_testing_) |
| observer_for_testing_->OnBackToPaymentSheetNavigation(); |
| } |
| |
| void PaymentRequestDialogView::ShowContactProfileSheet() { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| ProfileListViewController::GetContactProfileViewController( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnContactInfoOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowOrderSummary() { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<OrderSummaryViewController>( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnOrderSummaryOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowPaymentMethodSheet() { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<PaymentMethodViewController>( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnPaymentMethodOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowShippingProfileSheet() { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| ProfileListViewController::GetShippingProfileViewController( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnShippingAddressSectionOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowShippingOptionSheet() { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<ShippingOptionViewController>( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnShippingOptionSectionOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowCvcUnmaskPrompt( |
| const autofill::CreditCard& credit_card, |
| base::WeakPtr<autofill::payments::FullCardRequest::ResultDelegate> |
| result_delegate, |
| content::WebContents* web_contents) { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<CvcUnmaskViewController>( |
| request_->spec(), request_->state(), this, |
| credit_card, result_delegate, web_contents), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnCvcPromptShown(); |
| } |
| |
| void PaymentRequestDialogView::ShowCreditCardEditor( |
| BackNavigationType back_navigation_type, |
| int next_ui_tag, |
| base::OnceClosure on_edited, |
| base::OnceCallback<void(const autofill::CreditCard&)> on_added, |
| autofill::CreditCard* credit_card) { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| std::make_unique<CreditCardEditorViewController>( |
| request_->spec(), request_->state(), this, back_navigation_type, |
| next_ui_tag, std::move(on_edited), std::move(on_added), |
| credit_card, request_->IsIncognito()), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnCreditCardEditorOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowShippingAddressEditor( |
| BackNavigationType back_navigation_type, |
| base::OnceClosure on_edited, |
| base::OnceCallback<void(const autofill::AutofillProfile&)> on_added, |
| autofill::AutofillProfile* profile) { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| std::make_unique<ShippingAddressEditorViewController>( |
| request_->spec(), request_->state(), this, back_navigation_type, |
| std::move(on_edited), std::move(on_added), profile, |
| request_->IsIncognito()), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnShippingAddressEditorOpened(); |
| } |
| |
| void PaymentRequestDialogView::ShowContactInfoEditor( |
| BackNavigationType back_navigation_type, |
| base::OnceClosure on_edited, |
| base::OnceCallback<void(const autofill::AutofillProfile&)> on_added, |
| autofill::AutofillProfile* profile) { |
| view_stack_->Push( |
| CreateViewAndInstallController( |
| std::make_unique<ContactInfoEditorViewController>( |
| request_->spec(), request_->state(), this, back_navigation_type, |
| std::move(on_edited), std::move(on_added), profile, |
| request_->IsIncognito()), |
| &controller_map_), |
| /* animate = */ true); |
| if (observer_for_testing_) |
| observer_for_testing_->OnContactInfoEditorOpened(); |
| } |
| |
| void PaymentRequestDialogView::EditorViewUpdated() { |
| if (observer_for_testing_) |
| observer_for_testing_->OnEditorViewUpdated(); |
| } |
| |
| void PaymentRequestDialogView::HideProcessingSpinner() { |
| throbber_.Stop(); |
| throbber_overlay_.SetVisible(false); |
| if (observer_for_testing_) |
| observer_for_testing_->OnProcessingSpinnerHidden(); |
| } |
| |
| Profile* PaymentRequestDialogView::GetProfile() { |
| return Profile::FromBrowserContext( |
| request_->web_contents()->GetBrowserContext()); |
| } |
| |
| void PaymentRequestDialogView::ShowInitialPaymentSheet() { |
| view_stack_->Push(CreateViewAndInstallController( |
| std::make_unique<PaymentSheetViewController>( |
| request_->spec(), request_->state(), this), |
| &controller_map_), |
| /* animate = */ false); |
| |
| if (number_of_initialization_tasks_ > 0) |
| return; |
| |
| if (request_->state()->are_requested_methods_supported()) { |
| request_->RecordDialogShownEventInJourneyLogger(); |
| if (observer_for_testing_) |
| observer_for_testing_->OnDialogOpened(); |
| } |
| } |
| |
| void PaymentRequestDialogView::SetupSpinnerOverlay() { |
| throbber_.set_owned_by_client(); |
| |
| throbber_overlay_.set_owned_by_client(); |
| throbber_overlay_.SetPaintToLayer(); |
| throbber_overlay_.SetVisible(false); |
| // The throbber overlay has to have a solid white background to hide whatever |
| // would be under it. |
| throbber_overlay_.SetBackground(views::CreateThemedSolidBackground( |
| &throbber_overlay_, ui::NativeTheme::kColorId_DialogBackground)); |
| |
| views::GridLayout* layout = throbber_overlay_.SetLayoutManager( |
| std::make_unique<views::GridLayout>(&throbber_overlay_)); |
| views::ColumnSet* throbber_columns = layout->AddColumnSet(0); |
| throbber_columns->AddPaddingColumn(0.5, 0); |
| throbber_columns->AddColumn(views::GridLayout::Alignment::CENTER, |
| views::GridLayout::Alignment::TRAILING, |
| views::GridLayout::kFixedSize, |
| views::GridLayout::SizeType::USE_PREF, 0, 0); |
| throbber_columns->AddPaddingColumn(0.5, 0); |
| |
| views::ColumnSet* label_columns = layout->AddColumnSet(1); |
| label_columns->AddPaddingColumn(0.5, 0); |
| label_columns->AddColumn(views::GridLayout::Alignment::CENTER, |
| views::GridLayout::Alignment::LEADING, |
| views::GridLayout::kFixedSize, |
| views::GridLayout::SizeType::USE_PREF, 0, 0); |
| label_columns->AddPaddingColumn(0.5, 0); |
| |
| layout->StartRow(0.5, 0); |
| layout->AddView(&throbber_); |
| |
| layout->StartRow(0.5, 1); |
| layout->AddView(new views::Label( |
| l10n_util::GetStringUTF16(IDS_PAYMENTS_PROCESSING_MESSAGE))); |
| |
| AddChildView(&throbber_overlay_); |
| } |
| |
| gfx::Size PaymentRequestDialogView::CalculatePreferredSize() const { |
| return gfx::Size(GetActualDialogWidth(), kDialogHeight); |
| } |
| |
| void PaymentRequestDialogView::ViewHierarchyChanged( |
| const ViewHierarchyChangedDetails& details) { |
| if (being_closed_) |
| return; |
| |
| // When a view that is associated with a controller is removed from this |
| // view's descendants, dispose of the controller. |
| if (!details.is_add && |
| controller_map_.find(details.child) != controller_map_.end()) { |
| DCHECK(!details.move_view); |
| controller_map_.erase(details.child); |
| } |
| } |
| |
| } // namespace payments |