blob: 064401750eea07066e74f22d2df29c093e548099 [file] [log] [blame]
// 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 "chrome/browser/cart/cart_service.h"
#include "base/json/json_reader.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/cart/cart_discount_metric_collector.h"
#include "chrome/browser/cart/chrome_cart.mojom.h"
#include "chrome/browser/commerce/coupons/coupon_service_factory.h"
#include "chrome/browser/commerce/shopping_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/commerce/commerce_prompt.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/autofill/core/browser/data_model/autofill_offer_data.h"
#include "components/commerce/core/commerce_feature_list.h"
#include "components/commerce/core/commerce_heuristics_data.h"
#include "components/commerce/core/commerce_heuristics_data_metrics_helper.h"
#include "components/commerce/core/proto/cart_db_content.pb.h"
#include "components/commerce/core/proto/price_tracking.pb.h"
#include "components/commerce/core/shopping_service.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/search/ntp_features.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
namespace {
constexpr char kFakeDataPrefix[] = "Fake:";
constexpr char kUTMSourceTag[] = "chrome";
constexpr char kUTMMediumTag[] = "app";
constexpr char kUTMCampaignChromeCartTag[] = "chrome-cart";
constexpr char kUTMCampaignDiscountTag[] = "chrome-cart-discount-on";
constexpr char kUTMCampaignNoDiscountTag[] = "chrome-cart-discount-off";
constexpr char kCartPrefsKey[] = "chrome_cart";
constexpr base::FeatureParam<std::string> kSkipCartExtractionPattern{
&ntp_features::kNtpChromeCartModule, "skip-cart-extraction-pattern",
// This regex does not match anything.
"\\b\\B"};
constexpr base::FeatureParam<bool> kRbdUtmParam{
&ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleAbandonedCartDiscountUseUtmParam, true};
constexpr base::FeatureParam<bool> kBypassDisocuntFetchingThreshold{
&commerce::kCommerceDeveloper, "bypass-discount-fetching-threshold", false};
std::string eTLDPlusOne(const GURL& url) {
return net::registry_controlled_domains::GetDomainAndRegistry(
url, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
}
std::string GetKeyForURL(const GURL& url) {
std::string domain = eTLDPlusOne(url);
return commerce::IsFakeDataEnabled() ? std::string(kFakeDataPrefix) + domain
: domain;
}
bool CompareTimeStampForProtoPair(const CartDB::KeyAndValue pair1,
CartDB::KeyAndValue pair2) {
return pair1.second.timestamp() > pair2.second.timestamp();
}
absl::optional<base::Value> JSONToDictionary(int resource_id) {
absl::optional<base::Value> value = base::JSONReader::Read(
ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
resource_id));
DCHECK(value && value.has_value() && value->is_dict());
return value;
}
const re2::RE2& GetSkipCartExtractionPattern() {
re2::RE2::Options options;
options.set_case_sensitive(false);
static base::NoDestructor<re2::RE2> instance(kSkipCartExtractionPattern.Get(),
options);
return *instance;
}
// Check if any product in existing_proto is no longer in new_proto.
bool ProductsRemoved(cart_db::ChromeCartContentProto existing_proto,
cart_db::ChromeCartContentProto new_proto) {
if (existing_proto.product_infos_size() == 0)
return false;
if (existing_proto.product_infos_size() > new_proto.product_infos_size())
return true;
std::set<std::string> new_proto_product_ids;
for (auto new_product : new_proto.product_infos()) {
new_proto_product_ids.insert(new_product.product_id());
}
for (auto existing_product : existing_proto.product_infos()) {
bool product_remains =
new_proto_product_ids.find(existing_product.product_id()) !=
new_proto_product_ids.end();
if (!product_remains)
return true;
}
return false;
}
// Check if products in existing_proto are the same as the new_proto.
bool HaveSameProducts(cart_db::ChromeCartContentProto existing_proto,
cart_db::ChromeCartContentProto new_proto) {
if (existing_proto.product_image_urls_size() !=
new_proto.product_image_urls_size()) {
return false;
}
for (int i = 0; i < existing_proto.product_image_urls_size(); i++) {
if (existing_proto.product_image_urls()[i] !=
new_proto.product_image_urls()[i]) {
return false;
}
}
return true;
}
} // namespace
CartService::CartService(Profile* profile)
: profile_(profile),
cart_db_(std::make_unique<CartDB>(profile_)),
domain_name_mapping_(JSONToDictionary(IDR_CART_DOMAIN_NAME_MAPPING_JSON)),
domain_cart_url_mapping_(
JSONToDictionary(IDR_CART_DOMAIN_CART_URL_MAPPING_JSON)),
discount_link_fetcher_(std::make_unique<CartDiscountLinkFetcher>()),
coupon_service_(CouponServiceFactory::GetForProfile(profile)),
shopping_service_(
commerce::ShoppingServiceFactory::GetForBrowserContext(profile)) {
history_service_observation_.Observe(HistoryServiceFactory::GetForProfile(
profile_, ServiceAccessType::EXPLICIT_ACCESS));
coupon_service_->MaybeFeatureStatusChanged(IsCartAndDiscountEnabled());
if (commerce::IsFakeDataEnabled()) {
AddCartsWithFakeData();
} else {
// In case last deconstruction is interrupted and fake data is not deleted.
DeleteCartsWithFakeData();
}
if (IsCartDiscountEnabled()) {
StartGettingDiscount();
}
optimization_guide_decider_ =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
if (optimization_guide_decider_) {
optimization_guide_decider_->RegisterOptimizationTypes(
{optimization_guide::proto::SHOPPING_PAGE_PREDICTOR});
}
pref_change_registrar_.Init(profile->GetPrefs());
auto callback = base::BindRepeating(&CartService::OnCartFeaturesChanged,
weak_ptr_factory_.GetWeakPtr());
pref_change_registrar_.Add(prefs::kNtpDisabledModules, callback);
pref_change_registrar_.Add(prefs::kCartDiscountEnabled, callback);
pref_change_registrar_.Add(prefs::kNtpModulesVisible, callback);
}
CartService::~CartService() = default;
void CartService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kCartModuleHidden, false);
registry->RegisterIntegerPref(prefs::kCartModuleWelcomeSurfaceShownTimes, 0);
registry->RegisterBooleanPref(prefs::kCartDiscountAcknowledged, false);
registry->RegisterBooleanPref(prefs::kCartDiscountEnabled, false);
registry->RegisterDictionaryPref(prefs::kCartUsedDiscounts);
registry->RegisterTimePref(prefs::kCartDiscountLastFetchedTime, base::Time());
registry->RegisterBooleanPref(prefs::kCartDiscountConsentShown, false);
registry->RegisterTimePref(prefs::kDiscountConsentLastDimissedTime,
base::Time());
registry->RegisterIntegerPref(prefs::kDiscountConsentPastDismissedCount, 0);
registry->RegisterIntegerPref(prefs::kDiscountConsentDecisionMadeIn, 0);
registry->RegisterIntegerPref(prefs::kDiscountConsentDismissedIn, 0);
registry->RegisterIntegerPref(prefs::kDiscountConsentLastShownInVariation, 0);
registry->RegisterBooleanPref(prefs::kDiscountConsentShowInterest, false);
registry->RegisterIntegerPref(prefs::kDiscountConsentShowInterestIn, 0);
}
void CartService::Hide() {
profile_->GetPrefs()->SetBoolean(prefs::kCartModuleHidden, true);
}
void CartService::RestoreHidden() {
profile_->GetPrefs()->SetBoolean(prefs::kCartModuleHidden, false);
}
bool CartService::IsHidden() {
return !base::FeatureList::IsEnabled(ntp_features::kNtpModulesRedesigned) &&
profile_->GetPrefs()->GetBoolean(prefs::kCartModuleHidden);
}
void CartService::LoadCart(const std::string& domain,
CartDB::LoadCallback callback) {
cart_db_->LoadCart(domain, std::move(callback));
}
void CartService::LoadAllActiveCarts(CartDB::LoadCallback callback) {
cart_db_->LoadAllCarts(base::BindOnce(&CartService::OnLoadCarts,
weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void CartService::AddCart(const GURL& navigation_url,
const absl::optional<GURL>& cart_url,
const cart_db::ChromeCartContentProto& proto) {
cart_db_->LoadCart(
eTLDPlusOne(navigation_url),
base::BindOnce(&CartService::OnAddCart, weak_ptr_factory_.GetWeakPtr(),
navigation_url, cart_url, proto));
}
void CartService::DeleteCart(const GURL& url, bool ignore_remove_status) {
if (ignore_remove_status) {
coupon_service_->DeleteFreeListingCouponsForUrl(url);
cart_db_->DeleteCart(eTLDPlusOne(url),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
return;
}
cart_db_->LoadCart(eTLDPlusOne(url),
base::BindOnce(&CartService::OnDeleteCart,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::HideCart(const GURL& cart_url,
CartDB::OperationCallback callback) {
cart_db_->LoadCart(GetKeyForURL(cart_url),
base::BindOnce(&CartService::SetCartHiddenStatus,
weak_ptr_factory_.GetWeakPtr(), true,
std::move(callback)));
}
void CartService::RestoreHiddenCart(const GURL& cart_url,
CartDB::OperationCallback callback) {
cart_db_->LoadCart(GetKeyForURL(cart_url),
base::BindOnce(&CartService::SetCartHiddenStatus,
weak_ptr_factory_.GetWeakPtr(), false,
std::move(callback)));
}
void CartService::RemoveCart(const GURL& cart_url,
CartDB::OperationCallback callback) {
cart_db_->LoadCart(GetKeyForURL(cart_url),
base::BindOnce(&CartService::SetCartRemovedStatus,
weak_ptr_factory_.GetWeakPtr(), true,
std::move(callback)));
}
void CartService::RestoreRemovedCart(const GURL& cart_url,
CartDB::OperationCallback callback) {
cart_db_->LoadCart(GetKeyForURL(cart_url),
base::BindOnce(&CartService::SetCartRemovedStatus,
weak_ptr_factory_.GetWeakPtr(), false,
std::move(callback)));
}
void CartService::IncreaseWelcomeSurfaceCounter() {
if (!ShouldShowWelcomeSurface())
return;
int times = profile_->GetPrefs()->GetInteger(
prefs::kCartModuleWelcomeSurfaceShownTimes);
profile_->GetPrefs()->SetInteger(prefs::kCartModuleWelcomeSurfaceShownTimes,
times + 1);
}
bool CartService::ShouldShowWelcomeSurface() {
return profile_->GetPrefs()->GetInteger(
prefs::kCartModuleWelcomeSurfaceShownTimes) <
kWelcomSurfaceShowLimit;
}
void CartService::AcknowledgeDiscountConsent(bool should_enable) {
if (commerce::IsFakeDataEnabled()) {
return;
}
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountAcknowledged, true);
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, should_enable);
profile_->GetPrefs()->SetInteger(
prefs::kDiscountConsentDecisionMadeIn,
commerce::kNtpChromeCartModuleDiscountConsentNtpVariation.Get());
if (should_enable && commerce::IsCartDiscountFeatureEnabled()) {
StartGettingDiscount();
}
}
void CartService::DismissedDiscountConsent() {
if (commerce::IsFakeDataEnabled()) {
return;
}
profile_->GetPrefs()->SetTime(prefs::kDiscountConsentLastDimissedTime,
base::Time::Now());
int past_dimissed_count = profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentPastDismissedCount);
profile_->GetPrefs()->SetInteger(prefs::kDiscountConsentPastDismissedCount,
past_dimissed_count + 1);
profile_->GetPrefs()->SetInteger(
prefs::kDiscountConsentDismissedIn,
commerce::kNtpChromeCartModuleDiscountConsentNtpVariation.Get());
}
void CartService::InterestedInDiscountConsent() {
if (commerce::IsFakeDataEnabled()) {
return;
}
profile_->GetPrefs()->SetBoolean(prefs::kDiscountConsentShowInterest, true);
profile_->GetPrefs()->SetInteger(
prefs::kDiscountConsentShowInterestIn,
commerce::kNtpChromeCartModuleDiscountConsentNtpVariation.Get());
}
const GURL CartService::AppendUTM(const GURL& base_url) {
DCHECK(base_url.is_valid());
if (!kRbdUtmParam.Get())
return base_url;
auto url = base_url;
url = net::AppendOrReplaceQueryParameter(url, "utm_source", kUTMSourceTag);
url = net::AppendOrReplaceQueryParameter(url, "utm_medium", kUTMMediumTag);
if (commerce::IsPartnerMerchant(base_url)) {
return net::AppendOrReplaceQueryParameter(url, "utm_campaign",
IsCartDiscountEnabled()
? kUTMCampaignDiscountTag
: kUTMCampaignNoDiscountTag);
}
return net::AppendOrReplaceQueryParameter(url, "utm_campaign",
kUTMCampaignChromeCartTag);
}
void CartService::HasActiveCartForURL(const GURL& url,
base::OnceCallback<void(bool)> callback) {
LoadCart(eTLDPlusOne(url),
base::BindOnce(&CartService::HasActiveCartForURLCallback,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
bool CartService::IsCartEnabled() {
const base::Value::List& list =
profile_->GetPrefs()->GetList(prefs::kNtpDisabledModules);
return !base::Contains(list, base::Value(kCartPrefsKey));
}
void CartService::ShouldShowDiscountConsent(
base::OnceCallback<void(bool)> callback) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (commerce::IsFakeDataEnabled()) {
content::GetUIThreadTaskRunner({base::TaskPriority::USER_BLOCKING})
->PostTask(FROM_HERE, base::BindOnce(
[](base::OnceCallback<void(bool)> callback) {
std::move(callback).Run(true);
},
std::move(callback)));
return;
}
LoadAllActiveCarts(
base::BindOnce(&CartService::ShouldShowDiscountConsentCallback,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void CartService::RecordDiscountConsentStatusAtLoad(bool should_show_consent) {
if (profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountAcknowledged)) {
CartDiscountMetricCollector::RecordDiscountConsentStatus(
profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled)
? CartDiscountMetricCollector::DiscountConsentStatus::ACCEPTED
: CartDiscountMetricCollector::DiscountConsentStatus::DECLINED);
commerce::DiscountConsentNtpVariation decision_made_in_variation =
static_cast<commerce::DiscountConsentNtpVariation>(
profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentDecisionMadeIn));
if (profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled)) {
CartDiscountMetricCollector::RecordDiscountConsentStatusAcceptedIn(
decision_made_in_variation);
} else {
CartDiscountMetricCollector::RecordDiscountConsentStatusRejectedIn(
decision_made_in_variation);
}
CartDiscountMetricCollector::
RecordDiscountConsentStatusNoShowAfterDecidedIn(
decision_made_in_variation);
} else {
if (profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentPastDismissedCount) > 0) {
CartDiscountMetricCollector::RecordDiscountConsentStatusDismissedIn(
static_cast<commerce::DiscountConsentNtpVariation>(
profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentDismissedIn)));
}
if (profile_->GetPrefs()->GetBoolean(prefs::kDiscountConsentShowInterest)) {
CartDiscountMetricCollector::RecordDiscountConsentStatusShowInterestIn(
static_cast<commerce::DiscountConsentNtpVariation>(
profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentShowInterestIn)));
}
commerce::DiscountConsentNtpVariation last_shown_in_variation =
static_cast<commerce::DiscountConsentNtpVariation>(
profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentLastShownInVariation));
commerce::DiscountConsentNtpVariation current_variation =
static_cast<commerce::DiscountConsentNtpVariation>(
commerce::kNtpChromeCartModuleDiscountConsentNtpVariation.Get());
if (!should_show_consent) {
CartDiscountMetricCollector::RecordDiscountConsentStatus(
profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountConsentShown)
? CartDiscountMetricCollector::DiscountConsentStatus::NO_SHOW
: CartDiscountMetricCollector::DiscountConsentStatus::
NEVER_SHOWN);
if (current_variation != last_shown_in_variation) {
CartDiscountMetricCollector::RecordDiscountConsentStatusNeverShowIn(
current_variation);
} else if (profile_->GetPrefs()->GetBoolean(
prefs::kCartDiscountConsentShown)) {
CartDiscountMetricCollector::RecordDiscountConsentStatusNoShowIn(
current_variation);
}
} else {
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountConsentShown, true);
CartDiscountMetricCollector::RecordDiscountConsentStatus(
CartDiscountMetricCollector::DiscountConsentStatus::IGNORED);
CartDiscountMetricCollector::RecordDiscountConsentStatusIgnoredIn(
last_shown_in_variation);
CartDiscountMetricCollector::RecordDiscountConsentStatusShownIn(
current_variation);
profile_->GetPrefs()->SetInteger(
prefs::kDiscountConsentLastShownInVariation,
static_cast<int>(current_variation));
}
}
}
bool CartService::IsCartExpired(const cart_db::ChromeCartContentProto& proto) {
return (base::Time::Now() - base::Time::FromDoubleT(proto.timestamp()))
.InDays() > kCartExpirationTimeInDays;
}
void CartService::HasActiveCartForURLCallback(
base::OnceCallback<void(bool)> callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
// Check if there is a cart for the corresponding domain and
// if the cart has not expired.
if (!success || proto_pairs.size() == 0) {
std::move(callback).Run(false);
return;
}
DCHECK(proto_pairs.size() == 1);
std::move(callback).Run(!IsCartExpired(proto_pairs[0].second));
}
void CartService::MaybeCommitDeletion(GURL url) {
std::string domain = eTLDPlusOne(url);
if (pending_deletion_map_.contains(domain)) {
coupon_service_->DeleteFreeListingCouponsForUrl(url);
pending_deletion_map_.erase(domain);
}
}
void CartService::ShouldShowDiscountConsentCallback(
base::OnceCallback<void(bool)> callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (proto_pairs.size() == 0 || ShouldShowWelcomeSurface() ||
!commerce::IsCartDiscountFeatureEnabled()) {
std::move(callback).Run(false);
return;
}
bool should_show = false;
if (!profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountAcknowledged)) {
// Only show discount consent when there is abandoned cart(s) from partner
// merchants or it's not in the no discount merchant list.
for (auto proto_pair : proto_pairs) {
auto cart_url = proto_pair.second.merchant_cart_url();
should_show |= commerce::IsPartnerMerchant(GURL(cart_url));
should_show |=
(base::FeatureList::IsEnabled(commerce::kMerchantWidePromotion) &&
!commerce::IsNoDiscountMerchant(GURL(cart_url)));
}
if (base::FeatureList::IsEnabled(commerce::kDiscountConsentV2)) {
base::Time last_dismissed_time = profile_->GetPrefs()->GetTime(
prefs::kDiscountConsentLastDimissedTime);
base::TimeDelta reshow_time_delta =
commerce::kNtpChromeCartModuleDiscountConsentReshowTime.Get() -
(base::Time::Now() - last_dismissed_time);
int last_dismissed_count = profile_->GetPrefs()->GetInteger(
prefs::kDiscountConsentPastDismissedCount);
should_show &=
(last_dismissed_time == base::Time() ||
reshow_time_delta.is_negative()) &&
last_dismissed_count <=
commerce::kNtpChromeCartModuleDiscountConsentMaxDismissalCount
.Get();
}
}
RecordDiscountConsentStatusAtLoad(should_show);
std::move(callback).Run(should_show);
}
bool CartService::ShouldShowDiscountToggle() {
return profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountAcknowledged);
}
bool CartService::IsCartDiscountEnabled() {
if (!commerce::IsCartDiscountFeatureEnabled()) {
return false;
}
return profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled);
}
void CartService::SetCartDiscountEnabled(bool enabled) {
DCHECK(commerce::IsCartDiscountFeatureEnabled());
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, enabled);
if (enabled) {
StartGettingDiscount();
} else {
// TODO(crbug.com/1207197): Use sequence checker instead.
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
fetch_discount_worker_.reset();
}
}
void CartService::GetDiscountURL(
const GURL& cart_url,
base::OnceCallback<void(const ::GURL&)> callback) {
auto url = AppendUTM(cart_url);
if (commerce::IsFakeDataEnabled() || !IsCartDiscountEnabled()) {
std::move(callback).Run(url);
if (!commerce::IsFakeDataEnabled()) {
CartDiscountMetricCollector::RecordClickedOnDiscount(false);
}
return;
}
LoadCart(eTLDPlusOne(cart_url), base::BindOnce(&CartService::OnGetDiscountURL,
weak_ptr_factory_.GetWeakPtr(),
url, std::move(callback)));
}
void CartService::OnGetDiscountURL(
const GURL& default_cart_url,
base::OnceCallback<void(const ::GURL&)> callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
DCHECK_EQ(proto_pairs.size(), 1U);
if (proto_pairs.size() != 1U) {
std::move(callback).Run(default_cart_url);
CartDiscountMetricCollector::RecordClickedOnDiscount(false);
return;
}
auto& cart_proto = proto_pairs[0].second;
if (!IsCartDiscountEnabled() ||
cart_proto.discount_info().rule_discount_info().empty()) {
if (cart_proto.discount_info().has_coupons()) {
CartDiscountMetricCollector::RecordClickedOnDiscount(true);
} else {
CartDiscountMetricCollector::RecordClickedOnDiscount(false);
}
std::move(callback).Run(default_cart_url);
return;
}
auto pending_factory = profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess()
->Clone();
discount_link_fetcher_->Fetch(
std::move(pending_factory), cart_proto,
base::BindOnce(&CartService::OnDiscountURLFetched,
weak_ptr_factory_.GetWeakPtr(), default_cart_url,
std::move(callback), cart_proto));
CartDiscountMetricCollector::RecordClickedOnDiscount(true);
}
void CartService::OnDiscountURLFetched(
const GURL& default_cart_url,
base::OnceCallback<void(const ::GURL&)> callback,
const cart_db::ChromeCartContentProto& cart_proto,
const GURL& discount_url) {
std::move(callback).Run(discount_url.is_valid() ? AppendUTM(discount_url)
: default_cart_url);
if (discount_url.is_valid()) {
CacheUsedDiscounts(cart_proto);
CleanUpDiscounts(cart_proto);
CartDiscountMetricCollector::RecordAppliedDiscount();
}
}
void CartService::PrepareForNavigation(const GURL& cart_url,
bool is_navigating) {
if (!metrics_tracker_) {
metrics_tracker_ = std::make_unique<CartMetricsTracker>(
chrome::FindTabbedBrowser(profile_, false));
}
metrics_tracker_->PrepareToRecordUKM(cart_url);
if (is_navigating || !commerce::IsRuleDiscountPartnerMerchant(cart_url) ||
!IsCartDiscountEnabled()) {
return;
}
if (!discount_url_loader_) {
discount_url_loader_ = std::make_unique<DiscountURLLoader>(
chrome::FindTabbedBrowser(profile_, false), profile_);
}
discount_url_loader_->PrepareURLForDiscountLoad(cart_url);
}
void CartService::LoadCartsWithFakeData(CartDB::LoadCallback callback) {
cart_db_->LoadCartsWithPrefix(
kFakeDataPrefix,
base::BindOnce(&CartService::OnLoadCarts, weak_ptr_factory_.GetWeakPtr(),
std::move(callback)));
}
void CartService::OnOperationFinished(bool success) {
DCHECK(success) << "database operation failed.";
}
void CartService::OnOperationFinishedWithCallback(
CartDB::OperationCallback callback,
bool success) {
DCHECK(success) << "database operation failed.";
std::move(callback).Run(success);
}
void CartService::Shutdown() {
history_service_observation_.Reset();
if (commerce::IsFakeDataEnabled()) {
DeleteCartsWithFakeData();
}
// Delete content of all carts that are removed.
cart_db_->LoadAllCarts(base::BindOnce(&CartService::DeleteRemovedCartsContent,
weak_ptr_factory_.GetWeakPtr()));
if (metrics_tracker_) {
metrics_tracker_->ShutDown();
}
if (discount_url_loader_) {
discount_url_loader_->ShutDown();
}
}
void CartService::OnURLsDeleted(history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) {
// TODO(crbug.com/1157892): Add more fine-grained deletion of cart data when
// history deletion happens.
cart_db_->DeleteAllCarts(base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
coupon_service_->DeleteAllFreeListingCoupons();
}
CartDB* CartService::GetDB() {
return cart_db_.get();
}
void CartService::AddCartsWithFakeData() {
DeleteCartsWithFakeData();
// Polulate and add some carts with fake data.
double time_now = base::Time::Now().ToDoubleT();
cart_db::ChromeCartContentProto dummy_proto1;
GURL dummy_url1 = GURL("https://www.example.com");
dummy_proto1.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url1));
dummy_proto1.set_merchant("Cart Foo");
dummy_proto1.set_merchant_cart_url(dummy_url1.spec());
dummy_proto1.set_timestamp(time_now + 6);
dummy_proto1.mutable_discount_info()->set_discount_text(
l10n_util::GetStringFUTF8(IDS_NTP_MODULES_CART_DISCOUNT_CHIP_AMOUNT,
u"15%"));
dummy_proto1.mutable_discount_info()->set_has_coupons(true);
dummy_proto1.add_product_image_urls(
"https://encrypted-tbn3.gstatic.com/"
"shopping?q=tbn:ANd9GcQpn38jB2_BANnHUFa7kHJsf6SyubcgeU1lNYO_"
"ZxM1Q2ju_ZMjv2EwNh0Zx_zbqYy_mFg_aiIhWYnD5PQ7t-uFzLM5cN77s_2_"
"DFNeumI-LMPJMYjW-BOSaA&usqp=CAY");
dummy_proto1.add_product_image_urls(
"https://encrypted-tbn0.gstatic.com/"
"shopping?q=tbn:ANd9GcQyMRYWeM2Yq095nOXTL0-"
"EUUnm79kh6hnw8yctJUNrAuse607KEr1CVxEa24r-"
"8XHBuhTwcuC4GXeN94h9Kn19DhdBGsXG0qrD74veYSDJNLrUP-sru0jH&usqp=CAY");
dummy_proto1.add_product_image_urls(
"https://encrypted-tbn1.gstatic.com/"
"shopping?q=tbn:ANd9GcT2ew6Aydzu5VzRV756ORGha6fyjKp_On7iTlr_"
"tL9vODnlNtFo_xsxj6_lCop-3J0Vk44lHfk-AxoBJDABVHPVFN-"
"EiWLcZvzkdpHFqcurm7fBVmWtYKo2rg&usqp=CAY");
cart_db_->AddCart(dummy_proto1.key(), dummy_proto1,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
// Add a fake code coupon associated with this dummy cart. If more fake coupon
// data is added, please also delete them in DeleteCartsWithFakeData.
base::flat_map<GURL,
std::vector<std::unique_ptr<autofill::AutofillOfferData>>>
coupon_map;
int64_t offer_id = 123;
base::Time expiry = base::Time::Now() + base::Days(3);
std::vector<GURL> merchant_origins;
merchant_origins.emplace_back(dummy_url1);
GURL offer_details_url = GURL();
autofill::DisplayStrings display_strings;
display_strings.value_prop_text = "15% off on everything";
std::string promo_code = "15PERCENTOFF";
auto offer = std::make_unique<autofill::AutofillOfferData>(
autofill::AutofillOfferData::FreeListingCouponOffer(
offer_id, expiry, merchant_origins, offer_details_url,
display_strings, promo_code));
coupon_map[dummy_url1].emplace_back(std::move(offer));
coupon_service_->UpdateFreeListingCoupons(coupon_map);
cart_db::ChromeCartContentProto dummy_proto2;
GURL dummy_url2 = GURL("https://www.amazon.com/");
dummy_proto2.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url2));
dummy_proto2.set_merchant("Cart Bar");
dummy_proto2.set_merchant_cart_url(dummy_url2.spec());
dummy_proto2.set_timestamp(time_now + 3);
dummy_proto2.mutable_discount_info()->set_discount_text(
l10n_util::GetStringFUTF8(IDS_NTP_MODULES_CART_DISCOUNT_CHIP_AMOUNT,
u"20%"));
dummy_proto2.add_product_image_urls(
"https://encrypted-tbn3.gstatic.com/"
"shopping?q=tbn:ANd9GcQpn38jB2_BANnHUFa7kHJsf6SyubcgeU1lNYO_"
"ZxM1Q2ju_ZMjv2EwNh0Zx_zbqYy_mFg_aiIhWYnD5PQ7t-uFzLM5cN77s_2_"
"DFNeumI-LMPJMYjW-BOSaA&usqp=CAY");
dummy_proto2.add_product_image_urls(
"https://encrypted-tbn0.gstatic.com/"
"shopping?q=tbn:ANd9GcQyMRYWeM2Yq095nOXTL0-"
"EUUnm79kh6hnw8yctJUNrAuse607KEr1CVxEa24r-"
"8XHBuhTwcuC4GXeN94h9Kn19DhdBGsXG0qrD74veYSDJNLrUP-sru0jH&usqp=CAY");
cart_db_->AddCart(dummy_proto2.key(), dummy_proto2,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
cart_db::ChromeCartContentProto dummy_proto3;
GURL dummy_url3 = GURL("https://www.ebay.com/");
dummy_proto3.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url3));
dummy_proto3.set_merchant("Cart Baz");
dummy_proto3.set_merchant_cart_url(dummy_url3.spec());
dummy_proto3.set_timestamp(time_now + 4);
dummy_proto3.mutable_discount_info()->set_discount_text(
l10n_util::GetStringFUTF8(IDS_NTP_MODULES_CART_DISCOUNT_CHIP_UP_TO_AMOUNT,
u"$50"));
dummy_proto3.add_product_image_urls(
"https://encrypted-tbn3.gstatic.com/"
"shopping?q=tbn:ANd9GcQpn38jB2_BANnHUFa7kHJsf6SyubcgeU1lNYO_"
"ZxM1Q2ju_ZMjv2EwNh0Zx_zbqYy_mFg_aiIhWYnD5PQ7t-uFzLM5cN77s_2_"
"DFNeumI-LMPJMYjW-BOSaA&usqp=CAY");
cart_db_->AddCart(dummy_proto3.key(), dummy_proto3,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
cart_db::ChromeCartContentProto dummy_proto4;
GURL dummy_url4 = GURL("https://www.walmart.com/");
dummy_proto4.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url4));
dummy_proto4.set_merchant("Cart Qux");
dummy_proto4.set_merchant_cart_url(dummy_url4.spec());
dummy_proto4.set_timestamp(time_now + 5);
dummy_proto4.add_product_image_urls(
"https://encrypted-tbn0.gstatic.com/"
"shopping?q=tbn:ANd9GcQyMRYWeM2Yq095nOXTL0-"
"EUUnm79kh6hnw8yctJUNrAuse607KEr1CVxEa24r-"
"8XHBuhTwcuC4GXeN94h9Kn19DhdBGsXG0qrD74veYSDJNLrUP-sru0jH&usqp=CAY");
dummy_proto4.add_product_image_urls(
"https://encrypted-tbn1.gstatic.com/"
"shopping?q=tbn:ANd9GcT2ew6Aydzu5VzRV756ORGha6fyjKp_On7iTlr_"
"tL9vODnlNtFo_xsxj6_lCop-3J0Vk44lHfk-AxoBJDABVHPVFN-"
"EiWLcZvzkdpHFqcurm7fBVmWtYKo2rg&usqp=CAY");
cart_db_->AddCart(dummy_proto4.key(), dummy_proto4,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
cart_db::ChromeCartContentProto dummy_proto5;
GURL dummy_url5 = GURL("https://www.bestbuy.com/");
dummy_proto5.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url5));
dummy_proto5.set_merchant("Cart Corge");
dummy_proto5.set_merchant_cart_url(dummy_url5.spec());
dummy_proto5.set_timestamp(time_now + 2);
dummy_proto5.add_product_image_urls(
"https://encrypted-tbn3.gstatic.com/"
"shopping?q=tbn:ANd9GcQpn38jB2_BANnHUFa7kHJsf6SyubcgeU1lNYO_"
"ZxM1Q2ju_ZMjv2EwNh0Zx_zbqYy_mFg_aiIhWYnD5PQ7t-uFzLM5cN77s_2_"
"DFNeumI-LMPJMYjW-BOSaA&usqp=CAY");
cart_db_->AddCart(dummy_proto5.key(), dummy_proto5,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
cart_db::ChromeCartContentProto dummy_proto6;
GURL dummy_url6 = GURL("https://www.nike.com/");
dummy_proto6.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url6));
dummy_proto6.set_merchant("Cart Flob");
dummy_proto6.set_merchant_cart_url(dummy_url6.spec());
dummy_proto6.set_timestamp(time_now + 1);
dummy_proto6.add_product_image_urls(
"https://encrypted-tbn3.gstatic.com/"
"shopping?q=tbn:ANd9GcQpn38jB2_BANnHUFa7kHJsf6SyubcgeU1lNYO_"
"ZxM1Q2ju_ZMjv2EwNh0Zx_zbqYy_mFg_aiIhWYnD5PQ7t-uFzLM5cN77s_2_"
"DFNeumI-LMPJMYjW-BOSaA&usqp=CAY");
dummy_proto6.add_product_image_urls(
"https://encrypted-tbn0.gstatic.com/"
"shopping?q=tbn:ANd9GcQyMRYWeM2Yq095nOXTL0-"
"EUUnm79kh6hnw8yctJUNrAuse607KEr1CVxEa24r-"
"8XHBuhTwcuC4GXeN94h9Kn19DhdBGsXG0qrD74veYSDJNLrUP-sru0jH&usqp=CAY");
dummy_proto6.add_product_image_urls(
"https://encrypted-tbn1.gstatic.com/"
"shopping?q=tbn:ANd9GcT2ew6Aydzu5VzRV756ORGha6fyjKp_On7iTlr_"
"tL9vODnlNtFo_xsxj6_lCop-3J0Vk44lHfk-AxoBJDABVHPVFN-"
"EiWLcZvzkdpHFqcurm7fBVmWtYKo2rg&usqp=CAY");
cart_db_->AddCart(dummy_proto6.key(), dummy_proto6,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
cart_db::ChromeCartContentProto dummy_proto7;
GURL dummy_url7 = GURL("https://www.bestbuy.com/");
dummy_proto7.set_key(std::string(kFakeDataPrefix) + eTLDPlusOne(dummy_url7));
dummy_proto7.set_merchant("Cart Gob");
dummy_proto7.set_merchant_cart_url(dummy_url7.spec());
dummy_proto7.set_timestamp(time_now + 2);
cart_db_->AddCart(dummy_proto7.key(), dummy_proto7,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::DeleteCartsWithFakeData() {
coupon_service_->DeleteFreeListingCouponsForUrl(
GURL("https://www.example.com"));
cart_db_->DeleteCartsWithPrefix(
kFakeDataPrefix, base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::DeleteRemovedCartsContent(
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
for (CartDB::KeyAndValue proto_pair : proto_pairs) {
if (proto_pair.second.is_removed()) {
// Delete removed cart content by overwriting it with an entry with only
// removed status data.
cart_db::ChromeCartContentProto empty_proto;
empty_proto.set_key(proto_pair.first);
empty_proto.set_is_removed(true);
cart_db_->AddCart(proto_pair.first, empty_proto,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
}
}
bool CartService::ShouldSkip(const GURL& url) {
if (!optimization_guide_decider_) {
return false;
}
optimization_guide::OptimizationMetadata metadata;
auto decision = optimization_guide_decider_->CanApplyOptimization(
url, optimization_guide::proto::SHOPPING_PAGE_PREDICTOR, &metadata);
DVLOG(1) << "SHOPPING_PAGE_PREDICTOR = " << static_cast<int>(decision);
return optimization_guide::OptimizationGuideDecision::kFalse == decision;
}
void CartService::OnLoadCarts(CartDB::LoadCallback callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
DCHECK(success);
if (!success) {
std::move(callback).Run(success, {});
return;
}
if (commerce::IsFakeDataEnabled()) {
std::sort(proto_pairs.begin(), proto_pairs.end(),
CompareTimeStampForProtoPair);
std::move(callback).Run(success, std::move(proto_pairs));
return;
}
if (IsHidden()) {
std::move(callback).Run(success, {});
return;
}
std::set<std::string> merchants_to_erase;
for (CartDB::KeyAndValue kv : proto_pairs) {
const GURL& cart_url(GURL(kv.second.merchant_cart_url()));
if (IsCartExpired(kv.second) || ShouldSkip(cart_url)) {
// Removed carts should remain removed.
if (!kv.second.is_removed()) {
DeleteCart(cart_url, true);
}
merchants_to_erase.emplace(kv.second.key());
}
}
proto_pairs.erase(
std::remove_if(proto_pairs.begin(), proto_pairs.end(),
[merchants_to_erase](CartDB::KeyAndValue kv) {
return kv.second.is_hidden() || kv.second.is_removed() ||
merchants_to_erase.find(kv.second.key()) !=
merchants_to_erase.end();
}),
proto_pairs.end());
for (auto proto_pair : proto_pairs) {
if (RE2::FullMatch(re2::StringPiece(proto_pair.first),
GetSkipCartExtractionPattern())) {
proto_pair.second.clear_product_image_urls();
cart_db_->AddCart(proto_pair.first, proto_pair.second,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
}
// Sort items in timestamp descending order.
std::sort(proto_pairs.begin(), proto_pairs.end(),
CompareTimeStampForProtoPair);
std::move(callback).Run(success, std::move(proto_pairs));
}
void CartService::SetCartHiddenStatus(
bool isHidden,
CartDB::OperationCallback callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
DCHECK(success);
DCHECK_EQ(1U, proto_pairs.size());
if (!success || proto_pairs.size() != 1) {
return;
}
CartDB::KeyAndValue proto_pair = proto_pairs[0];
proto_pair.second.set_is_hidden(isHidden);
cart_db_->AddCart(
proto_pair.first, proto_pair.second,
base::BindOnce(&CartService::OnOperationFinishedWithCallback,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void CartService::SetCartRemovedStatus(
bool isRemoved,
CartDB::OperationCallback callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
DCHECK(success);
DCHECK_EQ(1U, proto_pairs.size());
if (!success || proto_pairs.size() != 1) {
return;
}
CartDB::KeyAndValue proto_pair = proto_pairs[0];
proto_pair.second.set_is_removed(isRemoved);
cart_db_->AddCart(
proto_pair.first, proto_pair.second,
base::BindOnce(&CartService::OnOperationFinishedWithCallback,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void CartService::OnAddCart(const GURL& navigation_url,
const absl::optional<GURL>& cart_url,
cart_db::ChromeCartContentProto proto,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (!success) {
return;
}
std::string domain = eTLDPlusOne(navigation_url);
// Restore module visibility anytime a cart-related action happens.
RestoreHidden();
// Cancel pending closure if the cart being closed has the same content as the
// cart being added.
if (pending_deletion_map_.contains(domain) &&
pending_deletion_map_[domain].merchant_cart_url() ==
proto.merchant_cart_url() &&
HaveSameProducts(pending_deletion_map_[domain], proto)) {
cart_db_->AddCart(domain, pending_deletion_map_[domain],
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
pending_deletion_map_.erase(domain);
return;
}
absl::optional<std::string> merchant_name_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetMerchantName(domain);
std::string* merchant_name_from_resource =
domain_name_mapping_->FindStringKey(domain);
if (merchant_name_from_component.has_value()) {
proto.set_merchant(*merchant_name_from_component);
CommerceHeuristicsDataMetricsHelper::RecordMerchantNameSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_COMPONENT);
} else if (merchant_name_from_resource) {
proto.set_merchant(*merchant_name_from_resource);
CommerceHeuristicsDataMetricsHelper::RecordMerchantNameSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::FROM_RESOURCE);
} else {
CommerceHeuristicsDataMetricsHelper::RecordMerchantNameSource(
CommerceHeuristicsDataMetricsHelper::HeuristicsSource::MISSING);
}
if (cart_url) {
proto.set_merchant_cart_url(cart_url->spec());
} else {
absl::optional<std::string> fallback_url_from_component =
commerce_heuristics::CommerceHeuristicsData::GetInstance()
.GetMerchantCartURL(domain);
std::string* fallback_url_from_resource =
domain_cart_url_mapping_->FindStringKey(domain);
if (fallback_url_from_component.has_value()) {
proto.set_merchant_cart_url(*fallback_url_from_component);
} else if (fallback_url_from_resource) {
proto.set_merchant_cart_url(*fallback_url_from_resource);
}
}
// Skip extracting the block list.
if (RE2::FullMatch(re2::StringPiece(domain),
GetSkipCartExtractionPattern())) {
proto.clear_product_image_urls();
proto.clear_product_infos();
cart_db_->AddCart(domain, std::move(proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
return;
}
bool has_product_image = proto.product_image_urls().size();
absl::optional<GURL> cached_image_url;
// When this cart addition is caused by AddToCart detection and there
// is no product image detected on the renderer side, try to get cached
// product image from ShoppingService using navigation_url which could be PDP
// URL.
if (!has_product_image && commerce::kAddToCartProductImage.Get()) {
absl::optional<commerce::ProductInfo> info =
shopping_service_->GetAvailableProductInfoForUrl(navigation_url);
if (info.has_value() && info.value().image_url.is_valid()) {
cached_image_url = info.value().image_url;
}
}
if (proto_pairs.size() == 0) {
if (cached_image_url.has_value()) {
proto.add_product_image_urls(cached_image_url.value().spec());
}
cart_db_->AddCart(domain, std::move(proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
return;
}
DCHECK_EQ(1U, proto_pairs.size());
cart_db::ChromeCartContentProto existing_proto = proto_pairs[0].second;
// Do nothing for carts that has been explicitly removed.
if (existing_proto.is_removed()) {
return;
}
// If the new proto has product images, we can copy the product images to the
// existing proto without worrying about overwriting as it reflects the latest
// state.
if (has_product_image) {
*(existing_proto.mutable_product_image_urls()) =
std::move(proto.product_image_urls());
}
existing_proto.set_is_hidden(false);
existing_proto.set_timestamp(proto.timestamp());
if (cart_url) {
existing_proto.set_merchant_cart_url(cart_url->spec());
}
// If some products in the existing cart are no longer in the new cart, remove
// the corresponding coupons.
if (has_product_image && ProductsRemoved(existing_proto, proto)) {
coupon_service_->DeleteFreeListingCouponsForUrl(
GURL(existing_proto.merchant_cart_url()));
}
if (proto.product_infos().size()) {
// If no product images, this addition comes from AddToCart detection and
// should have only one product (if any). Add this product to the existing
// cart if not included already.
if (!has_product_image) {
DCHECK_EQ(1, proto.product_infos().size());
if (proto.product_infos().size() == 1) {
auto new_product_info = std::move(proto.product_infos().at(0));
bool is_included = false;
for (auto product_proto : existing_proto.product_infos()) {
is_included |=
(product_proto.product_id() == new_product_info.product_id());
if (is_included)
break;
}
if (!is_included) {
auto* added_product = existing_proto.add_product_infos();
*added_product = std::move(new_product_info);
}
}
} else {
*(existing_proto.mutable_product_infos()) =
std::move(proto.product_infos());
}
}
// Add the cached product image from ShoppingService to the existing proto if
// it's not already included.
if (!has_product_image && cached_image_url.has_value()) {
std::string url_string = cached_image_url.value().spec();
auto existing_images = existing_proto.product_image_urls();
if (std::find(existing_images.begin(), existing_images.end(), url_string) ==
existing_images.end()) {
existing_proto.add_product_image_urls(url_string);
}
}
cart_db_->AddCart(domain, std::move(existing_proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::UpdateDiscounts(const GURL& cart_url,
cart_db::ChromeCartContentProto new_proto,
const bool is_tester) {
if (!cart_url.is_valid()) {
VLOG(1) << __func__
<< "update discounts with invalid cart_url: " << cart_url;
return;
}
// Filter used codeless Rule-based Discounts.
if (new_proto.has_discount_info() &&
!new_proto.discount_info().rule_discount_info().empty()) {
std::vector<cart_db::RuleDiscountInfoProto> rule_discount_info_protos;
for (const cart_db::RuleDiscountInfoProto& rule_discount :
new_proto.discount_info().rule_discount_info()) {
if (is_tester || !IsDiscountUsed(rule_discount.rule_id())) {
rule_discount_info_protos.emplace_back(rule_discount);
}
}
if (rule_discount_info_protos.empty()) {
new_proto.clear_discount_info();
} else {
*new_proto.mutable_discount_info()->mutable_rule_discount_info() = {
rule_discount_info_protos.begin(), rule_discount_info_protos.end()};
}
}
std::string domain = eTLDPlusOne(cart_url);
cart_db_->AddCart(domain, std::move(new_proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::StartGettingDiscount() {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
DCHECK(IsCartDiscountEnabled())
<< "Should be called only if the discount feature is enabled.";
DCHECK(!fetch_discount_worker_)
<< "fetch_discount_worker_ should not be valid at this point.";
base::Time last_fetched_time =
profile_->GetPrefs()->GetTime(prefs::kCartDiscountLastFetchedTime);
base::TimeDelta fetch_delay = commerce::GetDiscountFetchDelay() -
(base::Time::Now() - last_fetched_time);
if (last_fetched_time == base::Time() || fetch_delay.is_negative() ||
kBypassDisocuntFetchingThreshold.Get()) {
fetch_delay = base::TimeDelta();
}
if (fetch_discount_worker_for_testing_) {
fetch_discount_worker_for_testing_->Start(fetch_delay);
return;
}
fetch_discount_worker_ = std::make_unique<FetchDiscountWorker>(
profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess(),
std::make_unique<CartDiscountFetcherFactory>(),
std::make_unique<CartDiscountServiceDelegate>(this),
IdentityManagerFactory::GetForProfile(profile_),
profile_->GetVariationsClient());
fetch_discount_worker_->Start(fetch_delay);
}
bool CartService::IsDiscountUsed(const std::string& rule_id) {
return profile_->GetPrefs()
->GetDict(prefs::kCartUsedDiscounts)
.FindBool(rule_id) != absl::nullopt;
}
void CartService::RecordFetchTimestamp() {
profile_->GetPrefs()->SetTime(prefs::kCartDiscountLastFetchedTime,
base::Time::Now());
}
void CartService::UpdateFreeListingCoupons(
const CouponService::CouponsMap& map) {
coupon_service_->UpdateFreeListingCoupons(map);
}
void CartService::CacheUsedDiscounts(
const cart_db::ChromeCartContentProto& proto) {
if (!proto.has_discount_info() ||
proto.discount_info().rule_discount_info().empty()) {
VLOG(1) << "Empty rule based discounts, cache nothing";
return;
}
ScopedDictPrefUpdate update(profile_->GetPrefs(), prefs::kCartUsedDiscounts);
for (auto rule_discount_info : proto.discount_info().rule_discount_info()) {
update->Set(rule_discount_info.rule_id(), true);
}
}
void CartService::CleanUpDiscounts(cart_db::ChromeCartContentProto proto) {
if (proto.merchant_cart_url().empty()) {
NOTREACHED() << "proto does not have merchant_cart_url";
return;
}
if (!proto.has_discount_info()) {
NOTREACHED() << "proto does not have discount_info";
return;
}
// Clean up the rule-based discounts.
if (!proto.discount_info().rule_discount_info().empty()) {
proto.clear_discount_info();
}
cart_db_->AddCart(eTLDPlusOne(GURL(proto.merchant_cart_url())), proto,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::OnDeleteCart(bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (proto_pairs.size() != 1 || proto_pairs[0].second.is_removed())
return;
// Postpone cart deletion commit to avoid ephemeral cart deletions.
content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CartService::MaybeCommitDeletion,
weak_ptr_factory_.GetWeakPtr(),
GURL(proto_pairs[0].second.merchant_cart_url())),
commerce::kCodeBasedRuleDiscountCouponDeletionTime.Get());
pending_deletion_map_[proto_pairs[0].first] = proto_pairs[0].second;
cart_db_->DeleteCart(proto_pairs[0].first,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::OnCartFeaturesChanged(const std::string& pref_name) {
coupon_service_->MaybeFeatureStatusChanged(IsCartAndDiscountEnabled());
}
bool CartService::IsCartAndDiscountEnabled() {
const base::Value::List& list =
profile_->GetPrefs()->GetList(prefs::kNtpDisabledModules);
if (base::Contains(list, base::Value(kCartPrefsKey))) {
return false;
}
return profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled) &&
profile_->GetPrefs()->GetBoolean(prefs::kNtpModulesVisible);
}
void CartService::SetCartDiscountLinkFetcherForTesting(
std::unique_ptr<CartDiscountLinkFetcher> discount_link_fetcher) {
discount_link_fetcher_ = std::move(discount_link_fetcher);
}
void CartService::SetFetchDiscountWorkerForTesting(
std::unique_ptr<FetchDiscountWorker> fetch_discount_worker) {
fetch_discount_worker_for_testing_ = std::move(fetch_discount_worker);
}
void CartService::SetCouponServiceForTesting(CouponService* coupon_service) {
coupon_service_ = coupon_service;
}
void CartService::ShowNativeConsentDialog(
Browser* browser,
base::OnceCallback<void(chrome_cart::mojom::ConsentStatus)>
consent_status_callback) {
commerce::ShowDiscountConsentPrompt(browser,
std::move(consent_status_callback));
}