blob: a68acb24d1cc788e0056d22fe1dd234986587752 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/commerce/commerce_ui_tab_helper.h"
#include <memory>
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/commerce/commerce_page_action_controller.h"
#include "chrome/browser/ui/commerce/discounts_page_action_controller.h"
#include "chrome/browser/ui/commerce/price_tracking_page_action_controller.h"
#include "chrome/browser/ui/commerce/product_specifications_entry_point_controller.h"
#include "chrome/browser/ui/commerce/product_specifications_page_action_controller.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/commerce/discounts_bubble_dialog_view.h"
#include "chrome/browser/ui/views/commerce/discounts_page_action_view_controller.h"
#include "chrome/browser/ui/views/commerce/price_insights_icon_view.h"
#include "chrome/browser/ui/views/commerce/price_insights_page_action_view_controller.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/toolbar_button_provider.h"
#include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry.h"
#include "chrome/browser/ui/views/side_panel/side_panel_entry_key.h"
#include "chrome/browser/ui/views/side_panel/side_panel_registry.h"
#include "chrome/browser/ui/views/side_panel/side_panel_ui.h"
#include "chrome/browser/ui/views/side_panel/side_panel_web_ui_view.h"
#include "chrome/browser/ui/webui/commerce/shopping_insights_side_panel_ui.h"
#include "chrome/common/pref_names.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/commerce/core/commerce_constants.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/commerce_utils.h"
#include "components/commerce/core/feature_utils.h"
#include "components/commerce/core/metrics/discounts_metric_collector.h"
#include "components/commerce/core/metrics/metrics_utils.h"
#include "components/commerce/core/price_tracking_utils.h"
#include "components/commerce/core/shopping_service.h"
#include "components/image_fetcher/core/image_fetcher.h"
#include "components/strings/grit/components_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view_class_properties.h"
#include "url/gurl.h"
using SidePanelWebUIViewT_ShoppingInsightsSidePanelUI =
SidePanelWebUIViewT<ShoppingInsightsSidePanelUI>;
using commerce::metrics::ShoppingContextualFeature;
BEGIN_TEMPLATE_METADATA(SidePanelWebUIViewT_ShoppingInsightsSidePanelUI,
SidePanelWebUIViewT)
END_METADATA
namespace commerce {
DEFINE_USER_DATA(CommerceUiTabHelper);
// static
CommerceUiTabHelper* CommerceUiTabHelper::From(tabs::TabModel* tab) {
return Get(tab->GetUnownedUserDataHost());
}
CommerceUiTabHelper::CommerceUiTabHelper(
tabs::TabInterface& tab_interface,
ShoppingService* shopping_service,
bookmarks::BookmarkModel* model,
image_fetcher::ImageFetcher* image_fetcher,
SidePanelRegistry* side_panel_registry)
: ContentsObservingTabFeature(tab_interface),
shopping_service_(shopping_service),
bookmark_model_(model),
image_fetcher_(image_fetcher),
side_panel_registry_(side_panel_registry),
price_insights_label_type_(PriceInsightsIconLabelType::kNone),
scoped_unowned_user_data_(tab_interface.GetUnownedUserDataHost(), *this) {
if (!image_fetcher_) {
CHECK_IS_TEST();
}
if (shopping_service_) {
shopping_service_->WaitForReady(
base::BindOnce(&CommerceUiTabHelper::UpdateUiForShoppingServiceReady,
weak_ptr_factory_.GetWeakPtr()));
} else {
CHECK_IS_TEST();
}
auto* tracker = feature_engagement::TrackerFactory::GetForBrowserContext(
web_contents()->GetBrowserContext());
price_tracking_controller_ =
std::make_unique<PriceTrackingPageActionController>(
GetPageActionControllerNotificationCallback(base::BindRepeating(
&CommerceUiTabHelper::UpdatePriceTrackingIconView,
weak_ptr_factory_.GetWeakPtr())),
shopping_service_, image_fetcher_, tracker);
product_specifications_controller_ =
std::make_unique<ProductSpecificationsPageActionController>(
GetPageActionControllerNotificationCallback(base::BindRepeating(
&CommerceUiTabHelper::UpdateProductSpecificationsIconView,
weak_ptr_factory_.GetWeakPtr())),
shopping_service_);
discounts_page_action_controller_ =
std::make_unique<DiscountsPageActionController>(
GetPageActionControllerNotificationCallback(
base::BindRepeating(&CommerceUiTabHelper::UpdateDiscountsIconView,
weak_ptr_factory_.GetWeakPtr())),
shopping_service_);
discounts_bubble_coordinator_ =
std::make_unique<DiscountsBubbleCoordinator>();
}
CommerceUiTabHelper::~CommerceUiTabHelper() = default;
// static
void CommerceUiTabHelper::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kShouldShowPriceTrackFUEBubble, true);
}
void CommerceUiTabHelper::UpdateUiForShoppingServiceReady(
ShoppingService* service) {
// This will happen in tests that don't pass CHECK_IS_TEST.
if (!service) {
return;
}
if (commerce::IsPriceInsightsEligible(service->GetAccountChecker())) {
UpdatePriceInsightsIconView();
}
}
void CommerceUiTabHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
ShouldIgnoreSameUrlNavigation() ||
IsSameDocumentWithSameCommittedUrl(navigation_handle)) {
is_initial_navigation_committed_ = true;
return;
}
// The page action icon may not have been used for the last page load. If
// that's the case, make sure we record it.
RecordPriceInsightsIconMetrics(/*from_icon_use=*/false);
previous_main_frame_url_ = navigation_handle->GetURL();
product_info_for_page_.reset();
is_page_action_expansion_computed_for_page_ = false;
got_discounts_response_for_page_ = false;
got_insights_response_for_page_ = false;
page_has_discounts_ = false;
page_action_to_expand_ = std::nullopt;
page_action_expanded_ = std::nullopt;
pending_tracking_state_.reset();
price_insights_info_.reset();
icon_use_recorded_for_page_.clear();
price_insights_label_type_ = PriceInsightsIconLabelType::kNone;
MakeShoppingInsightsSidePanelUnavailable();
if (!shopping_service_) {
return;
}
page_action_icon_compute_start_time_ = base::TimeTicks::Now();
discounts_page_action_controller_->ResetForNewNavigation(
web_contents()->GetLastCommittedURL());
// This value should not be a class member variable as it might change within
// a browser session.
bool is_price_insights_eligible =
commerce::IsPriceInsightsEligible(shopping_service_->GetAccountChecker());
if (is_price_insights_eligible) {
UpdatePriceInsightsIconView();
}
price_tracking_controller_->ResetForNewNavigation(
web_contents()->GetLastCommittedURL());
product_specifications_controller_->ResetForNewNavigation(
web_contents()->GetLastCommittedURL());
if (is_price_insights_eligible) {
// Price insights needs product info to get the product cluster title.
shopping_service_->GetProductInfoForUrl(
web_contents()->GetLastCommittedURL(),
base::BindOnce(&CommerceUiTabHelper::HandleProductInfoResponse,
weak_ptr_factory_.GetWeakPtr()));
}
if (BrowserWindowInterface* bwi = tab().GetBrowserWindowInterface()) {
auto* product_specifications_entry_point_controller =
commerce::ProductSpecificationsEntryPointController::From(bwi);
if (product_specifications_entry_point_controller) {
product_specifications_entry_point_controller->DidFinishNavigation(
web_contents());
}
}
}
bool CommerceUiTabHelper::ShouldIgnoreSameUrlNavigation() {
return previous_main_frame_url_ == web_contents()->GetLastCommittedURL() &&
is_initial_navigation_committed_;
}
bool CommerceUiTabHelper::IsSameDocumentWithSameCommittedUrl(
content::NavigationHandle* navigation_handle) {
return previous_main_frame_url_ == web_contents()->GetLastCommittedURL() &&
navigation_handle->IsSameDocument();
}
void CommerceUiTabHelper::WebContentsDestroyed() {
// If the tab or browser is closed, try recording whether the price tracking
// icon was used.
RecordPriceInsightsIconMetrics(/*from_icon_use=*/false);
}
void CommerceUiTabHelper::TriggerUpdateForIconView() {
if (!shopping_service_) {
return;
}
if (commerce::IsPriceInsightsEligible(
shopping_service_->GetAccountChecker())) {
UpdatePriceInsightsIconView();
}
UpdatePriceTrackingIconView();
}
void CommerceUiTabHelper::UpdatePriceInsightsIconView() {
if (IsPageActionMigrated(PageActionIconType::kPriceInsights)) {
tab()
.GetTabFeatures()
->commerce_price_insights_page_action_view_controller()
->UpdatePageActionIcon(
ShouldShowPriceInsightsIconView(),
ShouldExpandPageActionIcon(PageActionIconType::kPriceInsights),
GetPriceInsightsIconLabelTypeForPage());
return;
}
UpdatePageActionIconView(PageActionIconType::kPriceInsights);
}
void CommerceUiTabHelper::SetImageFetcherForTesting(
image_fetcher::ImageFetcher* image_fetcher) {
CHECK_IS_TEST();
image_fetcher_ = image_fetcher;
}
bool CommerceUiTabHelper::ShouldShowDiscountsIconView() {
return discounts_page_action_controller_->ShouldShowForNavigation().value_or(
false);
}
bool CommerceUiTabHelper::ShouldShowPriceTrackingIconView() {
return price_tracking_controller_->ShouldShowForNavigation().value_or(false);
}
bool CommerceUiTabHelper::ShouldShowPriceInsightsIconView() {
return shopping_service_ &&
commerce::IsPriceInsightsEligible(
shopping_service_->GetAccountChecker()) &&
price_insights_info_.has_value();
}
bool CommerceUiTabHelper::ShouldShowProductSpecificationsIconView() {
return product_specifications_controller_->ShouldShowForNavigation().value_or(
false);
}
void CommerceUiTabHelper::HandleProductInfoResponse(
const GURL& url,
const std::optional<const ProductInfo>& info) {
if (url != web_contents()->GetLastCommittedURL() || !info.has_value()) {
// Price insights is depended on ProductInfo, it's impossible to get
// insights without it.
got_insights_response_for_page_ = true;
MaybeComputePageActionToExpand();
return;
}
product_info_for_page_ = info;
if (commerce::IsPriceInsightsEligible(
shopping_service_->GetAccountChecker())) {
if (!info->product_cluster_title.empty()) {
shopping_service_->GetPriceInsightsInfoForUrl(
url,
base::BindOnce(&CommerceUiTabHelper::HandlePriceInsightsInfoResponse,
weak_ptr_factory_.GetWeakPtr()));
} else {
// If we were blocked because of the title, consider it a response of
// false.
got_insights_response_for_page_ = true;
}
}
}
void CommerceUiTabHelper::HandlePriceInsightsInfoResponse(
const GURL& url,
const std::optional<PriceInsightsInfo>& info) {
got_insights_response_for_page_ = true;
if (url != web_contents()->GetLastCommittedURL() || !info.has_value()) {
MaybeComputePageActionToExpand();
return;
}
price_insights_info_.emplace(info.value());
MaybeComputePageActionToExpand();
MakeShoppingInsightsSidePanelAvailable();
TriggerUpdateForIconView();
}
void CommerceUiTabHelper::MaybeComputePageActionToExpand() {
if (!shopping_service_) {
return;
}
// Make sure we have responses from all the relevant features first.
if (!discounts_page_action_controller_->ShouldShowForNavigation()
.has_value()) {
return;
}
if (commerce::IsPriceInsightsEligible(
shopping_service_->GetAccountChecker()) &&
!got_insights_response_for_page_) {
return;
}
if (!price_tracking_controller_->ShouldShowForNavigation().has_value()) {
return;
}
if (!product_specifications_controller_->ShouldShowForNavigation()
.has_value()) {
return;
}
if (is_page_action_expansion_computed_for_page_) {
return;
}
ComputePageActionToExpand();
is_page_action_expansion_computed_for_page_ = true;
if (ShouldShowPriceInsightsIconView()) {
base::UmaHistogramEnumeration("Commerce.PriceInsights.OmniboxIconShown",
price_insights_label_type_);
}
UpdateProductSpecificationsIconView();
UpdateDiscountsIconView();
UpdatePriceTrackingIconView();
UpdatePriceInsightsIconView();
if (ShouldShowDiscountsIconView()) {
commerce::metrics::DiscountsMetricCollector::
RecordDiscountsPageActionIconExpandState(
IsPageActionIconExpanded(PageActionIconType::kDiscounts),
GetDiscounts());
}
}
void CommerceUiTabHelper::SetPriceTrackingState(
bool enable,
bool is_new_bookmark,
base::OnceCallback<void(bool)> callback) {
const bookmarks::BookmarkNode* node =
bookmark_model_->GetMostRecentlyAddedUserNodeForURL(
web_contents()->GetLastCommittedURL());
base::OnceCallback<void(bool)> wrapped_callback = base::BindOnce(
[](base::WeakPtr<CommerceUiTabHelper> helper,
base::OnceCallback<void(bool)> callback, bool is_tracked,
bool success) {
if (helper) {
helper->pending_tracking_state_.reset();
}
std::move(callback).Run(success);
},
weak_ptr_factory_.GetWeakPtr(), std::move(callback), enable);
pending_tracking_state_.emplace(enable);
if (node) {
commerce::SetPriceTrackingStateForBookmark(
shopping_service_.get(), bookmark_model_.get(), node, enable,
std::move(wrapped_callback), enable && is_new_bookmark);
} else {
DCHECK(!enable);
std::optional<commerce::ProductInfo> info =
shopping_service_->GetAvailableProductInfoForUrl(
web_contents()->GetLastCommittedURL());
if (info.has_value()) {
commerce::SetPriceTrackingStateForClusterId(
shopping_service_.get(), bookmark_model_,
info->product_cluster_id.value(), enable,
std::move(wrapped_callback));
}
}
}
void CommerceUiTabHelper::OnPriceInsightsIconClicked() {
auto* side_panel_ui = GetSidePanelUI();
DCHECK(side_panel_ui);
SidePanelEntry* const entry = side_panel_registry_->GetEntryForKey(
SidePanelEntryKey(SidePanelEntry::Id::kShoppingInsights));
DCHECK(entry);
if (side_panel_ui->IsSidePanelEntryShowing(
SidePanelEntryKey(SidePanelEntryId::kShoppingInsights))) {
side_panel_ui->Close(entry->type());
} else {
side_panel_ui->Show(SidePanelEntryId::kShoppingInsights);
if (price_insights_info_.has_value()) {
base::UmaHistogramEnumeration("Commerce.PriceInsights.OmniboxIconClicked",
price_insights_label_type_);
base::UmaHistogramBoolean(
"Commerce.PriceInsights.SidePanelOpenWithMultipleCatalogs",
price_insights_info_->has_multiple_catalogs);
commerce::metrics::RecordShoppingActionUKM(
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId(),
commerce::metrics::ShoppingAction::kPriceInsightsOpened);
}
}
RecordPriceInsightsIconMetrics(true);
}
const gfx::Image& CommerceUiTabHelper::GetProductImage() {
return price_tracking_controller_->GetLastFetchedImage();
}
const GURL& CommerceUiTabHelper::GetProductImageURL() {
return price_tracking_controller_->GetLastFetchedImageUrl();
}
bool CommerceUiTabHelper::IsPriceTracking() {
return pending_tracking_state_.value_or(
price_tracking_controller_->IsPriceTrackingCurrentProduct());
}
bool CommerceUiTabHelper::IsInRecommendedSet() {
return product_specifications_controller_->IsInRecommendedSet();
}
GURL CommerceUiTabHelper::GetComparisonTableURL() {
return product_specifications_controller_->GetComparisonTableURL();
}
void CommerceUiTabHelper::OnOpenComparePageClicked() {
auto* tab_strip_model = tab().GetBrowserWindowInterface()->GetTabStripModel();
GURL comparison_table_url = GetComparisonTableURL();
for (int index = 0; index < tab_strip_model->count(); index++) {
auto* tab_web_contents = tab_strip_model->GetWebContentsAt(index);
if (tab_web_contents->GetLastCommittedURL() == comparison_table_url) {
tab_strip_model->ActivateTabAt(index);
return;
}
}
int active_index = tab_strip_model->active_index();
chrome::AddTabAt(
tab().GetBrowserWindowInterface()->GetBrowserForMigrationOnly(),
comparison_table_url, active_index + 1, true,
tab_strip_model->GetTabGroupForTab(active_index));
}
std::u16string CommerceUiTabHelper::GetComparisonSetName() {
return product_specifications_controller_->GetComparisonSetName();
}
std::u16string CommerceUiTabHelper::GetProductSpecificationsLabel(
bool is_added) {
return product_specifications_controller_->GetProductSpecificationsLabel(
is_added);
}
const std::vector<DiscountInfo>& CommerceUiTabHelper::GetDiscounts() {
return discounts_page_action_controller_->GetDiscounts();
}
void CommerceUiTabHelper::UpdatePriceTrackingIconView() {
if (IsPageActionMigrated(PageActionIconType::kPriceTracking)) {
return;
}
UpdatePageActionIconView(PageActionIconType::kPriceTracking);
}
void CommerceUiTabHelper::UpdateProductSpecificationsIconView() {
UpdatePageActionIconView(PageActionIconType::kProductSpecifications);
}
void CommerceUiTabHelper::MakeShoppingInsightsSidePanelAvailable() {
auto entry = std::make_unique<SidePanelEntry>(
SidePanelEntry::Key(SidePanelEntry::Id::kShoppingInsights),
base::BindRepeating(&CommerceUiTabHelper::CreateShoppingInsightsWebView,
base::Unretained(this)),
/*default_content_width_callback=*/base::NullCallback());
side_panel_registry_->Register(std::move(entry));
}
void CommerceUiTabHelper::MakeShoppingInsightsSidePanelUnavailable() {
SidePanelEntry* const entry = side_panel_registry_->GetEntryForKey(
SidePanelEntryKey(SidePanelEntry::Id::kShoppingInsights));
if (!entry) {
return;
}
auto* side_panel_ui = GetSidePanelUI();
if (side_panel_ui && side_panel_ui->IsSidePanelEntryShowing(entry->key())) {
side_panel_ui->Close(entry->type());
base::RecordAction(base::UserMetricsAction(
"Commerce.PriceInsights.NavigationClosedSidePanel"));
}
side_panel_registry_->Deregister(entry->key());
}
std::unique_ptr<views::View> CommerceUiTabHelper::CreateShoppingInsightsWebView(
SidePanelEntryScope& scope) {
auto shopping_insights_web_view =
std::make_unique<SidePanelWebUIViewT<ShoppingInsightsSidePanelUI>>(
scope, base::RepeatingClosure(), base::RepeatingClosure(),
std::make_unique<WebUIContentsWrapperT<ShoppingInsightsSidePanelUI>>(
GURL(kChromeUIShoppingInsightsSidePanelUrl),
Profile::FromBrowserContext(web_contents()->GetBrowserContext()),
IDS_SHOPPING_INSIGHTS_SIDE_PANEL_TITLE,
/*esc_closes_ui=*/false));
// Call ShowUI() to make the UI ready, this doesn't really open/switch the
// side panel.
shopping_insights_web_view->ShowUI();
return shopping_insights_web_view;
}
SidePanelUI* CommerceUiTabHelper::GetSidePanelUI() {
if (BrowserWindowInterface* bwi = tab().GetBrowserWindowInterface()) {
return bwi->GetFeatures().side_panel_ui();
}
return nullptr;
}
const std::optional<bool>&
CommerceUiTabHelper::GetPendingTrackingStateForTesting() {
return pending_tracking_state_;
}
const std::optional<PriceInsightsInfo>&
CommerceUiTabHelper::GetPriceInsightsInfo() {
return price_insights_info_;
}
void CommerceUiTabHelper::ShowDiscountBubble(
const DiscountInfo& discount,
base::OnceClosure one_bubble_closing_callback) {
discounts_bubble_coordinator_->Show(GetDiscountsIconView(),
tab().GetContents(), discount,
std::move(one_bubble_closing_callback));
}
void CommerceUiTabHelper::UpdateDiscountsIconView() {
if (IsPageActionMigrated(PageActionIconType::kDiscounts)) {
tab()
.GetTabFeatures()
->commerce_discounts_page_action_view_controller()
->UpdatePageIcon(
ShouldShowDiscountsIconView(),
ShouldExpandPageActionIcon(PageActionIconType::kDiscounts));
return;
}
UpdatePageActionIconView(PageActionIconType::kDiscounts);
}
const DiscountsBubbleCoordinator&
CommerceUiTabHelper::GetDiscountsBubbleCoordinator() const {
return *discounts_bubble_coordinator_;
}
views::View* CommerceUiTabHelper::GetDiscountsIconView() {
BrowserWindowInterface* bwi = tab().GetBrowserWindowInterface();
CHECK(bwi);
// TODO(https://crbug.com/425953501): Remove GetBrowserForMigrationOnly since
// Browser* will not be needed once ToolBarButtonProvider is migrated to
// BrowserWindowInterface.
auto* browser_view =
BrowserView::GetBrowserViewForBrowser(bwi->GetBrowserForMigrationOnly());
if (!browser_view) {
return nullptr;
}
auto* toolbar_button_provider = browser_view->toolbar_button_provider();
if (!toolbar_button_provider) {
return nullptr;
}
return toolbar_button_provider->GetPageActionView(kActionCommerceDiscounts);
}
void CommerceUiTabHelper::ComputePageActionToExpand() {
if (!page_action_icon_compute_start_time_.is_null()) {
base::UmaHistogramTimes(
"Commerce.IconComputationTime",
base::TimeTicks::Now() - page_action_icon_compute_start_time_);
page_action_to_expand_ = std::nullopt;
}
page_action_icon_compute_start_time_ = base::TimeTicks();
if (!web_contents() || !web_contents()->GetBrowserContext()) {
page_action_to_expand_ = std::nullopt;
return;
}
auto* tracker = feature_engagement::TrackerFactory::GetForBrowserContext(
web_contents()->GetBrowserContext());
// TODO(b:301440117): Splitting the triggering logic for each icon into
// delegates would make this much easier to test.
if (discounts_page_action_controller_->WantsExpandedUi()) {
page_action_to_expand_ = PageActionIconType::kDiscounts;
MaybeRecordShoppingInformationUKM(PageActionIconType::kDiscounts);
return;
}
if (ShouldShowProductSpecificationsIconView()) {
page_action_to_expand_ = PageActionIconType::kProductSpecifications;
return;
}
// Prioritize the price insights icon.
if (ShouldShowPriceInsightsIconView()) {
PriceInsightsIconLabelType label_type =
GetPriceInsightsIconLabelTypeForPage();
bool icon_has_label = label_type != PriceInsightsIconLabelType::kNone;
if (icon_has_label && tracker &&
tracker->ShouldTriggerHelpUI(
feature_engagement::kIPHPriceInsightsPageActionIconLabelFeature)) {
// Note that `Dismiss()` in these cases does not dismiss the UI. It's
// telling the FE backend that the promo is done so that other promos can
// run. Showing the label should not block other promos from displaying.
tracker->Dismissed(
feature_engagement::kIPHPriceInsightsPageActionIconLabelFeature);
page_action_to_expand_ = PageActionIconType::kPriceInsights;
MaybeRecordShoppingInformationUKM(PageActionIconType::kPriceInsights);
price_insights_label_type_ = label_type;
return;
}
}
if (price_tracking_controller_->WantsExpandedUi()) {
page_action_to_expand_ = PageActionIconType::kPriceTracking;
MaybeRecordShoppingInformationUKM(PageActionIconType::kPriceTracking);
return;
}
MaybeRecordShoppingInformationUKM(std::nullopt);
}
PriceInsightsIconLabelType
CommerceUiTabHelper::GetPriceInsightsIconLabelTypeForPage() {
auto& price_insights_info = GetPriceInsightsInfo();
if (!price_insights_info.has_value() ||
!price_insights_info->typical_low_price_micros.has_value() ||
!price_insights_info->typical_high_price_micros.has_value() ||
price_insights_info->catalog_history_prices.empty()) {
return PriceInsightsIconLabelType::kNone;
} else if (price_insights_info->price_bucket ==
commerce::PriceBucket::kLowPrice) {
return PriceInsightsIconLabelType::kPriceIsLow;
} else if (price_insights_info->price_bucket ==
commerce::PriceBucket::kHighPrice &&
commerce::kPriceInsightsChipLabelExpandOnHighPrice.Get()) {
return PriceInsightsIconLabelType::kPriceIsHigh;
} else {
return PriceInsightsIconLabelType::kNone;
}
}
bool CommerceUiTabHelper::ShouldExpandPageActionIcon(PageActionIconType type) {
// Only allow the requesting icon to expand once. This prevents the icon from
// expanding multiple times per page load.
if (page_action_to_expand_.has_value() &&
type == page_action_to_expand_.value()) {
page_action_expanded_ = page_action_to_expand_.value();
page_action_to_expand_ = std::nullopt;
return true;
}
return false;
}
bool CommerceUiTabHelper::IsPageActionIconExpanded(PageActionIconType type) {
return page_action_expanded_.has_value() &&
type == page_action_expanded_.value();
}
void CommerceUiTabHelper::OnPriceTrackingIconClicked() {
price_tracking_controller_->OnIconClicked();
}
void CommerceUiTabHelper::OnProductSpecificationsIconClicked() {
product_specifications_controller_->OnIconClicked();
}
void CommerceUiTabHelper::OnDiscountsCouponCodeCopied() {
discounts_page_action_controller_->CouponCodeCopied();
}
bool CommerceUiTabHelper::IsDiscountsCouponCodeCopied() {
return discounts_page_action_controller_->IsCouponCodeCopied();
}
bool CommerceUiTabHelper::ShouldAutoShowDiscountsBubble(uint64_t discount_id,
bool is_merchant_wide) {
return discounts_page_action_controller_->ShouldAutoShowBubble(
discount_id, is_merchant_wide);
}
void CommerceUiTabHelper::DiscountsBubbleShown(uint64_t discount_id) {
discounts_page_action_controller_->DiscountsBubbleShown(discount_id);
}
void CommerceUiTabHelper::RecordIconMetrics(PageActionIconType page_action,
bool from_icon_use) {
if (icon_use_recorded_for_page_.contains(page_action)) {
return;
}
icon_use_recorded_for_page_.insert(page_action);
std::string histogram_name;
switch (page_action) {
case PageActionIconType::kPriceInsights:
histogram_name = "Commerce.PriceInsights.IconInteractionState";
break;
default:
return;
}
bool expanded = page_action_expanded_.has_value() &&
page_action_expanded_.value() == page_action;
if (from_icon_use) {
if (expanded) {
base::UmaHistogramEnumeration(
histogram_name, PageActionIconInteractionState::kClickedExpanded);
} else {
base::UmaHistogramEnumeration(histogram_name,
PageActionIconInteractionState::kClicked);
}
} else {
if (expanded) {
base::UmaHistogramEnumeration(
histogram_name, PageActionIconInteractionState::kNotClickedExpanded);
} else {
base::UmaHistogramEnumeration(
histogram_name, PageActionIconInteractionState::kNotClicked);
}
}
}
void CommerceUiTabHelper::RecordPriceInsightsIconMetrics(bool from_icon_use) {
if (ShouldShowPriceInsightsIconView()) {
RecordIconMetrics(PageActionIconType::kPriceInsights, from_icon_use);
}
}
void CommerceUiTabHelper::MaybeRecordShoppingInformationUKM(
std::optional<PageActionIconType> page_action_type) {
// This is our current definition of shopping content.
if (!product_info_for_page_.has_value()) {
return;
}
auto ukm_builder = ukm::builders::Shopping_ShoppingInformation(
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId());
if (page_action_type.has_value()) {
int64_t promoted_feature = 0;
if (page_action_type == PageActionIconType::kDiscounts) {
promoted_feature =
static_cast<int64_t>(ShoppingContextualFeature::kDiscounts);
} else if (page_action_type == PageActionIconType::kPriceInsights) {
promoted_feature =
static_cast<int64_t>(ShoppingContextualFeature::kPriceInsights);
} else if (page_action_type == PageActionIconType::kPriceTracking) {
promoted_feature =
static_cast<int64_t>(ShoppingContextualFeature::kPriceTracking);
} else {
NOTREACHED();
}
ukm_builder.SetPromotedFeature(promoted_feature);
}
ukm_builder.SetHasPriceInsights(price_insights_info_.has_value())
.SetHasDiscount(
discounts_page_action_controller_->ShouldShowForNavigation().value_or(
false))
.SetIsPriceTrackable(true)
.SetIsShoppingContent(true)
.Record(ukm::UkmRecorder::Get());
}
PriceTrackingPageActionController*
CommerceUiTabHelper::GetPriceTrackingControllerForTesting() {
return price_tracking_controller_.get();
}
void CommerceUiTabHelper::OnPageActionControllerNotification(
base::RepeatingClosure page_action_icon_update_callback) {
MaybeComputePageActionToExpand();
page_action_icon_update_callback.Run();
}
base::RepeatingClosure
CommerceUiTabHelper::GetPageActionControllerNotificationCallback(
base::RepeatingClosure page_action_icon_update_callback) {
return base::BindRepeating(
&CommerceUiTabHelper::OnPageActionControllerNotification,
weak_ptr_factory_.GetWeakPtr(),
std::move(page_action_icon_update_callback));
}
void CommerceUiTabHelper::SetPriceTrackingControllerForTesting(
std::unique_ptr<PriceTrackingPageActionController> controller) {
price_tracking_controller_.reset(controller.release());
}
void CommerceUiTabHelper::UpdatePageActionIconView(PageActionIconType type) {
BrowserWindowInterface* bwi = tab().GetBrowserWindowInterface();
if (!bwi) {
return;
}
// TODO(https://crbug.com/376283687): Remove GetBrowserForMigrationOnly during
// the Discounts Page Actions Post Migration Cleanups since it will no longer
// be needed.
bwi->GetBrowserForMigrationOnly()->window()->UpdatePageActionIcon(type);
}
} // namespace commerce