// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/request.h"

#include <algorithm>
#include <random>
#include <vector>

#include "base/barrier_closure.h"
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/strings/escape.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/values.h"
#include "content/browser/bad_message.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webid/fake_identity_request_dialog_controller.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/identity_registry.h"
#include "content/browser/webid/idp_network_request_manager.h"
#include "content/browser/webid/mappers.h"
#include "content/browser/webid/request_page_data.h"
#include "content/browser/webid/request_service.h"
#include "content/browser/webid/url_computations.h"
#include "content/browser/webid/user_info_request.h"
#include "content/browser/webid/webid_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/webid/federated_embedder_login_request.h"
#include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/webid/federated_identity_auto_reauthn_permission_context_delegate.h"
#include "content/public/browser/webid/federated_identity_permission_context_delegate.h"
#include "content/public/browser/webid/identity_request_account.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/page_visibility_state.h"
#include "mojo/public/cpp/base/values_mojom_traits.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/webid/login_status_account.h"
#include "third_party/blink/public/common/webid/login_status_options.h"
#include "third_party/blink/public/mojom/webid/federated_request.mojom.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"

namespace content::webid {

using CompleteRequestWithErrorCallback =
    base::OnceCallback<void(blink::mojom::FederatedRequestResult,
                            std::optional<RequestIdTokenStatus>,
                            bool)>;
using ErrorDialogType = IdpNetworkRequestManager::FedCmErrorDialogType;
using ErrorUrlType = IdpNetworkRequestManager::FedCmErrorUrlType;
using FederatedApiPermissionStatus =
    FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using IdentityProviderDataPtr = scoped_refptr<IdentityProviderData>;
using IdentityProviderGetInfo = AccountsFetcher::IdentityProviderGetInfo;
using IdentityRequestAccountPtr = scoped_refptr<IdentityRequestAccount>;
using LoginState = IdentityRequestAccount::LoginState;
using MediationRequirement = ::password_manager::CredentialMediationRequirement;
using RpMode = blink::mojom::RpMode;
using SignInMode = IdentityRequestAccount::SignInMode;
using TokenError = IdentityCredentialTokenError;
using TokenResponseType = IdpNetworkRequestManager::FedCmTokenResponseType;
using TokenStatus = RequestIdTokenStatus;
using base::Value;
using blink::mojom::FederatedRequestResult;
using blink::mojom::IdentityProviderConfig;
using blink::mojom::IdentityProviderGetParametersPtr;
using blink::mojom::IdentityProviderRequestOptionsPtr;
using blink::mojom::RegisterIdpStatus;
using blink::mojom::RequestTokenStatus;
using blink::mojom::RequestUserInfoStatus;

namespace {
static constexpr base::TimeDelta kTokenRequestDelay = base::Seconds(3);
static constexpr base::TimeDelta kMaxRejectionTime = base::Seconds(60);

// Users spend less time on Android to dismiss the UI. Given the difference, we
// use two set of values. The values are calculated based on UMA data to follow
// lognormal distribution.
#if BUILDFLAG(IS_ANDROID)
static constexpr double kRejectionLogNormalMu = 7.4;
static constexpr double kRejectionLogNormalSigma = 1.24;
#else
static constexpr double kRejectionLogNormalMu = 8.6;
static constexpr double kRejectionLogNormalSigma = 1.4;
#endif  // BUILDFLAG(IS_ANDROID)

// The time from when the accounts dialog is shown to when a user explicitly
// closes it follows normal distribution. To make the random failures
// indistinguishable from user declines, we use lognormal distribution to
// generate the random number.
base::TimeDelta GetRandomRejectionTime() {
  base::RandomBitGenerator generator;
  std::lognormal_distribution<double> distribution(kRejectionLogNormalMu,
                                                   kRejectionLogNormalSigma);

  base::TimeDelta rejection_time =
      base::Seconds(distribution(generator) / 1000);

  return std::min(kMaxRejectionTime, rejection_time);
}

std::string FormatOriginForDisplay(const url::Origin& origin) {
  return FormatUrlToSite(origin.GetURL());
}

std::string GetTopFrameOriginForDisplay(const url::Origin& top_frame_origin) {
  return FormatOriginForDisplay(top_frame_origin);
}

bool IsFrameActive(RenderFrameHost* frame) {
  return frame && frame->IsActive();
}

bool IsFrameVisible(RenderFrameHost* frame) {
  return frame && frame->IsActive() &&
         frame->GetVisibilityState() == PageVisibilityState::kVisible;
}

bool CanBypassPermissionStatusCheck(
    const blink::mojom::RpMode& rp_mode,
    const MediationRequirement& mediation_requirement) {
  // Embargo or browser settings should not affect active mode. Since
  // conditional flow isn't intrusive which was the main reason we added such
  // controls, we can bypass the check for it as well.
  return rp_mode == RpMode::kActive ||
         (IsAutofillEnabled() &&
          mediation_requirement == MediationRequirement::kConditional);
}

}  // namespace

Request::FetchData::FetchData() = default;
Request::FetchData::~FetchData() = default;

Request::AutoReauthnInfo::AutoReauthnInfo() = default;
Request::AutoReauthnInfo::~AutoReauthnInfo() = default;
Request::AutoReauthnInfo::AutoReauthnInfo(const AutoReauthnInfo&) = default;
Request::AutoReauthnInfo& Request::AutoReauthnInfo::operator=(
    const AutoReauthnInfo&) = default;

Request::Request(RenderFrameHost* rfh, RequestService& request_service)
    : render_frame_host_(rfh),
      request_service_(request_service),
      perfetto_track_(CreatePerfettoTrackForFedCM(this)) {
  receivers_.set_disconnect_handler(
      base::BindRepeating(&Request::OnConnectionError, base::Unretained(this)));
}

Request::~Request() {
  // Ensures key data members are destructed in proper order and resolves any
  // pending promise.
  if (request_token_callback_) {
    CompleteRequestWithError(FederatedRequestResult::kError,
                             TokenStatus::kUnhandledRequest,
                             /*should_delay_callback=*/false);
  }
}

void Request::BindReceiver(
    mojo::PendingReceiver<blink::mojom::FederatedRequest> pending_receiver) {
  receivers_.Add(this, std::move(pending_receiver));
}

void Request::Abort() {
  if (!request_token_callback_) {
    // This can happen if the renderer requested an abort() after the browser
    // invoked the callback but before the renderer received the callback.
    return;
  }

  // Dialog will be hidden by the destructor of the dialog controller in
  // RequestService, triggered by CompleteRequest.

  CompleteRequestWithError(FederatedRequestResult::kCanceled,
                           TokenStatus::kAborted,
                           /*should_delay_callback=*/false);
}

void Request::OnConnectionError() {
  // If the renderer disconnected the FederatedRequest pipe without calling
  // Abort(), it means the frame was detached, the tab was closed, or the
  // renderer crashed. We complete the request with an error, which will
  // correctly record a `kUnhandledRequest` fallback metric and trigger the
  // service-level cleanup.
  CompleteRequestWithError(FederatedRequestResult::kError,
                           TokenStatus::kUnhandledRequest,
                           /*should_delay_callback=*/false);
}

std::vector<IdentityProviderRequestOptionsPtr>
Request::MaybeAddRegisteredProviders(
    std::vector<IdentityProviderRequestOptionsPtr>& providers) {
  std::vector<IdentityProviderRequestOptionsPtr> result;

  std::vector<GURL> registered_config_urls =
      permission_delegate()->GetRegisteredIdPs();

  // TODO(crbug.com/40252825): we insert the registered IdPs to
  // the list of IdPs in a reverse chronological order:
  // first IdPs to be registered goes first. It is not clear
  // yet what's the right order, but this seems like a reasonable
  // starting point.
  std::ranges::reverse(registered_config_urls);

  for (auto& provider : providers) {
    if (!provider->config->from_idp_registration_api) {
      result.emplace_back(provider->Clone());
      continue;
    }

    for (auto& configURL : registered_config_urls) {
      IdentityProviderRequestOptionsPtr idp = provider->Clone();
      // Keep `from_idp_registration_api` so it is clear this is a registered
      // provider.
      idp->config->config_url = configURL;
      result.emplace_back(std::move(idp));
    }
  }

  // TODO(crbug.com/40252825): Consider removing duplicate
  // IdPs in case they were present in the registry as well
  // as added individually.

  return result;
}

bool Request::RequestToken(
    std::vector<IdentityProviderGetParametersPtr> idp_get_params_ptrs,
    MediationRequirement requirement,
    NavigationHandle* navigation_handle,
    const GURL& intercepted_url,
    RequestTokenCallback callback) {
  CHECK(!HasPendingRequest());
  bool intercept = false;
  bool should_complete_request_immediately = false;
  devtools_instrumentation::WillSendFedCmRequest(
      render_frame_host(), &intercept, &should_complete_request_immediately);
  should_complete_request_immediately =
      (intercept && should_complete_request_immediately) ||
      api_permission_delegate()->ShouldCompleteRequestImmediately();

  // Expand the providers list with registered providers.
  if (IsIdPRegistrationEnabled()) {
    for (auto& idp_get_params_ptr : idp_get_params_ptrs) {
      std::vector<IdentityProviderRequestOptionsPtr> providers =
          MaybeAddRegisteredProviders(idp_get_params_ptr->providers);
      if (providers.empty()) {
        render_frame_host().AddMessageToConsole(
            blink::mojom::ConsoleMessageLevel::kError,
            "No identity providers are registered.");
        base::TimeDelta delay;
        if (!should_complete_request_immediately) {
          delay = GetRandomRejectionTime();
        }
        base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(std::move(callback), RequestTokenStatus::kError,
                           std::nullopt, std::nullopt,
                           /*error=*/nullptr,
                           /*is_auto_selected=*/false),
            delay);
        return true;
      }
      idp_get_params_ptr->providers = std::move(providers);
    }
  }

  if (!render_frame_host().GetPage().IsPrimary()) {
    // This should not be possible but seems to be happening, so we log
    // the lifecycle state for further investigation.
    RenderFrameHostImpl* host_impl =
        static_cast<RenderFrameHostImpl*>(&render_frame_host());

    RecordLifecycleStateFailureReason(
        LifecycleStateImplLifecycleStateImplToFedCmLifecycleStateFailureReason(
            host_impl->lifecycle_state()));
    std::move(callback).Run(RequestTokenStatus::kError, std::nullopt,
                            std::nullopt,
                            /*error=*/nullptr,
                            /*is_auto_selected=*/false);
    return false;
  }

  can_accept_redirect_to_ =
      request_service_->force_allow_redirect_to_for_testing() ||
      ((IsNavigationInterceptionEnabled() ||
        HasEmbedderLoginRequest(&render_frame_host())) &&
       navigation_handle != nullptr);

  had_transient_user_activation_ =
      (navigation_handle &&
       DidNavigationHandleHaveActivation(navigation_handle)) ||
      render_frame_host().HasTransientUserActivation();
  if (navigation_handle) {
    intercepted_url_ = intercepted_url;
  }

  idp_order_ = {};
  for (auto& idp_get_params_ptr : idp_get_params_ptrs) {
    for (auto& idp_ptr : idp_get_params_ptr->providers) {
      idp_order_.push_back(idp_ptr->config->config_url);
    }
  }

  // From here on out, all failures go through CompleteRequest, so this is
  // where we start the trace event.
  TRACE_EVENT_BEGIN("content.fedcm", "FedCM get", perfetto_track_);

  should_complete_request_immediately_ = should_complete_request_immediately;
  mediation_requirement_ = requirement;
  request_token_callback_ = std::move(callback);
  GetPageData(render_frame_host().GetPage())
      ->SetPendingWebIdentityRequest(this);
  network_manager_ = request_service_->CreateNetworkManager();

