blob: 362c6966722882861d9349520a81ace0df9c8a16 [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/views/commerce/price_tracking_view.h"
#include "base/metrics/user_metrics.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/commerce/shopping_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/price_tracking_utils.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets_outsets_base.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/layout/layout_provider.h"
namespace {
constexpr int kProductImageSize = 56;
constexpr int kImageBorderRadius = 4;
constexpr int kIconSize = 16;
constexpr int kIconMargin = 14;
constexpr int kIconSizeRefresh = 20;
constexpr int kIconMarginRefresh = 8;
int GetIconMargin() {
return features::IsChromeRefresh2023() ? kIconMarginRefresh : kIconMargin;
}
gfx::Size GetIconSize() {
return features::IsChromeRefresh2023()
? gfx::Size(kIconSizeRefresh, kIconSizeRefresh)
: gfx::Size(kIconSize, kIconSize);
}
} // namespace
PriceTrackingView::PriceTrackingView(Profile* profile,
const GURL& page_url,
const gfx::ImageSkia& product_image,
bool is_price_track_enabled,
const commerce::ProductInfo& product_info)
: profile_(profile),
is_price_track_enabled_(is_price_track_enabled),
product_info_(product_info) {
auto* layout_provider = views::LayoutProvider::Get();
const bool power_bookmarks_side_panel_enabled =
base::FeatureList::IsEnabled(features::kPowerBookmarksSidePanel);
const int label_spacing = power_bookmarks_side_panel_enabled ? 0 : 4;
const auto label_context = power_bookmarks_side_panel_enabled
? views::style::CONTEXT_LABEL
: views::style::CONTEXT_DIALOG_BODY_TEXT;
const int horizontal_spacing = layout_provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
const gfx::Insets dialog_insets =
layout_provider->GetInsetsMetric(views::INSETS_DIALOG);
if (power_bookmarks_side_panel_enabled) {
if (features::IsChromeRefresh2023()) {
SetCrossAxisAlignment(views::LayoutAlignment::kStart);
}
// Icon column
auto* icon = AddChildView(std::make_unique<views::ImageView>());
icon->SetImage(
ui::ImageModel::FromVectorIcon(omnibox::kPriceTrackingDisabledIcon,
kColorBookmarkDialogTrackPriceIcon));
icon->SetImageSize(GetIconSize());
icon->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(0, 0, 0, GetIconMargin()));
} else {
// image column
auto* product_image_containter =
AddChildView(std::make_unique<views::BoxLayoutView>());
product_image_containter->SetCrossAxisAlignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
product_image_containter->SetProperty(
views::kMarginsKey, gfx::Insets::TLBR(0, 0, 0, horizontal_spacing));
// Set product image.
product_image_containter->AddChildView(
views::Builder<views::ImageView>()
.SetImageSize(gfx::Size(kProductImageSize, kProductImageSize))
.SetPreferredSize(gfx::Size(kProductImageSize, kProductImageSize))
// TODO(meiliang@): Verify color and corner radius with UX.
.SetBorder(views::CreateThemedRoundedRectBorder(
1, kImageBorderRadius, kColorBookmarkDialogProductImageBorder))
.SetImage(
gfx::ImageSkiaOperations::CreateCroppedCenteredRoundRectImage(
gfx::Size(kProductImageSize, kProductImageSize),
kImageBorderRadius, product_image))
.Build());
}
// Text column
auto text_container = std::make_unique<views::FlexLayoutView>();
text_container->SetOrientation(views::LayoutOrientation::kVertical);
// Title label
auto* title_label =
text_container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_OMNIBOX_TRACK_PRICE_DIALOG_TITLE),
label_context,
features::IsChromeRefresh2023() ? views::style::STYLE_EMPHASIZED
: views::style::STYLE_PRIMARY));
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_label->SetFocusBehavior(View::FocusBehavior::ACCESSIBLE_ONLY);
// Body label
int body_string_id = IDS_BOOKMARK_STAR_DIALOG_TRACK_PRICE_DESCRIPTION;
// If "track by default" is enabled, the email consent comes after enabling
// price tracking for the first time, so it doesn't need to be included in the
// "email disabled" state. If we're not in the experiment, only show the
// "email disabled" message if it is explicitly disabled (it's turned on
// implicitly on when the first item is tracked).
bool email_pref_set_by_user =
commerce::IsEmailNotificationPrefSetByUser(profile_->GetPrefs());
bool email_pref_value =
commerce::GetEmailNotificationPrefValue(profile_->GetPrefs());
if ((base::FeatureList::IsEnabled(commerce::kShoppingListTrackByDefault) &&
!email_pref_set_by_user) ||
(email_pref_set_by_user && !email_pref_value)) {
body_string_id = IDS_BOOKMARK_STAR_DIALOG_TRACK_PRICE_DESCRIPTION_EMAIL_OFF;
}
body_label_ = text_container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(body_string_id), label_context,
views::style::STYLE_SECONDARY));
if (power_bookmarks_side_panel_enabled) {
body_label_->SetFontList(body_label_->font_list().DeriveWithSizeDelta(-1));
}
body_label_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(label_spacing, 0, 0, 0));
body_label_->SetMultiLine(true);
body_label_->SetAllowCharacterBreak(true);
body_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
body_label_->SetFocusBehavior(View::FocusBehavior::ACCESSIBLE_ONLY);
AddChildView(std::move(text_container));
// Toggle button column
toggle_button_ = AddChildView(std::make_unique<views::ToggleButton>(
base::BindRepeating(&PriceTrackingView::OnToggleButtonPressed,
weak_ptr_factory_.GetWeakPtr(), page_url)));
toggle_button_->SetIsOn(is_price_track_enabled_);
scoped_observation_.Observe(
commerce::ShoppingServiceFactory::GetForBrowserContext(profile_));
toggle_button_->SetAccessibleName(GetToggleAccessibleName());
toggle_button_->SetProperty(views::kMarginsKey,
gfx::Insets::TLBR(0, horizontal_spacing, 0, 0));
const int bubble_width = ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH);
int label_width;
if (power_bookmarks_side_panel_enabled) {
toggle_button_->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
features::IsChromeRefresh2023()
? views::MaximumFlexSizeRule::kPreferred
: views::MaximumFlexSizeRule::kUnbounded)
.WithAlignment(views::LayoutAlignment::kEnd));
label_width = bubble_width - horizontal_spacing * 2 -
dialog_insets.right() - GetIconMargin() -
GetIconSize().width() -
toggle_button_->GetPreferredSize().width();
} else {
label_width = bubble_width - horizontal_spacing * 2 -
dialog_insets.width() - kProductImageSize -
toggle_button_->GetPreferredSize().width();
}
body_label_->SizeToFit(label_width);
base::RecordAction(base::UserMetricsAction(
"Commerce.PriceTracking.BookmarkDialogPriceTrackViewShown"));
}
PriceTrackingView::~PriceTrackingView() = default;
bool PriceTrackingView::IsToggleOn() {
return toggle_button_->GetIsOn();
}
void PriceTrackingView::OnSubscribe(const commerce::CommerceSubscription& sub,
bool succeeded) {
if (succeeded) {
HandleSubscriptionUpdate(sub, true);
}
}
void PriceTrackingView::OnUnsubscribe(const commerce::CommerceSubscription& sub,
bool succeeded) {
if (succeeded) {
HandleSubscriptionUpdate(sub, false);
}
}
void PriceTrackingView::HandleSubscriptionUpdate(
const commerce::CommerceSubscription& sub,
bool is_tracking) {
if (sub.id_type == commerce::IdentifierType::kProductClusterId &&
base::NumberToString(product_info_.product_cluster_id.value_or(
commerce::kInvalidSubscriptionId)) == sub.id) {
is_price_track_enabled_ = is_tracking;
toggle_button_->SetIsOn(is_tracking);
toggle_button_->SetAccessibleName(GetToggleAccessibleName());
}
}
std::u16string PriceTrackingView::GetToggleAccessibleName() {
return l10n_util::GetStringUTF16(
IsToggleOn() ? IDS_PRICE_TRACKING_UNTRACK_PRODUCT_ACCESSIBILITY
: IDS_PRICE_TRACKING_TRACK_PRODUCT_ACCESSIBILITY);
}
void PriceTrackingView::OnToggleButtonPressed(const GURL& url) {
is_price_track_enabled_ = !is_price_track_enabled_;
if (is_price_track_enabled_) {
base::RecordAction(base::UserMetricsAction(
"Commerce.PriceTracking.BookmarkDialogPriceTrackViewTrackedPrice"));
} else {
base::RecordAction(base::UserMetricsAction(
"Commerce.PriceTracking.BookmarkDialogPriceTrackViewUntrackedPrice"));
}
toggle_button_->SetAccessibleName(GetToggleAccessibleName());
UpdatePriceTrackingState(url);
}
void PriceTrackingView::UpdatePriceTrackingState(const GURL& url) {
bookmarks::BookmarkModel* const model =
BookmarkModelFactory::GetForBrowserContext(profile_);
const bookmarks::BookmarkNode* node =
model->GetMostRecentlyAddedUserNodeForURL(url);
// If "track by default" is on, we'll show a dialog after saving to offer
// email notifications.
if (!base::FeatureList::IsEnabled(commerce::kShoppingListTrackByDefault) &&
profile_ && is_price_track_enabled_) {
commerce::MaybeEnableEmailNotifications(profile_->GetPrefs());
}
commerce::ShoppingService* service =
commerce::ShoppingServiceFactory::GetForBrowserContext(profile_);
base::OnceCallback<void(bool)> callback =
base::BindOnce(&PriceTrackingView::OnPriceTrackingStateUpdated,
weak_ptr_factory_.GetWeakPtr());
if (node) {
commerce::SetPriceTrackingStateForBookmark(
service, model, node, is_price_track_enabled_, std::move(callback));
} else {
DCHECK(!is_price_track_enabled_);
if (commerce::CanTrackPrice(product_info_)) {
commerce::SetPriceTrackingStateForClusterId(
service, model, product_info_.product_cluster_id.value(),
is_price_track_enabled_, std::move(callback));
}
}
}
void PriceTrackingView::OnPriceTrackingStateUpdated(bool success) {
// TODO(crbug.com/1346612): Record latency for the update status.
if (!success) {
is_price_track_enabled_ = !is_price_track_enabled_;
toggle_button_->SetIsOn(is_price_track_enabled_);
toggle_button_->SetAccessibleName(GetToggleAccessibleName());
body_label_->SetText(l10n_util::GetStringUTF16(
IDS_OMNIBOX_TRACK_PRICE_DIALOG_ERROR_DESCRIPTION));
}
}