blob: d94cad093abe099a02c1af728e71f7a3a96abb3f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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/strings/utf_string_conversions.h"
#include "chrome/browser/cart/cart_db_content.pb.h"
#include "chrome/browser/cart/fetch_discount_worker.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/search/ntp_features.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 "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
namespace {
constexpr char kFakeDataPrefix[] = "Fake:";
const int kDelayStartMs = 10;
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 base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleDataParam) == "fake"
? std::string(kFakeDataPrefix) + domain
: domain;
}
bool CompareTimeStampForProtoPair(const CartDB::KeyAndValue pair1,
CartDB::KeyAndValue pair2) {
return pair1.second.timestamp() > pair2.second.timestamp();
}
base::Optional<base::Value> JSONToDictionary(int resource_id) {
base::StringPiece json_resource(
ui::ResourceBundle::GetSharedInstance().GetRawDataResource(resource_id));
base::Optional<base::Value> value = base::JSONReader::Read(json_resource);
DCHECK(value && value.has_value() && value->is_dict());
return value;
}
bool IsExpired(const cart_db::ChromeCartContentProto& proto) {
return (base::Time::Now() - base::Time::FromDoubleT(proto.timestamp()))
.InDays() > 14;
}
} // namespace
CartService::CartService(Profile* profile)
: profile_(profile),
cart_db_(std::make_unique<CartDB>(profile_)),
history_service_(HistoryServiceFactory::GetForProfile(
profile_,
ServiceAccessType::EXPLICIT_ACCESS)),
domain_name_mapping_(JSONToDictionary(IDR_CART_DOMAIN_NAME_MAPPING_JSON)),
domain_cart_url_mapping_(
JSONToDictionary(IDR_CART_DOMAIN_CART_URL_MAPPING_JSON)) {
if (history_service_) {
history_service_observation_.Observe(history_service_);
}
if (base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleDataParam) == "fake") {
AddCartsWithFakeData();
} else {
// In case last deconstruction is interrupted and fake data is not deleted.
DeleteCartsWithFakeData();
}
if (IsCartDiscountEnabled()) {
StartGettingDiscount();
}
}
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);
}
void CartService::Hide() {
profile_->GetPrefs()->SetBoolean(prefs::kCartModuleHidden, true);
}
void CartService::RestoreHidden() {
profile_->GetPrefs()->SetBoolean(prefs::kCartModuleHidden, false);
}
bool CartService::IsHidden() {
return 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 std::string& domain,
const base::Optional<GURL>& cart_url,
const cart_db::ChromeCartContentProto& proto) {
cart_db_->LoadCart(domain, base::BindOnce(&CartService::OnAddCart,
weak_ptr_factory_.GetWeakPtr(),
domain, cart_url, proto));
}
void CartService::DeleteCart(const std::string& domain) {
cart_db_->DeleteCart(domain, base::BindOnce(&CartService::OnOperationFinished,
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 (base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleDataParam) == "fake") {
return;
}
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountAcknowledged, true);
profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, should_enable);
if (should_enable &&
base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam) ==
"true") {
StartGettingDiscount();
}
}
bool CartService::ShouldShowDiscountConsent() {
if (ShouldShowWelcomeSurface() ||
base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam) !=
"true") {
return false;
}
if (base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleDataParam) == "fake") {
return true;
}
return !profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountAcknowledged);
}
bool CartService::IsCartDiscountEnabled() {
if (base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam) !=
"true") {
return false;
}
return profile_->GetPrefs()->GetBoolean(prefs::kCartDiscountEnabled);
}
void CartService::SetCartDiscountEnabled(bool enabled) {
DCHECK(base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam) ==
"true");
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::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() {
if (history_service_) {
history_service_observation_.Reset();
}
DeleteCartsWithFakeData();
// Delete content of all carts that are removed.
cart_db_->LoadAllCarts(base::BindOnce(&CartService::DeleteRemovedCartsContent,
weak_ptr_factory_.GetWeakPtr()));
}
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()));
}
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.google.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.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()));
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 + 5);
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"));
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 + 3);
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);
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);
cart_db_->AddCart(dummy_proto6.key(), dummy_proto6,
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::DeleteCartsWithFakeData() {
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()));
}
}
}
void CartService::OnLoadCarts(CartDB::LoadCallback callback,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (IsHidden() &&
base::GetFieldTrialParamValueByFeature(
ntp_features::kNtpChromeCartModule,
ntp_features::kNtpChromeCartModuleDataParam) != "fake") {
std::move(callback).Run(success, {});
return;
}
std::set<std::string> expired_merchants;
for (CartDB::KeyAndValue kv : proto_pairs) {
if (IsExpired(kv.second)) {
DeleteCart(kv.second.key());
expired_merchants.emplace(kv.second.key());
}
}
proto_pairs.erase(
std::remove_if(proto_pairs.begin(), proto_pairs.end(),
[expired_merchants](CartDB::KeyAndValue kv) {
return kv.second.is_hidden() || kv.second.is_removed() ||
expired_merchants.find(kv.second.key()) !=
expired_merchants.end();
}),
proto_pairs.end());
// 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) {
if (!success) {
return;
}
DCHECK_EQ(1U, proto_pairs.size());
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) {
if (!success) {
return;
}
DCHECK_EQ(1U, proto_pairs.size());
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 std::string& domain,
const base::Optional<GURL>& cart_url,
cart_db::ChromeCartContentProto proto,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (!success) {
return;
}
// Restore module visibility anytime a cart-related action happens.
RestoreHidden();
std::string* merchant_name = domain_name_mapping_->FindStringKey(domain);
if (merchant_name) {
proto.set_merchant(*merchant_name);
}
if (cart_url) {
proto.set_merchant_cart_url(cart_url->spec());
} else {
std::string* fallback_url = domain_cart_url_mapping_->FindStringKey(domain);
if (fallback_url) {
proto.set_merchant_cart_url(*fallback_url);
}
}
if (proto_pairs.size() == 0) {
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 add it to the database without
// worrying about overwriting as it reflects the latest state; if not, we keep
// the existing proto while updating timestamp, hidden status and product
// information if any.
if (proto.product_image_urls().size()) {
cart_db_->AddCart(domain, std::move(proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
return;
}
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 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 (proto.product_infos().size()) {
DCHECK_EQ(1, proto.product_infos().size());
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);
}
}
cart_db_->AddCart(domain, std::move(existing_proto),
base::BindOnce(&CartService::OnOperationFinished,
weak_ptr_factory_.GetWeakPtr()));
}
void CartService::UpdateDiscounts(
const std::string& domain,
const double timestamp,
const std::vector<cart_db::DiscountInfoProto> discount_infos) {
auto update_discounts_callback = base::BindOnce(
&CartService::OnUpdateDiscount, weak_ptr_factory_.GetWeakPtr(), domain,
std::move(discount_infos), timestamp);
cart_db_->LoadCart(domain, std::move(update_discounts_callback));
}
void CartService::OnUpdateDiscount(
const std::string& domain,
const std::vector<cart_db::DiscountInfoProto> discount_infos,
const double timestamp,
bool success,
std::vector<CartDB::KeyAndValue> proto_pairs) {
if (!success || proto_pairs.size() == 0) {
return;
}
DCHECK_EQ(1U, proto_pairs.size());
auto cart_proto = proto_pairs[0].second;
cart_proto.mutable_discount_info()->set_last_fetched_timestamp(timestamp);
if (discount_infos.empty()) {
cart_proto.mutable_discount_info()->clear_discount_info();
} else {
for (cart_db::DiscountInfoProto discount_info : discount_infos) {
cart_db::DiscountInfoProto* added_discount =
cart_proto.mutable_discount_info()->add_discount_info();
added_discount->set_rule_id(discount_info.rule_id());
if (discount_info.has_amount_off()) {
added_discount->set_allocated_amount_off(
discount_info.release_amount_off());
} else {
added_discount->set_percent_off(discount_info.percent_off());
}
added_discount->set_raw_merchant_offer_id(
discount_info.raw_merchant_offer_id());
}
}
cart_db_->AddCart(domain, std::move(cart_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.";
fetch_discount_worker_ = std::make_unique<FetchDiscountWorker>(
profile_->GetDefaultStoragePartition()
->GetURLLoaderFactoryForBrowserProcess(),
std::make_unique<CartDiscountFetcherFactory>(),
std::make_unique<CartLoaderAndUpdaterFactory>(profile_));
fetch_discount_worker_->Start(
base::TimeDelta::FromMilliseconds(kDelayStartMs));
}