  start_time_ = base::TimeTicks::Now();
  if (!fedcm_metrics_) {
    fedcm_metrics_ = CreateFedCmMetrics();
  }
  std::set<GURL> idps_with_nonce;
  std::set<GURL> idps_with_nonce_outside_params_only;
  for (const auto& idp_get_params_ptr : idp_get_params_ptrs) {
    for (const auto& idp_ptr : idp_get_params_ptr->providers) {
      if (!idp_ptr->nonce.empty()) {
        idps_with_nonce.insert(idp_ptr->config->config_url);

        bool has_nonce_in_params = false;
        if (idp_ptr->params_json) {
          std::optional<base::Value> params = base::JSONReader::Read(
              *idp_ptr->params_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
          if (params && params->is_dict()) {
            if (params->GetDict().contains("nonce")) {
              has_nonce_in_params = true;
            }
          }
        }
        if (!has_nonce_in_params) {
          idps_with_nonce_outside_params_only.insert(
              idp_ptr->config->config_url);
        }
      }
    }
  }
  fedcm_metrics_->RecordHasNonce(idps_with_nonce);
  fedcm_metrics_->RecordHasNonceOutsideParamsOnly(
      idps_with_nonce_outside_params_only);

  if (idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kActive) {
    rp_mode_ = RpMode::kActive;
    if (!had_transient_user_activation_) {
      CompleteRequestWithError(
          FederatedRequestResult::kMissingTransientUserActivation,
          TokenStatus::kMissingTransientUserActivation,
          /*should_delay_callback=*/false);
      return false;
    }
  } else {
    rp_mode_ = RpMode::kPassive;
  }

  if (origin().opaque()) {
    CompleteRequestWithError(
        FederatedRequestResult::kRelyingPartyOriginIsOpaque,
        TokenStatus::kRpOriginIsOpaque,
        /*should_delay_callback=*/false);
    return false;
  }

  FederatedApiPermissionStatus permission_status = GetApiPermissionStatus();

  if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_)) {
    if (permission_status != FederatedApiPermissionStatus::GRANTED) {
      std::pair<FederatedRequestResult, TokenStatus> resultAndTokenStatus =
          PermissionStatusToRequestResultAndTokenStatus(permission_status);
      CompleteRequestWithError(resultAndTokenStatus.first,
                               resultAndTokenStatus.second,
                               /*should_delay_callback=*/true);
      return true;
    }
  }

  request_service_->IncrementNumRequests();

  std::set<GURL> unique_idps;
  for (auto& idp_get_params_ptr : idp_get_params_ptrs) {
    for (auto& idp_ptr : idp_get_params_ptr->providers) {
      // Throw an error if duplicate IDPs are specified.
      const bool is_unique_idp =
          unique_idps.insert(idp_ptr->config->config_url).second;
      if (!is_unique_idp) {
        CompleteRequestWithError(FederatedRequestResult::kError,
                                 /*token_status=*/std::nullopt,
                                 /*should_delay_callback=*/false);
        return false;
      }

      url::Origin idp_origin = url::Origin::Create(idp_ptr->config->config_url);
      if (!network::IsOriginPotentiallyTrustworthy(idp_origin)) {
        CompleteRequestWithError(
            FederatedRequestResult::kIdpNotPotentiallyTrustworthy,
            TokenStatus::kIdpNotPotentiallyTrustworthy,
            /*should_delay_callback=*/false);
        return false;
      }
    }
  }

  bool any_idp_has_custom_scopes = false;
  bool any_idp_has_parameters = false;
  for (auto& idp_get_params_ptr : idp_get_params_ptrs) {
    for (auto& idp_ptr : idp_get_params_ptr->providers) {
      bool has_failing_idp_signin_status =
          ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
              idp_ptr->config->config_url, permission_delegate());

      if (has_failing_idp_signin_status) {
        if (idp_get_params_ptr->mode == blink::mojom::RpMode::kPassive) {
          // In the multi IDP case, we do not want to complete the request
          // right away as there are other IDPs which may be logged in. But we
          // also do not want to fetch this IDP.
          unique_idps.erase(idp_ptr->config->config_url);
          continue;
        } else if (idp_get_params_ptr->mode == blink::mojom::RpMode::kActive) {
          // We fail sooner before, but just to double check, we assert that
          // we are inside a user gesture here again.
          CHECK(had_transient_user_activation_);
        }
      }
      if (ShouldFailBeforeFetchingAccounts(idp_ptr->config->config_url)) {
        // In the multi IDP case, we do not want to complete the request right
        // away as there are other IDPs which may be logged in. But we also do
        // not want to fetch this IDP.
        unique_idps.erase(idp_ptr->config->config_url);
        continue;
      }

      any_idp_has_custom_scopes = any_idp_has_custom_scopes ||
                                  GetDisclosureFields(idp_ptr->fields).empty();
      any_idp_has_parameters = any_idp_has_parameters || idp_ptr->params_json;

      blink::mojom::RpContext rp_context = idp_get_params_ptr->context;
      blink::mojom::RpMode rp_mode = idp_get_params_ptr->mode;
      const GURL& idp_config_url = idp_ptr->config->config_url;
      std::optional<blink::mojom::Format> format =
          IsDelegationEnabled() ? idp_ptr->format : std::nullopt;
      token_request_get_infos_.emplace(
          idp_config_url, IdentityProviderGetInfo(std::move(idp_ptr),
                                                  rp_context, rp_mode, format));
    }
  }

  if (any_idp_has_parameters || any_idp_has_custom_scopes) {
    RpParameters parameters;
    if (any_idp_has_custom_scopes && any_idp_has_parameters) {
      parameters = RpParameters::kHasParametersAndNonDefaultScope;
    } else if (any_idp_has_parameters) {
      parameters = RpParameters::kHasParameters;
    } else {
      DCHECK(any_idp_has_custom_scopes);
      parameters = RpParameters::kHasNonDefaultScope;
    }
    fedcm_metrics_->RecordRpParameters(parameters);
  }

  if (unique_idps.empty()) {
    // At this point either all IDPs are signed out or mediation:silent was used
    // and there are no returning accounts.
    auto result = mediation_requirement_ == MediationRequirement::kSilent
                      ? FederatedRequestResult::kSilentMediationFailure
                      : FederatedRequestResult::kNotSignedInWithIdp;
    auto token_status = mediation_requirement_ == MediationRequirement::kSilent
                            ? TokenStatus::kSilentMediationFailure
                            : TokenStatus::kNotSignedInWithIdp;
    CompleteRequestWithError(result, token_status,
                             /*should_delay_callback=*/true);
    return true;
  }

  // Show loading dialog while fetching endpoints if it is a active flow. This
  // is needed even if the LoginStatus is "logged-out" because we need to fetch
  // the config file to get the login_url which may take some time.
  if (rp_mode_ == RpMode::kActive) {
    CHECK_GT(idp_order_.size(), 0u);
    // If there is more than 1 IDP, do not show the IDP info in the loading
    // dialog.
    std::string idp_for_display =
        idp_order_.size() == 1u
            ? FormatOriginForDisplay(url::Origin::Create(idp_order_[0]))
            : "";
    blink::mojom::RpContext rp_context = idp_get_params_ptrs[0]->context;
    if (!GetDialogController()->ShowLoadingDialog(
            CreateRpData(/*client_metadata_received=*/false), idp_for_display,
            rp_context, rp_mode_,
            base::BindOnce(&Request::OnDialogDismissed,
                           weak_ptr_factory_.GetWeakPtr()))) {
      return false;
    }
    did_show_ui_ = true;
  }

  fedcm_metrics_->RecordIdentityProvidersCount(idp_order_.size());

  CHECK(!unique_idps.empty());
  if (rp_mode_ == RpMode::kPassive && idp_order_.size() == 1u) {
    GetDialogController()->GetPassiveDialogVolume(
        base::BindOnce(&Request::OnGetPassiveDialogVolume,
                       weak_ptr_factory_.GetWeakPtr(), std::move(unique_idps)));
    return true;
  }
  FetchEndpointsForIdps(std::move(unique_idps));
  return true;
}

void Request::OnIdpSigninStatusReceived(const url::Origin& idp_config_origin,
                                        bool idp_signin_status) {
  if (!idp_signin_status) {
    return;
  }

  for (const auto& [get_idp_config_url, get_info] : token_request_get_infos_) {
    if (url::Origin::Create(get_idp_config_url) == idp_config_origin) {
      permission_delegate()->RemoveIdpSigninStatusObserver(this);
      idps_user_tried_to_signin_to_.insert(get_idp_config_url);
      FetchEndpointsForIdps({get_idp_config_url});
      break;
    }
  }
}

bool Request::HasPendingRequest() const {
  RequestPageData* page_data = GetPageData(render_frame_host().GetPage());
  bool has_pending_request = page_data->PendingWebIdentityRequest() != nullptr;
  DCHECK(has_pending_request || !request_token_callback_);
  return has_pending_request;
}

void Request::FetchEndpointsForIdps(const std::set<GURL>& idp_config_urls) {
  int icon_ideal_size = GetDialogController()->GetBrandIconIdealSize(rp_mode_);
  int icon_minimum_size =
      GetDialogController()->GetBrandIconMinimumSize(rp_mode_);
  std::set<GURL> pending_idps = std::move(fetch_data_.pending_idps);
  pending_idps.insert(idp_config_urls.begin(), idp_config_urls.end());
  fetch_data_ = FetchData();
  fetch_data_.pending_idps = std::move(pending_idps);

  std::vector<ConfigFetcher::FetchRequest> idps;
  for (const auto& idp : idp_config_urls) {
    auto idp_get = token_request_get_infos_.find(idp);
    CHECK(idp_get != token_request_get_infos_.end());
    idps.emplace_back(
        idp, idp_get->second.provider->config->from_idp_registration_api);
  }

  fedcm_accounts_fetcher_ = std::make_unique<AccountsFetcher>(
      render_frame_host(), network_manager_.get(), api_permission_delegate(),
      permission_delegate(),
      AccountsFetcher::FedCmFetchingParams(
          rp_mode_, icon_ideal_size, icon_minimum_size, mediation_requirement_),
      base::BindOnce(&Request::OnAccountsResultsReceived,
                     weak_ptr_factory_.GetWeakPtr()));

  // When retrying (e.g. after IDP sign-in failure popup), there is only 1 IDP
  // requested and its .well-known and config endpoints/metadata are already
  // cached in `idp_infos_`. In this case, bypass ConfigFetcher and directly
  // fetch accounts for the cached IDP.
  if (idps.size() == 1u) {
    auto it = idp_infos_.find(idps[0].identity_provider_config_url);
    if (it != idp_infos_.end() && it->second) {
      std::vector<std::unique_ptr<IdentityProviderInfo>> cached_idp_infos;
      cached_idp_infos.push_back(
          std::make_unique<IdentityProviderInfo>(*it->second));
      fedcm_accounts_fetcher_->FetchAccountsForIdps(
          cached_idp_infos, token_request_get_infos_, fedcm_metrics_.get(),
          GetEmbeddingOrigin(),
          base::BindRepeating(&Request::FilterAccounts,
                              weak_ptr_factory_.GetWeakPtr()));
      return;
    }
  }

  fedcm_accounts_fetcher_->FetchEndpointsForIdps(
      idps, token_request_get_infos_, fedcm_metrics_.get(),
      GetEmbeddingOrigin(),
      base::BindRepeating(&Request::FilterAccounts,
                          weak_ptr_factory_.GetWeakPtr()));
}

void Request::FilterAccounts(const GURL& idp_config_url,
                             const GURL& idp_login_url,
                             std::vector<IdentityRequestAccountPtr>& accounts) {
  auto filter = [](const IdentityRequestAccountPtr& account) {
    return account->is_filtered_out;
  };
  if (idps_user_tried_to_signin_to_.find(idp_config_url) ==
          idps_user_tried_to_signin_to_.end() ||
      login_url_ != idp_login_url) {
    std::erase_if(accounts, filter);
  } else {
    // If the user is logging in to new accounts, only show filtered
    // accounts if there are no new unfiltered accounts. This includes in
    // particular the case where all accounts are filtered out.
    size_t new_unfiltered =
        std::count_if(accounts.begin(), accounts.end(),
                      [&](const IdentityRequestAccountPtr& account) {
                        return !account->is_filtered_out &&
                               account_ids_before_login_.find(account->id) ==
                                   account_ids_before_login_.end();
                      });
    if (new_unfiltered > 0u) {
      std::erase_if(accounts, filter);
    }
  }
}

void Request::OnAccountsResultsReceived(
    base::TimeTicks well_known_and_config_fetched_time,
    std::vector<AccountsFetcher::Result> results) {
  if (!well_known_and_config_fetched_time.is_null()) {
    SetWellKnownAndConfigFetchedTime(well_known_and_config_fetched_time);
  }

  for (auto& result : results) {
    if (result.idp_info) {
      SetIdpLoginInfo(result.idp_info->metadata.idp_login_url,
                      result.idp_info->provider->login_hint,
                      result.idp_info->provider->domain_hint);
    }
    if (result.accounts_fetched_time != base::TimeTicks()) {
      SetAccountsFetchedTime(result.accounts_fetched_time);
    }
    if (result.client_metadata_fetched_time != base::TimeTicks()) {
      SetClientMetadataFetchedTime(result.client_metadata_fetched_time);
    }

    if (result.show_active_mode_modal_dialog) {
      MaybeShowActiveModeModalDialog(result.idp_config_url,
                                     result.idp_info->metadata.idp_login_url);
      continue;
    }

    if (result.error) {
      OnFetchDataForIdpFailed(std::move(result.idp_info), *result.error,
                              result.token_status,
                              result.should_delay_callback);
      continue;
    }

    if (result.is_mismatch) {
      OnIdpMismatch(std::move(result.idp_info));
      continue;
    }

    // Success
    CHECK(result.accounts.has_value());
    idp_filtered_accounts_[result.idp_config_url] =
        std::move(result.filtered_accounts);
    OnFetchDataForIdpSucceeded(std::move(*result.accounts),
                               std::move(result.idp_info));
  }
}

