| // Copyright 2024 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/autofill/payments/desktop_payments_window_manager.h" |
| |
| #include "base/check_deref.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "base/types/expected.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/autofill/payments/payments_view_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/autofill/content/browser/content_autofill_client.h" |
| #include "components/autofill/core/browser/autofill_progress_dialog_type.h" |
| #include "components/autofill/core/browser/metrics/payments/bnpl_metrics.h" |
| #include "components/autofill/core/browser/metrics/payments/payments_window_metrics.h" |
| #include "components/autofill/core/browser/payments/card_unmask_challenge_option.h" |
| #include "components/autofill/core/browser/payments/payments_autofill_client.h" |
| #include "components/autofill/core/browser/payments/payments_network_interface.h" |
| #include "components/autofill/core/browser/payments/payments_requests/unmask_card_request.h" |
| #include "components/autofill/core/browser/payments/payments_util.h" |
| #include "components/autofill/core/browser/payments/payments_window_manager_util.h" |
| #include "components/autofill/core/browser/ui/payments/payments_window_user_consent_dialog_controller_impl.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "url/gurl.h" |
| |
| namespace autofill::payments { |
| |
| namespace { |
| |
| using Vcn3dsFlowEvent = autofill_metrics::Vcn3dsFlowEvent; |
| |
| gfx::Rect GetPopupSizeForVcn3ds() { |
| // The first two arguments do not matter as position gets overridden by |
| // the tab modal pop-up code. The 600x640 size of the pop-up was decided as |
| // the ideal size for user experience. This decision largely factored in how |
| // to minimize scrolling while maintaining a presentable pop-up. |
| return gfx::Rect(/*x=*/0, /*y=*/0, /*width=*/600, /*height=*/640); |
| } |
| |
| gfx::Rect GetPopupSizeForBnpl() { |
| // The first two arguments do not matter as position gets overridden by |
| // the tab modal pop-up code. The 600x840 size of the pop-up was decided as |
| // the ideal size for user experience. This decision largely factored in how |
| // to minimize scrolling while maintaining a presentable pop-up. |
| return gfx::Rect(/*x=*/0, /*y=*/0, /*width=*/600, /*height=*/840); |
| } |
| |
| } // namespace |
| |
| DesktopPaymentsWindowManager::DesktopPaymentsWindowManager( |
| ContentAutofillClient* client) |
| : client_(CHECK_DEREF(client)) { |
| #if BUILDFLAG(IS_LINUX) |
| scoped_observation_.Observe(BrowserList::GetInstance()); |
| #endif // BUILDFLAG(IS_LINUX) |
| } |
| |
| DesktopPaymentsWindowManager::~DesktopPaymentsWindowManager() = default; |
| |
| void DesktopPaymentsWindowManager::InitVcn3dsAuthentication( |
| Vcn3dsContext context) { |
| CHECK(!flow_state_.has_value()); |
| flow_state_ = FlowState(); |
| CHECK_EQ(context.card.record_type(), CreditCard::RecordType::kVirtualCard); |
| CHECK(!context.completion_callback.is_null()); |
| |
| // The VCN 3DS metadata fields are returned from the Payments server. They |
| // must always be present, so that Chrome knows what params to look for on |
| // navigation. Since they are outside of Chrome's control, unexpected values |
| // must be gracefully handled by displaying an error dialog. |
| if (const std::optional<Vcn3dsChallengeOptionMetadata>& metadata = |
| context.challenge_option.vcn_3ds_metadata; |
| !metadata.has_value() || metadata->url_to_open.is_empty() || |
| metadata->success_query_param_name.empty() || |
| metadata->failure_query_param_name.empty()) { |
| client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/false)); |
| return; |
| } |
| |
| flow_state_->flow_type = FlowType::kVcn3ds; |
| flow_state_->vcn_3ds_context = std::move(context); |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kFlowStarted, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| if (flow_state_->vcn_3ds_context->user_consent_already_given) { |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kUserConsentDialogSkipped, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| CreatePopup(flow_state_->vcn_3ds_context->challenge_option.vcn_3ds_metadata |
| ->url_to_open, |
| GetPopupSizeForVcn3ds()); |
| } else { |
| ShowVcn3dsConsentDialog(); |
| } |
| } |
| |
| void DesktopPaymentsWindowManager::InitBnplFlow(BnplContext context) { |
| CHECK(!flow_state_.has_value()); |
| flow_state_ = FlowState(); |
| |
| flow_state_->flow_type = FlowType::kBnpl; |
| flow_state_->bnpl_context = std::move(context); |
| CreatePopup(flow_state_->bnpl_context->initial_url, GetPopupSizeForBnpl()); |
| autofill_metrics::LogBnplPopupWindowShown( |
| flow_state_->bnpl_context->issuer_id); |
| } |
| |
| void DesktopPaymentsWindowManager::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| CHECK(flow_state_.has_value()); |
| flow_state_->most_recent_url_navigation = navigation_handle->GetURL(); |
| if (flow_state_->flow_type == FlowType::kVcn3ds) { |
| OnDidFinishNavigationForVcn3ds(); |
| } else if (flow_state_->flow_type == FlowType::kBnpl) { |
| OnDidFinishNavigationForBnpl(); |
| } |
| } |
| |
| void DesktopPaymentsWindowManager::WebContentsDestroyed() { |
| // Accessing the observed web contents should be avoided at this point, |
| // because it is unsafe to access during its destruction. Instead, set class |
| // variables earlier for context that needs to be known upon web contents |
| // destruction. `flow_state_->most_recent_url_navigation` is an example that |
| // can be followed. |
| // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to not access the |
| // observed web contents. |
| CHECK(flow_state_.has_value()); |
| if (flow_state_->flow_type == FlowType::kVcn3ds) { |
| OnWebContentsDestroyedForVcn3ds(); |
| } else if (flow_state_->flow_type == FlowType::kBnpl) { |
| TriggerCompletionCallbackAndLogMetricsForBnpl( |
| std::move(flow_state_.value())); |
| flow_state_.reset(); |
| } |
| |
| if (popup_closed_closure_for_testing_) { |
| popup_closed_closure_for_testing_.Run(); |
| popup_closed_closure_for_testing_.Reset(); |
| } |
| } |
| |
| #if BUILDFLAG(IS_LINUX) |
| void DesktopPaymentsWindowManager::OnBrowserSetLastActive(Browser* browser) { |
| // If there is an ongoing payments window manager pop-up flow, and the |
| // original tab's WebContents become active, activate the pop-up's |
| // WebContents. This functionality is only required on Linux, as on |
| // other desktop platforms the pop-up will always be the top-most browser |
| // window due to differences in window management on these platforms. |
| if (web_contents()) { |
| CHECK(flow_state_.has_value()); |
| CHECK_NE(flow_state_->flow_type, FlowType::kNoFlow); |
| if (browser->tab_strip_model()->GetActiveWebContents() == |
| &client_->GetWebContents()) { |
| web_contents()->GetDelegate()->ActivateContents(web_contents()); |
| } |
| } |
| } |
| #endif // BUILDFLAG(IS_LINUX) |
| |
| void DesktopPaymentsWindowManager::CreatePopup(const GURL& url, |
| gfx::Rect popup_size) { |
| // Create a pop-up window. The created pop-up will not have any relationship |
| // to the underlying tab, because `params.opener` is not set. Ensuring the |
| // original tab is not a related site instance to the pop-up is critical for |
| // security reasons. |
| CHECK(flow_state_.has_value()); |
| content::WebContents& source_contents = client_->GetWebContents(); |
| NavigateParams params( |
| Profile::FromBrowserContext(source_contents.GetBrowserContext()), url, |
| ui::PAGE_TRANSITION_LINK); |
| params.disposition = WindowOpenDisposition::NEW_POPUP; |
| params.window_action = NavigateParams::WindowAction::kShowWindow; |
| params.source_contents = &source_contents; |
| params.is_tab_modal_popup_deprecated = true; |
| params.window_features.bounds = std::move(popup_size); |
| |
| if (base::WeakPtr<content::NavigationHandle> navigation_handle = |
| Navigate(¶ms)) { |
| switch (flow_state_->flow_type) { |
| case FlowType::kVcn3ds: |
| flow_state_->vcn_3ds_popup_shown_timestamp = base::TimeTicks::Now(); |
| break; |
| case FlowType::kBnpl: |
| flow_state_->bnpl_popup_shown_timestamp = base::TimeTicks::Now(); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| content::WebContentsObserver::Observe(navigation_handle->GetWebContents()); |
| } else { |
| if (flow_state_->vcn_3ds_context.has_value()) { |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kPopupNotShown, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/false)); |
| } else if (flow_state_->bnpl_context.has_value()) { |
| client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithBnplPermanentOrTemporaryError( |
| /*is_permanent_error=*/false)); |
| } |
| } |
| } |
| |
| void DesktopPaymentsWindowManager::OnDidFinishNavigationForVcn3ds() { |
| CHECK(flow_state_.has_value()); |
| // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to use |
| // `flow_state_->most_recent_url_navigation`, similar to the BNPL flow. |
| base::expected<RedirectCompletionResult, Vcn3dsAuthenticationResult> result = |
| ParseUrlForVcn3ds(web_contents()->GetVisibleURL(), |
| flow_state_->vcn_3ds_context->challenge_option |
| .vcn_3ds_metadata.value()); |
| if (result.has_value() || |
| result.error() == Vcn3dsAuthenticationResult::kAuthenticationFailed) { |
| // To safely close the pop-up during a navigation event, a task must be |
| // posted to the current base::SequencedTaskRunner, as the web contents must |
| // complete notifying all of its observers of the navigation event before |
| // closing. Closing before this has finished can result in a use-after-free. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&content::WebContents::Close, |
| web_contents()->GetWeakPtr())); |
| } |
| } |
| |
| void DesktopPaymentsWindowManager::OnDidFinishNavigationForBnpl() { |
| CHECK(flow_state_.has_value()); |
| BnplPopupStatus status = |
| ParseUrlForBnpl(flow_state_->most_recent_url_navigation, |
| flow_state_->bnpl_context.value()); |
| if (status != BnplPopupStatus::kNotFinished) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&content::WebContents::Close, |
| web_contents()->GetWeakPtr())); |
| } |
| } |
| |
| void DesktopPaymentsWindowManager::OnWebContentsDestroyedForVcn3ds() { |
| CHECK(flow_state_.has_value()); |
| CHECK(flow_state_->vcn_3ds_popup_shown_timestamp.has_value()); |
| // TODO(crbug.com/388088113): Refactor the VCN 3DS flow to use |
| // `flow_state_->most_recent_url_navigation`, similar to the BNPL flow. |
| base::expected<RedirectCompletionResult, Vcn3dsAuthenticationResult> result = |
| ParseUrlForVcn3ds(web_contents()->GetVisibleURL(), |
| flow_state_->vcn_3ds_context->challenge_option |
| .vcn_3ds_metadata.value()); |
| |
| // If the result implies that the authentication inside of the pop-up was |
| // successful, continue the flow without resetting. |
| if (result.has_value()) { |
| CHECK(!result.value()->empty()); |
| autofill_metrics::LogVcn3dsAuthLatency( |
| base::TimeTicks::Now() - |
| flow_state_->vcn_3ds_popup_shown_timestamp.value(), |
| /*success=*/true); |
| client_->GetPaymentsAutofillClient()->ShowAutofillProgressDialog( |
| AutofillProgressDialogType::k3dsFetchVcnProgressDialog, |
| base::BindOnce(&DesktopPaymentsWindowManager:: |
| OnVcn3dsAuthenticationProgressDialogCancelled, |
| weak_ptr_factory_.GetWeakPtr())); |
| return client_->GetPaymentsAutofillClient()->LoadRiskData(base::BindOnce( |
| &DesktopPaymentsWindowManager::OnDidLoadRiskDataForVcn3ds, |
| weak_ptr_factory_.GetWeakPtr(), std::move(result.value()))); |
| } |
| |
| // If the authentication was known to fail inside of the pop-up (for example, |
| // a user retried too many times for the issuer or network's auth mechanism |
| // inside of the pop-up browser window), trigger the error dialog. Otherwise, |
| // it is assumed that the user manually closed the pop-up, so triggering an |
| // error dialog would be a bad user experience. If the Payments Server |
| // introduced invalid query parameters on the last redirect, this would fail |
| // to handle that correctly, but it is not feasible to distinguish that from |
| // the user closing the pop-up. |
| if (result.error() == Vcn3dsAuthenticationResult::kAuthenticationFailed) { |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kAuthenticationInsidePopupFailed, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| autofill_metrics::LogVcn3dsAuthLatency( |
| base::TimeTicks::Now() - |
| flow_state_->vcn_3ds_popup_shown_timestamp.value(), |
| /*success=*/false); |
| client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/true)); |
| } else { |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kFlowCancelledUserClosedPopup, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| } |
| |
| // The callback is always run at this point, which can be either when the user |
| // closed the pop-up or an error occurred. This is so that the requester is |
| // notified of the flow's completion. |
| // TODO(crbug.com/334967738): Check whether the user closed the pop-up window |
| // directly once an API for it is built. |
| Vcn3dsAuthenticationResponse response; |
| response.result = result.error(); |
| std::move(flow_state_->vcn_3ds_context->completion_callback) |
| .Run(std::move(response)); |
| flow_state_.reset(); |
| } |
| |
| void DesktopPaymentsWindowManager::OnDidLoadRiskDataForVcn3ds( |
| RedirectCompletionResult redirect_completion_result, |
| const std::string& risk_data) { |
| CHECK(flow_state_.has_value()); |
| flow_state_->vcn_3ds_context->risk_data = risk_data; |
| client_->GetPaymentsAutofillClient() |
| ->GetPaymentsNetworkInterface() |
| ->UnmaskCard(CreateUnmaskRequestDetailsForVcn3ds( |
| *client_, flow_state_->vcn_3ds_context.value(), |
| std::move(redirect_completion_result)), |
| base::BindOnce(&DesktopPaymentsWindowManager:: |
| OnVcn3dsAuthenticationResponseReceived, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void DesktopPaymentsWindowManager::OnVcn3dsAuthenticationResponseReceived( |
| PaymentsAutofillClient::PaymentsRpcResult result, |
| const UnmaskResponseDetails& response_details) { |
| CHECK(flow_state_.has_value()); |
| Vcn3dsAuthenticationResponse response = |
| CreateVcn3dsAuthenticationResponseFromServerResult( |
| result, response_details, |
| std::move(flow_state_->vcn_3ds_context->card)); |
| client_->GetPaymentsAutofillClient()->CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/response.card.has_value(), |
| /*no_interactive_authentication_callback=*/base::OnceClosure()); |
| if (!response.card.has_value()) { |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kFlowFailedWhileRetrievingVCN, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| client_->GetPaymentsAutofillClient()->ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/false)); |
| } |
| |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kFlowSucceeded, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| std::move(flow_state_->vcn_3ds_context->completion_callback) |
| .Run(std::move(response)); |
| flow_state_.reset(); |
| } |
| |
| void DesktopPaymentsWindowManager:: |
| OnVcn3dsAuthenticationProgressDialogCancelled() { |
| CHECK(flow_state_.has_value()); |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kProgressDialogCancelled, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| client_->GetPaymentsAutofillClient() |
| ->GetPaymentsNetworkInterface() |
| ->CancelRequest(); |
| // In the case of the dialog cancelled, we still run the callback to let the |
| // caller know the flow has finished unsuccessfully. |
| Vcn3dsAuthenticationResponse response; |
| response.result = Vcn3dsAuthenticationResult::kAuthenticationNotCompleted; |
| std::move(flow_state_->vcn_3ds_context->completion_callback) |
| .Run(std::move(response)); |
| flow_state_.reset(); |
| } |
| |
| void DesktopPaymentsWindowManager::ShowVcn3dsConsentDialog() { |
| payments_window_user_consent_dialog_controller_ = |
| std::make_unique<PaymentsWindowUserConsentDialogControllerImpl>( |
| /*accept_callback=*/base::BindOnce( |
| &DesktopPaymentsWindowManager::OnVcn3dsConsentDialogAccepted, |
| weak_ptr_factory_.GetWeakPtr()), |
| /*cancel_callback=*/base::BindOnce( |
| &DesktopPaymentsWindowManager::OnVcn3dsConsentDialogCancelled, |
| weak_ptr_factory_.GetWeakPtr())); |
| payments_window_user_consent_dialog_controller_->ShowDialog(base::BindOnce( |
| &CreateAndShowPaymentsWindowUserConsentDialog, |
| payments_window_user_consent_dialog_controller_->GetWeakPtr(), |
| base::Unretained(&client_->GetWebContents()))); |
| } |
| |
| void DesktopPaymentsWindowManager::OnVcn3dsConsentDialogAccepted() { |
| CHECK(flow_state_.has_value()); |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kUserConsentDialogAccepted, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| CreatePopup(flow_state_->vcn_3ds_context->challenge_option.vcn_3ds_metadata |
| ->url_to_open, |
| GetPopupSizeForVcn3ds()); |
| } |
| |
| void DesktopPaymentsWindowManager::OnVcn3dsConsentDialogCancelled() { |
| CHECK(flow_state_.has_value()); |
| autofill_metrics::LogVcn3dsFlowEvent( |
| Vcn3dsFlowEvent::kUserConsentDialogDeclined, |
| /*user_consent_already_given=*/flow_state_->vcn_3ds_context |
| ->user_consent_already_given); |
| // In the case of the dialog cancelled, we still run the callback to let the |
| // caller know the flow has finished unsuccessfully. |
| Vcn3dsAuthenticationResponse response; |
| response.result = Vcn3dsAuthenticationResult::kAuthenticationNotCompleted; |
| std::move(flow_state_->vcn_3ds_context->completion_callback) |
| .Run(std::move(response)); |
| flow_state_.reset(); |
| } |
| |
| } // namespace autofill::payments |