| // 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 "components/commerce/core/webui/webui_utils.h" |
| |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/commerce/core/commerce_constants.h" |
| #include "components/commerce/core/commerce_types.h" |
| #include "components/commerce/core/mojom/shared.mojom.h" |
| #include "components/commerce/core/proto/price_tracking.pb.h" |
| #include "components/payments/core/currency_formatter.h" |
| #include "components/power_bookmarks/core/power_bookmark_utils.h" |
| #include "components/power_bookmarks/core/proto/power_bookmark_meta.pb.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "url/gurl.h" |
| |
| namespace commerce { |
| |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_PRICE_UNDETERMINED; |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE; |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE_LOWER_BOUND; |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE_UPPER_BOUND; |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_SINGLE_PRICE; |
| using BuyableProduct_PriceDisplayRecommendation:: |
| BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_UNSPECIFIED; |
| |
| using PriceSummary_ProductOfferCondition:: |
| PriceSummary_ProductOfferCondition_CONDITION_ANY; |
| |
| namespace { |
| |
| std::string GetFormattedCurrencyFromMicros( |
| uint64_t amount_micros, |
| payments::CurrencyFormatter* formatter) { |
| return base::UTF16ToUTF8(formatter->Format(base::NumberToString( |
| static_cast<float>(amount_micros) / kToMicroCurrency))); |
| } |
| |
| // Generate a string that displays the price summary in the format specified by |
| // the provided product info. |
| std::string GetFormattedPriceSummary(const ProductInfo& product_info, |
| payments::CurrencyFormatter* formatter) { |
| std::string current_price = |
| GetFormattedCurrencyFromMicros(product_info.amount_micros, formatter); |
| size_t index = 0; |
| for (const auto& summary : product_info.price_summary) { |
| if (summary.is_preferred()) { |
| break; |
| } |
| index++; |
| } |
| |
| if (index >= product_info.price_summary.size()) { |
| return current_price; |
| } |
| |
| const PriceSummary& summary = product_info.price_summary[index]; |
| |
| std::string lowest_price = ""; |
| if (summary.has_lowest_price()) { |
| lowest_price = GetFormattedCurrencyFromMicros( |
| summary.lowest_price().amount_micros(), formatter); |
| } |
| |
| std::string highest_price = ""; |
| if (summary.has_highest_price()) { |
| highest_price = GetFormattedCurrencyFromMicros( |
| summary.highest_price().amount_micros(), formatter); |
| } |
| |
| switch (product_info.price_display_recommendation.value()) { |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE: |
| if (!lowest_price.empty() && !highest_price.empty()) { |
| return lowest_price + " - " + highest_price; |
| } |
| return current_price; |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE_LOWER_BOUND: |
| if (!lowest_price.empty()) { |
| return lowest_price + "+"; |
| } |
| return current_price; |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_RANGE_UPPER_BOUND: |
| if (!highest_price.empty()) { |
| return highest_price; |
| } |
| return current_price; |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_PRICE_UNDETERMINED: |
| return "-"; |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_SHOW_SINGLE_PRICE: |
| return current_price; |
| case BuyableProduct_PriceDisplayRecommendation_RECOMMENDATION_UNSPECIFIED: |
| return ""; |
| default: |
| break; |
| } |
| |
| return current_price; |
| } |
| |
| } // namespace |
| |
| shared::mojom::ProductInfoPtr ProductInfoToMojoProduct( |
| const GURL& url, |
| const std::optional<const ProductInfo>& info, |
| const std::string& locale) { |
| auto product_info = shared::mojom::ProductInfo::New(); |
| |
| if (!info.has_value()) { |
| return product_info; |
| } |
| |
| product_info->title = info->title; |
| product_info->cluster_title = info->product_cluster_title; |
| product_info->domain = base::UTF16ToUTF8( |
| url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains( |
| GURL(url))); |
| product_info->product_url = url; |
| product_info->image_url = info->image_url; |
| if (info->product_cluster_id.has_value()) { |
| product_info->cluster_id = info->product_cluster_id.value(); |
| } |
| |
| std::unique_ptr<payments::CurrencyFormatter> formatter = |
| std::make_unique<payments::CurrencyFormatter>(info->currency_code, |
| locale); |
| formatter->SetMaxFractionalDigits(2); |
| |
| product_info->current_price = |
| base::UTF16ToUTF8(formatter->Format(base::NumberToString( |
| static_cast<float>(info->amount_micros) / kToMicroCurrency))); |
| |
| // Only send the previous price if it is higher than the current price. |
| if (info->previous_amount_micros.has_value() && |
| info->previous_amount_micros.value() > info->amount_micros) { |
| product_info->previous_price = |
| base::UTF16ToUTF8(formatter->Format(base::NumberToString( |
| static_cast<float>(info->previous_amount_micros.value()) / |
| kToMicroCurrency))); |
| } |
| |
| for (const auto& product_category : |
| info->category_data.product_categories()) { |
| std::string category_labels; |
| bool is_first = true; |
| for (const auto& category_label : product_category.category_labels()) { |
| if (!is_first) { |
| category_labels.append(">"); |
| } |
| is_first = false; |
| category_labels.append(category_label.category_short_label().empty() |
| ? category_label.category_default_label() |
| : category_label.category_short_label()); |
| } |
| product_info->category_labels.push_back(std::move(category_labels)); |
| } |
| |
| if (info->price_summary.size() > 0) { |
| product_info->price_summary = |
| GetFormattedPriceSummary(info.value(), formatter.get()); |
| } |
| |
| return product_info; |
| } |
| |
| shared::mojom::ProductSpecificationsSetPtr ProductSpecsSetToMojo( |
| const ProductSpecificationsSet& set) { |
| auto set_ptr = shared::mojom::ProductSpecificationsSet::New(); |
| |
| set_ptr->name = set.name(); |
| set_ptr->uuid = set.uuid(); |
| |
| for (const auto& url : set.urls()) { |
| set_ptr->urls.push_back(url); |
| } |
| |
| return set_ptr; |
| } |
| |
| shared::mojom::BookmarkProductInfoPtr BookmarkNodeToMojoProduct( |
| bookmarks::BookmarkModel& model, |
| const bookmarks::BookmarkNode* node, |
| const std::string& locale) { |
| auto bookmark_info = shared::mojom::BookmarkProductInfo::New(); |
| bookmark_info->bookmark_id = node->id(); |
| |
| std::unique_ptr<power_bookmarks::PowerBookmarkMeta> meta = |
| power_bookmarks::GetNodePowerBookmarkMeta(&model, node); |
| const power_bookmarks::ShoppingSpecifics specifics = |
| meta->shopping_specifics(); |
| |
| bookmark_info->info = shared::mojom::ProductInfo::New(); |
| bookmark_info->info->title = specifics.title(); |
| bookmark_info->info->domain = base::UTF16ToUTF8( |
| url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains( |
| GURL(node->url()))); |
| |
| bookmark_info->info->product_url = node->url(); |
| bookmark_info->info->image_url = GURL(meta->lead_image().url()); |
| bookmark_info->info->cluster_id = specifics.product_cluster_id(); |
| |
| const power_bookmarks::ProductPrice price = specifics.current_price(); |
| std::string currency_code = price.currency_code(); |
| |
| std::unique_ptr<payments::CurrencyFormatter> formatter = |
| std::make_unique<payments::CurrencyFormatter>(currency_code, locale); |
| formatter->SetMaxFractionalDigits(2); |
| |
| bookmark_info->info->current_price = |
| base::UTF16ToUTF8(formatter->Format(base::NumberToString( |
| static_cast<float>(price.amount_micros()) / kToMicroCurrency))); |
| |
| // Only send the previous price if it is higher than the current price. This |
| // is exclusively used to decide whether to show the price drop chip in the |
| // UI. |
| if (specifics.has_previous_price() && |
| specifics.previous_price().amount_micros() > |
| specifics.current_price().amount_micros()) { |
| const power_bookmarks::ProductPrice previous_price = |
| specifics.previous_price(); |
| bookmark_info->info->previous_price = |
| base::UTF16ToUTF8(formatter->Format(base::NumberToString( |
| static_cast<float>(previous_price.amount_micros()) / |
| kToMicroCurrency))); |
| } |
| |
| return bookmark_info; |
| } |
| |
| } // namespace commerce |