bool Request::CanShowContinueOnPopup() const {
  if (mediation_requirement_ == MediationRequirement::kConditional) {
    // Because conditional mediation always requires a user gesture to sign in,
    // we can always allow the continuation popup.
    return true;
  }

  if (mediation_requirement_ == MediationRequirement::kSilent) {
    return false;
  }

  if (mediation_requirement_ == MediationRequirement::kRequired) {
    // In this case, we always have a user gesture (the user had to choose
    // an account), so we can show a popup.
    return true;
  }

  if (identity_selection_type_ == kExplicit) {
    return true;
  }

  return had_transient_user_activation_;
}

UseOtherAccountResult Request::ComputeUseOtherAccountResult(
    blink::mojom::FederatedRequestResult result,
    const std::optional<GURL>& selected_idp_config_url) {
  if (result != FederatedRequestResult::kSuccess) {
    return UseOtherAccountResult::kUserDoesNotSignIn;
  }

  CHECK(selected_idp_config_url);
  if (IsEndpointSameOrigin(*selected_idp_config_url, login_url_) &&
      !account_ids_before_login_.contains(account_id_)) {
    return UseOtherAccountResult::kUserSignsInWithNewAccount;
  }
  return UseOtherAccountResult::kUserSignsInWithExistingAccount;
}

void Request::OnFetchDataForIdpSucceeded(
    IdpNetworkRequestManager::AccountsResponse accounts,
    std::unique_ptr<IdentityProviderInfo> idp_info) {
  fetch_data_.did_succeed_for_at_least_one_idp = true;

  const GURL& idp_config_url = idp_info->provider->config->config_url;
  // If the IDP data existed before, we need to remove the old accounts data.
  // This can happen with the 'use other account' feature.
  if (idp_infos_.find(idp_config_url) != idp_infos_.end()) {
    std::erase_if(accounts_, [&idp_config_url](const auto& account) {
      return account->identity_provider->idp_metadata.config_url ==
             idp_config_url;
    });
    std::erase_if(filtered_accounts_, [&idp_config_url](const auto& account) {
      return account->identity_provider->idp_metadata.config_url ==
             idp_config_url;
    });
  }
  idp_infos_[idp_config_url] = std::move(idp_info);
  idp_accounts_[idp_config_url] = std::move(accounts.accounts);

  fetch_data_.pending_idps.erase(idp_config_url);
  MaybeShowAccountsDialog();
}

void Request::SetIdpLoginInfo(const GURL& idp_login_url,
                              const std::string& login_hint,
                              const std::string& domain_hint) {
  idp_login_infos_[idp_login_url] = {login_hint, domain_hint};
}

void Request::SetWellKnownAndConfigFetchedTime(base::TimeTicks time) {
  well_known_and_config_fetched_time_ = time;
  fedcm_metrics_->RecordWellKnownAndConfigFetchTime(
      well_known_and_config_fetched_time_ - start_time_);
}

void Request::OnFetchDataForIdpFailed(
    const std::unique_ptr<IdentityProviderInfo> idp_info,
    blink::mojom::FederatedRequestResult result,
    std::optional<RequestIdTokenStatus> token_status,
    bool should_delay_callback) {
  const GURL& idp_config_url = idp_info->provider->config->config_url;
  fetch_data_.pending_idps.erase(idp_config_url);

  if (fetch_data_.pending_idps.empty() &&
      !fetch_data_.did_succeed_for_at_least_one_idp) {
    CompleteRequestWithError(result, token_status, should_delay_callback);
    return;
  }

  AddDevToolsIssue(result);
  AddConsoleErrorMessage(result);

  // We do not call both OnFetchDataForIdpFailed() after OnFetchDataSucceeded()
  // for the same IDP.
  DCHECK(idp_infos_.find(idp_config_url) == idp_infos_.end());
  MaybeShowAccountsDialog();
}

const std::optional<std::vector<IdentityRequestAccountPtr>>
Request::GetAutofillSuggestions() const {
  // Requires conditional FedCM to be enabled.
  if (!IsAutofillEnabled()) {
    return std::nullopt;
  }

  // There isn't a request hanging.
  if (!HasPendingRequest()) {
    return std::nullopt;
  }

  // We only augment autofill when it is a conditional mediation request.
  if (mediation_requirement_ != MediationRequirement::kConditional) {
    return std::nullopt;
  }

  return GetAccounts();
}

void Request::AssembleAndSortAccounts() {
  idp_data_for_display_.clear();
  filtered_accounts_.clear();

  for (const auto& idp : idp_order_) {
    auto idp_info_it = idp_infos_.find(idp);
    if (idp_info_it != idp_infos_.end() && idp_info_it->second->data) {
      idp_info_it->second->data->idp_metadata.has_filtered_out_account = false;
      idp_data_for_display_.push_back(idp_info_it->second->data);
    }
    auto accounts_it = idp_accounts_.find(idp);
    if (accounts_it != idp_accounts_.end()) {
      accounts_.insert(accounts_.end(),
                       std::make_move_iterator(accounts_it->second.begin()),
                       std::make_move_iterator(accounts_it->second.end()));
    }
    auto filtered_it = idp_filtered_accounts_.find(idp);
    if (filtered_it != idp_filtered_accounts_.end()) {
      filtered_accounts_.insert(
          filtered_accounts_.end(),
          std::make_move_iterator(filtered_it->second.begin()),
          std::make_move_iterator(filtered_it->second.end()));
    }
  }
  idp_accounts_.clear();
  idp_filtered_accounts_.clear();

  std::stable_sort(
      accounts_.begin(), accounts_.end(),
      [&](const auto& account1, const auto& account2) {
        // Show filtered accounts after valid ones.
        if (account1->is_filtered_out || account2->is_filtered_out) {
          return !account1->is_filtered_out;
        }
        // Show newly logged in accounts, if any.
        bool is_account1_new = IsNewlyLoggedIn(*account1);
        bool is_account2_new = IsNewlyLoggedIn(*account2);
        if (is_account1_new || is_account2_new) {
          return !is_account2_new;
        }
        // Show returning accounts before non-returning.
        if (account1->idp_claimed_login_state.value_or(
                account1->browser_trusted_login_state) == LoginState::kSignUp ||
            account2->idp_claimed_login_state.value_or(
                account2->browser_trusted_login_state) == LoginState::kSignUp) {
          return account1->idp_claimed_login_state.value_or(
                     account1->browser_trusted_login_state) ==
                 LoginState::kSignIn;
        }
        // Within returning accounts, prefer those with last used
        // timestamp.
        if (!account1->last_used_timestamp || !account2->last_used_timestamp) {
          return !!account1->last_used_timestamp;
        }
        // If both have last used timestamp, prefer the latest.
        return *account1->last_used_timestamp > *account2->last_used_timestamp;
      });
  // Set the display priority for newly logged in accounts.
  for (const auto& account : accounts_) {
    if (IsNewlyLoggedIn(*account)) {
      account->display_priority = IdentityRequestAccount::DisplayPriority::kNew;
    } else {
      account->display_priority =
          IdentityRequestAccount::DisplayPriority::kRegular;
    }
    if (account->is_filtered_out) {
      account->identity_provider->idp_metadata.has_filtered_out_account = true;
    }
  }
}

Request::AutoReauthnInfo Request::CheckAutoReauthnEligibility() {
  AutoReauthnInfo result;

  // TODO(crbug.com/40246099): Handle auto_reauthn_ for multi IDP.
  // TODO(crbug.com/380367784): Handle auto_reauthn_ for delegated IdP.
  bool auto_reauthn_enabled =
      mediation_requirement_ != MediationRequirement::kRequired;

  if (!auto_reauthn_enabled) {
    return result;
  }

  bool is_auto_reauthn_setting_enabled =
      auto_reauthn_permission_delegate()->IsAutoReauthnSettingEnabled();
  bool is_auto_reauthn_embargoed =
      auto_reauthn_permission_delegate()->IsAutoReauthnEmbargoed(
          GetEmbeddingOrigin());
  bool is_auto_reauthn_blocked_by_embedder =
      auto_reauthn_permission_delegate()->IsAutoReauthnDisabledByEmbedder(
          WebContents::FromRenderFrameHost(&render_frame_host()));

  if (is_auto_reauthn_embargoed) {
    // See `kFederatedIdentityAutoReauthnEmbargoDuration`.
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kInfo,
        "Auto re-authn was previously triggered less than 10 minutes ago. "
        "Only one auto re-authn request can be made every 10 minutes.");
  }
  bool requires_user_mediation = RequiresUserMediation();
  // Auto signs in returning users if they have a single returning account and
  // are signing in.
  IdentityProviderDataPtr auto_reauthn_idp = nullptr;
  IdentityRequestAccountPtr auto_reauthn_account = nullptr;
  bool has_single_returning_account =
      GetAccountForAutoReauthn(&auto_reauthn_idp, &auto_reauthn_account);

  bool is_eligible =
      !requires_user_mediation && is_auto_reauthn_setting_enabled &&
      !is_auto_reauthn_embargoed && has_single_returning_account &&
      !is_auto_reauthn_blocked_by_embedder;

  fedcm_metrics_->RecordAutoReauthnMetrics(
      has_single_returning_account, auto_reauthn_account.get(), is_eligible,
      !is_auto_reauthn_setting_enabled, is_auto_reauthn_embargoed,
      is_auto_reauthn_blocked_by_embedder, requires_user_mediation);

  if (is_eligible) {
    result.is_eligible = true;
    result.idp = auto_reauthn_idp;
    result.account = auto_reauthn_account;
  }
  return result;
}

