diff --git a/browser/android/preferences/BUILD.gn b/browser/android/preferences/BUILD.gn index 4a4854a960b9..cbd5cba5fcdd 100644 --- a/browser/android/preferences/BUILD.gn +++ b/browser/android/preferences/BUILD.gn @@ -33,8 +33,8 @@ source_set("preferences") { java_cpp_strings("java_pref_names_srcjar") { sources = [ "//brave/components/brave_rewards/common/pref_names.cc", - "//brave/components/brave_vpn/pref_names.cc", "//brave/components/brave_shields/common/pref_names.cc", + "//brave/components/brave_vpn/pref_names.cc", "//brave/components/constants/pref_names.cc", "//brave/components/ntp_background_images/common/pref_names.cc", "//brave/components/omnibox/browser/brave_omnibox_prefs.cc", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 63eed1c10fa1..a4a78f48fe44 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -651,6 +651,8 @@ source_set("ui") { "wallet_bubble_manager_delegate_impl.h", "webui/brave_wallet/ledger/ledger_ui.cc", "webui/brave_wallet/ledger/ledger_ui.h", + "webui/brave_wallet/market/market_ui.cc", + "webui/brave_wallet/market/market_ui.h", "webui/brave_wallet/nft/nft_ui.cc", "webui/brave_wallet/nft/nft_ui.h", "webui/brave_wallet/page_handler/wallet_page_handler.cc", @@ -673,6 +675,7 @@ source_set("ui") { "//brave/components/brave_wallet/common:mojom", "//brave/components/brave_wallet_ui:resources", "//brave/components/brave_wallet_ui/ledger:ledger_bridge_generated", + "//brave/components/brave_wallet_ui/market:market_display_generated", "//brave/components/brave_wallet_ui/nft:nft_display_generated", "//brave/components/brave_wallet_ui/page:brave_wallet_page_generated", "//brave/components/brave_wallet_ui/panel:brave_wallet_panel_generated", diff --git a/browser/ui/webui/brave_wallet/market/market_ui.cc b/browser/ui/webui/brave_wallet/market/market_ui.cc new file mode 100644 index 000000000000..a182eb1c13b9 --- /dev/null +++ b/browser/ui/webui/brave_wallet/market/market_ui.cc @@ -0,0 +1,82 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/webui/brave_wallet/market/market_ui.h" + +#include + +#include "brave/components/brave_wallet/browser/brave_wallet_constants.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/l10n/common/locale_util.h" +#include "brave/components/market_display/resources/grit/market_display_generated_map.h" +#include "chrome/browser/ui/webui/webui_util.h" +#include "chrome/grit/browser_resources.h" +#include "chrome/grit/generated_resources.h" +#include "components/grit/brave_components_resources.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui_data_source.h" +#include "ui/resources/grit/webui_generated_resources.h" + +namespace market { + +UntrustedMarketUI::UntrustedMarketUI(content::WebUI* web_ui) + : ui::UntrustedWebUIController(web_ui) { + auto* untrusted_source = + content::WebUIDataSource::Create(kUntrustedMarketURL); + + for (const auto& str : brave_wallet::kLocalizedStrings) { + std::u16string l10n_str = + brave_l10n::GetLocalizedResourceUTF16String(str.id); + untrusted_source->AddString(str.name, l10n_str); + } + untrusted_source->SetDefaultResource(IDR_BRAVE_WALLET_MARKET_DISPLAY_HTML); + untrusted_source->AddResourcePaths( + base::make_span(kMarketDisplayGenerated, kMarketDisplayGeneratedSize)); + untrusted_source->AddFrameAncestor(GURL(kBraveUIWalletPageURL)); + untrusted_source->AddFrameAncestor(GURL(kBraveUIWalletPanelURL)); + webui::SetupWebUIDataSource( + untrusted_source, + base::make_span(kMarketDisplayGenerated, kMarketDisplayGeneratedSize), + IDR_BRAVE_WALLET_MARKET_DISPLAY_HTML); + + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ScriptSrc, + std::string("script-src 'self' chrome-untrusted://resources " + "chrome-untrusted://brave-resources;")); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::StyleSrc, + std::string("style-src 'self' 'unsafe-inline';")); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FontSrc, + std::string("font-src 'self' data:;")); + untrusted_source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::ImgSrc, + std::string("img-src 'self' https://assets.cgproxy.brave.com;")); + + untrusted_source->AddResourcePath("load_time_data.js", + IDR_WEBUI_JS_LOAD_TIME_DATA_JS); + untrusted_source->UseStringsJs(); + untrusted_source->AddString("braveWalletTrezorBridgeUrl", + kUntrustedTrezorURL); + untrusted_source->AddString("braveWalletLedgerBridgeUrl", + kUntrustedLedgerURL); + untrusted_source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); + untrusted_source->AddString("braveWalletMarketUiBridgeUrl", + kUntrustedMarketURL); + auto* browser_context = web_ui->GetWebContents()->GetBrowserContext(); + content::WebUIDataSource::Add(browser_context, untrusted_source); +} + +UntrustedMarketUI::~UntrustedMarketUI() = default; + +std::unique_ptr +UntrustedMarketUIConfig::CreateWebUIController(content::WebUI* web_ui) { + return std::make_unique(web_ui); +} + +UntrustedMarketUIConfig::UntrustedMarketUIConfig() + : WebUIConfig(content::kChromeUIUntrustedScheme, kUntrustedMarketHost) {} + +} // namespace market diff --git a/browser/ui/webui/brave_wallet/market/market_ui.h b/browser/ui/webui/brave_wallet/market/market_ui.h new file mode 100644 index 000000000000..7eaa3245041d --- /dev/null +++ b/browser/ui/webui/brave_wallet/market/market_ui.h @@ -0,0 +1,37 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_MARKET_MARKET_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_MARKET_MARKET_UI_H_ + +#include + +#include "content/public/browser/web_ui.h" +#include "content/public/browser/webui_config.h" +#include "content/public/common/url_constants.h" +#include "ui/webui/untrusted_web_ui_controller.h" + +namespace market { + +class UntrustedMarketUI : public ui::UntrustedWebUIController { + public: + explicit UntrustedMarketUI(content::WebUI* web_ui); + UntrustedMarketUI(const UntrustedMarketUI&) = delete; + UntrustedMarketUI& operator=(const UntrustedMarketUI&) = delete; + ~UntrustedMarketUI() override; +}; + +class UntrustedMarketUIConfig : public content::WebUIConfig { + public: + UntrustedMarketUIConfig(); + ~UntrustedMarketUIConfig() override = default; + + std::unique_ptr CreateWebUIController( + content::WebUI* web_ui) override; +}; + +} // namespace market + +#endif // BRAVE_BROWSER_UI_WEBUI_BRAVE_WALLET_MARKET_MARKET_UI_H_ diff --git a/browser/ui/webui/brave_wallet/nft/nft_ui.cc b/browser/ui/webui/brave_wallet/nft/nft_ui.cc index 8adc2cfbf2d9..2c2cf7d02d03 100644 --- a/browser/ui/webui/brave_wallet/nft/nft_ui.cc +++ b/browser/ui/webui/brave_wallet/nft/nft_ui.cc @@ -58,6 +58,8 @@ UntrustedNftUI::UntrustedNftUI(content::WebUI* web_ui) kUntrustedTrezorURL); untrusted_source->AddString("braveWalletLedgerBridgeUrl", kUntrustedLedgerURL); + untrusted_source->AddString("braveWalletMarketUiBridgeUrl", + kUntrustedMarketURL); untrusted_source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ImgSrc, std::string("img-src 'self' https: data:;")); diff --git a/browser/ui/webui/brave_wallet/wallet_page_ui.cc b/browser/ui/webui/brave_wallet/wallet_page_ui.cc index 2ac43166c9e6..dce7da200646 100644 --- a/browser/ui/webui/brave_wallet/wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_page_ui.cc @@ -60,9 +60,11 @@ WalletPageUI::WalletPageUI(content::WebUI* web_ui) source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::FrameSrc, std::string("frame-src ") + kUntrustedTrezorURL + " " + - kUntrustedLedgerURL + " " + kUntrustedNftURL + ";"); + kUntrustedLedgerURL + " " + kUntrustedNftURL + " " + + kUntrustedMarketURL + ";"); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); + source->AddString("braveWalletMarketUiBridgeUrl", kUntrustedMarketURL); auto* profile = Profile::FromWebUI(web_ui); content::WebUIDataSource::Add(profile, source); content::URLDataSource::Add(profile, diff --git a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc index 0a4c92584289..c4a5d8a7c69d 100644 --- a/browser/ui/webui/brave_wallet/wallet_panel_ui.cc +++ b/browser/ui/webui/brave_wallet/wallet_panel_ui.cc @@ -65,6 +65,8 @@ WalletPanelUI::WalletPanelUI(content::WebUI* web_ui) kUntrustedLedgerURL + ";"); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); + source->AddString("braveWalletMarketUiBridgeUrl", kUntrustedMarketURL); + if (ShouldDisableCSPForTesting()) { source->DisableContentSecurityPolicy(); } diff --git a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc index 47bb94dc3823..94684ab2e4dd 100644 --- a/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc +++ b/chromium_src/chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.cc @@ -6,6 +6,7 @@ #include "chrome/browser/ui/webui/chrome_untrusted_web_ui_configs.h" #include "brave/browser/ui/webui/brave_wallet/ledger/ledger_ui.h" +#include "brave/browser/ui/webui/brave_wallet/market/market_ui.h" #include "brave/browser/ui/webui/brave_wallet/nft/nft_ui.h" #include "brave/browser/ui/webui/brave_wallet/trezor/trezor_ui.h" #include "brave/components/brave_vpn/buildflags/buildflags.h" @@ -29,6 +30,8 @@ void RegisterChromeUntrustedWebUIConfigs() { #if !BUILDFLAG(IS_ANDROID) content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( std::make_unique()); + content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( + std::make_unique()); content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( std::make_unique()); content::WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig( diff --git a/components/brave_wallet/browser/asset_ratio_response_parser.cc b/components/brave_wallet/browser/asset_ratio_response_parser.cc index 9ca7bc603cf1..985c42ee7531 100644 --- a/components/brave_wallet/browser/asset_ratio_response_parser.cc +++ b/components/brave_wallet/browser/asset_ratio_response_parser.cc @@ -301,4 +301,134 @@ mojom::BlockchainTokenPtr ParseTokenInfo(const std::string& json, *symbol, decimals, true, "", "", chain_id, coin); } +bool ParseCoinMarkets(const std::string& json, + std::vector* values) { + DCHECK(values); + // { + // "payload": [ + // { + // "id": "bitcoin", + // "symbol": "btc", + // "name": "Bitcoin", + // "image": + // "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", + // "market_cap": 727960800075, + // "market_cap_rank": 1, + // "current_price": 38357, + // "price_change_24h": -1229.64683216549, + // "price_change_percentage_24h": -3.10625, + // "total_volume": 17160995925 + // }, + // { + // "id": "ethereum", + // "symbol": "eth", + // "name": "Ethereum", + // "image": + // "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880", + // "market_cap": 304535808667, + // "market_cap_rank": 2, + // "current_price": 2539.82, + // "price_change_24h": -136.841895278459, + // "price_change_percentage_24h": -5.11242, + // "total_volume": 9583014937 + // } + // ], + // "lastUpdated": "2022-03-07T00:25:12.259823452Z" + // } + base::JSONReader::ValueWithError value_with_error = + base::JSONReader::ReadAndReturnValueWithError( + json, base::JSON_PARSE_CHROMIUM_EXTENSIONS | + base::JSONParserOptions::JSON_PARSE_RFC); + absl::optional& records_v = value_with_error.value; + if (!records_v) { + VLOG(0) << "Invalid response, could not parse JSON, JSON is: " << json; + return false; + } + + if (!records_v->is_dict()) { + return false; + } + + auto* payload = records_v->FindListKey("payload"); + if (!payload) { + return false; + } + + for (const auto& coin_market_list_it : payload->GetList()) { + if (!coin_market_list_it.is_dict()) { + return false; + } + auto coin_market = mojom::CoinMarket::New(); + auto* id = coin_market_list_it.FindStringKey("id"); + if (!id) { + return false; + } + coin_market->id = *id; + + auto* symbol = coin_market_list_it.FindStringKey("symbol"); + if (!symbol) { + return false; + } + coin_market->symbol = *symbol; + + auto* name = coin_market_list_it.FindStringKey("name"); + if (!name) { + return false; + } + coin_market->name = *name; + + auto* image = coin_market_list_it.FindStringKey("image"); + if (!image) { + return false; + } + coin_market->image = *image; + + absl::optional market_cap = + coin_market_list_it.FindDoubleKey("market_cap"); + if (!market_cap) { + return false; + } + coin_market->market_cap = *market_cap; + + absl::optional market_cap_rank = + coin_market_list_it.FindIntKey("market_cap_rank"); + if (!market_cap_rank) { + return false; + } + coin_market->market_cap_rank = *market_cap_rank; + + absl::optional current_price = + coin_market_list_it.FindDoubleKey("current_price"); + if (!current_price) { + return false; + } + coin_market->current_price = *current_price; + + absl::optional price_change_24h = + coin_market_list_it.FindDoubleKey("price_change_24h"); + if (!price_change_24h) { + return false; + } + coin_market->price_change_24h = *price_change_24h; + + absl::optional price_change_percentage_24h = + coin_market_list_it.FindDoubleKey("price_change_percentage_24h"); + if (!price_change_percentage_24h) { + return false; + } + coin_market->price_change_percentage_24h = *price_change_percentage_24h; + + absl::optional total_volume = + coin_market_list_it.FindDoubleKey("total_volume"); + if (!total_volume) { + return false; + } + coin_market->total_volume = *total_volume; + + values->push_back(std::move(coin_market)); + } + + return true; +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/asset_ratio_response_parser.h b/components/brave_wallet/browser/asset_ratio_response_parser.h index fd66ee8b2555..cf875a15ba3b 100644 --- a/components/brave_wallet/browser/asset_ratio_response_parser.h +++ b/components/brave_wallet/browser/asset_ratio_response_parser.h @@ -23,6 +23,8 @@ bool ParseAssetPrice(const std::string& json, std::vector* values); bool ParseAssetPriceHistory(const std::string& json, std::vector* values); +bool ParseCoinMarkets(const std::string& json, + std::vector* values); std::string ParseEstimatedTime(const std::string& json); mojom::BlockchainTokenPtr ParseTokenInfo(const std::string& json, diff --git a/components/brave_wallet/browser/asset_ratio_response_parser_unittest.cc b/components/brave_wallet/browser/asset_ratio_response_parser_unittest.cc index 35e92729bd93..6efe117aef85 100644 --- a/components/brave_wallet/browser/asset_ratio_response_parser_unittest.cc +++ b/components/brave_wallet/browser/asset_ratio_response_parser_unittest.cc @@ -308,4 +308,54 @@ TEST(AssetRatioResponseParserUnitTest, ParseGetTokenInfo) { EXPECT_FALSE(ParseTokenInfo(R"({"payload":{})", "0x1", mojom::CoinType::ETH)); } +TEST(AssetRatioResponseParserUnitTest, ParseCoinMarkets) { + // https://ratios.rewards.brave.software/v2/market/provider/coingecko\?vsCurrency\=usd\&limit\=2 + std::string json(R"( + { + "payload": [ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", + "market_cap": 727960800075, + "market_cap_rank": 1, + "current_price": 38357, + "price_change_24h": -1229.64683216549, + "price_change_percentage_24h": -3.10625, + "total_volume": 17160995925 + } + ] + } + )"); + + std::vector values; + ASSERT_TRUE(ParseCoinMarkets(json, &values)); + ASSERT_EQ(values.size(), 1UL); + EXPECT_EQ(values[0]->id, "bitcoin"); + EXPECT_EQ(values[0]->symbol, "btc"); + EXPECT_EQ(values[0]->name, "Bitcoin"); + EXPECT_EQ(values[0]->image, + "https://assets.coingecko.com/coins/images/1/large/" + "bitcoin.png?1547033579"); + EXPECT_EQ(values[0]->market_cap, 727960800075); + EXPECT_EQ(values[0]->market_cap_rank, uint32_t(1)); + EXPECT_EQ(values[0]->current_price, 38357); + EXPECT_EQ(values[0]->price_change_24h, -1229.64683216549); + EXPECT_EQ(values[0]->price_change_percentage_24h, -3.10625); + EXPECT_EQ(values[0]->total_volume, 17160995925); + + // Invalid input + json = R"({"id": [])"; + EXPECT_FALSE(ParseCoinMarkets(json, &values)); + json = R"({"id": []})"; + EXPECT_FALSE(ParseCoinMarkets(json, &values)); + json = "3"; + EXPECT_FALSE(ParseCoinMarkets(json, &values)); + json = "[3]"; + EXPECT_FALSE(ParseCoinMarkets(json, &values)); + json = ""; + EXPECT_FALSE(ParseCoinMarkets(json, &values)); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/asset_ratio_service.cc b/components/brave_wallet/browser/asset_ratio_service.cc index b124a879f492..a1fe583854bf 100644 --- a/components/brave_wallet/browser/asset_ratio_service.cc +++ b/components/brave_wallet/browser/asset_ratio_service.cc @@ -367,4 +367,46 @@ void AssetRatioService::OnGetTokenInfo( ParseTokenInfo(body, mojom::kMainnetChainId, mojom::CoinType::ETH)); } +// static +GURL AssetRatioService::GetCoinMarketsURL(const std::string& vs_asset, + const uint8_t limit) { + GURL url = GURL(base::StringPrintf("%sv2/market/provider/coingecko", + base_url_for_test_.is_empty() + ? kAssetRatioBaseURL + : base_url_for_test_.spec().c_str())); + url = net::AppendQueryParameter(url, "vsCurrency", vs_asset); + url = net::AppendQueryParameter(url, "limit", std::to_string(limit)); + return url; +} + +void AssetRatioService::GetCoinMarkets(const std::string& vs_asset, + const uint8_t limit, + GetCoinMarketsCallback callback) { + std::string vs_asset_lower = base::ToLowerASCII(vs_asset); + auto internal_callback = + base::BindOnce(&AssetRatioService::OnGetCoinMarkets, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)); + api_request_helper_->Request("GET", GetCoinMarketsURL(vs_asset_lower, limit), + "", "", true, std::move(internal_callback)); +} + +void AssetRatioService::OnGetCoinMarkets( + GetCoinMarketsCallback callback, + const int status, + const std::string& body, + const base::flat_map& headers) { + std::vector values; + if (status != 200) { + std::move(callback).Run(false, std::move(values)); + return; + } + + if (!ParseCoinMarkets(body, &values)) { + std::move(callback).Run(false, std::move(values)); + return; + } + + std::move(callback).Run(true, std::move(values)); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/asset_ratio_service.h b/components/brave_wallet/browser/asset_ratio_service.h index a93de556f7cb..3ac0897a479c 100644 --- a/components/brave_wallet/browser/asset_ratio_service.h +++ b/components/brave_wallet/browser/asset_ratio_service.h @@ -64,6 +64,9 @@ class AssetRatioService : public KeyedService, public mojom::AssetRatioService { GetPriceHistoryCallback callback) override; void GetTokenInfo(const std::string& contract_address, GetTokenInfoCallback callback) override; + void GetCoinMarkets(const std::string& vs_asset, + const uint8_t limit, + GetCoinMarketsCallback callback) override; static GURL GetSardineBuyURL(const std::string network, const std::string address, @@ -80,6 +83,7 @@ class AssetRatioService : public KeyedService, public mojom::AssetRatioService { const std::string& vs_asset, brave_wallet::mojom::AssetPriceTimeframe timeframe); static GURL GetTokenInfoURL(const std::string& contract_address); + static GURL GetCoinMarketsURL(const std::string& vs_asset, uint8_t limit); static void SetBaseURLForTest(const GURL& base_url_for_test); void SetAPIRequestHelperForTesting( @@ -114,6 +118,12 @@ class AssetRatioService : public KeyedService, public mojom::AssetRatioService { const std::string& body, const base::flat_map& headers); + void OnGetCoinMarkets( + GetCoinMarketsCallback callback, + const int status, + const std::string& body, + const base::flat_map& headers); + mojo::ReceiverSet receivers_; static GURL base_url_for_test_; diff --git a/components/brave_wallet/browser/asset_ratio_service_unittest.cc b/components/brave_wallet/browser/asset_ratio_service_unittest.cc index 51fc6897792d..25f51220a8b5 100644 --- a/components/brave_wallet/browser/asset_ratio_service_unittest.cc +++ b/components/brave_wallet/browser/asset_ratio_service_unittest.cc @@ -39,6 +39,17 @@ void OnGetPriceHistory( *callback_run = true; } +void OnGetCoinMarkets( + bool* callback_run, + bool expected_success, + std::vector expected_values, + bool success, + std::vector values) { + EXPECT_EQ(expected_success, success); + EXPECT_EQ(expected_values, values); + *callback_run = true; +} + } // namespace namespace brave_wallet { @@ -439,4 +450,64 @@ TEST_F(AssetRatioServiceUnitTest, GetTokenInfo) { GetTokenInfo("0xdac17f958d2ee523a2206206994597c13d831ec7", nullptr); } +TEST_F(AssetRatioServiceUnitTest, GetCoinMarkets) { + SetInterceptor(R"({ + "payload": [ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin", + "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", + "market_cap": 727960800075, + "market_cap_rank": 1, + "current_price": 38357, + "price_change_24h": -1229.64683216549, + "price_change_percentage_24h": -3.10625, + "total_volume": 17160995925 + } + ] + })"); + std::vector + expected_coin_markets_response; + + auto coin_market = brave_wallet::mojom::CoinMarket::New(); + coin_market->id = "bitcoin"; + coin_market->symbol = "btc"; + coin_market->name = "Bitcoin"; + coin_market->image = + "https://assets.coingecko.com/coins/images/1/large/" + "bitcoin.png?1547033579"; + coin_market->market_cap = 727960800075; + coin_market->market_cap_rank = 1; + coin_market->current_price = 38357; + coin_market->price_change_24h = -1229.64683216549; + coin_market->price_change_percentage_24h = -3.10625; + coin_market->total_volume = 17160995925; + expected_coin_markets_response.push_back(std::move(coin_market)); + + bool callback_run = false; + asset_ratio_service_->GetCoinMarkets( + "usd", 100, + base::BindOnce(&OnGetCoinMarkets, &callback_run, true, + std::move(expected_coin_markets_response))); + + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(callback_run); +} + +TEST_F(AssetRatioServiceUnitTest, GetCoinMarketsUnexpectedResponse) { + SetInterceptor("wingardium leviosa"); + std::vector + expected_coin_markets_response; + + bool callback_run = false; + asset_ratio_service_->GetCoinMarkets( + "bat", 99, + base::BindOnce(&OnGetCoinMarkets, &callback_run, false, + std::move(expected_coin_markets_response))); + + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(callback_run); +} + } // namespace brave_wallet diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index bae85b464775..4904aa7aa456 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -70,6 +70,7 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletTopTabApps", IDS_BRAVE_WALLET_TOP_TAB_APPS}, {"braveWalletTopNavNFTS", IDS_BRAVE_WALLET_TOP_NAV_N_F_T_S}, {"braveWalletTopNavAccounts", IDS_BRAVE_WALLET_TOP_NAV_ACCOUNTS}, + {"braveWalletTopNavMarket", IDS_BRAVE_WALLET_TOP_NAV_MARKET}, {"braveWalletChartLive", IDS_BRAVE_WALLET_CHART_LIVE}, {"braveWalletChartOneDay", IDS_BRAVE_WALLET_CHART_ONE_DAY}, {"braveWalletChartOneWeek", IDS_BRAVE_WALLET_CHART_ONE_WEEK}, @@ -771,7 +772,58 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletFundWalletDescription", IDS_BRAVE_WALLET_FUND_WALLET_DESCRIPTION}, {"braveWalletLearnMoreAboutBraveWallet", - IDS_BRAVE_WALLET_LEARN_MORE_ABOUT_BRAVE_WALLET}}; + IDS_BRAVE_WALLET_LEARN_MORE_ABOUT_BRAVE_WALLET}, + {"braveWalletMarketDataAllAssetsFilter", + IDS_BRAVE_WALLET_MARKET_DATA_ALL_ASSETS_FILTER}, + {"braveWalletMarketDataTradableFilter", + IDS_BRAVE_WALLET_MARKET_DATA_TRADABLE_FILTER}, + {"braveWalletMarketDataAssetsColumn", + IDS_BRAVE_WALLET_MARKET_DATA_ASSETS_COLUMN}, + {"braveWalletMarketDataPriceColumn", + IDS_BRAVE_WALLET_MARKET_DATA_PRICE_COLUMN}, + {"braveWalletMarketData24HrColumn", + IDS_BRAVE_WALLET_MARKET_DATA_24Hr_COLUMN}, + {"braveWalletMarketDataMarketCapColumn", + IDS_BRAVE_WALLET_MARKET_DATA_MARKETCAP_COLUMN}, + {"braveWalletMarketDataVolumeColumn", + IDS_BRAVE_WALLET_MARKET_DATA_VOLUME_COLUMN}, + {"braveWalletMarketDataNoAssetsFound", + IDS_BRAVE_WALLET_MARKET_DATA_NO_ASSETS_FOUND}, + {"braveWalletMarketDataCoinNotSupported", + IDS_BRAVE_WALLET_MARKET_DATA_COIN_NOT_SUPPORTED}, + {"braveWalletInformation", IDS_BRAVE_WALLET_INFORMATION}, + {"braveWalletRankStat", IDS_BRAVE_WALLET_RANK_STAT}, + {"braveWalletVolumeStat", IDS_BRAVE_WALLET_VOLUME_STAT}, + {"braveWalletMarketCapStat", IDS_BRAVE_WALLET_MARKET_CAP_STAT}, + {"braveWalletBuyTapBuyNotSupportedMessage", + IDS_BRAVE_WALLET_BUY_TAB_BUY_NOT_SUPPORTED_MESSAGE}, + {"braveWalletHelpCenterText", IDS_BRAVE_WALLET_HELP_CENTER_TEXT}, + {"braveWalletMarketDataAllAssetsFilter", + IDS_BRAVE_WALLET_MARKET_DATA_ALL_ASSETS_FILTER}, + {"braveWalletMarketDataTradableFilter", + IDS_BRAVE_WALLET_MARKET_DATA_TRADABLE_FILTER}, + {"braveWalletMarketDataAssetsColumn", + IDS_BRAVE_WALLET_MARKET_DATA_ASSETS_COLUMN}, + {"braveWalletMarketDataPriceColumn", + IDS_BRAVE_WALLET_MARKET_DATA_PRICE_COLUMN}, + {"braveWalletMarketData24HrColumn", + IDS_BRAVE_WALLET_MARKET_DATA_24Hr_COLUMN}, + {"braveWalletMarketDataMarketCapColumn", + IDS_BRAVE_WALLET_MARKET_DATA_MARKETCAP_COLUMN}, + {"braveWalletMarketDataVolumeColumn", + IDS_BRAVE_WALLET_MARKET_DATA_VOLUME_COLUMN}, + {"braveWalletMarketDataNoAssetsFound", + IDS_BRAVE_WALLET_MARKET_DATA_NO_ASSETS_FOUND}, + {"braveWalletMarketDataCoinNotSupported", + IDS_BRAVE_WALLET_MARKET_DATA_COIN_NOT_SUPPORTED}, + {"braveWalletInformation", IDS_BRAVE_WALLET_INFORMATION}, + {"braveWalletRankStat", IDS_BRAVE_WALLET_RANK_STAT}, + {"braveWalletVolumeStat", IDS_BRAVE_WALLET_VOLUME_STAT}, + {"braveWalletMarketCapStat", IDS_BRAVE_WALLET_MARKET_CAP_STAT}, + {"braveWalletHelpCenter", IDS_BRAVE_WALLET_HELP_CENTER}, + {"braveWalletBuyTapBuyNotSupportedMessage", + IDS_BRAVE_WALLET_BUY_TAB_BUY_NOT_SUPPORTED_MESSAGE}, +}; // 0x swap constants constexpr char kRopstenSwapBaseAPIURL[] = "https://ropsten.api.0x.org/"; diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index d552dd839053..a1d7c5a190a2 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -295,6 +295,19 @@ struct AssetPrice { string asset_timeframe_change; }; +struct CoinMarket { + string id; + string symbol; + string name; + string image; + double market_cap; + uint32 market_cap_rank; + double current_price; + double price_change_24h; + double price_change_percentage_24h; + double total_volume; +}; + // Structs to model 0x (swap) HTTP API interactions // Docs: https://docs.0x.org/0x-api-swap struct SwapParams { @@ -675,6 +688,9 @@ interface AssetRatioService { // Obtain token info via contract address through etherscan API. // This is only for Ethereum mainnet. GetTokenInfo(string contract_address) => (BlockchainToken? token); + + // Obtain list of top currencies and their market data + GetCoinMarkets(string vs_asset, uint8 limit) => (bool success, array values); }; // Implements swapping related functionality through the 0x API. @@ -889,7 +905,7 @@ interface JsonRpcService { // address, otherwise balance is 0x0. GetERC721TokenBalance(string contract_address, string token_id, string account_address, string chain_id) => (string balance, ProviderError error, string error_message); - // Obtains the quanitity of ERC1155 tokens a user has + // Obtains the quantity of ERC1155 tokens a user has GetERC1155TokenBalance(string contract_address, string token_id, string account_address, string chain_id) => (string balance, ProviderError error, string error_message); // Solana JSON RPCs diff --git a/components/brave_wallet_ui/BUILD.gn b/components/brave_wallet_ui/BUILD.gn index d9410c78d315..a648d8aa6e8e 100644 --- a/components/brave_wallet_ui/BUILD.gn +++ b/components/brave_wallet_ui/BUILD.gn @@ -11,6 +11,7 @@ import("//tools/grit/repack.gni") repack("resources") { deps = [ "//brave/components/brave_wallet_ui/ledger:ledger_bridge_generated", + "//brave/components/brave_wallet_ui/market:market_display_generated", "//brave/components/brave_wallet_ui/nft:nft_display_generated", "//brave/components/brave_wallet_ui/page:brave_wallet_page_generated", "//brave/components/brave_wallet_ui/panel:brave_wallet_panel_generated", @@ -20,6 +21,7 @@ repack("resources") { "$root_gen_dir/brave/components/brave_wallet_page/resources/brave_wallet_page_generated.pak", "$root_gen_dir/brave/components/brave_wallet_panel/resources/brave_wallet_panel_generated.pak", "$root_gen_dir/brave/components/ledger_bridge/resources/ledger_bridge_generated.pak", + "$root_gen_dir/brave/components/market_display/resources/market_display_generated.pak", "$root_gen_dir/brave/components/nft_display/resources/nft_display_generated.pak", "$root_gen_dir/brave/components/trezor_bridge/resources/trezor_bridge_generated.pak", ] diff --git a/components/brave_wallet_ui/assets/svg-icons/arrow-down-fill.svg b/components/brave_wallet_ui/assets/svg-icons/arrow-down-fill.svg new file mode 100644 index 000000000000..19accc365b62 --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/arrow-down-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/arrow-down-white-icon.svg b/components/brave_wallet_ui/assets/svg-icons/arrow-down-white-icon.svg new file mode 100644 index 000000000000..91b33346f51a --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/arrow-down-white-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/arrow-up-fill.svg b/components/brave_wallet_ui/assets/svg-icons/arrow-up-fill.svg new file mode 100644 index 000000000000..3b39a0a3458d --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/arrow-up-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/brave_wallet_ui/assets/svg-icons/arrow-up-white-icon.svg b/components/brave_wallet_ui/assets/svg-icons/arrow-up-white-icon.svg new file mode 100644 index 000000000000..a7127d711e13 --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/arrow-up-white-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/carat-down.svg b/components/brave_wallet_ui/assets/svg-icons/carat-down.svg new file mode 100644 index 000000000000..8203d7ce37ed --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/carat-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/share.svg b/components/brave_wallet_ui/assets/svg-icons/share.svg new file mode 100644 index 000000000000..1b65836878dc --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/share.svg @@ -0,0 +1,4 @@ + + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/star-active-icon.svg b/components/brave_wallet_ui/assets/svg-icons/star-active-icon.svg new file mode 100644 index 000000000000..f8b1406d919f --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/star-active-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/assets/svg-icons/star-icon.svg b/components/brave_wallet_ui/assets/svg-icons/star-icon.svg new file mode 100644 index 000000000000..cab9b68465a5 --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/star-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/common/actions/wallet_actions.ts b/components/brave_wallet_ui/common/actions/wallet_actions.ts index 1825a7f7ef17..0348e33c14b2 100644 --- a/components/brave_wallet_ui/common/actions/wallet_actions.ts +++ b/components/brave_wallet_ui/common/actions/wallet_actions.ts @@ -23,7 +23,9 @@ import { UpdateUnapprovedTransactionSpendAllowanceType, UpdateUnapprovedTransactionNonceType, SetTransactionProviderErrorType, - SelectedAccountChangedPayloadType + SelectedAccountChangedPayloadType, + GetCoinMarketPayload, + GetCoinMarketsResponse } from '../constants/action_types' import { BraveWallet, @@ -128,6 +130,8 @@ export const refreshBalancesAndPriceHistory = createAction('refreshBalancesAndPr export const setTransactionProviderError = createAction('setTransactionProviderError') export const setSelectedCoin = createAction('setSelectedCoin') export const setDefaultNetworks = createAction('setDefaultNetworks') +export const getCoinMarkets = createAction('getCoinMarkets') +export const setCoinMarkets = createAction('setCoinMarkets') export const setSelectedNetworkFilter = createAction('setSelectedNetworkFilter') export const setSelectedAssetFilterItem = createAction('setSelectedAssetFilterItem') export const setDefaultAccounts = createAction('setDefaultAccounts') diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index 2ea3866dc27e..997da385ed4a 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -15,7 +15,8 @@ import { UpdateUnapprovedTransactionSpendAllowanceType, TransactionStatusChanged, UpdateUnapprovedTransactionNonceType, - SelectedAccountChangedPayloadType + SelectedAccountChangedPayloadType, + GetCoinMarketPayload } from '../constants/action_types' import { BraveWallet, @@ -646,6 +647,12 @@ handler.on(WalletActions.expandWalletNetworks.getType(), async (store) => { }) }) +handler.on(WalletActions.getCoinMarkets.getType(), async (store: Store, payload: GetCoinMarketPayload) => { + const assetRatioService = getAPIProxy().assetRatioService + const result = await assetRatioService.getCoinMarkets(payload.vsAsset, payload.limit) + store.dispatch(WalletActions.setCoinMarkets(result)) +}) + handler.on(WalletActions.setSelectedNetworkFilter.getType(), async (store: Store, payload: BraveWallet.NetworkInfo) => { const state = getWalletState(store) const { selectedPortfolioTimeline } = state diff --git a/components/brave_wallet_ui/common/constants/action_types.ts b/components/brave_wallet_ui/common/constants/action_types.ts index 5f38d5383931..29a746663b3d 100644 --- a/components/brave_wallet_ui/common/constants/action_types.ts +++ b/components/brave_wallet_ui/common/constants/action_types.ts @@ -108,6 +108,16 @@ export type AddSitePermissionPayloadType = { account: string } +export type GetCoinMarketPayload = { + vsAsset: string + limit: number +} + +export type GetCoinMarketsResponse = { + success: boolean + values: BraveWallet.CoinMarket[] +} + export type SetTransactionProviderErrorType = { transaction: BraveWallet.TransactionInfo providerError: TransactionProviderError diff --git a/components/brave_wallet_ui/common/hooks/market-data-management.ts b/components/brave_wallet_ui/common/hooks/market-data-management.ts new file mode 100644 index 000000000000..1009128476f6 --- /dev/null +++ b/components/brave_wallet_ui/common/hooks/market-data-management.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import Fuse from 'fuse.js' +import { BraveWallet, MarketDataTableColumnTypes, SortOrder } from '../../constants/types' + +export const useMarketDataManagement = (marketData: BraveWallet.CoinMarket[], sortOrder: SortOrder, columnId: MarketDataTableColumnTypes) => { + const sortCoinMarketData = React.useCallback(() => { + const sortedMarketData = [...marketData] + + if (sortOrder === 'asc') { + sortedMarketData.sort((a, b) => a[columnId] - b[columnId]) + } else { + sortedMarketData.sort((a, b) => b[columnId] - a[columnId]) + } + + return sortedMarketData + }, [marketData, sortOrder, columnId]) + + const searchCoinMarkets = React.useCallback((searchList: BraveWallet.CoinMarket[], searchTerm: string) => { + if (!searchTerm) { + return searchList + } + + const options = { + shouldSort: true, + threshold: 0.1, + location: 0, + distance: 0, + minMatchCharLength: 1, + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'symbol', weight: 0.5 } + ] + } + + const fuse = new Fuse(searchList, options) + const results = fuse.search(searchTerm).map((result: Fuse.FuseResult) => result.item) + + return results + }, [marketData]) + + return { + sortCoinMarketData, + searchCoinMarkets + } +} diff --git a/components/brave_wallet_ui/common/reducers/wallet_reducer.ts b/components/brave_wallet_ui/common/reducers/wallet_reducer.ts index b193464693bd..6bb816ecb789 100644 --- a/components/brave_wallet_ui/common/reducers/wallet_reducer.ts +++ b/components/brave_wallet_ui/common/reducers/wallet_reducer.ts @@ -22,6 +22,7 @@ import { AssetFilterOption } from '../../constants/types' import { + GetCoinMarketsResponse, IsEip1559Changed, NewUnapprovedTxAdded, SetTransactionProviderErrorType, @@ -96,6 +97,8 @@ const defaultState: WalletState = { crypto: '' }, transactionProviderErrorRegistry: {}, + isLoadingCoinMarketData: true, + coinMarketData: [], defaultNetworks: [] as BraveWallet.NetworkInfo[], defaultAccounts: [] as BraveWallet.AccountInfo[], selectedNetworkFilter: AllNetworksOption, @@ -514,6 +517,17 @@ export const createWalletReducer = (initialState: WalletState) => { } }) + reducer.on(WalletActions.setCoinMarkets, (state: WalletState, payload: GetCoinMarketsResponse): WalletState => { + return { + ...state, + coinMarketData: payload.success ? payload.values.map(coin => { + coin.image = coin.image.replace('https://assets.coingecko.com', ' https://assets.cgproxy.brave.com') + return coin + }) : [], + isLoadingCoinMarketData: false + } + }) + reducer.on(WalletActions.setDefaultAccounts, (state: WalletState, payload: BraveWallet.AccountInfo[]): WalletState => { return { ...state, diff --git a/components/brave_wallet_ui/components/asset-name-and-icon/index.tsx b/components/brave_wallet_ui/components/asset-name-and-icon/index.tsx new file mode 100644 index 000000000000..5ea08a6be7a7 --- /dev/null +++ b/components/brave_wallet_ui/components/asset-name-and-icon/index.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +import { StyledWrapper, NameAndSymbolWrapper, AssetName, AssetSymbol, AssetIcon } from './style' + +export interface Props { + symbol: string + assetName: string + assetLogo: string +} + +export const AssetNameAndIcon = (props: Props) => { + const { + assetLogo, + assetName, + symbol + } = props + + return ( + + + + {assetName} + {symbol} + + + ) +} diff --git a/components/brave_wallet_ui/components/asset-name-and-icon/style.ts b/components/brave_wallet_ui/components/asset-name-and-icon/style.ts new file mode 100644 index 000000000000..17b00e77d907 --- /dev/null +++ b/components/brave_wallet_ui/components/asset-name-and-icon/style.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +export const StyledWrapper = styled.div` + display: flex; + flex-direction: row; + height: 40px; + +` + +export const AssetIcon = styled.img` + width: 40px; + height: auto; + margin-right: 12px; +` + +export const NameAndSymbolWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: 1px 0; +` + +export const AssetName = styled.span` + font-family: Poppins; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.01em; + color: ${p => p.theme.color.text01}; +` + +export const AssetSymbol = styled(AssetName)` + color: ${p => p.theme.color.text03}; + text-transform: uppercase; +` diff --git a/components/brave_wallet_ui/components/asset-price-change/index.tsx b/components/brave_wallet_ui/components/asset-price-change/index.tsx new file mode 100644 index 000000000000..d6d84073f23d --- /dev/null +++ b/components/brave_wallet_ui/components/asset-price-change/index.tsx @@ -0,0 +1,26 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +import { StyledWrapper, PriceChange, ArrowDown, ArrowUp } from './style' + +export interface Props { + isDown: boolean + priceChangePercentage: string +} + +export const AssetPriceChange = (props: Props) => { + const { isDown, priceChangePercentage } = props + + return ( + + {isDown ? : } + + {priceChangePercentage} + + + ) +} diff --git a/components/brave_wallet_ui/components/asset-price-change/style.ts b/components/brave_wallet_ui/components/asset-price-change/style.ts new file mode 100644 index 000000000000..546ca4764c6c --- /dev/null +++ b/components/brave_wallet_ui/components/asset-price-change/style.ts @@ -0,0 +1,53 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +import ArrowDownIcon from '../../assets/svg-icons/arrow-down-white-icon.svg' +import ArrowUpIcon from '../../assets/svg-icons/arrow-up-white-icon.svg' + +export interface StyleProps { + isDown: boolean +} + +export const StyledWrapper = styled.span` + display: flex; + align-items: center; + padding: 4px 9px; + border-radius: 8px; + background-color: ${p => p.isDown ? '#F75A3A' : '#2AC194'}; + width: 62px; + height: 24px; +` +export const PriceChange = styled.span` + display: flex; + align-items: center; + font-family: Poppins; + font-size: 11px; + font-weight: 400; + letter-spacing: 0.01em; + color: ${p => p.theme.color.background01}; + + @media (prefers-color-scheme: dark) { + color: ${p => p.theme.color.text}; + } +` + +export const ArrowBase = styled.span` + width: 12px; + height: 11px; + background-repeat: no-repeat; + background-size: contain; + margin-right: 2px; + background-position: center center; +` + +export const ArrowUp = styled(ArrowBase)` + background-image: url(${ArrowUpIcon}); +` + +export const ArrowDown = styled(ArrowBase)` + background-image: url(${ArrowDownIcon}); +` diff --git a/components/brave_wallet_ui/components/asset-wishlist-star/index.tsx b/components/brave_wallet_ui/components/asset-wishlist-star/index.tsx new file mode 100644 index 000000000000..eae15e5bfed9 --- /dev/null +++ b/components/brave_wallet_ui/components/asset-wishlist-star/index.tsx @@ -0,0 +1,31 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +import { StyledWrapper, Star } from './style' + +export interface Props { + active: boolean + onCheck?: (status: boolean) => void +} + +export const AssetWishlistStar = (props: Props) => { + const { active, onCheck } = props + + const onClick = () => { + if (onCheck) { + onCheck(!active) + } + } + return ( + + + + ) +} diff --git a/components/brave_wallet_ui/components/asset-wishlist-star/style.ts b/components/brave_wallet_ui/components/asset-wishlist-star/style.ts new file mode 100644 index 000000000000..6a4019659345 --- /dev/null +++ b/components/brave_wallet_ui/components/asset-wishlist-star/style.ts @@ -0,0 +1,33 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +import StarActiveIcon from '../../assets/svg-icons/star-active-icon.svg' +import StarIcon from '../../assets/svg-icons/star-icon.svg' + +export interface StyleProps { + active: boolean +} + +export const StyledWrapper = styled.div` + display: flex; + align-items: center; + width: 17px; + height: 17px; +` + +export const Star = styled.button` + width: 100%; + height: 100%; + background-repeat: no-repeat; + background-size: contain; + background-position: center center; + background-image: url(${p => p.active ? StarActiveIcon : StarIcon}); + background-color: transparent; + border: none; + box-shadow: none; + cursor: pointer; +` diff --git a/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/index.tsx b/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/index.tsx new file mode 100644 index 000000000000..eadc865b8e5f --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/index.tsx @@ -0,0 +1,68 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { AssetFilter } from '../../../constants/types' +import * as React from 'react' + +import { StyledWrapper, Button, CaratDown, Dropdown } from './style' +import { AssetsFilterOption } from '../assets-filter-option' + +export interface Props { + options: AssetFilter[] + value: string + closeOnSelect?: boolean + onSelectFilter: (value: string) => void +} + +const AssetsFilterDropdown = (props: Props) => { + const { options, value, closeOnSelect, onSelectFilter } = props + const [isOpen, setIsOpen] = React.useState(false) + + const buttonLabel = React.useMemo(() => { + const selected = options.find(option => option.value === value) + + return selected !== undefined ? selected.label : '' + }, [value, options]) + + const onClick = () => { + setIsOpen(prevIsOpen => !prevIsOpen) + } + + const onOptionSelect = React.useCallback((value: string) => { + if (closeOnSelect) { + setIsOpen(false) + } + + onSelectFilter(value) + }, [closeOnSelect, onSelectFilter]) + + return ( + + + {isOpen && + + {options.map(option => + + )} + + } + + ) +} + +AssetsFilterDropdown.defaultProps = { + closeOnSelect: true +} + +export default AssetsFilterDropdown diff --git a/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/style.ts b/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/style.ts new file mode 100644 index 000000000000..4045b919ca17 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/assets-filter-dropdown/style.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +import CaratDownIcon from '../../../assets/svg-icons/carat-down.svg' +import { WalletButton } from '../../shared/style' + +export const StyledWrapper = styled.div` + display: flex; + align-items: center; + position: relative; + min-width: 134px; + box-sizing: border-box; + ` + +export const Button = styled(WalletButton)` + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + width: 100%; + height: 36px; + border: none; + color: ${(p) => p.theme.color.text01}; + font-family: Poppins; + box-sizing: border-box; + background-color: ${(p) => p.theme.color.background02}; + border: ${(p) => `1px solid ${p.theme.color.interactive08}`}; + padding: 6px 12px; + border-radius: 8px; + cursor: pointer; +` + +export const Dropdown = styled.ul` + display: flex; + flex-direction: column; + align-items: flex-start; + box-shadow: 0px 0px 16px rgba(99, 105, 110, 0.18); + border-radius: 4px; + width: 100%; + background-color: ${(p) => p.theme.color.background02}; + padding: 10px 12px; + margin: 0; + position: absolute; + top: calc(100% + 2px); + z-index: 3; +` + +export const CaratDown = styled.div` + width: 16px; + height: 16px; + background-color: ${(p) => p.theme.color.text02}; + -webkit-mask-image: url(${CaratDownIcon}); + mask-image: url(${CaratDownIcon}); +` diff --git a/components/brave_wallet_ui/components/desktop/assets-filter-option/index.tsx b/components/brave_wallet_ui/components/desktop/assets-filter-option/index.tsx new file mode 100644 index 000000000000..7f2548586e8a --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/assets-filter-option/index.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +import { Option } from './style' + +export interface Props { + label: string + value: string + selected: boolean + onSelect: (value: string) => void +} + +export const AssetsFilterOption = (props: Props) => { + const { selected, label, value, onSelect } = props + + const onClick = () => { + onSelect(value) + } + + return ( + + ) +} diff --git a/components/brave_wallet_ui/components/desktop/assets-filter-option/style.ts b/components/brave_wallet_ui/components/desktop/assets-filter-option/style.ts new file mode 100644 index 000000000000..0d742b9d7439 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/assets-filter-option/style.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +interface StyleProps { + selected?: boolean +} + +export const Option = styled.li>` + display: flex; + align-items: center; + padding: 10px 0; + font-family: Poppins; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; + font-weight: ${(p) => p.selected ? 600 : 'normal'}; + cursor: pointer; +` diff --git a/components/brave_wallet_ui/components/desktop/index.ts b/components/brave_wallet_ui/components/desktop/index.ts index daf6c30b50de..c40ed074c146 100644 --- a/components/brave_wallet_ui/components/desktop/index.ts +++ b/components/brave_wallet_ui/components/desktop/index.ts @@ -31,6 +31,7 @@ import WithHideBalancePlaceholder from './with-hide-balance-placeholder' import NetworkFilterSelector from './network-filter-selector' import { CryptoView, PortfolioAsset, PortfolioOverview } from './views' import { AssetFilterSelector } from './asset-filter-selector/asset-filter-selector' +import AssetsFilterDropdown from './assets-filter-dropdown' export { SideNavButton, @@ -61,6 +62,7 @@ export { TransactionPopup, SwapTooltip, WithHideBalancePlaceholder, + AssetsFilterDropdown, NetworkFilterSelector, AssetFilterSelector } diff --git a/components/brave_wallet_ui/components/desktop/line-chart/index.tsx b/components/brave_wallet_ui/components/desktop/line-chart/index.tsx index 35f39e838b01..a9a1e7f54c39 100644 --- a/components/brave_wallet_ui/components/desktop/line-chart/index.tsx +++ b/components/brave_wallet_ui/components/desktop/line-chart/index.tsx @@ -1,4 +1,6 @@ import * as React from 'react' +import { CSSProperties } from 'styled-components' + import { AreaChart, Area, @@ -27,6 +29,9 @@ export interface Props { isDown: boolean isLoading: boolean isDisabled: boolean + showPulsatingDot?: boolean + customStyle?: CSSProperties + showTooltip?: boolean } const EmptyChartData = [ @@ -50,7 +55,10 @@ function LineChart ({ isAsset, isDown, isLoading, - isDisabled + isDisabled, + customStyle, + showPulsatingDot, + showTooltip }: Props) { // state const [position, setPosition] = React.useState(0) @@ -71,7 +79,7 @@ function LineChart ({ // render return ( - + @@ -90,7 +98,7 @@ function LineChart ({ - {priceData.length > 0 && !isDisabled && + {priceData.length > 0 && !isDisabled && showTooltip && - - } - /> + {showPulsatingDot && + + } + /> + } ) } +LineChart.defaultProps = { + showPulsatingDot: true, + showTooltip: true +} + export default LineChart diff --git a/components/brave_wallet_ui/components/desktop/line-chart/style.ts b/components/brave_wallet_ui/components/desktop/line-chart/style.ts index 1ced31da6ea3..ee286ac5b358 100644 --- a/components/brave_wallet_ui/components/desktop/line-chart/style.ts +++ b/components/brave_wallet_ui/components/desktop/line-chart/style.ts @@ -1,18 +1,25 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { LoaderIcon } from 'brave-ui/components/icons' interface StyleProps { labelPosition: 'start' | 'middle' | 'end' labelTranslate: number isLoading: boolean + customStyle?: {[key: string]: string} } -export const StyledWrapper = styled.div` +export const StyledWrapper = styled.div>` width: 100%; height: 200px; margin-bottom: 30px; box-sizing: border-box; position: relative; + ${p => p.customStyle + ? css` + ${p.customStyle} + ` + : '' + }; ` export const LabelWrapper = styled.div>` diff --git a/components/brave_wallet_ui/components/desktop/top-tab-nav/style.ts b/components/brave_wallet_ui/components/desktop/top-tab-nav/style.ts index c4cba02b405f..c6ba585dadfb 100644 --- a/components/brave_wallet_ui/components/desktop/top-tab-nav/style.ts +++ b/components/brave_wallet_ui/components/desktop/top-tab-nav/style.ts @@ -8,7 +8,7 @@ export const StyledWrapper = styled.div` align-items: center; justify-content: flex-start; width: 100%; - margin-bottom: 20px; + margin-bottom: 24px; ` export const MoreRow = styled.div` diff --git a/components/brave_wallet_ui/components/desktop/views/crypto/index.tsx b/components/brave_wallet_ui/components/desktop/views/crypto/index.tsx index 5a1023714dd6..d04f5c59b871 100644 --- a/components/brave_wallet_ui/components/desktop/views/crypto/index.tsx +++ b/components/brave_wallet_ui/components/desktop/views/crypto/index.tsx @@ -25,6 +25,7 @@ import { StyledWrapper } from './style' import { TopTabNav, WalletBanner, EditVisibleAssetsModal } from '../../' import { PortfolioOverview } from '../portfolio/portfolio-overview' import { PortfolioAsset } from '../portfolio/portfolio-asset' +import { MarketView } from '../market' import { Accounts } from '../accounts/accounts' import { Account } from '../accounts/account' import { AddAccountModal } from '../../popup-modals/add-account-modal/add-account-modal' @@ -224,6 +225,18 @@ const CryptoView = (props: Props) => { + + {nav} + + + + + {nav} + + + diff --git a/components/brave_wallet_ui/components/desktop/views/index.ts b/components/brave_wallet_ui/components/desktop/views/index.ts index a598dc393afc..42350097052c 100644 --- a/components/brave_wallet_ui/components/desktop/views/index.ts +++ b/components/brave_wallet_ui/components/desktop/views/index.ts @@ -6,11 +6,13 @@ import { Account, Accounts } from './accounts' import CryptoView from './crypto' import { PortfolioAsset, PortfolioOverview } from './portfolio' +import { MarketView } from './market' export { Account, Accounts, CryptoView, PortfolioAsset, - PortfolioOverview + PortfolioOverview, + MarketView } diff --git a/components/brave_wallet_ui/components/desktop/views/market/index.tsx b/components/brave_wallet_ui/components/desktop/views/market/index.tsx new file mode 100644 index 000000000000..2635e47783a9 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/views/market/index.tsx @@ -0,0 +1,110 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router' + +// Constants +import { BraveWallet, WalletRoutes, WalletState } from '../../../../constants/types' + +// Actions +import { WalletActions } from '../../../../common/actions' + +// Styled Components +import { LoadIcon, LoadIconWrapper, MarketDataIframe, StyledWrapper } from './style' + +// Utils +import { WalletPageActions } from '../../../../page/actions' +import { + braveMarketUiOrigin, + MarketCommandMessage, + MarketUiCommand, + SelectCoinMarketMessage, + sendMessageToMarketUiFrame, + UpdateCoinMarketMessage, + UpdateTradableAssetsMessage +} from '../../../../market/market-ui-messages' + +const defaultCurrency = 'usd' +const assetsRequestLimit = 250 + +export const MarketView = () => { + const [iframeLoaded, setIframeLoaded] = React.useState(false) + const marketDataIframeRef = React.useRef(null) + + // Redux + const dispatch = useDispatch() + const { + isLoadingCoinMarketData, + coinMarketData: allCoins, + userVisibleTokensInfo: tradableAssets + } = useSelector(({ wallet }: { wallet: WalletState }) => wallet) + + // Hooks + const history = useHistory() + + const onSelectCoinMarket = React.useCallback((coinMarket: BraveWallet.CoinMarket) => { + dispatch(WalletPageActions.selectCoinMarket(coinMarket)) + history.push(`${WalletRoutes.Market}/${coinMarket.symbol}`) + }, []) + + const onMessageEventListener = React.useCallback((event: MessageEvent) => { + // validate message origin + if (event.origin !== braveMarketUiOrigin) return + + const message = event.data + const { payload } = message as SelectCoinMarketMessage + onSelectCoinMarket(payload) + }, []) + + const onMarketDataFrameLoad = React.useCallback(() => setIframeLoaded(true), []) + + React.useEffect(() => { + if (allCoins.length === 0) { + dispatch(WalletActions.getCoinMarkets({ + vsAsset: defaultCurrency, + limit: assetsRequestLimit + })) + } + }, [allCoins]) + + React.useEffect(() => { + if (!iframeLoaded || !marketDataIframeRef?.current) return + + const updateCoinsMsg: UpdateCoinMarketMessage = { + command: MarketUiCommand.UpdateCoinMarkets, + payload: allCoins + } + sendMessageToMarketUiFrame(marketDataIframeRef.current.contentWindow, updateCoinsMsg) + + const updateAssetsMsg: UpdateTradableAssetsMessage = { + command: MarketUiCommand.UpdateTradableAssets, + payload: tradableAssets + } + sendMessageToMarketUiFrame(marketDataIframeRef.current.contentWindow, updateAssetsMsg) + }, [iframeLoaded, marketDataIframeRef, allCoins]) + + React.useEffect(() => { + window.addEventListener('message', onMessageEventListener) + return () => window.removeEventListener('message', onMessageEventListener) + }, []) + + return ( + + {isLoadingCoinMarketData + ? + + + : + } + + ) +} diff --git a/components/brave_wallet_ui/components/desktop/views/market/style.ts b/components/brave_wallet_ui/components/desktop/views/market/style.ts new file mode 100644 index 000000000000..496d1a893d67 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/views/market/style.ts @@ -0,0 +1,82 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { LoaderIcon } from 'brave-ui/components/icons' + +export interface StyleProps { + alignment: 'right' | 'left' +} + +export const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + width: 100%; + margin-bottom: 24px; +` + +export const TopRow = styled.div` + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + width: 100%; + margin-bottom: 20px; + gap: 10px; +` + +export const AssetsColumnWrapper = styled.div` + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: left; +` + +export const AssetsColumnItemSpacer = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 19px; +` +export const TextWrapper = styled.div` + display: flex; + justify-content: ${p => p.alignment === 'right' ? 'flex-end' : 'flex-start'}; + width: 100%; + font-family: Poppins; + font-size: 14px; + letter-spacing: 0.01em; +` + +export const LineChartWrapper = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + height: 30px; + max-width: 120px; + margin: 0 auto; +` + +export const LoadIcon = styled(LoaderIcon)` + color: ${p => p.theme.color.interactive08}; + height: 70px; + width: 70px; + opacity: .4; +` + +export const LoadIconWrapper = styled.div` + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + align-items: center; + justify-content: center; +` +export const MarketDataIframe = styled.iframe` + width: 100%; + min-height: 100vh; + border: none; +` diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/accounts-and-transctions-list/index.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/components/accounts-and-transctions-list/index.tsx index d7e1d78119d7..8cafb11009d2 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/accounts-and-transctions-list/index.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/accounts-and-transctions-list/index.tsx @@ -12,6 +12,7 @@ import { // Utils import { getLocale } from '../../../../../../../common/locale' import Amount from '../../../../../../utils/amount' +import { getTokensCoinType, getTokensNetwork } from '../../../../../../utils/network-utils' // Components import { @@ -32,10 +33,8 @@ import { EmptyTransactionContainer, TransactionPlaceholderText, AssetBalanceDisplay, - DividerRow, - CoinGeckoText + DividerRow } from '../../style' -import { getTokensCoinType, getTokensNetwork } from '../../../../../../utils/network-utils' export interface Props { selectedAsset: BraveWallet.BlockchainToken | undefined @@ -152,7 +151,6 @@ const AccountsAndTransactionsList = (props: Props) => { {getLocale('braveWalletTransactionPlaceholder')} )} - {getLocale('braveWalletPoweredByCoinGecko')} } diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats-styles.ts b/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats-styles.ts new file mode 100644 index 000000000000..800ede7d7ef2 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats-styles.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +export const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; +` + +export const StatWrapper = styled.div` + display: flex; + flex-direction: column; + margin-right: 95px; + ` + +export const StatValue = styled.div` + display: flex; + flex-direction: row; + align-items: center; + font-family: 'Poppins'; + font-style: normal; + font-weight: 500; + font-size: 24px; + line-height: 20px; + letter-spacing: 0.01em; + color: ${p => p.theme.color.text01}; + margin-bottom: 12px; + justify-content: center; +` + +export const StatLabel = styled.div` + display: flex; + align-items: center; + text-align: center; + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.01em; + color: ${p => p.theme.color.text02}; + justify-content: center; +` + +export const Row = styled.div` + display: flex; + flex-direction: row; + margin: 16px 0 16px 0; +` + +export const Currency = styled.sup` + display: flex; + font-family: 'Poppins'; + font-style: normal; + font-weight: 500; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${p => p.theme.color.text02}; + top: -0.8em; + margin-right: 2px; +` diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats.tsx new file mode 100644 index 000000000000..bea3be9de481 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/coin-stats/coin-stats.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// React +import { getLocale } from '$web-common/locale' + +// Utils +import Amount from '../../../../../../utils/amount' + +// Styled components +import { + Currency, + Row, + StatLabel, + StatValue, + StatWrapper, + StyledWrapper +} from './coin-stats-styles' +import { DividerText, SubDivider } from '../../style' + +interface Props { + marketCapRank: number + volume: number + marketCap: number +} + +export const CoinStats = (props: Props) => { + const { marketCapRank, marketCap, volume } = props + const formattedMarketCap = new Amount(marketCap).abbreviate(2, undefined, 'billion') + const formattedVolume = new Amount(volume).abbreviate(2, undefined, 'billion') + + return ( + + {getLocale('braveWalletInformation')} + + + + {marketCapRank} + {getLocale('braveWalletMarketCapStat')} + + + + + $ + {formattedVolume} + + {getLocale('braveWalletVolumeStat')} + + + + + $ + {formattedMarketCap} + + {getLocale('braveWalletMarketCapStat')} + + + + ) +} diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx index a536ce8b1431..d3b2c9b0300c 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-asset.tsx @@ -30,7 +30,7 @@ import { UpdateSelectedAssetMessage, UpdateTokenNetworkMessage } from '../../../../nft/nft-ui-messages' import { auroraSupportedContractAddresses } from '../../../../utils/asset-utils' - +import { getLocale } from '../../../../../common/locale' // actions import { WalletPageActions } from '../../../../page/actions' @@ -55,7 +55,8 @@ import { AssetIcon, AssetNameText, AssetRow, - BalanceRow, BridgeToAuroraButton, + BalanceRow, + BridgeToAuroraButton, DetailText, InfoColumn, NetworkDescription, @@ -66,18 +67,27 @@ import { PriceText, ShowBalanceButton, StyledWrapper, - TopRow + TopRow, + SubDivider, + NotSupportedText } from './style' import { Skeleton } from '../../../shared/loading-skeleton/styles' +import { CoinStats } from './components/coin-stats/coin-stats' const AssetIconWithPlaceholder = withPlaceholderIcon(AssetIcon, { size: 'big', marginLeft: 0, marginRight: 12 }) const rainbowbridgeLink = 'https://rainbowbridge.app' const bridgeToAuroraWarningShownKey = 'bridgeToAuroraWarningShown' -export const PortfolioAsset = () => { +interface Props { + isShowingMarketData?: boolean +} + +export const PortfolioAsset = (props: Props) => { + const { isShowingMarketData } = props // state const [showBridgeToAuroraModal, setShowBridgeToAuroraModal] = React.useState(false) const [bridgeToAuroraWarningShown, setBridgeToAuroraWarningShown] = React.useState() + const [isTokenSupported, setIsTokenSupported] = React.useState() // routing const history = useHistory() const { id: assetId, tokenId } = useParams<{ id?: string, tokenId?: string }>() @@ -96,7 +106,9 @@ export const PortfolioAsset = () => { transactions, isFetchingPortfolioPriceHistory, transactionSpotPrices, - selectedNetworkFilter + selectedNetworkFilter, + coinMarketData, + fullTokenList } = useSelector(({ wallet }: { wallet: WalletState }) => wallet) const { @@ -107,7 +119,8 @@ export const PortfolioAsset = () => { selectedAssetPriceHistory, selectedTimeline, isFetchingNFTMetadata, - nftMetadata + nftMetadata, + selectedCoinMarket } = useSelector(({ page }: { page: PageState }) => page) // custom hooks const getAccountBalance = useBalance(networkList) @@ -132,6 +145,10 @@ export const PortfolioAsset = () => { }) }, [accounts, networkList, getAccountBalance]) + const tokensWithCoingeckoId = React.useMemo(() => { + return fullTokenList.filter(token => token.coingeckoId !== '') + }, [fullTokenList]) + // This looks at the users asset list and returns the full balance for each asset const userAssetList = React.useMemo(() => { const allAssets = userVisibleTokensInfo.map((asset) => ({ @@ -176,9 +193,27 @@ export const PortfolioAsset = () => { } // If the id length is greater than 15 assumes it's a contractAddress - return assetId.length > 15 + let token = assetId.length > 15 ? userVisibleTokensInfo.find((token) => tokenId ? token.contractAddress === assetId && token.tokenId === tokenId : token.contractAddress === assetId) : userVisibleTokensInfo.find((token) => token.symbol.toLowerCase() === assetId?.toLowerCase()) + + if (!token && assetId.length < 15) { + const coinMarket = coinMarketData.find(token => token.symbol.toLowerCase() === assetId?.toLowerCase()) + if (coinMarket) { + token = new BraveWallet.BlockchainToken() + token.coingeckoId = coinMarket.id + token.name = coinMarket.name + token.contractAddress = '' + token.symbol = coinMarket.symbol.toUpperCase() + token.logo = coinMarket.image + } + const foundToken = tokensWithCoingeckoId?.find(token => token.coingeckoId.toLowerCase() === coinMarket?.id?.toLowerCase()) + setIsTokenSupported(foundToken !== undefined) + } else { + setIsTokenSupported(true) + } + + return token }, [assetId, userVisibleTokensInfo, selectedTimeline, tokenId]) const isSelectedAssetBridgeSupported = React.useMemo(() => { @@ -293,8 +328,13 @@ export const PortfolioAsset = () => { const goBack = React.useCallback(() => { dispatch(WalletPageActions.selectAsset({ asset: undefined, timeFrame: selectedTimeline })) - history.push(WalletRoutes.Portfolio) + dispatch(WalletPageActions.selectCoinMarket(undefined)) setfilteredAssetList(userAssetList) + if (isShowingMarketData) { + history.push(WalletRoutes.Market) + } else { + history.push(WalletRoutes.Portfolio) + } }, [ userAssetList, selectedTimeline @@ -428,7 +468,7 @@ export const PortfolioAsset = () => { {selectedAssetFromParams.name} - {selectedAssetFromParams.symbol} on {selectedAssetsNetwork?.chainName ?? ''} + {selectedAssetFromParams.symbol} { selectedAssetsNetwork?.chainName && `on ${selectedAssetsNetwork?.chainName}`} @@ -490,15 +530,29 @@ export const PortfolioAsset = () => { src='chrome-untrusted://nft-display' /> - + {isTokenSupported + ? + : <> + + {getLocale('braveWalletMarketDataCoinNotSupported')} + + } + + {isShowingMarketData && selectedCoinMarket && + + } ) } diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-overview.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-overview.tsx index fb652651cea3..ceef33adfe37 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-overview.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-overview.tsx @@ -248,7 +248,7 @@ export const PortfolioOverview = () => { defaultCurrencies={defaultCurrencies} action={() => onSelectAsset(item.asset)} key={ - item.asset.isErc721 + !item.asset.isErc721 ? `${item.asset.contractAddress}-${item.asset.symbol}-${item.asset.chainId}` : `${item.asset.contractAddress}-${item.asset.tokenId}-${item.asset.chainId}` } diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts b/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts index 5953e979a266..37ac8bd8f9a4 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/style.ts @@ -277,3 +277,14 @@ export const BridgeToAuroraButton = styled(WalletButton)` border: none; margin-bottom: 32px; ` + +export const NotSupportedText = styled.span` + font-family: Poppins; + font-size: 13px; + line-height: 20px; + letter-spacing: 0.01em; + font-weight: 600; + color: ${(p) => p.theme.color.text03}; + margin-left: 10px; + padding: 28px 0 40px; +` diff --git a/components/brave_wallet_ui/components/market-datatable/index.tsx b/components/brave_wallet_ui/components/market-datatable/index.tsx new file mode 100644 index 000000000000..5d4a0e3191c6 --- /dev/null +++ b/components/brave_wallet_ui/components/market-datatable/index.tsx @@ -0,0 +1,159 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Utils +import { getLocale } from '$web-common/locale' +import { BraveWallet, MarketDataTableColumnTypes, SortOrder } from '../../constants/types' +import Amount from '../../utils/amount' + +// Styled components +import { + AssetsColumnItemSpacer, + AssetsColumnWrapper, + StyledWrapper, + TableWrapper, + TextWrapper +} from './style' + +// Components +import { Table, Cell, Header, Row } from '../shared/datatable' +import { AssetNameAndIcon } from '../asset-name-and-icon' +import { AssetPriceChange } from '../asset-price-change' +import { CoinGeckoText } from '../desktop/views/portfolio/style' + +export interface MarketDataHeader extends Header { + id: MarketDataTableColumnTypes +} + +export interface Props { + headers: MarketDataHeader[] + coinMarketData: BraveWallet.CoinMarket[] + showEmptyState: boolean + onSort?: (column: MarketDataTableColumnTypes, newSortOrder: SortOrder) => void + onSelectCoinMarket: (coinMarket: BraveWallet.CoinMarket) => void +} + +const renderCells = (coinMarkDataItem: BraveWallet.CoinMarket) => { + const { + name, + symbol, + image, + currentPrice, + priceChange24h, + priceChangePercentage24h, + marketCap, + marketCapRank, + totalVolume + } = coinMarkDataItem + + const formattedPrice = new Amount(currentPrice).formatAsFiat('USD') + const formattedPercentageChange = new Amount(priceChangePercentage24h).value?.absoluteValue().toFixed(2) + '%' + const formattedMarketCap = new Amount(marketCap).abbreviate(1, 'USD', 'billion') + const formattedVolume = new Amount(totalVolume).abbreviate(1, 'USD') + const isDown = priceChange24h < 0 + + const cellsContent: React.ReactNode[] = [ + + {/* Hidden until wishlist feature is available on the backend */} + {/* + + */} + + {marketCapRank} + + + , + + // Price Column + {formattedPrice}, + + // Price Change Column + + + , + + // Market Cap Column + {formattedMarketCap}, + + // Volume Column + {formattedVolume} + + // Line Chart Column + // Commented out because priceHistory data is yet to be + // available from the backend + // + // {}} + // showPulsatingDot={false} + // showTooltip={false} + // customStyle={{ + // height: '20px', + // width: '100%', + // marginBottom: '0px' + // }} + // /> + // + ] + + const cells: Cell[] = cellsContent.map(cellContent => { + return { + content: cellContent + } + }) + + return cells +} + +export const MarketDataTable = (props: Props) => { + const { + headers, + coinMarketData, + showEmptyState, + onSort, + onSelectCoinMarket + } = props + + const rows: Row[] = React.useMemo(() => { + return coinMarketData.map((coinMarketItem: BraveWallet.CoinMarket) => { + return { + id: `coin-row-${coinMarketItem.symbol}-${coinMarketItem.marketCapRank}`, + content: renderCells(coinMarketItem), + data: coinMarketItem, + onClick: onSelectCoinMarket + } + }) + }, [coinMarketData]) + + return ( + + + + {/* Empty state message */} + {showEmptyState && getLocale('braveWalletMarketDataNoAssetsFound')} +
+
+ {getLocale('braveWalletPoweredByCoinGecko')} +
+ ) +} diff --git a/components/brave_wallet_ui/components/market-datatable/style.ts b/components/brave_wallet_ui/components/market-datatable/style.ts new file mode 100644 index 000000000000..566f49feb322 --- /dev/null +++ b/components/brave_wallet_ui/components/market-datatable/style.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' + +export const StyledWrapper = styled.div` + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + + & > div.infinite-scroll-component { + width: 100%; + } +` + +export const TableWrapper = styled.div` + width: 100%; +` + +export const AssetsColumnWrapper = styled.div` + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: left; +` + +export const AssetsColumnItemSpacer = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 19px; +` +export const TextWrapper = styled.div<{ alignment: 'right' | 'left' | 'center' }>` + display: flex; + justify-content: ${p => { + switch (p.alignment) { + case 'left': + return 'flex-start' + case 'right': + return 'flex-end' + case 'center': + return 'center' + default: + return 'center' + } + }}; + width: 100%; + min-width: 30px; + font-family: Poppins; + font-size: 14px; + letter-spacing: 0.01em; +` + +export const LineChartWrapper = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + height: 30px; + max-width: 120px; + margin: 0 auto; +` +export const LoadIconWrapper = styled.div` + display: flex; + justify-content: center; +` +export const CoinGeckoText = styled.span` + font-family: Arial; + font-size: 10px; + font-weight: normal; + color: ${(p) => p.theme.color.text03}; + margin: 15px 0px; +` diff --git a/components/brave_wallet_ui/components/shared/datatable/index.tsx b/components/brave_wallet_ui/components/shared/datatable/index.tsx new file mode 100644 index 000000000000..2ec45cfb89b1 --- /dev/null +++ b/components/brave_wallet_ui/components/shared/datatable/index.tsx @@ -0,0 +1,138 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react' +import { CSSProperties } from 'styled-components' + +import { + StyledWrapper, + StyledTH, + StyledNoContent, + StyledTable, + StyledTHead, + StyledTBody, + StyledTD, + ArrowDown, + ArrowUp, + ArrowWrapper +} from './style' + +import { SortOrder } from '../../../constants/types' + +export interface Header { + id: string + customStyle?: CSSProperties + content: React.ReactNode + sortable?: boolean + sortOrder?: SortOrder +} +export interface Cell { + customStyle?: CSSProperties + content: React.ReactNode +} + +export interface Row { + id: string + customStyle?: CSSProperties + content: Cell[] + data: any + onClick?: (data: any) => void +} + +export interface Props { + id?: string + headers?: Header[] + children?: React.ReactNode + rows?: Row[] + rowTheme?: {[key: string]: string} + stickyHeaders?: boolean + onSort?: (column: string, newSortOrder: SortOrder) => void +} + +export const Table = (props: Props) => { + const { id, headers, rows, children, stickyHeaders, onSort } = props + + const onHeaderClick = React.useCallback((header: Header) => () => { + if (!header.sortable) return + const currentSortOrder = header.sortOrder ?? 'desc' + const newSortOrder = currentSortOrder === 'asc' ? 'desc' : 'asc' + + if (onSort) { + onSort(header.id, newSortOrder) + } + }, [onSort]) + + const onRowClick = React.useCallback((row: Row) => () => { + if (row.onClick) { + row.onClick(row.data) + } + }, []) + + return ( + + {headers && headers.length > 0 && + + {headers && + + + { + headers.map((header) => + + {header.sortable && + + {header.sortOrder === 'asc' ? : null} + {header.sortOrder === 'desc' ? : null} + + } + {header.content} + + ) + } + + + } + {rows && + + { + rows.map((row: Row, i: number) => + + { + row.content.map((cell: Cell, j: number) => + + {cell.content} + + ) + } + + ) + } + + } + + } + {(!rows || rows.length === 0) && + + {children} + + } + + ) +} diff --git a/components/brave_wallet_ui/components/shared/datatable/style.ts b/components/brave_wallet_ui/components/shared/datatable/style.ts new file mode 100644 index 000000000000..258f7885115d --- /dev/null +++ b/components/brave_wallet_ui/components/shared/datatable/style.ts @@ -0,0 +1,118 @@ +/* Copyright (c) 2022 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { SortOrder } from '../../../constants/types' +import styled from 'styled-components' + +import ArrowDownIcon from '../../../assets/svg-icons/arrow-down-fill.svg' +import ArrowUpIcon from '../../../assets/svg-icons/arrow-up-fill.svg' + +export interface StyleProps { + sortOrder: SortOrder + sortable?: boolean + stickyHeaders?: boolean +} + +export const StyledWrapper = styled.div` + width: 100%; +` + +export const StyledTBody = styled.tbody` + ::before { + content: ''; + display: block; + height: 24px; + } +` + +export const StyledNoContent = styled('div')<{}>` + text-align: center; + padding: 30px 0; + color: ${p => p.theme.color.text03}; + font-size: 14px; + font-family: Poppins; +` + +export const StyledTable = styled('table')` + position: relative; + min-width: 100%; + border-collapse: separate; + border-spacing: 0; + + tr { + cursor: pointer; + } +` + +export const StyledTHead = styled('thead')` +` + +export const StyledTH = styled('th')>` + text-align: left; + font-family: Poppins; + font-size: 12px; + font-weight: 600; + line-height: 18px; + letter-spacing: 0.01em; + border-bottom: ${(p) => `2px solid ${p.theme.color.disabled}`}; + color: ${(p) => p.sortOrder !== undefined ? p.theme.color.text02 : p.theme.color.text03}; + padding: 10px 0 10px 0px; + cursor: ${p => p.sortable ? 'pointer' : 'default'}; + position: ${p => p.stickyHeaders ? 'sticky' : 'relative'}; + background-color: ${(p) => p.theme.color.background02}; + top: ${p => p.stickyHeaders ? 0 : 'inherit'}; + z-index: 2; + + &:hover { + color: ${p => p.sortable ? p.theme.color.text02 : p.theme.color.text03}; + } + + &:last-child { + padding-right: 20px; + } +` + +export const StyledTD = styled('td')` + vertical-align: middle; + letter-spacing: 0.01em; + font-family:Poppins; + font-size: 14px; + font-weight: 400; + color: ${p => p.theme.color.text01}; + font-family: Poppins; + font-size: 14px; + line-height: 20px; + padding: 0 0 16px 10px; + + &:last-child { + padding-right: 20px; + } +` +export const ArrowWrapper = styled.div` + display: inline-flex; + align-items: center; + justify-content: center; + width: 9px; + height: 6px; + margin-right: 3px; +` + +export const ArrowBase = styled.div` + display: inline-flex; + width: 100%; + height: 100%; + background-repeat: no-repeat; +` +export const ArrowDown = styled(ArrowBase)` + background-image: url(${ArrowDownIcon}); +` + +export const ArrowUp = styled(ArrowBase)` + background-image: url(${ArrowUpIcon}); +` +export const CellContentWrapper = styled.div` + display: flex; + align-items: center; +` diff --git a/components/brave_wallet_ui/components/shared/search-bar/index.tsx b/components/brave_wallet_ui/components/shared/search-bar/index.tsx index 4db77a00a1b5..39cd46c68937 100644 --- a/components/brave_wallet_ui/components/shared/search-bar/index.tsx +++ b/components/brave_wallet_ui/components/shared/search-bar/index.tsx @@ -12,11 +12,12 @@ export interface Props { action?: (event: any) => void | undefined autoFocus?: boolean value?: string + disabled?: boolean } export default class SearchBar extends React.PureComponent { render () { - const { autoFocus, placeholder, action, value } = this.props + const { autoFocus, placeholder, action, value, disabled } = this.props return ( @@ -25,6 +26,7 @@ export default class SearchBar extends React.PureComponent { value={value} placeholder={placeholder} onChange={action} + disabled={disabled} /> ) diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index efd91c7ca6c5..9514f566dcc8 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -126,6 +126,7 @@ export type TopTabNavTypes = | 'portfolio' | 'apps' | 'accounts' + | 'market' export type AddAccountNavTypes = | 'create' @@ -236,6 +237,8 @@ export interface WalletState { defaultCurrencies: DefaultCurrencies transactionProviderErrorRegistry: TransactionProviderErrorRegistry defaultNetworks: BraveWallet.NetworkInfo[] + isLoadingCoinMarketData: boolean + coinMarketData: BraveWallet.CoinMarket[] selectedNetworkFilter: BraveWallet.NetworkInfo selectedAssetFilter: AssetFilterOption defaultAccounts: BraveWallet.AccountInfo[] @@ -287,6 +290,7 @@ export interface PageState { isImportWalletsCheckComplete: boolean importWalletAttempts: number walletTermsAcknowledged: boolean + selectedCoinMarket: BraveWallet.CoinMarket | undefined } export interface WalletPageState { @@ -626,6 +630,10 @@ export enum WalletRoutes { FundWalletPage = '/crypto/fund-wallet', DepositFundsPage = '/crypto/deposit-funds', + // market + Market = '/crypto/market', + MarketSub = '/crypto/market/:id?', + // accounts Accounts = '/crypto/accounts', Account = '/crypto/accounts/:id', @@ -757,3 +765,31 @@ export interface AssetFilterOption { } export type ImportAccountErrorType = boolean | undefined + +export type MarketAssetFilterOption = + | 'all' + | 'tradable' + +export type AssetFilter = { + value: MarketAssetFilterOption + label: string +} + +export type SortOrder = + | 'asc' + | 'desc' + +export type MarketDataTableColumnTypes = + | 'assets' + | 'currentPrice' + | 'duration' + | 'totalVolume' + | 'marketCap' + | 'priceChange24h' + | 'priceChangePercentage24h' + +export type AbbreviationOptions = + | 'thousand' + | 'million' + | 'billion' + | 'trillion' diff --git a/components/brave_wallet_ui/market/BUILD.gn b/components/brave_wallet_ui/market/BUILD.gn new file mode 100644 index 000000000000..a79578eeee2c --- /dev/null +++ b/components/brave_wallet_ui/market/BUILD.gn @@ -0,0 +1,29 @@ +# Copyright (c) 2021 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. + +import("//brave/components/common/typescript.gni") +import("//chrome/common/features.gni") +import("//mojo/public/tools/bindings/mojom.gni") +import("//tools/grit/preprocess_if_expr.gni") +import("//tools/grit/repack.gni") + +transpile_web_ui("market_display_ui") { + entry_points = [ [ + "market", + rebase_path("market.tsx"), + ] ] + webpack_aliases = [ "browser" ] + resource_name = "market_display" + deps = [ + "//brave/components/brave_wallet/common:mojom_js", + "//brave/components/brave_wallet/common:preprocess_mojo", + ] +} + +pack_web_resources("market_display_generated") { + resource_name = "market_display" + output_dir = "$root_gen_dir/brave/components/market_display/resources" + deps = [ ":market_display_ui" ] +} diff --git a/components/brave_wallet_ui/market/css/market.css b/components/brave_wallet_ui/market/css/market.css new file mode 100644 index 000000000000..07886ac1a968 --- /dev/null +++ b/components/brave_wallet_ui/market/css/market.css @@ -0,0 +1,9 @@ +body { + -ms-overflow-style: none; + scrollbar-width: none; + overflow-y: scroll; +} + +body::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/components/brave_wallet_ui/market/market-ui-messages.ts b/components/brave_wallet_ui/market/market-ui-messages.ts new file mode 100644 index 000000000000..639e21282069 --- /dev/null +++ b/components/brave_wallet_ui/market/market-ui-messages.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { loadTimeData } from '../../common/loadTimeData' +import { BraveWallet } from '../constants/types' + +const marketUiOrigin = loadTimeData.getString('braveWalletMarketUiBridgeUrl') + +// remove trailing / +export const braveMarketUiOrigin = marketUiOrigin.endsWith('/') ? marketUiOrigin.slice(0, -1) : marketUiOrigin +export const braveWalletOrigin = 'chrome://wallet' + +export const enum MarketUiCommand { + UpdateCoinMarkets = 'update-coin-markets', + SelectCoinMarket = 'select-coin-market', + UpdateTradableAssets = 'update-tradable-assets' +} + +export type MarketCommandMessage = { + command: MarketUiCommand +} + +export type UpdateCoinMarketMessage = MarketCommandMessage & { + payload: BraveWallet.CoinMarket[] +} + +export type SelectCoinMarketMessage = MarketCommandMessage & { + payload: BraveWallet.CoinMarket +} + +export type UpdateTradableAssetsMessage = MarketCommandMessage & { + payload: BraveWallet.BlockchainToken[] +} + +export const sendMessageToMarketUiFrame = (targetWindow: Window | null, message: MarketCommandMessage) => { + if (targetWindow) { + targetWindow.postMessage(message, braveMarketUiOrigin) + } +} + +export const sendMessageToWalletUi = (targetWindow: Window | null, message: MarketCommandMessage) => { + if (targetWindow) { + targetWindow.postMessage(message, braveWalletOrigin) + } +} diff --git a/components/brave_wallet_ui/market/market.html b/components/brave_wallet_ui/market/market.html new file mode 100644 index 000000000000..da09db4b0ab5 --- /dev/null +++ b/components/brave_wallet_ui/market/market.html @@ -0,0 +1,11 @@ + + + + Market iframe + + + + + +
+ diff --git a/components/brave_wallet_ui/market/market.tsx b/components/brave_wallet_ui/market/market.tsx new file mode 100644 index 000000000000..39bfabe49a6b --- /dev/null +++ b/components/brave_wallet_ui/market/market.tsx @@ -0,0 +1,160 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { render } from 'react-dom' +import { BrowserRouter } from 'react-router-dom' +import { initLocale } from 'brave-ui' +import { loadTimeData } from '../../common/loadTimeData' + +// css +import 'emptykit.css' +import '../../../ui/webui/resources/fonts/poppins.css' +import './css/market.css' + +// theme setup +import BraveCoreThemeProvider from '../../common/BraveCoreThemeProvider' +import walletDarkTheme from '../theme/wallet-dark' +import walletLightTheme from '../theme/wallet-light' + +// constants +import { BraveWallet, MarketAssetFilterOption, MarketDataTableColumnTypes, SortOrder } from '../constants/types' + +// utils +import { + braveWalletOrigin, + MarketCommandMessage, + MarketUiCommand, + SelectCoinMarketMessage, + sendMessageToWalletUi, + UpdateCoinMarketMessage, + UpdateTradableAssetsMessage + +} from './market-ui-messages' +import { filterCoinMarkets, searchCoinMarkets, sortCoinMarkets } from '../utils/coin-market-utils' + +// Options +import { AssetFilterOptions } from '../options/market-data-filter-options' +import { marketDataTableHeaders } from '../options/market-data-headers' + +// components +import { MarketDataTable } from '../components/market-datatable' +import { TopRow } from '../components/desktop/views/market/style' +import { AssetsFilterDropdown } from '../components/desktop' +import { SearchBar } from '../components/shared' + +const App = () => { + // State + const [tableHeaders, setTableHeaders] = React.useState(marketDataTableHeaders) + const [currentFilter, setCurrentFilter] = React.useState('all') + const [sortOrder, setSortOrder] = React.useState('desc') + const [sortByColumnId, setSortByColumnId] = React.useState('marketCap') + const [searchTerm, setSearchTerm] = React.useState('') + const [coinMarkets, setCoinMarkets] = React.useState([]) + const [tradableAssets, setTradableAssets] = React.useState([]) + + // Memos + const visibleCoinMarkets = React.useMemo(() => { + const searchResults = searchTerm === '' ? coinMarkets : searchCoinMarkets(coinMarkets, searchTerm) + const filteredCoins = filterCoinMarkets(searchResults, tradableAssets, currentFilter) + return [...sortCoinMarkets(filteredCoins, sortOrder, sortByColumnId)] + }, [coinMarkets, sortOrder, sortByColumnId, searchTerm, currentFilter]) + + const onSelectFilter = (value: MarketAssetFilterOption) => { + setCurrentFilter(value) + } + + const onMessageEventListener = React.useCallback((event: MessageEvent) => { + // validate message origin + if (event.origin !== braveWalletOrigin) return + + const message = event.data + switch (message.command) { + case MarketUiCommand.UpdateCoinMarkets: { + const { payload } = message as UpdateCoinMarketMessage + setCoinMarkets(payload) + break + } + + case MarketUiCommand.UpdateTradableAssets: { + const { payload } = message as UpdateTradableAssetsMessage + setTradableAssets(payload) + } + } + }, []) + + const onSort = React.useCallback((columnId: MarketDataTableColumnTypes, newSortOrder: SortOrder) => { + const updatedTableHeaders = tableHeaders.map(header => { + if (header.id === columnId) { + return { + ...header, + sortOrder: newSortOrder + } + } else { + return { + ...header, + sortOrder: undefined + } + } + }) + + setTableHeaders(updatedTableHeaders) + setSortByColumnId(columnId) + setSortOrder(newSortOrder) + }, []) + + const onSelectCoinMarket = React.useCallback((coinMarket: BraveWallet.CoinMarket) => { + const message: SelectCoinMarketMessage = { + command: MarketUiCommand.SelectCoinMarket, + payload: coinMarket + } + sendMessageToWalletUi(parent, message) + }, []) + + React.useEffect(() => { + window.addEventListener('message', onMessageEventListener) + return () => window.removeEventListener('message', onMessageEventListener) + }, []) + + return ( + + + <> + + + { + setSearchTerm(event.target.value) + }} + /> + + + + + +) +} + +function initialize () { + initLocale(loadTimeData.data_) + render(, document.getElementById('mountPoint')) +} + +document.addEventListener('DOMContentLoaded', initialize) diff --git a/components/brave_wallet_ui/options/market-data-filter-options.ts b/components/brave_wallet_ui/options/market-data-filter-options.ts new file mode 100644 index 000000000000..1fb8c5a19e1b --- /dev/null +++ b/components/brave_wallet_ui/options/market-data-filter-options.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { AssetFilter } from '../constants/types' +import { getLocale } from '$web-common/locale' + +export const AssetFilterOptions: AssetFilter[] = [ + { + value: 'all', + label: getLocale('braveWalletMarketDataAllAssetsFilter') + }, + { + value: 'tradable', + label: getLocale('braveWalletMarketDataTradableFilter') + } +] diff --git a/components/brave_wallet_ui/options/market-data-headers.ts b/components/brave_wallet_ui/options/market-data-headers.ts new file mode 100644 index 000000000000..ac6803aa3999 --- /dev/null +++ b/components/brave_wallet_ui/options/market-data-headers.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// you can obtain one at http://mozilla.org/MPL/2.0/. + +import { getLocale } from '$web-common/locale' +import { MarketDataHeader } from '../components/market-datatable' + +// The id field matches values in MarketDataTableColumnTypes +// which match property fields in CoinMarketMetadata +// this helps in finding the correct header to sort by +export const marketDataTableHeaders: MarketDataHeader[] = [ + { + id: 'assets', + content: getLocale('braveWalletMarketDataAssetsColumn'), + customStyle: { + width: 350, + paddingLeft: 10 + } + }, + { + id: 'currentPrice', + content: getLocale('braveWalletMarketDataPriceColumn'), + sortable: true, + customStyle: { + width: 150, + textAlign: 'right' + } + }, + { + id: 'priceChangePercentage24h', + content: getLocale('braveWalletMarketData24HrColumn'), + sortable: true, + customStyle: { + textAlign: 'right' + } + }, + { + id: 'marketCap', + content: getLocale('braveWalletMarketDataMarketCapColumn'), + sortable: true, + sortOrder: 'desc', + customStyle: { + textAlign: 'right' + } + }, + { + id: 'totalVolume', + content: getLocale('braveWalletMarketDataVolumeColumn'), + sortable: true, + customStyle: { + textAlign: 'right' + } + } + // Hiden because price History data is not available + // { + // id: 'lineGraph', + // content: '' + // } +] diff --git a/components/brave_wallet_ui/options/top-nav-options.ts b/components/brave_wallet_ui/options/top-nav-options.ts index df59a1b7f92c..21d008f86675 100644 --- a/components/brave_wallet_ui/options/top-nav-options.ts +++ b/components/brave_wallet_ui/options/top-nav-options.ts @@ -4,13 +4,17 @@ // you can obtain one at http://mozilla.org/MPL/2.0/. import { TopTabNavObjectType } from '../constants/types' -import { getLocale } from '../../common/locale' +import { getLocale } from '$web-common/locale' export const TopNavOptions = (): TopTabNavObjectType[] => [ { id: 'portfolio', name: getLocale('braveWalletTopNavPortfolio') }, + { + id: 'market', + name: getLocale('braveWalletTopNavMarket') + }, { id: 'accounts', name: getLocale('braveWalletTopNavAccounts') diff --git a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts index 43f9d9b88f3f..db4bb3b4562b 100644 --- a/components/brave_wallet_ui/page/actions/wallet_page_actions.ts +++ b/components/brave_wallet_ui/page/actions/wallet_page_actions.ts @@ -66,3 +66,4 @@ export const setIsFetchingNFTMetadata = createAction('setIsFetchingNFTM export const updateNFTMetadata = createAction('updateNFTMetadata') export const onOnboardingShown = createAction('onOnboardingShown') export const agreeToWalletTerms = createAction('agreeToWalletTerms') +export const selectCoinMarket = createAction('selectCoinMarket') diff --git a/components/brave_wallet_ui/page/reducers/page_reducer.ts b/components/brave_wallet_ui/page/reducers/page_reducer.ts index 562dabc84b79..e05339dbd7f1 100644 --- a/components/brave_wallet_ui/page/reducers/page_reducer.ts +++ b/components/brave_wallet_ui/page/reducers/page_reducer.ts @@ -41,7 +41,8 @@ const defaultState: PageState = { isMetaMaskInitialized: false, isImportWalletsCheckComplete: false, importWalletAttempts: 0, - walletTermsAcknowledged: false + walletTermsAcknowledged: false, + selectedCoinMarket: undefined } export const createPageReducer = (initialState: PageState) => { @@ -209,6 +210,12 @@ export const createPageReducer = (initialState: PageState) => { } }) + reducer.on(Actions.selectCoinMarket, (state: PageState, payload: BraveWallet.CoinMarket) => { + return { + ...state, + selectedCoinMarket: payload + } + }) return reducer } diff --git a/components/brave_wallet_ui/page/wallet_page.html b/components/brave_wallet_ui/page/wallet_page.html index dfe168b13fee..8107f23b0f5d 100644 --- a/components/brave_wallet_ui/page/wallet_page.html +++ b/components/brave_wallet_ui/page/wallet_page.html @@ -11,6 +11,11 @@