blob: 27a3a389f3f86a5e120d5e038e6d31b427bdcc31 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// 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/functional/bind.h"
#include "base/logging.h"
#include "base/notimplemented.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/views/extensions/security_dialog_tracker.h"
#include "chrome/browser/ui/views/payments/contact_info_editor_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/data_model/addresses/autofill_profile.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/payments/content/payment_request.h"
#include "components/strings/grit/components_strings.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_provider.h"
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
// static
base::WeakPtr<PaymentRequestDialogView> PaymentRequestDialogView::Create(
base::WeakPtr<PaymentRequest> request,
base::WeakPtr<PaymentRequestDialogView::ObserverForTest> observer) {
return (new PaymentRequestDialogView(request, observer))
->weak_ptr_factory_.GetWeakPtr();
}
void PaymentRequestDialogView::RequestFocus() {
view_stack_->RequestFocus();
}
views::View* PaymentRequestDialogView::GetInitiallyFocusedView() {
return view_stack_;
}
void PaymentRequestDialogView::OnDialogClosed() {
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;
for (const auto& controller : controller_map_) {
controller.second->Stop();
}
RemoveChildViewT(view_stack_.get());
controller_map_.clear();
request_->OnUserCancelled();
if (observer_for_testing_) {
observer_for_testing_->OnDialogClosed();
}
}
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;
}
void PaymentRequestDialogView::ShowDialog() {
views::Widget* widget = constrained_window::ShowWebModalDialogViews(
this, request_->web_contents());
extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(widget);
occlusion_observation_.Observe(widget);
}
void PaymentRequestDialogView::CloseDialog() {
// This calls PaymentRequestDialogView::Cancel() before closing.
// ViewHierarchyChanged() also gets called after Cancel().
GetWidget()->Close();
}
void PaymentRequestDialogView::ShowErrorMessage() {
if (being_closed_ || !request_->spec()) {
return;
}
view_stack_->Push(CreateViewAndInstallController(
std::make_unique<ErrorMessageViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ false);
HideProcessingSpinner();
if (observer_for_testing_) {
observer_for_testing_->OnErrorMessageShown();
}
}
void PaymentRequestDialogView::ShowProcessingSpinner() {
throbber_->Start();
throbber_overlay_->SetVisible(true);
throbber_overlay_->GetViewAccessibility().SetIsIgnored(false);
throbber_overlay_->GetViewAccessibility().SetIsLeaf(false);
if (observer_for_testing_) {
observer_for_testing_->OnProcessingSpinnerShown();
}
}
bool PaymentRequestDialogView::IsInteractive() const {
return !throbber_overlay_->GetVisible();
}
void PaymentRequestDialogView::ShowPaymentHandlerScreen(
const GURL& url,
PaymentHandlerOpenWindowCallback callback) {
if (!request_->spec()) {
return;
}
// The Payment Handler window is larger than the Payment Request sheet, which
// causes us to make different decisions when e.g. animating it.
is_showing_large_payment_handler_window_ = true;
// Calculate |payment_handler_window_height_|
auto* browser = chrome::FindBrowserWithTab(request_->web_contents());
int browser_window_content_height =
browser->window()->GetContentsSize().height();
payment_handler_window_height_ =
std::max(kDialogHeight, std::min(kPreferredPaymentHandlerDialogHeight,
browser_window_content_height));
ResizeDialogWindow();
view_stack_->Push(
CreateViewAndInstallController(
std::make_unique<PaymentHandlerWebFlowViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr(), request_->web_contents(),
GetProfile(), url, std::move(callback)),
&controller_map_),
// Do not animate the view when the dialog size changes or payment sheet
// is skipped.
/* animate = */ !is_showing_large_payment_handler_window_ &&
!request_->skipped_payment_request_ui());
request_->OnPaymentHandlerOpenWindowCalled();
HideProcessingSpinner();
if (observer_for_testing_) {
observer_for_testing_->OnPaymentHandlerWindowOpened();
}
}
void PaymentRequestDialogView::RetryDialog() {
if (!request_->spec()) {
return;
}
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,
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,
request_->state(), profile),
/*on_added=*/
base::OnceCallback<void(const autofill::AutofillProfile&)>(), profile);
}
}
void PaymentRequestDialogView::ConfirmPaymentForTesting() {
Pay();
}
bool PaymentRequestDialogView::ClickOptOutForTesting() {
NOTIMPLEMENTED();
return false;
}
void PaymentRequestDialogView::OnStartUpdating(
PaymentRequestSpec::UpdateReason reason) {
ShowProcessingSpinner();
}
void PaymentRequestDialogView::OnSpecUpdated() {
if (!request_->spec()) {
return;
}
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()) {
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;
}
// Do not animate views when the dialog size changes.
view_stack_->Pop(!is_showing_large_payment_handler_window_ /* = animate */);
// Back navigation from payment handler window should resize the dialog;
if (is_showing_large_payment_handler_window_) {
is_showing_large_payment_handler_window_ = false;
ResizeDialogWindow();
}
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_->GetSize() > 1) {
// Do not animate views when the dialog size changes.
view_stack_->PopMany(view_stack_->GetSize() - 1,
animate && !is_showing_large_payment_handler_window_);
// Back navigation from payment handler window should resize the dialog;
if (is_showing_large_payment_handler_window_) {
ResizeDialogWindow();
is_showing_large_payment_handler_window_ = false;
}
}
if (observer_for_testing_) {
observer_for_testing_->OnBackToPaymentSheetNavigation();
}
}
void PaymentRequestDialogView::ShowContactProfileSheet() {
if (!request_->spec()) {
return;
}
view_stack_->Push(
CreateViewAndInstallController(
ProfileListViewController::GetContactProfileViewController(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate */ true);
if (observer_for_testing_) {
observer_for_testing_->OnContactInfoOpened();
}
}
void PaymentRequestDialogView::ShowOrderSummary() {
if (!request_->spec()) {
return;
}
view_stack_->Push(CreateViewAndInstallController(
std::make_unique<OrderSummaryViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ true);
if (observer_for_testing_) {
observer_for_testing_->OnOrderSummaryOpened();
}
}
void PaymentRequestDialogView::ShowPaymentMethodSheet() {
if (!request_->spec()) {
return;
}
view_stack_->Push(CreateViewAndInstallController(
std::make_unique<PaymentMethodViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ true);
if (observer_for_testing_) {
observer_for_testing_->OnPaymentMethodOpened();
}
}
void PaymentRequestDialogView::ShowShippingProfileSheet() {
if (!request_->spec()) {
return;
}
view_stack_->Push(
CreateViewAndInstallController(
ProfileListViewController::GetShippingProfileViewController(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ true);
if (observer_for_testing_) {
observer_for_testing_->OnShippingAddressSectionOpened();
}
}
void PaymentRequestDialogView::ShowShippingOptionSheet() {
if (!request_->spec()) {
return;
}
view_stack_->Push(CreateViewAndInstallController(
std::make_unique<ShippingOptionViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ true);
if (observer_for_testing_) {
observer_for_testing_->OnShippingOptionSectionOpened();
}
}
void PaymentRequestDialogView::ShowShippingAddressEditor(
BackNavigationType back_navigation_type,
base::OnceClosure on_edited,
base::OnceCallback<void(const autofill::AutofillProfile&)> on_added,
autofill::AutofillProfile* profile) {
if (!request_->spec()) {
return;
}
view_stack_->Push(
CreateViewAndInstallController(
std::make_unique<ShippingAddressEditorViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr(), back_navigation_type,
std::move(on_edited), std::move(on_added), profile,
request_->IsOffTheRecord()),
&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) {
if (!request_->spec()) {
return;
}
view_stack_->Push(
CreateViewAndInstallController(
std::make_unique<ContactInfoEditorViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr(), back_navigation_type,
std::move(on_edited), std::move(on_added), profile,
request_->IsOffTheRecord()),
&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();
// TODO(crbug.com/40894873): Instead of setting the throbber to invisible, can
// we destroy and remove it from the view when it's not being used?
throbber_overlay_->SetVisible(false);
// Screen readers do not ignore invisible elements, so force the screen
// reader to skip the invisible throbber by making it an ignored leaf node in
// the accessibility tree.
throbber_overlay_->GetViewAccessibility().SetIsIgnored(true);
throbber_overlay_->GetViewAccessibility().SetIsLeaf(true);
if (observer_for_testing_) {
observer_for_testing_->OnProcessingSpinnerHidden();
}
}
Profile* PaymentRequestDialogView::GetProfile() {
return Profile::FromBrowserContext(
request_->web_contents()->GetBrowserContext());
}
PaymentRequestDialogView::PaymentRequestDialogView(
base::WeakPtr<PaymentRequest> request,
base::WeakPtr<PaymentRequestDialogView::ObserverForTest> observer)
: request_(request), observer_for_testing_(observer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(request);
DCHECK(request->spec());
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
SetModalType(ui::mojom::ModalType::kChild);
SetCloseCallback(base::BindOnce(&PaymentRequestDialogView::OnDialogClosed,
weak_ptr_factory_.GetWeakPtr()));
// The dialog's CancelCallback may be called during initialization e.g. by
// pressing the escape key, calling OnDialogClosed will ensure the correct
// order of destruction.
SetCancelCallback(base::BindOnce(&PaymentRequestDialogView::OnDialogClosed,
weak_ptr_factory_.GetWeakPtr()));
request->spec()->AddObserver(this);
SetLayoutManager(std::make_unique<views::FillLayout>());
view_stack_ = AddChildView(std::make_unique<ViewStack>());
// ViewStack paints to a layer, and currently layers don't clip to the bounds
// of the window opaque layer. Until this is fixed, we have to set rounded
// corners directly here.
//
// TODO(crbug.com/358379367): Remove once layers obey the clip by default.
view_stack_->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(GetCornerRadius()));
SetupSpinnerOverlay();
if (!request->state()->IsInitialized()) {
request->state()->AddInitializationObserver(this);
++number_of_initialization_tasks_;
}
if (!request->spec()->IsInitialized()) {
request->spec()->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();
}
PaymentRequestDialogView::~PaymentRequestDialogView() = default;
void PaymentRequestDialogView::OnDialogOpened() {
if (!request_->spec()) {
return;
}
if (observer_for_testing_) {
observer_for_testing_->OnDialogOpened();
}
}
void PaymentRequestDialogView::ShowInitialPaymentSheet() {
if (!request_->spec()) {
return;
}
view_stack_->Push(CreateViewAndInstallController(
std::make_unique<PaymentSheetViewController>(
request_->spec(), request_->state(),
weak_ptr_factory_.GetWeakPtr()),
&controller_map_),
/* animate = */ false);
if (number_of_initialization_tasks_ > 0) {
return;
}
if (request_->state()->are_requested_methods_supported()) {
OnDialogOpened();
}
}
void PaymentRequestDialogView::SetupSpinnerOverlay() {
throbber_overlay_ = AddChildView(std::make_unique<views::View>());
throbber_overlay_->SetPaintToLayer();
// Currently layers don't clip to the bounds of the parent window opaque
// layer. Until this is fixed, we have to set rounded corners directly here.
//
// TODO(crbug.com/358379367): Remove once layers obey the clip by default.
throbber_overlay_->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(GetCornerRadius()));
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::CreateSolidBackground(ui::kColorDialogBackground));
views::BoxLayout* layout =
throbber_overlay_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
throbber_ =
throbber_overlay_->AddChildView(std::make_unique<views::Throbber>());
throbber_overlay_->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_PAYMENTS_PROCESSING_MESSAGE)));
}
gfx::Size PaymentRequestDialogView::CalculatePreferredSize(
const views::SizeBounds& /*available_size*/) const {
if (is_showing_large_payment_handler_window_) {
return gfx::Size(GetActualDialogWidth(),
GetActualPaymentHandlerDialogHeight());
}
return gfx::Size(GetActualDialogWidth(), kDialogHeight);
}
int PaymentRequestDialogView::GetActualPaymentHandlerDialogHeight() const {
DCHECK_NE(0, payment_handler_window_height_);
return payment_handler_window_height_ > 0 ? payment_handler_window_height_
: kDialogHeight;
}
int PaymentRequestDialogView::GetActualDialogWidth() const {
int actual_width = views::LayoutProvider::Get()->GetSnappedDialogWidth(
is_showing_large_payment_handler_window_
? kPreferredPaymentHandlerDialogWidth
: kDialogMinWidth);
return actual_width;
}
void PaymentRequestDialogView::OnPaymentHandlerTitleSet() {
if (observer_for_testing_) {
observer_for_testing_->OnPaymentHandlerTitleSet();
}
}
void PaymentRequestDialogView::ViewHierarchyChanged(
const views::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);
}
}
void PaymentRequestDialogView::OnOcclusionStateChanged(bool occluded) {
if (occluded) {
SetEnabled(false);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&PaymentRequestDialogView::CloseDialog,
weak_ptr_factory_.GetWeakPtr()));
}
}
void PaymentRequestDialogView::ResizeDialogWindow() {
content::WebContents* const web_contents = request_->web_contents();
if (GetWidget() && web_contents) {
constrained_window::UpdateWebContentsModalDialogPosition(
GetWidget(),
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
->delegate()
->GetWebContentsModalDialogHost(web_contents));
}
}
BEGIN_METADATA(PaymentRequestDialogView)
ADD_READONLY_PROPERTY_METADATA(int, ActualPaymentHandlerDialogHeight)
ADD_READONLY_PROPERTY_METADATA(int, ActualDialogWidth)
END_METADATA
} // namespace payments