void Request::MaybeShowAccountsDialog() {
  if (!fetch_data_.pending_idps.empty()) {
    return;
  }

  // The accounts fetch could be delayed for legitimate reasons. A user may be
  // able to disable FedCM API (e.g. via settings or dismissing another FedCM UI
  // on the same RP origin) before the browser receives the accounts response.
  // We should exit early without showing any UI.
  if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_) &&
      GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED) {
    CompleteRequestWithError(FederatedRequestResult::kDisabledInSettings,
                             TokenStatus::kDisabledInSettings,
                             /*should_delay_callback=*/true);
    return;
  }

  AssembleAndSortAccounts();

  // Conditional mediation doesn't display the account chooser when called,
  // it instead waits for another UI surface (say, autofill) to trigger the
  // account chooser.
  if (mediation_requirement_ == MediationRequirement::kConditional) {
    GetDialogController()->NotifyAutofillSourceReadyForTesting();
    return;
  }

  AutoReauthnInfo auto_reauthn = CheckAutoReauthnEligibility();

  if (!auto_reauthn.is_eligible &&
      mediation_requirement_ == MediationRequirement::kSilent) {
    // By this moment we know that the user has granted permission in the past
    // for the RP/IdP. Because otherwise we have returned already in
    // `ShouldFailBeforeFetchingAccounts`. It means that we don't need to show
    // any UI to respect `mediation: silent`.
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: the user has used FedCM with multiple "
        "accounts on this site.");
    CompleteRequestWithError(FederatedRequestResult::kSilentMediationFailure,
                             TokenStatus::kSilentMediationFailure,
                             /*should_delay_callback=*/true);
    return;
  }

  if (auto_reauthn.is_eligible) {
    dialog_type_ = DialogType::kAutoReauth;
    accounts_ = {auto_reauthn.account};
    idp_data_for_display_ = {auto_reauthn.idp};
    accounts_[0]->identity_provider = idp_data_for_display_[0];

    identity_selection_type_ = (rp_mode_ == blink::mojom::RpMode::kPassive)
                                   ? kAutoPassive
                                   : kAutoActive;
  } else {
    dialog_type_ = DialogType::kSelectAccount;
    identity_selection_type_ = kExplicit;
  }

  // The RenderFrameHost may be alive but not visible in the following
  // situations:
  // Situation #1: User switched tabs
  // Situation #2: User navigated the page with bfcache
  //
  // - If this fetch is as a result of an IdP sign-in status change, the FedCM
  // dialog is either visible or temporarily hidden. Update the contents of
  // the dialog.
  // - If the FedCM dialog has not already been shown, do not show the dialog
  // if the RenderFrameHost is hidden because the user does not seem interested
  // in the contents of the current page.
  if (idps_user_tried_to_signin_to_.empty()) {
    bool is_active = IsFrameActive(render_frame_host().GetMainFrame());
    fedcm_metrics_->RecordWebContentsStatusUponReadyToShowDialog(
        IsFrameVisible(render_frame_host().GetMainFrame()), is_active);

    if (!is_active) {
      CompleteRequestWithError(FederatedRequestResult::kRpPageNotVisible,
                               TokenStatus::kRpPageNotVisible,
                               /*should_delay_callback=*/true);
      return;
    }

    ready_to_display_accounts_dialog_time_ = base::TimeTicks::Now();
    fedcm_metrics_->RecordShowAccountsDialogTime(
        idp_data_for_display_,
        ready_to_display_accounts_dialog_time_ - start_time_);

    fedcm_metrics_->RecordShowAccountsDialogTimeBreakdown(
        well_known_and_config_fetched_time_ - start_time_,
        accounts_fetched_time_ - well_known_and_config_fetched_time_,
        client_metadata_fetched_time_ != base::TimeTicks()
            ? client_metadata_fetched_time_ - accounts_fetched_time_
            : base::TimeDelta());
  }
  bool did_succeed_for_at_least_one_idp =
      fetch_data_.did_succeed_for_at_least_one_idp;

  fetch_data_ = FetchData();

  // RenderFrameHost should be in the primary page (ex not in the BFCache).
  DCHECK(render_frame_host().GetPage().IsPrimary());

  bool intercept = false;
  // In tests (content_shell or when --use-fake-ui-for-fedcm is used), the
  // dialog controller will immediately select an account. But if browser
  // automation is enabled, we don't want that to happen because automation
  // should be able to choose which account to select or to cancel.
  // So we use this call to see whether interception is enabled.
  // It is not needed in regular Chrome even when automation is used because
  // there, the dialog will wait for user input anyway.
  devtools_instrumentation::WillShowFedCmDialog(render_frame_host(),
                                                &intercept);
  // Since we don't reuse the controller for each request, and intercept
  // defaults to false, we only need to call this if intercept is true.
  if (intercept) {
    GetDialogController()->SetIsInterceptionEnabled(intercept);
  }

  if (identity_selection_type_ != kExplicit) {
    OnAccountSelected(accounts_[0]->identity_provider->idp_metadata.config_url,
                      accounts_[0]->id, /*is_sign_in=*/true);
    if (!GetDialogController()->ShowVerifyingDialog(
            CreateRpData(/*client_metadata_received=*/true), auto_reauthn.idp,
            accounts_[0], SignInMode::kAuto, rp_mode_,
            base::BindOnce(&Request::OnAccountsDisplayed,
                           weak_ptr_factory_.GetWeakPtr()))) {
      return;
    }
  } else {
    if (!GetDialogController()->ShowAccountsDialog(
            CreateRpData(/*client_metadata_received=*/true),
            idp_data_for_display_, accounts_, filtered_accounts_, rp_mode_,
            base::BindOnce(&Request::OnAccountSelected,
                           weak_ptr_factory_.GetWeakPtr()),
            base::BindRepeating(&Request::LoginToIdP,
                                weak_ptr_factory_.GetWeakPtr(),
                                /*can_append_hints=*/false),
            base::BindOnce(&Request::OnDialogDismissed,
                           weak_ptr_factory_.GetWeakPtr()),
            base::BindOnce(&Request::OnAccountsDisplayed,
                           weak_ptr_factory_.GetWeakPtr()))) {
      return;
    }
  }
  AfterAccountsDialogShown(did_succeed_for_at_least_one_idp);
}

void Request::OnGetPassiveDialogVolume(
    const std::set<GURL>& unique_idps,
    IdentityRequestDialogController::PassiveDialogVolume dialog_volume) {
  passive_dialog_volume_ = dialog_volume;
  FetchEndpointsForIdps(std::move(unique_idps));
}

void Request::AfterAccountsDialogShown(bool did_succeed_for_at_least_one_idp) {
  devtools_instrumentation::DidShowFedCmDialog(render_frame_host());

  if (identity_selection_type_ == kExplicit &&
      did_succeed_for_at_least_one_idp) {
    // We omit recording the accounts dialog shown metric for auto re-authn
    // because the metric is used to detect IDPs flashing UI. Auto re-authn
    // verifying UI cannot be flashed since it is destroyed automatically after
    // 3 seconds and cannot be destroyed earlier for a11y reasons.
    accounts_dialog_shown_time_ = base::TimeTicks::Now();
  }

  // Note that accounts dialog shown after mismatch dialog is also recorded.
  // Although not useful for catching malicious IDPs, it should only be a very
  // small percentage of the samples recorded.
  fedcm_metrics_->RecordAccountsDialogShown(idp_data_for_display_);
  fedcm_metrics_->RecordRpUrlHasPath(
      render_frame_host().GetMainFrame()->GetLastCommittedURL().GetPath() !=
      "/");
}

