blob: aff7619992f4eded345a0a218a631870f9fd5517 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/commerce/discounts_bubble_dialog_view.h"
#include <memory>
#include <string>
#include <utility>
#include "base/functional/callback_forward.h"
#include "base/i18n/time_formatting.h"
#include "chrome/browser/ui/commerce/commerce_ui_tab_helper.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/accessibility/theme_tracking_non_accessible_image_view.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/commerce/discounts_coupon_code_label_view.h"
#include "chrome/browser/ui/views/controls/page_switcher_view.h"
#include "chrome/browser/ui/views/controls/subpage_view.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/commerce/core/commerce_types.h"
#include "components/commerce/core/metrics/discounts_metric_collector.h"
#include "components/strings/grit/components_strings.h"
#include "components/tabs/public/tab_interface.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_tracker.h"
#include "url/gurl.h"
DEFINE_ELEMENT_IDENTIFIER_VALUE(kDiscountsBubbleDialogId);
DEFINE_ELEMENT_IDENTIFIER_VALUE(kDiscountsBubbleMainPageId);
DEFINE_ELEMENT_IDENTIFIER_VALUE(kDiscountsBubbleTermsAndConditionLabelId);
DEFINE_ELEMENT_IDENTIFIER_VALUE(kDiscountsBubbleTermsAndConditionPageId);
// DiscountsBubbleDialogView
DiscountsBubbleDialogView::DiscountsBubbleDialogView(
View* anchor_view,
content::WebContents* web_contents,
const commerce::DiscountInfo& discount_info)
: LocationBarBubbleDelegateView(anchor_view, web_contents, true),
discount_info_(discount_info),
ukm_source_id_(
web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId()) {
SetProperty(views::kElementIdentifierKey, kDiscountsBubbleDialogId);
SetShowCloseButton(true);
SetCloseCallback(base::BindOnce(&DiscountsBubbleDialogView::OnDialogClosing,
weak_factory_.GetWeakPtr()));
set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH));
SetLayoutManager(std::make_unique<views::FillLayout>());
auto dialog_insets =
ChromeLayoutProvider::Get()->GetDialogInsetsForContentType(
views::DialogContentType::kControl, views::DialogContentType::kText);
set_margins(
gfx::Insets::TLBR(dialog_insets.top(), 0, dialog_insets.bottom(), 0));
SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
GURL url = web_contents->GetLastCommittedURL();
std::string seller_domain = url.spec();
page_container_ = AddChildView(std::make_unique<PageSwitcherView>(
CreateMainPageContent(discount_info, seller_domain)));
}
DiscountsBubbleDialogView::~DiscountsBubbleDialogView() = default;
void DiscountsBubbleDialogView::AddedToWidget() {
GetBubbleFrameView()->SetHeaderView(CreateMainPageHeaderView());
GetBubbleFrameView()->SetTitleView(CreateMainPageTitleView(discount_info_));
}
std::unique_ptr<views::View>
DiscountsBubbleDialogView::CreateMainPageHeaderView() {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
auto image_view =
std::make_unique<views::ImageView>(bundle.GetThemedLottieImageNamed(
IDR_SHOPPING_DISCOUNTS_AVAILABLE_LOTTIE));
image_view->GetViewAccessibility().SetIsInvisible(true);
return image_view;
}
std::unique_ptr<views::View> DiscountsBubbleDialogView::CreateMainPageTitleView(
const commerce::DiscountInfo& discount_info) {
auto title_view = std::make_unique<views::Label>(
base::ASCIIToUTF16(discount_info_.description_detail),
views::style::CONTEXT_DIALOG_TITLE);
title_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_view->SetMultiLine(true);
return title_view;
}
std::unique_ptr<views::View> DiscountsBubbleDialogView::CreateMainPageContent(
const commerce::DiscountInfo& discount_info,
const std::string& seller_domain) {
auto main_page_view = std::make_unique<views::View>();
main_page_view->SetProperty(views::kElementIdentifierKey,
kDiscountsBubbleMainPageId);
gfx::Insets dialog_insets =
ChromeLayoutProvider::Get()->GetDialogInsetsForContentType(
views::DialogContentType::kControl, views::DialogContentType::kText);
auto* layout =
main_page_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets::TLBR(0, dialog_insets.left(), 0, dialog_insets.right()),
ChromeLayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStart);
// coupon code
main_page_view->AddChildView(std::make_unique<DiscountsCouponCodeLabelView>(
base::ASCIIToUTF16(discount_info.discount_code.value()),
base::BindRepeating(&DiscountsBubbleDialogView::CopyButtonClicked,
weak_factory_.GetWeakPtr())));
// additional info with expiration date, and terms and conditions
auto* additional_info_label = main_page_view->AddChildView(
views::Builder<views::StyledLabel>()
.SetDefaultTextStyle(views::style::STYLE_SECONDARY)
.SetTextContext(views::style::CONTEXT_DIALOG_BODY_TEXT)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)
.Build());
auto additional_info_text =
discount_info.expiry_time_sec.has_value()
? l10n_util::GetStringFUTF16(
IDS_DISCOUNT_USE_THIS_CODE_AT_CHECKOUT_WITH_EXPIRATION_DATE,
TimeFormatShortDate(base::Time::FromSecondsSinceUnixEpoch(
discount_info.expiry_time_sec.value())))
: l10n_util::GetStringUTF16(IDS_DISCOUNT_USE_THIS_CODE_AT_CHECKOUT);
if (discount_info.terms_and_conditions.has_value() &&
!discount_info.terms_and_conditions.value().empty()) {
std::vector<size_t> offsets;
additional_info_text = l10n_util::GetStringFUTF16(
IDS_TWO_STRINGS_CONNECTOR_WITH_SPACE, additional_info_text,
l10n_util::GetStringUTF16(IDS_SEE_SELLER_TERMS_AND_CONDITIONS),
&offsets);
additional_info_label->SetText(additional_info_text);
size_t terms_and_conditions_offset = offsets[1];
base::RepeatingCallback<void()> callback = base::BindRepeating(
&DiscountsBubbleDialogView::OpenTermsAndConditionsPage,
weak_factory_.GetWeakPtr(), discount_info, seller_domain);
views::StyledLabel::RangeStyleInfo terms_and_conditions_style_info =
views::StyledLabel::RangeStyleInfo::CreateForLink(std::move(callback));
additional_info_label->AddStyleRange(
gfx::Range(terms_and_conditions_offset, additional_info_text.length()),
terms_and_conditions_style_info);
additional_info_label->SetProperty(
views::kElementIdentifierKey, kDiscountsBubbleTermsAndConditionLabelId);
} else {
additional_info_label->SetText(additional_info_text);
}
return main_page_view;
}
void DiscountsBubbleDialogView::OpenMainPage(
commerce::DiscountInfo discount_info,
std::string seller_domain) {
GetBubbleFrameView()->SetHeaderView(CreateMainPageHeaderView());
GetBubbleFrameView()->SetTitleView(CreateMainPageTitleView(discount_info));
page_container_->SwitchToPage(
CreateMainPageContent(discount_info, seller_domain));
GetBubbleFrameView()->SetFootnoteView(nullptr);
}
void DiscountsBubbleDialogView::OpenTermsAndConditionsPage(
commerce::DiscountInfo discount_info,
std::string seller_domain) {
auto bubble_width = views::LayoutProvider::Get()->GetDistanceMetric(
views::DISTANCE_BUBBLE_PREFERRED_WIDTH);
gfx::FontList font_list = views::TypographyProvider::Get().GetFont(
views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY);
auto footer_message = l10n_util::GetStringFUTF16(
IDS_SELLER_TERMS_AND_CONDITIONS_DIALOG_FOOTER,
url_formatter::ElideHost(GURL(seller_domain), font_list, bubble_width));
page_container_->SwitchToPage(
views::Builder<SubpageView>(
std::make_unique<SubpageView>(
base::BindRepeating(&DiscountsBubbleDialogView::OpenMainPage,
weak_factory_.GetWeakPtr(), discount_info,
seller_domain),
GetBubbleFrameView()))
.SetProperty(views::kElementIdentifierKey,
kDiscountsBubbleTermsAndConditionPageId)
.SetTitle(l10n_util::GetStringUTF16(
IDS_SELLER_TERMS_AND_CONDITIONS_DIALOG_TITLE))
.SetContentView(
views::Builder<views::Label>()
.SetText(base::ASCIIToUTF16(
discount_info.terms_and_conditions.value()))
.SetMultiLine(true)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
.Build())
.SetHeaderView(nullptr)
.SetFootnoteView(
views::Builder<views::Label>()
.SetText(footer_message)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
.SetMultiLine(true)
.SetAllowCharacterBreak(true)
.Build())
.Build());
}
void DiscountsBubbleDialogView::CopyButtonClicked() {
commerce::metrics::DiscountsMetricCollector::
RecordDiscountsBubbleCopyButtonClicked(ukm_source_id_);
auto* tab = tabs::TabInterface::MaybeGetFromContents(web_contents());
if (!tab || !tab->GetTabFeatures()) {
return;
}
auto* tab_helper = tab->GetTabFeatures()->commerce_ui_tab_helper();
if (!tab_helper) {
return;
}
tab_helper->OnDiscountsCouponCodeCopied();
}
void DiscountsBubbleDialogView::OnDialogClosing() {
auto* tab = tabs::TabInterface::MaybeGetFromContents(web_contents());
if (!tab || !tab->GetTabFeatures()) {
return;
}
auto* tab_helper = tab->GetTabFeatures()->commerce_ui_tab_helper();
if (!tab_helper) {
return;
}
commerce::metrics::DiscountsMetricCollector::
DiscountsBubbleCopyStatusOnBubbleClosed(
tab_helper->IsDiscountsCouponCodeCopied(),
tab_helper->GetDiscounts());
}
BEGIN_METADATA(DiscountsBubbleDialogView)
END_METADATA
// DiscountsBubbleCoordinator
DiscountsBubbleCoordinator::DiscountsBubbleCoordinator() = default;
DiscountsBubbleCoordinator::~DiscountsBubbleCoordinator() = default;
// WidgetObserver:
void DiscountsBubbleCoordinator::OnWidgetDestroying(views::Widget* widget) {
CHECK(bubble_widget_observation_.IsObservingSource(widget));
bubble_widget_observation_.Reset();
std::move(on_dialog_closing_callback_).Run();
}
void DiscountsBubbleCoordinator::Show(
views::View* anchor_view,
content::WebContents* web_contents,
const commerce::DiscountInfo& discount_info,
base::OnceClosure on_dialog_closing_callback) {
CHECK(!IsShowing());
on_dialog_closing_callback_ = std::move(on_dialog_closing_callback);
auto bubble = std::make_unique<DiscountsBubbleDialogView>(
anchor_view, web_contents, discount_info);
tracker_.SetView(bubble.get());
auto* widget = DiscountsBubbleDialogView::CreateBubble(std::move(bubble));
bubble_widget_observation_.Observe(widget);
widget->Show();
}
void DiscountsBubbleCoordinator::Hide() {
if (IsShowing()) {
tracker_.view()->GetWidget()->Close();
}
tracker_.SetView(nullptr);
}
DiscountsBubbleDialogView* DiscountsBubbleCoordinator::GetBubble() const {
return tracker_.view() ? views::AsViewClass<DiscountsBubbleDialogView>(
const_cast<views::View*>(tracker_.view()))
: nullptr;
}
bool DiscountsBubbleCoordinator::IsShowing() {
return tracker_.view() != nullptr;
}