chromium/components/facilitated_payments/core/browser/facilitated_payments_manager.cc

// 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 "components/facilitated_payments/core/browser/facilitated_payments_manager.h"

#include <algorithm>
#include <utility>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/functional/callback_helpers.h"
#include "components/autofill/core/browser/data_model/bank_account.h"
#include "components/autofill/core/browser/payments/payments_util.h"
#include "components/autofill/core/browser/payments_data_manager.h"
#include "components/facilitated_payments/core/browser/facilitated_payments_client.h"
#include "components/facilitated_payments/core/browser/network_api/facilitated_payments_network_interface.h"
#include "components/facilitated_payments/core/features/features.h"
#include "components/facilitated_payments/core/metrics/facilitated_payments_metrics.h"
#include "services/metrics/public/cpp/ukm_builders.h"

namespace payments::facilitated {

FacilitatedPaymentsManager::FacilitatedPaymentsManager(
    FacilitatedPaymentsDriver* driver,
    FacilitatedPaymentsClient* client,
    FacilitatedPaymentsApiClientCreator api_client_creator,
    optimization_guide::OptimizationGuideDecider* optimization_guide_decider)
    : driver_(CHECK_DEREF(driver)),
      client_(CHECK_DEREF(client)),
      api_client_creator_(std::move(api_client_creator)),
      optimization_guide_decider_(optimization_guide_decider),
      initiate_payment_request_details_(
          std::make_unique<
              FacilitatedPaymentsInitiatePaymentRequestDetails>()) {
  DCHECK(optimization_guide_decider_);
  RegisterPixAllowlist();
}

FacilitatedPaymentsManager::~FacilitatedPaymentsManager() {
  client_->DismissPrompt();
}

void FacilitatedPaymentsManager::Reset() {
  // In tests, when the payment flow is abandoned, do not reset so the final
  // states can be verified.
  if (is_test_) {
    return;
  }
  has_payflow_started_ = false;
  pix_code_detection_attempt_count_ = 0;
  ukm_source_id_ = 0;
  trigger_source_ = TriggerSource::kUnknown;
  pix_code_detection_triggering_timer_.Stop();
  initiate_payment_request_details_ =
      std::make_unique<FacilitatedPaymentsInitiatePaymentRequestDetails>();
  weak_ptr_factory_.InvalidateWeakPtrs();
}

void FacilitatedPaymentsManager::
    DelayedCheckAllowlistAndTriggerPixCodeDetection(const GURL& url,
                                                    ukm::SourceId ukm_source_id,
                                                    int attempt_number) {
  // TODO: b/362781719 - Deprecate Pix code detection.
  Reset();
  switch (GetAllowlistCheckResult(url)) {
    case optimization_guide::OptimizationGuideDecision::kTrue: {
      ukm_source_id_ = ukm_source_id;
      initiate_payment_request_details_->merchant_payment_page_hostname_ =
          url.host();
      // The PIX code detection should be triggered after `kPageLoadWaitTime`.
      // Time spent waiting for the allowlist checking infra should be accounted
      // for.
      base::TimeDelta trigger_pix_code_detection_delay =
          std::max(base::Seconds(0),
                   kPageLoadWaitTime - (attempt_number - 1) *
                                           kOptimizationGuideDeciderWaitTime);
      DelayedTriggerPixCodeDetection(trigger_pix_code_detection_delay);
      break;
    }
    case optimization_guide::OptimizationGuideDecision::kUnknown: {
      if (attempt_number >= kMaxAttemptsForAllowlistCheck) {
        break;
      }
      pix_code_detection_triggering_timer_.Start(
          FROM_HERE, kOptimizationGuideDeciderWaitTime,
          base::BindOnce(&FacilitatedPaymentsManager::
                             DelayedCheckAllowlistAndTriggerPixCodeDetection,
                         weak_ptr_factory_.GetWeakPtr(), url, ukm_source_id,
                         attempt_number + 1));
      break;
    }
    case optimization_guide::OptimizationGuideDecision::kFalse:
      break;
  }
}

void FacilitatedPaymentsManager::OnPixCodeCopiedToClipboard(
    const GURL& render_frame_host_url,
    const std::string& pix_code,
    ukm::SourceId ukm_source_id) {
  if (has_payflow_started_) {
    return;
  }
  has_payflow_started_ = true;
  ukm_source_id_ = ukm_source_id;
  trigger_source_ = TriggerSource::kCopyEvent;
  // Check whether the domain for the render_frame_host_url is allowlisted.
  auto decision = optimization_guide_decider_->CanApplyOptimization(
      render_frame_host_url,
      optimization_guide::proto::PIX_MERCHANT_ORIGINS_ALLOWLIST,
      /*optimization_metadata=*/nullptr);
  if (decision != optimization_guide::OptimizationGuideDecision::kTrue) {
    // The merchant is not part of the allowlist, ignore the copy event.
    return;
  }
  initiate_payment_request_details_->merchant_payment_page_hostname_ =
      render_frame_host_url.host();
  // Trigger Pix code validation.
  utility_process_validator_.ValidatePixCode(
      pix_code, base::BindOnce(&FacilitatedPaymentsManager::OnPixCodeValidated,
                               weak_ptr_factory_.GetWeakPtr(), pix_code));
}

void FacilitatedPaymentsManager::RegisterPixAllowlist() const {
  optimization_guide_decider_->RegisterOptimizationTypes(
      {optimization_guide::proto::PIX_PAYMENT_MERCHANT_ALLOWLIST,
       optimization_guide::proto::PIX_MERCHANT_ORIGINS_ALLOWLIST});
}

optimization_guide::OptimizationGuideDecision
FacilitatedPaymentsManager::GetAllowlistCheckResult(const GURL& url) const {
  // Since the optimization guide decider integration corresponding to PIX
  // merchant lists are allowlists for the question "Can this site be
  // optimized?", a match on the allowlist answers the question with "yes".
  // Therefore, `kTrue` indicates that `url` is allowed for running PIX code
  // detection. If the optimization type was not registered in time when we
  // queried it, it will be `kUnknown`.
  return optimization_guide_decider_->CanApplyOptimization(
      url, optimization_guide::proto::PIX_PAYMENT_MERCHANT_ALLOWLIST,
      /*optimization_metadata=*/nullptr);
}

void FacilitatedPaymentsManager::DelayedTriggerPixCodeDetection(
    base::TimeDelta delay) {
  pix_code_detection_triggering_timer_.Start(
      FROM_HERE, delay,
      base::BindOnce(&FacilitatedPaymentsManager::TriggerPixCodeDetection,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FacilitatedPaymentsManager::TriggerPixCodeDetection() {
  pix_code_detection_attempt_count_++;
  StartPixCodeDetectionLatencyTimer();
  driver_->TriggerPixCodeDetection(
      base::BindOnce(&FacilitatedPaymentsManager::ProcessPixCodeDetectionResult,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FacilitatedPaymentsManager::ProcessPixCodeDetectionResult(
    mojom::PixCodeDetectionResult result, const std::string& pix_code) {
  // If a PIX code was not found, re-trigger PIX code detection after a short
  // duration to allow async content to load completely.
  if (result == mojom::PixCodeDetectionResult::kPixCodeNotFound &&
      pix_code_detection_attempt_count_ < kMaxAttemptsForPixCodeDetection) {
    DelayedTriggerPixCodeDetection(kRetriggerPixCodeDetectionWaitTime);
    return;
  }
  ukm::builders::FacilitatedPayments_PixCodeDetectionResult(ukm_source_id_)
      .SetResult(static_cast<uint8_t>(result))
      .SetLatencyInMillis(GetPixCodeDetectionLatencyInMillis())
      .SetAttempts(pix_code_detection_attempt_count_)
      .SetDetectionTriggeredOnDomContentLoaded(
          base::FeatureList::IsEnabled(kEnablePixDetectionOnDomContentLoaded))
      .Record(ukm::UkmRecorder::Get());

  if (result != mojom::PixCodeDetectionResult::kValidPixCodeFound) {
    Reset();
    return;
  }
  // Clicking on the copy button could have initiated the payflow.
  if (has_payflow_started_) {
    return;
  }
  has_payflow_started_ = true;
  trigger_source_ = TriggerSource::kDOMSearch;
}

void FacilitatedPaymentsManager::OnPixCodeValidated(
    std::string pix_code,
    base::expected<bool, std::string> is_pix_code_valid) {
  if (!is_pix_code_valid.has_value()) {
    // Pix code validator encountered an error.
    LogPaymentNotOfferedReason(PaymentNotOfferedReason::kCodeValidatorFailed);
    Reset();
    return;
  }

  if (!is_pix_code_valid.value()) {
    // Pix code is not valid.
    LogPaymentNotOfferedReason(PaymentNotOfferedReason::kInvalidCode);
    Reset();
    return;
  }
  // If a valid PIX code is found, and the user has Google wallet linked PIX
  // accounts, verify that the payments API is available, and then show the PIX
  // payment prompt.
  auto* payments_data_manager = client_->GetPaymentsDataManager();
  if (!payments_data_manager ||
      !payments_data_manager->IsFacilitatedPaymentsPixUserPrefEnabled() ||
      !payments_data_manager->HasMaskedBankAccounts()) {
    Reset();
    return;
  }

  if (!GetApiClient()) {
    Reset();
    return;
  }

  initiate_payment_request_details_->pix_code_ = std::move(pix_code);
  api_availability_check_start_time_ = base::TimeTicks::Now();
  GetApiClient()->IsAvailable(
      base::BindOnce(&FacilitatedPaymentsManager::OnApiAvailabilityReceived,
                     weak_ptr_factory_.GetWeakPtr()));
}

FacilitatedPaymentsApiClient* FacilitatedPaymentsManager::GetApiClient() {
  if (!api_client_) {
    if (api_client_creator_) {
      api_client_ = std::move(api_client_creator_).Run();
    }
  }

  return api_client_.get();
}

void FacilitatedPaymentsManager::StartPixCodeDetectionLatencyTimer() {
  pix_code_detection_latency_measuring_timestamp_ = base::TimeTicks::Now();
}

int64_t FacilitatedPaymentsManager::GetPixCodeDetectionLatencyInMillis() const {
  return (base::TimeTicks::Now() -
          pix_code_detection_latency_measuring_timestamp_)
      .InMilliseconds();
}

void FacilitatedPaymentsManager::OnApiAvailabilityReceived(
    bool is_api_available) {
  LogIsApiAvailableResult(
      is_api_available,
      (base::TimeTicks::Now() - api_availability_check_start_time_));
  if (!is_api_available) {
    LogPaymentNotOfferedReason(PaymentNotOfferedReason::kApiNotAvailable);
    Reset();
    return;
  }

  // If the payments data manager isn't available, then the flow should have
  // been abandoned already in `ProcessPixCodeDetectionResult`.
  CHECK(client_->GetPaymentsDataManager());
  initiate_payment_request_details_->billing_customer_number_ =
      autofill::payments::GetBillingCustomerId(
          client_->GetPaymentsDataManager());
  // Before showing the payment prompt, load the risk data required for
  // initiating payment request. The risk data is collected once per page load
  // if a PIX code was detected.
  if (initiate_payment_request_details_->risk_data_.empty()) {
    client_->LoadRiskData(
        base::BindOnce(&FacilitatedPaymentsManager::OnRiskDataLoaded,
                       weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now()));
  }

  bool promptShown = client_->ShowPixPaymentPrompt(
      client_->GetPaymentsDataManager()->GetMaskedBankAccounts(),
      base::BindOnce(&FacilitatedPaymentsManager::OnPixPaymentPromptResult,
                     weak_ptr_factory_.GetWeakPtr()));
  LogFopSelectorShown(promptShown);
  if (promptShown) {
    fop_selector_shown_time_ = base::TimeTicks::Now();
  }
}

void FacilitatedPaymentsManager::OnRiskDataLoaded(
    base::TimeTicks start_time,
    const std::string& risk_data) {
  LogLoadRiskDataResultAndLatency(/*was_successful=*/!risk_data.empty(),
                                  base::TimeTicks::Now() - start_time);
  if (risk_data.empty()) {
    // TODO: b/348143700 - Show error screen if the loading screen is being
    // shown.
    LogPaymentNotOfferedReason(PaymentNotOfferedReason::kRiskDataEmpty);
    return;
  }
  initiate_payment_request_details_->risk_data_ = risk_data;

  // Populating the risk data and showing the payment prompt may occur
  // asynchronously. If the user has already selected the payment account, send
  // the request to initiate payment.
  if (initiate_payment_request_details_->IsReadyForPixPayment()) {
    SendInitiatePaymentRequest();
  }
}

void FacilitatedPaymentsManager::OnPixPaymentPromptResult(
    bool is_prompt_accepted,
    int64_t selected_instrument_id) {
  if (!is_prompt_accepted) {
    LogTransactionResult(TransactionResult::kAbandoned, trigger_source_,
                         base::TimeTicks::Now() - fop_selector_shown_time_,
                         ukm_source_id_);
    Reset();
    return;
  }

  client_->ShowProgressScreen();

  initiate_payment_request_details_->instrument_id_ = selected_instrument_id;
  get_client_token_loading_start_time_ = base::TimeTicks::Now();
  GetApiClient()->GetClientToken(
      base::BindOnce(&FacilitatedPaymentsManager::OnGetClientToken,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FacilitatedPaymentsManager::OnGetClientToken(
    std::vector<uint8_t> client_token) {
  LogGetClientTokenResult(
      !client_token.empty(),
      (base::TimeTicks::Now() - get_client_token_loading_start_time_));
  if (client_token.empty()) {
    client_->ShowErrorScreen();
    LogTransactionResult(TransactionResult::kFailed, trigger_source_,
                         base::TimeTicks::Now() - fop_selector_shown_time_,
                         ukm_source_id_);
    Reset();
    return;
  }
  initiate_payment_request_details_->client_token_ = client_token;

  if (initiate_payment_request_details_->IsReadyForPixPayment()) {
    SendInitiatePaymentRequest();
  }
}

void FacilitatedPaymentsManager::SendInitiatePaymentRequest() {
  initiate_payment_network_start_time_ = base::TimeTicks::Now();
  if (FacilitatedPaymentsNetworkInterface* payments_network_interface =
          client_->GetFacilitatedPaymentsNetworkInterface()) {
    payments_network_interface->InitiatePayment(
        std::move(initiate_payment_request_details_),
        base::BindOnce(
            &FacilitatedPaymentsManager::OnInitiatePaymentResponseReceived,
            weak_ptr_factory_.GetWeakPtr()),
        client_->GetPaymentsDataManager()->app_locale());
  }
}

void FacilitatedPaymentsManager::OnInitiatePaymentResponseReceived(
    autofill::payments::PaymentsAutofillClient::PaymentsRpcResult result,
    std::unique_ptr<FacilitatedPaymentsInitiatePaymentResponseDetails>
        response_details) {
  base::TimeDelta latency =
      base::TimeTicks::Now() - initiate_payment_network_start_time_;
  if (result !=
      autofill::payments::PaymentsAutofillClient::PaymentsRpcResult::kSuccess) {
    LogInitiatePaymentResult(/*result=*/false, latency);
    client_->ShowErrorScreen();
    LogTransactionResult(TransactionResult::kFailed, trigger_source_,
                         base::TimeTicks::Now() - fop_selector_shown_time_,
                         ukm_source_id_);
    Reset();
    return;
  }
  LogInitiatePaymentResult(/*result=*/true, latency);
  DCHECK(response_details);
  if (response_details->action_token_.empty()) {
    client_->ShowErrorScreen();
    LogTransactionResult(TransactionResult::kFailed, trigger_source_,
                         base::TimeTicks::Now() - fop_selector_shown_time_,
                         ukm_source_id_);
    Reset();
    return;
  }
  std::optional<CoreAccountInfo> account_info = client_->GetCoreAccountInfo();
  // If the user logged out after selecting the payment method, the
  // `account_info` would be empty, and the `FacilitatedPaymentsManager` should
  // abandon the payment flow.
  if (!account_info.has_value() || account_info.value().IsEmpty()) {
    client_->ShowErrorScreen();
    LogTransactionResult(TransactionResult::kFailed, trigger_source_,
                         base::TimeTicks::Now() - fop_selector_shown_time_,
                         ukm_source_id_);
    Reset();
    return;
  }
  purchase_action_start_time_ = base::TimeTicks::Now();
  GetApiClient()->InvokePurchaseAction(
      account_info.value(), response_details->action_token_,
      base::BindOnce(&FacilitatedPaymentsManager::OnPurchaseActionResult,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FacilitatedPaymentsManager::OnPurchaseActionResult(
    FacilitatedPaymentsApiClient::PurchaseActionResult result) {
  // When server responds to the purchase action, Google Play Services takes
  // over, and the progress screen gets dismissed. Calling `DismissPrompt`
  // clears the associated Java objects.
  client_->DismissPrompt();
  Reset();
  LogInitiatePurchaseActionResult(
      /*result=*/result ==
          FacilitatedPaymentsApiClient::PurchaseActionResult::kResultOk,
      base::TimeTicks::Now() - purchase_action_start_time_);
  // Map the result received from the purchase action to overall transaction
  // result.
  TransactionResult transaction_result = TransactionResult::kFailed;
  switch (result) {
    case FacilitatedPaymentsApiClient::PurchaseActionResult::kResultOk:
      transaction_result = TransactionResult::kSuccess;
      break;
    case FacilitatedPaymentsApiClient::PurchaseActionResult::kCouldNotInvoke:
      transaction_result = TransactionResult::kFailed;
      break;
    case FacilitatedPaymentsApiClient::PurchaseActionResult::kResultCanceled:
      transaction_result = TransactionResult::kAbandoned;
      break;
  }
  LogTransactionResult(transaction_result, trigger_source_,
                       base::TimeTicks::Now() - fop_selector_shown_time_,
                       ukm_source_id_);
}

void FacilitatedPaymentsManager::ResetForTesting() {
  is_test_ = false;
  Reset();
  is_test_ = true;
}

}  // namespace payments::facilitated