void Request::NotifyAutofillSuggestionAccepted(
    const GURL& idp,
    const std::string& account_id,
    bool show_modal,
    OnFederatedTokenReceivedCallback callback) {
  token_received_callback_for_autofill_ = std::move(callback);

  auto get_info_it = token_request_get_infos_.find(idp);
  auto idp_info_it = idp_infos_.find(idp);
  bool is_account_id_valid =
      std::ranges::any_of(accounts_, [&](const auto& account) {
        return account->identity_provider->idp_metadata.config_url == idp &&
               account->id == account_id;
      });

  if (get_info_it == token_request_get_infos_.end() ||
      idp_info_it == idp_infos_.end() || !is_account_id_valid) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(token_received_callback_for_autofill_),
                       false));
    return;
  }

  // Currently the verified email flow opens a modal UI upon notification and
  // the autofill dropdown UI gets dismissed immediately. i.e. it doesn't need a
  // valid callback. However, if a user is presented a full federated account,
  // upon the account selection we'd proceed with fetching tokens directly and
  // update he autofill dropdown UI to a loading UI.
  if (!show_modal) {
    OnAccountSelected(idp, account_id, true);
    return;
  }
  // TODO(crbug.com/380367784): The third argument of OnAccountSelected checks
  // if this is a sign-in or a sign-up moment. In delegation, however, by
  // design, the IdP doesn't get to learn about the presentations, so wouldn't
  // know whether this is a sign-in or sign-up moment (e.g. wouldn't have a
  // approved_clients array). We should figure out how to reconcile these two
  // modes.

  // TODO(crbug.com/412640661): Currently, in order to skip the account chooser
  // and go straight to the disclosure UI, we have to call ShowLoadingDialog()
  // before we can call ShowAccountsDialog() to create the internal state
  // necessary in the dialog controller. We should probably be able to create
  // the internal state on demand in case it isn't available.
  if (!GetDialogController()->ShowLoadingDialog(
          CreateRpData(/*client_metadata_received=*/true),
          FormatOriginForDisplay(url::Origin::Create(idp)),
          get_info_it->second.rp_context, blink::mojom::RpMode::kActive,
          base::BindOnce(&Request::OnDialogDismissed,
                         weak_ptr_factory_.GetWeakPtr()))) {
    return;
  }
  did_show_ui_ = true;

  std::vector<IdentityRequestAccountPtr> selected;

  for (auto account : accounts_) {
    if (account->identity_provider->idp_metadata.config_url == idp &&
        account->id == account_id) {
      selected.push_back(account);
    }
  }

  // TODO(crbug.com/412640661): in order to skip the account chooser, we
  // set the display priority to `kNew`. We should probably refactor the API to
  // support this use case, rather than overload an unintended use.
  for (const auto& account : selected) {
    account->display_priority = IdentityRequestAccount::DisplayPriority::kNew;
  }
  if (!GetDialogController()->ShowAccountsDialog(
          CreateRpData(/*client_metadata_received=*/true),
          idp_data_for_display_, selected, filtered_accounts_,
          blink::mojom::RpMode::kActive,
          base::BindOnce(&Request::OnAccountSelected,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindRepeating(&Request::LoginToIdP,
                              weak_ptr_factory_.GetWeakPtr(),
                              /*can_append_hints=*/false),
          base::BindOnce(&Request::OnDialogDismissed,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindOnce(&Request::OnAccountsDisplayed,
                         weak_ptr_factory_.GetWeakPtr()))) {
    return;
  }
  // TODO(crbug.com/435216589): Should we call AfterAccountsDialogShown here?
}

void Request::OnAccountsDisplayed() {
  accounts_dialog_display_time_ = base::TimeTicks::Now();
  did_show_ui_ = true;
}

void Request::OnIdpMismatch(std::unique_ptr<IdentityProviderInfo> idp_info) {
  const GURL& idp_config_url = idp_info->provider->config->config_url;

  idp_infos_[idp_config_url] = std::move(idp_info);

  fetch_data_.pending_idps.erase(idp_config_url);
  if (!fetch_data_.pending_idps.empty()) {
    return;
  }

  // Invoke the accounts dialog flow if there is at least one account or more
  // than one IDP for which we should show the mismatch dialog.
  // TODO(crbug.com/331426009): make this code clearer by creating a separate
  // method for showing multiple mismatch UI.
  if (fetch_data_.did_succeed_for_at_least_one_idp || idp_infos_.size() > 1u) {
    MaybeShowAccountsDialog();
    // If there are no successful IDPs, this is the multi IDP case where all are
    // mismatch.
    if (!fetch_data_.did_succeed_for_at_least_one_idp) {
      mismatch_dialog_shown_time_ = base::TimeTicks::Now();
      has_shown_mismatch_ = true;
      devtools_instrumentation::DidShowFedCmDialog(render_frame_host());
    }
    return;
  }

  if (rp_mode_ == RpMode::kActive) {
    MaybeShowActiveModeModalDialog(
        idp_config_url, idp_infos_[idp_config_url]->metadata.idp_login_url);
    return;
  }

  ShowSingleIdpFailureDialog();
}

void Request::ShowSingleIdpFailureDialog() {
  CHECK_EQ(idp_infos_.size(), 1u);
  IdentityProviderInfo* idp_info = idp_infos_.begin()->second.get();
  url::Origin idp_origin =
      url::Origin::Create(idp_info->provider->config->config_url);
  // RenderFrameHost should be in the primary page (ex not in the BFCache).
  DCHECK(render_frame_host().GetPage().IsPrimary());

  fetch_data_ = FetchData();

  // Set `idp_data_for_display_` so it is always the case that we can rely on it
  // to know which IDPs have been seen in the UI.
  CHECK(idp_info->data);
  idp_data_for_display_ = {idp_info->data};

  // If IdP login status mismatch dialog is already visible, calling
  // ShowFailureDialog() a 2nd time should notify the user that login
  // failed.
  dialog_type_ = DialogType::kConfirmIdpLogin;
  config_url_ = idp_info->provider->config->config_url;
  login_url_ = idp_info->metadata.idp_login_url;

  // Store variables used in RecordMismatchDialogShown since they may be cleaned
  // up in ShowFailureDialog().
  bool has_shown_mismatch = has_shown_mismatch_;
  bool has_hints = !idp_info->provider->login_hint.empty() ||
                   !idp_info->provider->domain_hint.empty() ||
                   !idp_info->metadata.requested_label.empty();

  if (!GetDialogController()->ShowFailureDialog(
          CreateRpData(/*client_metadata_received=*/true),
          FormatOriginForDisplay(idp_origin), idp_info->rp_context, rp_mode_,
          idp_info->metadata, filtered_accounts_,
          base::BindOnce(&Request::OnDismissFailureDialog,
                         weak_ptr_factory_.GetWeakPtr()),
          base::BindRepeating(&Request::LoginToIdP,
                              weak_ptr_factory_.GetWeakPtr(),
                              /*can_append_hints=*/true))) {
    return;
  }
  did_show_ui_ = true;

  CHECK_EQ(idp_data_for_display_.size(), 1u);
  fedcm_metrics_->RecordSingleIdpMismatchDialogShown(
      *idp_data_for_display_[0], has_shown_mismatch, has_hints);
  mismatch_dialog_shown_time_ = base::TimeTicks::Now();
  has_shown_mismatch_ = true;
  devtools_instrumentation::DidShowFedCmDialog(render_frame_host());
}

void Request::OnAccountSelected(const GURL& idp_config_url,
                                const std::string& account_id,
                                bool is_sign_in) {
  DCHECK(!account_id.empty());
  const IdentityProviderInfo& idp_info = *idp_infos_[idp_config_url];

  // Check if the user has disabled the FedCM API after the FedCM UI is
  // displayed. This ensures that requests are not wrongfully sent to IDPs when
  // settings are changed while an existing FedCM UI is displayed. Ideally, we
  // should enforce this check before all requests but users typically won't
  // have time to disable the FedCM API in other types of requests.
  // Note that for the active flow is not affected by the permission status.
  if (!CanBypassPermissionStatusCheck(rp_mode_, mediation_requirement_) &&
      GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED) {
    CompleteRequestWithError(FederatedRequestResult::kDisabledInSettings,
                             TokenStatus::kDisabledInSettings,
                             /*should_delay_callback=*/true);
    return;
  }

  if (identity_selection_type_ != kExplicit) {
    // Embargo auto re-authn to mitigate a deadloop where an auto
    // re-authenticated user gets auto re-authenticated again soon after logging
    // out of the active session.
    auto_reauthn_permission_delegate()->RecordEmbargoForAutoReauthn(
        GetEmbeddingOrigin());
  } else {
    // Once a user has explicitly selected an account, there is no need to block
    // auto re-authn with embargo.
    auto_reauthn_permission_delegate()->RemoveEmbargoForAutoReauthn(
        GetEmbeddingOrigin());

    // Record page scroll Y-axis position upon account selection to analyse
    // for intrusion. Do not record for auto re-authn because we want to detect
    // whether users scroll the webpage before choosing to sign-in.
    RenderFrameHostImpl* host_impl = static_cast<RenderFrameHostImpl*>(
        render_frame_host().GetOutermostMainFrame());
    host_impl->GetAssociatedLocalFrame()->GetScrollPosition(
        base::BindOnce(&RecordAccountSelectionScrollPosition,
                       render_frame_host().GetPageUkmSourceId(),
                       fedcm_metrics_->GetSessionID()));
  }

  fedcm_metrics_->RecordIsSignInUser(is_sign_in);

  api_permission_delegate()->RemoveEmbargoAndResetCounts(GetEmbeddingOrigin());

  account_id_ = account_id;
  select_account_time_ = base::TimeTicks::Now();
  fedcm_metrics_->RecordContinueOnPopupTime(
      idp_config_url, select_account_time_ - accounts_dialog_display_time_);

  IdpNetworkRequestManager::ContinueOnCallback continue_on = base::BindOnce(
      &Request::OnContinueOnResponseReceived, weak_ptr_factory_.GetWeakPtr(),
      idp_info.provider->Clone());

  IdpNetworkRequestManager::RedirectToCallback redirect_to;
  if (can_accept_redirect_to_) {
    redirect_to = base::BindOnce(&Request::OnRedirectToResponseReceived,
                                 weak_ptr_factory_.GetWeakPtr(),
                                 idp_info.provider->Clone());
  }

  std::vector<std::string> disclosure_shown_for;
  if (!is_sign_in) {
    disclosure_shown_for =
        DisclosureFieldsToStringList(idp_info.data->disclosure_fields);
  }

  CHECK(idp_info.data);

  has_sent_token_request_ = true;

  bool idp_blindness =
      idp_info.provider->format &&
      *idp_info.provider->format == blink::mojom::Format::kSdJwt;

  GURL endpoint;
  std::string query;
  if (idp_blindness) {
    // Checked previously.
    DCHECK(IsDelegationEnabled());

    endpoint = idp_info.endpoints.issuance;
    federated_sdjwt_handler_ = std::make_unique<FederatedSdJwtHandler>(
        idp_info.provider, render_frame_host(), this);
    query = federated_sdjwt_handler_->ComputeUrlEncodedTokenPostDataForIssuers(
        account_id);
  } else {
    endpoint = idp_info.endpoints.token;
    query = ComputeUrlEncodedTokenPostData(
        render_frame_host(), idp_info.provider->config->client_id,
        idp_info.provider->nonce, account_id,
        identity_selection_type_ != kExplicit, rp_mode_,
        idp_info.provider->fields, disclosure_shown_for,
        idp_info.provider->params_json.value_or(""),
        idp_info.provider->config->type);
  }

  network_manager_->SendTokenRequest(
      endpoint, account_id_, query, idp_blindness,
      base::BindOnce(&Request::OnTokenResponseReceived,
                     weak_ptr_factory_.GetWeakPtr(),
                     idp_info.provider->Clone()),
      std::move(continue_on), std::move(redirect_to),
      base::BindOnce(&Request::RecordErrorMetrics,
                     weak_ptr_factory_.GetWeakPtr(),
                     idp_info.provider->Clone()));
}

void Request::OnDismissFailureDialog(
    IdentityRequestDialogController::DismissReason dismiss_reason) {
  // Clicking the close active and swiping away the account chooser are more
  // intentional than other ways of dismissing the account chooser such as
  // the virtual keyboard showing on Android. Dismissal through closing the
  // pop-up window is not embargoed since the user has taken some action to
  // continue to open the pop-up window.
  bool should_embargo =
      dismiss_reason ==
          IdentityRequestDialogController::DismissReason::kCloseButton ||
      dismiss_reason == IdentityRequestDialogController::DismissReason::kSwipe;
  fedcm_metrics_->RecordCancelReason(dismiss_reason);

  should_embargo &= rp_mode_ == RpMode::kPassive && !IsUsingAmbient();
  if (should_embargo) {
    api_permission_delegate()->RecordDismissAndEmbargo(GetEmbeddingOrigin());
  }

  CompleteRequestWithError(should_embargo
                               ? FederatedRequestResult::kShouldEmbargo
                               : FederatedRequestResult::kUiDismissedNoEmbargo,
                           should_embargo ? TokenStatus::kShouldEmbargo
                                          : TokenStatus::kNotSignedInWithIdp,

                           /*should_delay_callback=*/false);
}

void Request::OnDismissErrorDialog(
    const GURL& idp_config_url,
    FetchStatus status,
    IdentityRequestDialogController::DismissReason dismiss_reason) {
  bool has_url = token_error_ && !token_error_->url.is_empty();
  ErrorDialogResult result =
      DismissReasonToErrorDialogResult(dismiss_reason, has_url);
  fedcm_metrics_->RecordErrorDialogResult(result, idp_config_url);

  CompleteTokenRequest(idp_config_url, status, /*token=*/std::nullopt,
                       token_error_, /*should_delay_callback=*/false);
}

void Request::OnDialogDismissed(
    IdentityRequestDialogController::DismissReason dismiss_reason) {
  // If the request has already completed, ignore any subsequent dismissals.
  if (!request_token_callback_) {
    return;
  }

  // Ignore dismissals triggered during the synchronous execution of RedirectTo.
  if (in_redirect_to_) {
    return;
  }

  if (has_sent_token_request_) {
    verifying_dialog_result_ = identity_selection_type_ == kExplicit
                                   ? VerifyingDialogResult::kCancelExplicit
                                   : VerifyingDialogResult::kCancelAutoReauthn;
  }

  if (dialog_type_ == DialogType::kContinueOnPopup) {
    fedcm_metrics_->RecordContinueOnPopupResult(
        ContinueOnPopupResult::kWindowClosed);
    // Popups always get dismissed with reason kOther, so we never embargo.
    CompleteRequestWithError(FederatedRequestResult::kError,
                             TokenStatus::kContinuationPopupClosedByUser,
                             /*should_delay_callback=*/false);
    return;
  }

  // Clicking the close active and swiping away the account chooser are more
  // intentional than other ways of dismissing the account chooser such as
  // the virtual keyboard showing on Android.
  bool should_embargo =
      dismiss_reason ==
          IdentityRequestDialogController::DismissReason::kCloseButton ||
      dismiss_reason == IdentityRequestDialogController::DismissReason::kSwipe;
  if (should_embargo) {
    base::TimeTicks dismiss_dialog_time = base::TimeTicks::Now();
    fedcm_metrics_->RecordCancelOnDialogTime(
        idp_data_for_display_,
        dismiss_dialog_time - accounts_dialog_display_time_);
  }
  fedcm_metrics_->RecordCancelReason(dismiss_reason);

  should_embargo &= rp_mode_ == RpMode::kPassive && !IsUsingAmbient();
  if (should_embargo) {
    api_permission_delegate()->RecordDismissAndEmbargo(GetEmbeddingOrigin());
  }

  TokenStatus token_status;
  FederatedRequestResult result;
  if (should_embargo) {
    token_status = TokenStatus::kShouldEmbargo;
    result = FederatedRequestResult::kShouldEmbargo;
  } else {
    token_status = TokenStatus::kNotSelectAccount;
    result = FederatedRequestResult::kUiDismissedNoEmbargo;
  }

  // Reject the promise immediately if the UI is dismissed without selecting
  // an account. Meanwhile, we fuzz the rejection time for other failures to
  // make it indistinguishable.
  CompleteRequestWithError(result, token_status,
                           /*should_delay_callback=*/false);
}

void Request::ShowModalDialog(DialogType dialog_type,
                              const GURL& idp_config_url,
                              const GURL& url_to_show) {
  // Reset dialog type, since we are typically not showing a FedCM dialog while
  // the popup window is open. When using the active flow the dialog may
  // still be up in some cases, but we do not expect that browser automation
  // needs to interact with the account chooser in this case.
  if (dialog_type_ != DialogType::kNone && dialog_type_ != dialog_type) {
    // This call ensures that we send a dialogClosed event if an account
    // chooser or mismatch dialog is open.
    devtools_instrumentation::DidCloseFedCmDialog(render_frame_host());
  }
  // TODO(crbug.com/336815315): Should we notify browser automation of this
  // dialog?
  if (dialog_type_ != dialog_type) {
    UMA_HISTOGRAM_ENUMERATION("Blink.FedCm.Popup.DialogType", dialog_type);
  }
  dialog_type_ = dialog_type;
  config_url_ = idp_config_url;

  auto create_registry_async = [](base::WeakPtr<Request> weak_this,
                                  const GURL& idp_config_url,
                                  WebContents* web_contents) {
    if (web_contents && weak_this) {
      IdentityRegistry::CreateForWebContents(web_contents, weak_this,
                                             idp_config_url);
    }
  };

  WebContents* web_contents = GetDialogController()->ShowModalDialog(
      url_to_show, rp_mode_,
      base::BindOnce(&Request::OnDialogDismissed,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindOnce(create_registry_async, weak_ptr_factory_.GetWeakPtr(),
                     idp_config_url),
      base::BindOnce(&Request::OnNativeAppResult,
                     weak_ptr_factory_.GetWeakPtr(), dialog_type,
                     idp_config_url));
  did_show_ui_ = true;
  // This may be null on Android, as the method cannot return the WebContents of
  // the CCT that will be created.
  // If the showing of the model dialog was deferred, this will be null, and
  // we'll get the future WebContents via `create_registry_async`.
  if (web_contents) {
    IdentityRegistry::CreateForWebContents(
        web_contents, weak_ptr_factory_.GetWeakPtr(), idp_config_url);
  }

  // Samples are at most 10 minutes. This metric is used to determine a
  // reasonable minimum duration for the mismatch dialog to be shown to prevent
  // abuse through flashing UI. When users trigger the IDP sign-in flow, the
  // mismatch dialog is hidden so we record this metric upon user triggering the
  // flow.
  if (mismatch_dialog_shown_time_.has_value()) {
    fedcm_metrics_->RecordMismatchDialogShownDuration(
        idp_data_for_display_,
        base::TimeTicks::Now() - mismatch_dialog_shown_time_.value());
    mismatch_dialog_shown_time_ = std::nullopt;
  }
}

void Request::OnContinueOnResponseReceived(
    IdentityProviderRequestOptionsPtr idp,
    FetchStatus status,
    const GURL& continue_on) {
  id_assertion_response_time_ = base::TimeTicks::Now();

  GetContentClient()->browser()->LogWebFeatureForCurrentPage(
      &render_frame_host(), blink::mojom::WebFeature::kFedCmContinueOnResponse);

  url::Origin idp_origin = url::Origin::Create(idp->config->config_url);
  // We only allow loading continue_on urls that are same-origin
  // with the IdP.
  // This isn't necessarily final, but seemed like a safer
  // and sufficient default for now.
  // This behavior may change in https://crbug.com/1429083
  bool is_same_origin =
      url::Origin::Create(continue_on).IsSameOriginWith(idp_origin);

  bool can_show_popup = CanShowContinueOnPopup();
  if (!is_same_origin || !can_show_popup) {
    if (!is_same_origin && !can_show_popup) {
      fedcm_metrics_->RecordContinueOnPopupStatus(
          ContinueOnPopupStatus::kUrlNotSameOriginAndPopupNotAllowed);
    } else if (!is_same_origin) {
      fedcm_metrics_->RecordContinueOnPopupStatus(
          ContinueOnPopupStatus::kUrlNotSameOrigin);
    } else if (!can_show_popup) {
      fedcm_metrics_->RecordContinueOnPopupStatus(
          ContinueOnPopupStatus::kPopupNotAllowed);
    }

    CompleteRequestWithError(FederatedRequestResult::kIdTokenInvalidResponse,
                             TokenStatus::kIdTokenInvalidResponse,

                             /*should_delay_callback=*/false);
    return;
  }

  fedcm_metrics_->RecordContinueOnPopupStatus(
      ContinueOnPopupStatus::kPopupOpened);
  ShowModalDialog(DialogType::kContinueOnPopup, idp->config->config_url,
                  continue_on);
}

void Request::OnRedirectToResponseReceived(
    IdentityProviderRequestOptionsPtr idp,
    FetchStatus status,
    blink::mojom::RedirectParams::Tag method,
    const GURL& redirect_to,
    const std::string& request_body) {
  RedirectTo(idp->config->config_url, method, redirect_to, request_body);
}

void Request::RedirectTo(const GURL& idp_config_url,
                         blink::mojom::RedirectParams::Tag method,
                         const GURL& redirect_to,
                         const std::string& request_body) {
  // Navigate the top-level frame to the URL specified by the IdP.
  //
  // This is done here rather than in the callers of the Request because
  // that allows us to have a consistent experience regardless of how the token
  // was requested (e.g. via an interception or via the renderer process call).
  if (!can_accept_redirect_to_ || !redirect_to.SchemeIsHTTPOrHTTPS()) {
    CompleteRequestWithError(FederatedRequestResult::kIdTokenInvalidResponse,
                             TokenStatus::kIdTokenInvalidResponse,
                             /*should_delay_callback=*/false);
    return;
  }

  // can_accept_redirect_to_ is only true for primary main frames.
  DCHECK(render_frame_host().IsInPrimaryMainFrame());

  WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
      WebContents::FromRenderFrameHost(&render_frame_host()));

  if (!web_contents) {
    CompleteRequestWithError(FederatedRequestResult::kError,
                             /*token_status=*/std::nullopt,
                             /*should_delay_callback=*/false);
    return;
  }

  NavigationController::LoadURLParams params(redirect_to);
  params.transition_type = ui::PAGE_TRANSITION_LINK;
  params.initiator_frame_token = render_frame_host().GetFrameToken();
  params.initiator_process_id = render_frame_host().GetProcess()->GetID();
  params.initiator_origin = origin();
  params.initiator_navigation_state =
      RenderFrameHostImpl::From(&render_frame_host())
          ->CreateInitiatorStateFromCurrentFrame();
  params.source_site_instance = render_frame_host().GetSiteInstance();
  params.referrer =
      Referrer(intercepted_url_, network::mojom::ReferrerPolicy::kDefault);
  // Pretend this was renderer initiated like the load we intercepted.
  params.is_renderer_initiated = true;
  params.has_user_gesture = had_transient_user_activation_;
  // This is used for "Request Desktop Site" on Android.
  if (web_contents->ShouldOverrideUserAgentForRendererInitiatedNavigation()) {
    params.override_user_agent = NavigationController::UA_OVERRIDE_TRUE;
  } else {
    params.override_user_agent = NavigationController::UA_OVERRIDE_FALSE;
  }
  if (method == blink::mojom::RedirectParams::Tag::kPost) {
    params.transition_type = ui::PAGE_TRANSITION_FORM_SUBMIT;
    params.load_type = NavigationController::LOAD_TYPE_HTTP_POST;
    // It is very important that we only allow bytes in the post data, so that
    // it is not possible to trigger file uploads that bypass security checks.
    params.post_data = network::ResourceRequestBody::CreateFromCopyOfBytes(
        base::as_byte_span(request_body));
    params.extra_headers =
        "Content-Type: application/x-www-form-urlencoded\r\n";
  }
  in_redirect_to_ = true;
  web_contents->GetController().LoadURLWithParams(params);

  CompleteRequest(FederatedRequestResult::kSuccess,
                  TokenStatus::kSuccessUsingRedirectTo,
                  /*token_error=*/std::nullopt, idp_config_url,
                  /*token_data=*/base::Value(),
                  /*should_delay_callback=*/false);
}

void Request::ShowErrorDialog(const GURL& idp_config_url,
                              FetchStatus status,
                              std::optional<TokenError> token_error) {
  CHECK(idp_infos_.find(idp_config_url) != idp_infos_.end());

  dialog_type_ = DialogType::kError;
  config_url_ = idp_config_url;
  token_request_status_ = status;
  token_error_ = token_error;

  // TODO(crbug.com/40282657): Refactor IdentityCredentialTokenError
  if (!GetDialogController()->ShowErrorDialog(
          CreateRpData(/*client_metadata_received=*/true),
          FormatOriginForDisplay(url::Origin::Create(idp_config_url)),
          idp_infos_[idp_config_url]->rp_context, rp_mode_,
          idp_infos_[idp_config_url]->metadata, token_error,
          base::BindOnce(&Request::OnDismissErrorDialog,
                         weak_ptr_factory_.GetWeakPtr(), idp_config_url,
                         status),
          token_error && !token_error->url.is_empty()
              ? base::BindOnce(
                    &Request::ShowModalDialog, weak_ptr_factory_.GetWeakPtr(),
                    DialogType::kErrorUrlPopup, config_url_, token_error->url)
              : base::NullCallback())) {
    return;
  }
  did_show_ui_ = true;
  devtools_instrumentation::DidShowFedCmDialog(render_frame_host());
}

void Request::OnTokenResponseReceived(
    IdentityProviderRequestOptionsPtr idp,
    FetchStatus status,
    IdpNetworkRequestManager::TokenResult&& result) {
  CHECK(result.token.has_value() || result.error.has_value());

  verifying_dialog_result_ = identity_selection_type_ == kExplicit
                                 ? VerifyingDialogResult::kSuccessExplicit
                                 : VerifyingDialogResult::kSuccessAutoReauthn;

  bool should_show_error_ui =
      result.error || status.parse_status != ParseStatus::kSuccess;
  auto complete_request_callback =
      should_show_error_ui
          ? base::BindOnce(&Request::ShowErrorDialog,
                           weak_ptr_factory_.GetWeakPtr(),
                           idp->config->config_url, status, result.error)
          : base::BindOnce(&Request::CompleteTokenRequest,
                           weak_ptr_factory_.GetWeakPtr(),
                           idp->config->config_url, status,
                           std::move(result.token), result.error,
                           /*should_delay_callback=*/false);

  // When fetching id tokens we show a "Verify" sheet to users in case fetching
  // takes a long time due to latency etc. In case that the fetching process is
  // fast, we still want to show the "Verify" sheet for at least
  // `kTokenRequestDelay` seconds for better UX.
  // Note that for active flow, conditional flow, or when an error occurs we can
  // complete without delay.
  id_assertion_response_time_ = base::TimeTicks::Now();
  base::TimeDelta fetch_time =
      id_assertion_response_time_ - select_account_time_;
  if (should_complete_request_immediately_ || rp_mode_ == RpMode::kActive ||
      mediation_requirement_ == MediationRequirement::kConditional ||
      should_show_error_ui || fetch_time >= kTokenRequestDelay) {
    std::move(complete_request_callback).Run();
    return;
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, std::move(complete_request_callback),
      kTokenRequestDelay - fetch_time);
}

void Request::MarkUserAsSignedIn(const GURL& idp_config_url,
                                 const std::string& account_id) {
  CHECK(!account_id_.empty());
  // Auto re-authentication can only be triggered when there's already a
  // sharing permission OR the IdP is exempted with 3PC access. Either way
  // we shouldn't explicitly grant permission here.
  if (identity_selection_type_ == kAutoPassive ||
      identity_selection_type_ == kAutoActive) {
    permission_delegate()->RefreshExistingSharingPermission(
        origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url),
        account_id);
  } else {
    // A sharing permission should only be granted after we explicitly ask for
    // user permission to sign in. It has a high bar because its extensive
    // capability such as storage access auto-grant. If a login request is
    // initiated by the embedder, a deemed sign-in user may have not granted
    // such permission via a FedCM flow yet so we skip granting the sharing
    // permission in this case.
    CHECK_EQ(identity_selection_type_, kExplicit);
    FederatedEmbedderLoginRequest* embedder_login_request =
        FederatedEmbedderLoginRequest::Get(
            WebContents::FromRenderFrameHost(&render_frame_host()));
    if (!embedder_login_request) {
      permission_delegate()->GrantSharingPermission(
          origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url),
          account_id);
    }
  }

  SetRequiresUserMediation(false, base::DoNothing());
}

void Request::CompleteTokenRequest(const GURL& idp_config_url,
                                   FetchStatus status,
                                   std::optional<base::Value> token,
                                   std::optional<TokenError> token_error,
                                   bool should_delay_callback) {
  DCHECK(!start_time_.is_null());
  constexpr char kIdAssertionUrl[] = "id assertion endpoint";
  if (status.parse_status != ParseStatus::kSuccess) {
    MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl,
                                  status.response_code);
    std::pair<FederatedRequestResult, TokenStatus> resultAndTokenStatus =
        IdAssertionFetchStatusToRequestResultAndTokenStatus(status);
    CompleteRequestWithError(resultAndTokenStatus.first,
                             resultAndTokenStatus.second,
                             should_delay_callback);
    return;
  }
  if (token_error_) {
    MaybeAddResponseCodeToConsole(render_frame_host(), kIdAssertionUrl,
                                  status.response_code);
    if (error_url_type_ && *error_url_type_ == ErrorUrlType::kCrossSite) {
      CompleteRequestWithError(
          FederatedRequestResult::kIdTokenCrossSiteIdpErrorResponse,
          TokenStatus::kIdTokenCrossSiteIdpErrorResponse,
          should_delay_callback);
      return;
    }
    CompleteRequestWithError(FederatedRequestResult::kIdTokenIdpErrorResponse,
                             TokenStatus::kIdTokenIdpErrorResponse,
                             should_delay_callback);
    return;
  }

  MarkUserAsSignedIn(idp_config_url, account_id_);

  fedcm_metrics_->RecordTokenResponseAndTurnaroundTime(
      idp_config_url, id_assertion_response_time_ - select_account_time_,
      id_assertion_response_time_ - start_time_ -
          (accounts_dialog_display_time_ -
           ready_to_display_accounts_dialog_time_));

  const IdentityProviderRequestOptionsPtr& provider =
      idp_infos_[idp_config_url]->provider;
  DCHECK(provider);

  if (provider->format && *provider->format == blink::mojom::Format::kSdJwt) {
    if (token->is_string()) {
      federated_sdjwt_handler_->ProcessSdJwt(token->GetString());
      return;
    } else {
      CompleteRequestWithError(FederatedRequestResult::kError,
                               TokenStatus::kIdTokenInvalidResponse,
                               /*should_delay_callback=*/false);
      return;
    }
  }

  CompleteRequest(FederatedRequestResult::kSuccess,
                  TokenStatus::kSuccessUsingTokenInHttpResponse,
                  /*token_error=*/std::nullopt, idp_config_url,
                  std::move(*token),
                  /*should_delay_callback=*/false);
}

void Request::CompleteRequestWithError(
    blink::mojom::FederatedRequestResult result,
    std::optional<RequestIdTokenStatus> token_status,
    bool should_delay_callback) {
  CompleteRequest(result, token_status, token_error_,
                  /*selected_idp_config_url=*/std::nullopt,
                  /*token_data=*/std::nullopt, should_delay_callback);
}

void Request::CompleteRequest(
    blink::mojom::FederatedRequestResult result,
    std::optional<RequestIdTokenStatus> token_status,
    std::optional<TokenError> token_error,
    const std::optional<GURL>& selected_idp_config_url,
    std::optional<base::Value> token_data,
    bool should_delay_callback) {
  DCHECK(result == FederatedRequestResult::kSuccess || !token_data.has_value());
  if (!request_token_callback_) {
    return;
  }
  // We don't just return if `complete_request_delayed_` is true because in the
  // case of abort() we still want to invoke the callback.
  if (!complete_request_delayed_) {
    // Record metrics and console errors only the first time we complete the
    // request, even if the callback is delayed.
    RecordMetricsAndConsoleError(result, token_status, selected_idp_config_url);

    RenderFrameHostImpl::From(&render_frame_host())
        ->delegate()
        ->OnFedCmFederatedLogin(
            FederatedRequestResultToFederatedLoginResult(result));

    if (token_received_callback_for_autofill_) {
      std::move(token_received_callback_for_autofill_)
          .Run(result == FederatedRequestResult::kSuccess);
    }
  }

  if (!should_delay_callback || should_complete_request_immediately_) {
    bool is_auto_selected = identity_selection_type_ != kExplicit;
    CompleteRequestInternal(result, token_error, selected_idp_config_url,
                            std::move(token_data), is_auto_selected);
  } else {
    DCHECK(!complete_request_delayed_);
    complete_request_delayed_ = true;
    base::TimeDelta delay = GetRandomRejectionTime();
    TRACE_EVENT_INSTANT("content.fedcm", "Delaying FedCM rejection",
                        perfetto_track_, "delay", delay);
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&Request::CompleteRequestInternal,
                       weak_ptr_factory_.GetWeakPtr(),
                       FederatedRequestResult::kError,
                       /*token_error=*/std::nullopt,
                       /*selected_idp_config_url=*/std::nullopt,
                       /*token_data=*/std::nullopt, /*is_auto_selected=*/false),
        delay);
  }
}

void Request::RecordMetricsAndConsoleError(
    blink::mojom::FederatedRequestResult result,
    std::optional<RequestIdTokenStatus> token_status,
    const std::optional<GURL>& selected_idp_config_url) {
  if (accounts_dialog_shown_time_.has_value()) {
    fedcm_metrics_->RecordAccountsDialogShownDuration(
        idp_data_for_display_,
        base::TimeTicks::Now() - accounts_dialog_shown_time_.value());
  }

  if (mismatch_dialog_shown_time_.has_value()) {
    fedcm_metrics_->RecordMismatchDialogShownDuration(
        idp_data_for_display_,
        base::TimeTicks::Now() - mismatch_dialog_shown_time_.value());
  }

  if (token_status) {
    int num_idps_mismatch = std::count_if(
        idp_data_for_display_.begin(), idp_data_for_display_.end(),
        [](auto& provider) { return provider->has_login_status_mismatch; });
    std::optional<UseOtherAccountResult> use_other_account_result;
    // We know that use other account was used if and only if
    // account_ids_before_login_ is not empty.
    if (!account_ids_before_login_.empty()) {
      use_other_account_result =
          ComputeUseOtherAccountResult(result, selected_idp_config_url);
    }

    if (!verifying_dialog_result_ && has_sent_token_request_) {
      verifying_dialog_result_ =
          identity_selection_type_ == kExplicit
              ? VerifyingDialogResult::kDestroyExplicit
              : VerifyingDialogResult::kDestroyAutoReauthn;
    }

    std::optional<bool> has_signin_account;
    // Note: accounts_ does not include the ones that got filtered out. In case
    // that all accounts are filtered out, we'd show the mismatch UI and skip
    // recording the account status on the mismatch UI.
    for (const auto& account : accounts_) {
      has_signin_account = false;
      if (account->idp_claimed_login_state.value_or(
              account->browser_trusted_login_state) == LoginState::kSignIn) {
        has_signin_account = true;
        break;
      }
    }

    fedcm_metrics_->RecordRequestTokenStatus(
        *token_status, mediation_requirement_, idp_order_, num_idps_mismatch,
        selected_idp_config_url, rp_mode_, use_other_account_result,
        verifying_dialog_result_,
        api_permission_delegate()->AreThirdPartyCookiesEnabledInSettings()
            ? ThirdPartyCookiesStatus::kEnabledInSettings
            : ThirdPartyCookiesStatus::kDisabledInSettings,
        ComputeRequesterFrameType(render_frame_host(), origin(),
                                  GetEmbeddingOrigin()),
        has_signin_account, did_show_ui_);
  }

  if (result == FederatedRequestResult::kSuccess) {
    CHECK(selected_idp_config_url);
    CHECK(fedcm_accounts_fetcher_);
    if (IsMetricsEndpointEnabled()) {
      fedcm_accounts_fetcher_->SendSuccessfulTokenRequestMetrics(
          *selected_idp_config_url,
          ready_to_display_accounts_dialog_time_ - start_time_,
          select_account_time_ - accounts_dialog_display_time_,
          id_assertion_response_time_ - select_account_time_,
          id_assertion_response_time_ - start_time_ -
              (accounts_dialog_display_time_ -
               ready_to_display_accounts_dialog_time_),
          did_show_ui_);
    }
  } else {
    AddDevToolsIssue(result);
    AddConsoleErrorMessage(result);

    // fedcm_accounts_fetcher_ could be null if configs were not fetched, e.g.
    // because of cooldown.
    if (IsMetricsEndpointEnabled() && fedcm_accounts_fetcher_) {
      fedcm_accounts_fetcher_->SendAllFailedTokenRequestMetrics(result,
                                                                did_show_ui_);
    }
  }

  if (ShouldNotifyDevtoolsForDialogType(dialog_type_)) {
    devtools_instrumentation::DidCloseFedCmDialog(render_frame_host());
  }
}

void Request::CompleteRequestInternal(
    blink::mojom::FederatedRequestResult result,
    std::optional<TokenError> token_error,
    const std::optional<GURL>& selected_idp_config_url,
    std::optional<base::Value> token_data,
    bool is_auto_selected) {
  if (!request_token_callback_) {
    return;
  }
  CleanUp();
  auto* page_data = GetPageData(render_frame_host().GetPage());
  CHECK_EQ(page_data->PendingWebIdentityRequest(), this);
  page_data->SetPendingWebIdentityRequest(nullptr);

  blink::mojom::TokenErrorPtr error;
  if (token_error) {
    error = blink::mojom::TokenError::New();
    error->code = token_error->code;
    error->url = token_error->url;
  }
  RequestTokenStatus status =
      FederatedRequestResultToRequestTokenStatus(result);
  std::move(request_token_callback_)
      .Run(status, selected_idp_config_url, std::move(token_data),
           std::move(error), is_auto_selected);
  request_token_callback_.Reset();

  TRACE_EVENT_END("content.fedcm", perfetto_track_);
}

void Request::CleanUp() {
  // Cancel any pending callbacks and fetches from this request. No need to
  // reset other members since this object is not reused for a new request.
  // `fedcm_metrics` is reset to force metrics recording right away instead of
  // waiting for Request destruction, which might be delayed.
  weak_ptr_factory_.InvalidateWeakPtrs();

  permission_delegate()->RemoveIdpSigninStatusObserver(this);

  fedcm_accounts_fetcher_.reset();
  federated_sdjwt_handler_.reset();
  network_manager_.reset();
  fedcm_metrics_.reset();
}

void Request::AddDevToolsIssue(FederatedRequestResult result) {
  DCHECK_NE(result, FederatedRequestResult::kSuccess);

  // It would be possible to add this inspector issue on the renderer, which
  // will receive the callback. However, it is preferable to do so on the
  // browser because this is closer to the source, which means adding
  // additional metadata is easier. In addition, in the future we may only
  // need to pass a small amount of information to the renderer in the case of
  // an error, so it would be cleaner to do this by reporting the inspector
  // issue from the browser.
  auto details = blink::mojom::InspectorIssueDetails::New();
  auto federated_request_details =
      blink::mojom::FederatedRequestIssueDetails::New(result);
  details->federated_request_details = std::move(federated_request_details);
  render_frame_host().ReportInspectorIssue(
      blink::mojom::InspectorIssueInfo::New(
          blink::mojom::InspectorIssueCode::kFederatedAuthRequestIssue,
          std::move(details)));
}

void Request::AddConsoleErrorMessage(FederatedRequestResult result) {
  render_frame_host().AddMessageToConsole(
      blink::mojom::ConsoleMessageLevel::kError,
      GetConsoleErrorMessageFromResult(result));
}

url::Origin Request::GetEmbeddingOrigin() const {
  return render_frame_host().GetMainFrame()->GetLastCommittedOrigin();
}

IdentityRequestDialogController* Request::GetDialogController() {
  return request_service_->GetOrCreateDialogController();
}

base::WeakPtr<Request> Request::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

void Request::OnClose() {
  CHECK(request_service_->GetDialogController());
  request_service_->GetDialogController()->CloseModalDialog();

  // If we have not gotten a signin status change, abort the flow.
  // The same goes if we did get a status change but the accounts fetch
  // failed.
  if ((idps_user_tried_to_signin_to_.empty() ||
       (fetch_data_.pending_idps.empty() &&
        !fetch_data_.did_succeed_for_at_least_one_idp)) &&
      dialog_type_ == DialogType::kLoginToIdpPopup) {
    CompleteRequestWithError(FederatedRequestResult::kError,
                             TokenStatus::kLoginPopupClosedWithoutSignin,
                             /*should_delay_callback=*/false);
    return;
  }

  // When IdentityProvider.close is called in the continuation popup, we
  // should abort the flow.
  if (dialog_type_ == DialogType::kContinueOnPopup) {
    fedcm_metrics_->RecordContinueOnPopupResult(
        ContinueOnPopupResult::kClosedByIdentityProviderClose);
    // Popups always get dismissed with reason kOther, so we never embargo.
    CompleteRequestWithError(
        FederatedRequestResult::kError,
        TokenStatus::kContinuationPopupClosedByIdentityProviderClose,
        /*should_delay_callback=*/false);
    return;
  }
}

bool Request::OnResolve(GURL idp_config_url,
                        const std::optional<std::string>& account_id,
                        blink::mojom::ResolveTokenParamsPtr params) {
  // Close the pop-up window post user permission.
  auto* controller = request_service_->GetDialogController();
  if (!controller) {
    return false;
  }

  // IdentityProvider.resolve() is only allowed for continuation API.
  if (dialog_type_ != DialogType::kContinueOnPopup) {
    return false;
  }

  controller->CloseModalDialog();

  MarkUserAsSignedIn(idp_config_url, account_id.value_or(account_id_));

  fedcm_metrics_->RecordContinueOnResponseAndTurnaroundTime(
      id_assertion_response_time_ - select_account_time_,
      base::TimeTicks::Now() - start_time_ -
          (accounts_dialog_display_time_ -
           ready_to_display_accounts_dialog_time_));
  fedcm_metrics_->RecordContinueOnPopupResult(
      ContinueOnPopupResult::kTokenReceived);

  const IdentityProviderRequestOptionsPtr& provider =
      idp_infos_[idp_config_url]->provider;
  DCHECK(provider);

  if (params->is_redirect_to() && can_accept_redirect_to_) {
    const auto& redirect_to = params->get_redirect_to();
    if (redirect_to->is_get()) {
      RedirectTo(idp_config_url, blink::mojom::RedirectParams::Tag::kGet,
                 redirect_to->get_get()->url, "");
    } else {
      DCHECK(redirect_to->is_post());
      RedirectTo(idp_config_url, blink::mojom::RedirectParams::Tag::kPost,
                 redirect_to->get_post()->url,
                 redirect_to->get_post()->request_body);
    }
    return true;
  }

  if (!params->is_token()) {
    // This could happen if we get a redirect request but interception is
    // disabled, for example when we have no active embedder initiated login.
    return false;
  }

  const base::Value& token = params->get_token();
  if (provider->format && *provider->format == blink::mojom::Format::kSdJwt) {
    if (token.is_string()) {
      federated_sdjwt_handler_->ProcessSdJwt(token.GetString());
      return true;
    } else {
      CompleteRequestWithError(FederatedRequestResult::kError,
                               TokenStatus::kIdTokenInvalidResponse,
                               /*should_delay_callback=*/false);
      return false;
    }
  }

  CompleteRequest(FederatedRequestResult::kSuccess,
                  TokenStatus::kSuccessUsingIdentityProviderResolve,
                  /*token_error=*/std::nullopt, idp_config_url, token.Clone(),
                  /*should_delay_callback=*/false);
  return true;
}

void Request::OnOriginMismatch(Method method,
                               const url::Origin& expected,
                               const url::Origin& actual) {
  const char* method_string = method == Method::kClose ? "close" : "resolve";
  std::string error_messsage = base::StringPrintf(
      "IdentityProvider.%s called from incorrect origin '%s'; expected '%s'",
      method_string, actual.Serialize().c_str(), expected.Serialize().c_str());
  render_frame_host().AddMessageToConsole(
      blink::mojom::ConsoleMessageLevel::kError, error_messsage);
}

void Request::OnIntentResolved(const std::string& token) {
  blink::mojom::ResolveTokenParamsPtr params =
      blink::mojom::ResolveTokenParams::NewToken(base::Value(token));
  OnResolve(config_url_, std::nullopt, std::move(params));
}

void Request::OnNativeAppResult(
    DialogType dialog_type,
    const GURL& idp_config_url,
    IdentityRequestDialogController::NativeAppResult result) {
  if (!request_token_callback_) {
    return;
  }
  if (result.type ==
      IdentityRequestDialogController::NativeAppResult::Type::kToken) {
    if (dialog_type != DialogType::kContinueOnPopup) {
      CompleteRequestWithError(FederatedRequestResult::kError,
                               TokenStatus::kLoginPopupClosedWithoutSignin,
                               /*should_delay_callback=*/false);
      return;
    }
    OnIntentResolved(result.token);
  } else if (result.type == IdentityRequestDialogController::NativeAppResult::
                                Type::kLoginFinished) {
    if (dialog_type != DialogType::kLoginToIdpPopup) {
      CompleteRequestWithError(FederatedRequestResult::kError,
                               TokenStatus::kContinuationPopupClosedByUser,
                               /*should_delay_callback=*/false);
      return;
    }
    OnNativeAppLoginFinished(idp_config_url);
  }
}

void Request::OnNativeAppLoginFinished(const GURL& idp_config_url) {
  GetDialogController()->CloseModalDialog();
  permission_delegate()->RemoveIdpSigninStatusObserver(this);
  permission_delegate()->SetIdpSigninStatus(
      url::Origin::Create(idp_config_url), /*is_signed_in=*/true, std::nullopt);
  idps_user_tried_to_signin_to_.insert(idp_config_url);
  FetchEndpointsForIdps({idp_config_url});
}

FederatedApiPermissionStatus Request::GetApiPermissionStatus() {
  DCHECK(api_permission_delegate());
  return api_permission_delegate()->GetApiPermissionStatus(
      GetEmbeddingOrigin());
}

bool Request::ShouldNotifyDevtoolsForDialogType(DialogType type) {
  return type != DialogType::kNone && type != DialogType::kLoginToIdpPopup &&
         type != DialogType::kContinueOnPopup &&
         type != DialogType::kErrorUrlPopup;
}

void Request::AcceptAccountsDialogForDevtools(
    const GURL& config_url,
    const IdentityRequestAccount& account) {
  bool is_sign_in = account.idp_claimed_login_state.value_or(
                        account.browser_trusted_login_state) ==
                    IdentityRequestAccount::LoginState::kSignIn;
  OnAccountSelected(config_url, account.id, is_sign_in);
}

void Request::DismissAccountsDialogForDevtools(bool should_embargo) {
  // We somewhat arbitrarily pick a reason that does/does not trigger
  // cooldown.
  IdentityRequestDialogController::DismissReason reason =
      should_embargo
          ? IdentityRequestDialogController::DismissReason::kCloseButton
          : IdentityRequestDialogController::DismissReason::kOther;
  OnDialogDismissed(reason);
}

void Request::AcceptConfirmIdpLoginDialogForDevtools() {
  DCHECK(login_url_.is_valid());
  LoginToIdP(/*can_append_hints=*/true, config_url_, login_url_);
}

void Request::DismissConfirmIdpLoginDialogForDevtools() {
  // These values match what HandleAccountsFetchFailure passes.
  OnDismissFailureDialog(
      IdentityRequestDialogController::DismissReason::kOther);
}

bool Request::UseAnotherAccountForDevtools(
    const IdentityProviderData& provider) {
  if (!provider.idp_metadata.supports_add_account) {
    return false;
  }
  LoginToIdP(/*can_append_hints=*/true, provider.idp_metadata.config_url,
             provider.idp_metadata.idp_login_url);
  return true;
}

bool Request::HasMoreDetailsButtonForDevtools() {
  return token_error_ && token_error_->url.is_valid();
}

void Request::ClickErrorDialogGotItForDevtools() {
  DCHECK(token_error_);
  OnDismissErrorDialog(
      config_url_, token_request_status_,
      IdentityRequestDialogController::DismissReason::kGotItButton);
}

void Request::ClickErrorDialogMoreDetailsForDevtools() {
  DCHECK(token_error_ && token_error_->url.is_valid());
  ShowModalDialog(DialogType::kErrorUrlPopup, config_url_, token_error_->url);
  OnDismissErrorDialog(
      config_url_, token_request_status_,
      IdentityRequestDialogController::DismissReason::kMoreDetailsButton);
}

void Request::DismissErrorDialogForDevtools() {
  OnDismissErrorDialog(config_url_, token_request_status_,
                       IdentityRequestDialogController::DismissReason::kOther);
}

bool Request::GetAccountForAutoReauthn(IdentityProviderDataPtr* out_idp_data,
                                       IdentityRequestAccountPtr* out_account) {
  for (const auto& idp_info : idp_infos_) {
    if (idp_info.second->data->has_login_status_mismatch) {
      // If we need to show IDP login status mismatch UI, we cannot
      // auto-reauthenticate a user even if there really is a single returning
      // account.
      return false;
    }
  }
  for (const auto& account : accounts_) {
    if (account->idp_claimed_login_state.value_or(
            account->browser_trusted_login_state) == LoginState::kSignUp ||
        account->is_filtered_out) {
      continue;
    }
    // account.idp_claimed_login_state will be set to kSignIn if the client is
    // on the `approved_clients` list provided by IDP. However, in this case we
    // have to trust the browser observed sign-in unless the IDP can be
    // exempted. For example, they have third party cookies access on the RP
    // site.
    if (!HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
            render_frame_host(),
            /*provider_url=*/
            account->identity_provider->idp_metadata.config_url,
            GetEmbeddingOrigin(), origin(), account->id, permission_delegate(),
            api_permission_delegate())) {
      continue;
    }

    if (*out_account) {
      return false;
    }
    *out_idp_data = account->identity_provider;
    *out_account = account;
  }

  if (*out_account) {
    return true;
  }

  return false;
}

bool Request::ShouldFailBeforeFetchingAccounts(const GURL& config_url) {
  if (mediation_requirement_ != MediationRequirement::kSilent) {
    return false;
  }

  bool is_auto_reauthn_blocked_by_embedder =
      auto_reauthn_permission_delegate()->IsAutoReauthnDisabledByEmbedder(
          WebContents::FromRenderFrameHost(&render_frame_host()));
  if (is_auto_reauthn_blocked_by_embedder) {
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: ongoing actor task in the tab.");
  }

  bool is_auto_reauthn_setting_enabled =
      auto_reauthn_permission_delegate()->IsAutoReauthnSettingEnabled();
  if (!is_auto_reauthn_setting_enabled) {
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: the user has disabled auto re-authn.");
  }

  bool is_auto_reauthn_embargoed =
      auto_reauthn_permission_delegate()->IsAutoReauthnEmbargoed(
          GetEmbeddingOrigin());
  if (is_auto_reauthn_embargoed) {
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: auto re-authn is in quiet period because it "
        "was recently used on this site.");
  }

  bool has_sharing_permission_for_any_account =
      HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
          render_frame_host(), config_url, GetEmbeddingOrigin(), origin(),
          /*account_id=*/std::nullopt, permission_delegate(),
          api_permission_delegate());

  if (!has_sharing_permission_for_any_account) {
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: the user has not used FedCM on this site with "
        "this identity provider.");
  }

  bool requires_user_mediation = RequiresUserMediation();
  if (requires_user_mediation) {
    render_frame_host().AddMessageToConsole(
        blink::mojom::ConsoleMessageLevel::kError,
        "Silent mediation issue: preventSilentAccess() has been invoked on the "
        "site.");
  }

  if (requires_user_mediation || !is_auto_reauthn_setting_enabled ||
      is_auto_reauthn_embargoed || !has_sharing_permission_for_any_account ||
      is_auto_reauthn_blocked_by_embedder) {
    // Record the relevant auto reauthn metrics before aborting the FedCM flow.
    fedcm_metrics_->RecordAutoReauthnMetrics(
        /*has_single_returning_account=*/std::nullopt,
        /*auto_signin_account=*/nullptr,
        /*auto_reauthn_success=*/false, !is_auto_reauthn_setting_enabled,
        is_auto_reauthn_embargoed, is_auto_reauthn_blocked_by_embedder,
        requires_user_mediation);
    return true;
  }
  return false;
}

bool Request::RequiresUserMediation() {
  return auto_reauthn_permission_delegate()->RequiresUserMediation(origin());
}

void Request::SetRequiresUserMediation(bool requires_user_mediation,
                                       base::OnceClosure callback) {
  request_service_->SetRequiresUserMediation(requires_user_mediation,
                                             std::move(callback));
}

void Request::LoginToIdP(bool can_append_hints,
                         const GURL& idp_config_url,
                         GURL login_url) {
  const auto& it = idp_login_infos_.find(login_url);
  CHECK(it != idp_login_infos_.end());
  login_url_ = login_url;
  if (can_append_hints) {
    // Before invoking UI, append the query parameters to the `idp_login_url` if
    // needed.
    MaybeAppendQueryParameters(it->second, &login_url);
  }

  if (dialog_type_ == DialogType::kLoginToIdpPopup) {
    ShowModalDialog(DialogType::kLoginToIdpPopup, idp_config_url, login_url);
    return;
  }

  permission_delegate()->AddIdpSigninStatusObserver(this);

  account_ids_before_login_.clear();
  for (const auto& account : accounts_) {
    if (account->identity_provider->idp_metadata.idp_login_url == login_url) {
      account_ids_before_login_.insert(account->id);
    }
  }

  ShowModalDialog(DialogType::kLoginToIdpPopup, idp_config_url, login_url);
}

void Request::MaybeShowActiveModeModalDialog(const GURL& idp_config_url,
                                             const GURL& idp_login_url) {
  if (idp_infos_.size() > 1) {
    // TODO(crbug.com/40283218): handle the active flow and the
    // Multi IdP API (what should happen if you are logged in to some
    // IdPs but not to others).
    // TODO(crbug.com/326987150): This is temporary so we should degrade
    // gracefully.
    return;
  }

  // We fail sooner before, but just to double check, we assert that
  // we are inside a user gesture here again.
  CHECK(had_transient_user_activation_);
  // TODO(crbug.com/40283219): we should probably make idp_login_url
  // optional instead of empty.
  LoginToIdP(/*can_append_hints=*/false, idp_config_url, idp_login_url);
  return;
}

void Request::RecordErrorMetrics(
    IdentityProviderRequestOptionsPtr idp,
    TokenResponseType token_response_type,
    std::optional<ErrorDialogType> error_dialog_type,
    std::optional<ErrorUrlType> error_url_type) {
  fedcm_metrics_->RecordErrorMetricsBeforeShowingErrorDialog(
      token_response_type, error_dialog_type, error_url_type,
      idp->config->config_url);

  if (error_url_type) {
    // This is used to determine if we need to use the cross-site specific
    // devtools issue when failing the request.
    error_url_type_ = error_url_type;
  }
}

std::unique_ptr<Metrics> Request::CreateFedCmMetrics() {
  // Ensure the lifecycle state as GetPageUkmSourceId doesn't support the
  // prerendering page. As FederatedAithRequest runs behind the
  // BrowserInterfaceBinders, the service doesn't receive any request while
  // prerendering, and the CHECK should always meet the condition.
  CHECK(!render_frame_host().IsInLifecycleState(
      RenderFrameHost::LifecycleState::kPrerendering));

  return std::make_unique<Metrics>(render_frame_host().GetPageUkmSourceId());
}

bool Request::IsNewlyLoggedIn(const IdentityRequestAccount& account) {
  if (login_url_.is_empty() ||
      login_url_ != account.identity_provider->idp_metadata.idp_login_url) {
    return false;
  }
  // Exclude filtered out accounts so they are not shown at the top.
  return !account.is_filtered_out &&
         !account_ids_before_login_.contains(account.id);
}

bool Request::IsUsingAmbient() const {
  if (rp_mode_ != RpMode::kPassive || idp_order_.size() != 1u) {
    return false;
  }

  bool is_ambient_enabled =
      IsFedCmAmbientUIEnabled() ||
      passive_dialog_volume_ ==
          IdentityRequestDialogController::PassiveDialogVolume::kAmbient;
  if (!is_ambient_enabled) {
    return false;
  }

  size_t accounts_count = accounts_.size();

  // Currently, the Ambient UI only supports single accounts, for returning
  // users and new users. As we develop it, we'll allow more cases to be
  // handled by the Ambient UI, such as multiple accounts, multiple IdPs and
  // mismatch cases.

  return accounts_count == 1u;
}

RelyingPartyData Request::CreateRpData(bool client_metadata_received) const {
  // We want to show the iframe origin if any IDP requests it.
  bool show_iframe_origin = false;
  for (const auto& entry : idp_infos_) {
    if (entry.second->client_is_third_party_to_top_frame_origin) {
      show_iframe_origin = true;
      break;
    }
  }
  std::u16string iframe_origin;
  if (show_iframe_origin) {
    iframe_origin = base::UTF8ToUTF16(FormatOriginForDisplay(origin()));
  }
  bool display_strings_may_change =
      !client_metadata_received &&
      !net::SchemefulSite::IsSameSite(origin(), GetEmbeddingOrigin());
  return RelyingPartyData(
      base::UTF8ToUTF16(GetTopFrameOriginForDisplay(GetEmbeddingOrigin())),
      iframe_origin, display_strings_may_change);
}

FederatedIdentityApiPermissionContextDelegate*
Request::api_permission_delegate() const {
  return request_service_->api_permission_delegate_;
}

FederatedIdentityAutoReauthnPermissionContextDelegate*
Request::auto_reauthn_permission_delegate() const {
  return request_service_->auto_reauthn_permission_delegate_;
}

FederatedIdentityPermissionContextDelegate* Request::permission_delegate()
    const {
  return request_service_->permission_delegate_;
}

}  // namespace content::webid
