From d3f58d359c81e9b0013a0ceb3a86082162c65115 Mon Sep 17 00:00:00 2001 From: brave-builds Date: Wed, 30 Oct 2024 20:52:37 +0000 Subject: [PATCH] Uplift of #25088 (squashed) to beta --- .../android/android_wallet_page_ui.cc | 8 +- .../browser/brave_wallet_constants.h | 22 + .../common/async/__mocks__/bridge.ts | 27 +- .../common/constants/local-storage-keys.ts | 3 +- .../brave_wallet_ui/common/constants/mocks.ts | 264 +++++++++ .../hooks/use-multi-chain-buy-assets.ts | 65 ++- .../common/slices/api-base.slice.ts | 10 +- .../common/slices/api.slice.ts | 12 +- .../endpoints/meld_integration.endpoints.ts | 347 ++++++++++++ .../assets/page_terms_graphic.svg | 266 +++++++++ .../assets/panel_terms_graphic.svg | 266 +++++++++ .../partners_consent_modal.stories.tsx | 32 ++ .../partners_consent_modal.style.ts | 48 ++ .../partners_consent_modal.tsx | 114 ++++ .../components/desktop/views/market/index.tsx | 46 +- .../desktop/views/market/market_asset.tsx | 54 +- .../buy-send-swap-deposit-nav.tsx | 21 +- .../portfolio/portfolio-fungible-asset.tsx | 29 +- .../desktop/wallet-menus/asset-item-menu.tsx | 32 +- .../wallet-page-wrapper.style.ts | 2 +- .../shared/bottom_sheet/bottom_sheet.style.ts | 2 +- .../components/shared/style.tsx | 6 +- components/brave_wallet_ui/constants/types.ts | 12 + .../market/market-ui-messages.ts | 4 +- components/brave_wallet_ui/market/market.tsx | 13 +- .../page/router/unlocked_wallet_routes.tsx | 147 +++-- .../amount_button/amount_button.stories.tsx | 43 ++ .../amount_button/amount_button.style.ts | 63 +++ .../amount_button/amount_button.tsx | 85 +++ .../buy_quote/buy_quote.stories.tsx | 35 ++ .../components/buy_quote/buy_quote.style.ts | 133 +++++ .../components/buy_quote/buy_quote.tsx | 234 ++++++++ .../select_account/select_account.stories.tsx | 35 ++ .../select_account/select_account.style.ts | 17 + .../select_account/select_account.tsx | 153 +++++ .../select_account_button.stories.tsx | 32 ++ .../select_account_button.style.ts | 38 ++ .../select_account_button.tsx | 62 ++ .../select_asset/select_asset.stories.tsx | 48 ++ .../select_asset/select_asset.style.ts | 77 +++ .../components/select_asset/select_asset.tsx | 429 ++++++++++++++ .../select_asset_button.stories.tsx | 34 ++ .../select_asset_button.tsx | 99 ++++ .../select_currency.stories.tsx | 44 ++ .../select_currency/select_currency.style.ts | 75 +++ .../select_currency/select_currency.tsx | 188 ++++++ .../fund-wallet/components/shared/style.ts | 138 +++++ .../page/screens/fund-wallet/fund-wallet.tsx | 6 +- .../fund-wallet/fund_wallet_v2.stories.tsx | 29 + .../fund-wallet/fund_wallet_v2.style.ts | 124 ++++ .../screens/fund-wallet/fund_wallet_v2.tsx | 392 +++++++++++++ .../page/screens/fund-wallet/hooks/useBuy.ts | 535 ++++++++++++++++++ components/brave_wallet_ui/stories/locale.ts | 26 +- .../utils/coin-market-utils.ts | 10 +- .../brave_wallet_ui/utils/meld_utils.ts | 69 +++ .../brave_wallet_ui/utils/routes-utils.ts | 21 +- components/resources/wallet_strings.grdp | 19 +- ui/webui/resources/BUILD.gn | 2 +- 58 files changed, 4980 insertions(+), 167 deletions(-) create mode 100644 components/brave_wallet_ui/common/slices/endpoints/meld_integration.endpoints.ts create mode 100644 components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/page_terms_graphic.svg create mode 100644 components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/panel_terms_graphic.svg create mode 100644 components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.stories.tsx create mode 100644 components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.style.ts create mode 100644 components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/components/shared/style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.stories.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.style.ts create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.tsx create mode 100644 components/brave_wallet_ui/page/screens/fund-wallet/hooks/useBuy.ts create mode 100644 components/brave_wallet_ui/utils/meld_utils.ts diff --git a/browser/ui/webui/brave_wallet/android/android_wallet_page_ui.cc b/browser/ui/webui/brave_wallet/android/android_wallet_page_ui.cc index 292d2cc42166..627e9723102d 100644 --- a/browser/ui/webui/brave_wallet/android/android_wallet_page_ui.cc +++ b/browser/ui/webui/brave_wallet/android/android_wallet_page_ui.cc @@ -70,10 +70,10 @@ AndroidWalletPageUI::AndroidWalletPageUI(content::WebUI* web_ui, kUntrustedLineChartURL + " " + kUntrustedMarketURL + ";"); source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ImgSrc, - base::JoinString( - {"img-src", "'self'", "chrome://resources", - "chrome://erc-token-images", base::StrCat({"data:", ";"})}, - " ")); + base::JoinString({"img-src", "'self'", "chrome://resources", + "chrome://erc-token-images", "chrome://image", + base::StrCat({"data:", ";"})}, + " ")); source->AddString("braveWalletTrezorBridgeUrl", kUntrustedTrezorURL); source->AddString("braveWalletNftBridgeUrl", kUntrustedNftURL); source->AddString("braveWalletLineChartBridgeUrl", kUntrustedLineChartURL); diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index 184493fcb5d4..0b19884b37d1 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -1058,6 +1058,28 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletBuyWithRamp", IDS_BRAVE_WALLET_BUY_WITH_RAMP}, {"braveWalletSellWithProvider", IDS_BRAVE_WALLET_SELL_WITH_PROVIDER}, {"braveWalletBuyDisclaimer", IDS_BRAVE_WALLET_BUY_DISCLAIMER}, + {"braveWalletTransactionsPartner", IDS_BRAVE_WALLET_TRANSACTIONS_PARTNER}, + {"braveWalletTransactionPartnerConsent", + IDS_BRAVE_WALLET_TRANSACTION_PARTNER_CONSENT}, + {"braveWalletMeldTermsOfUse", IDS_BRAVE_WALLET_MELD_TERMS_OF_USE}, + {"braveWalletBestOption", IDS_BRAVE_WALLET_BEST_OPTION}, + {"braveWalletExchangeRateWithFees", + IDS_BRAVE_WALLET_EXCHANGE_RATE_WITH_FEES}, + {"braveWalletFees", IDS_BRAVE_WALLET_FEES}, + {"braveWalletPriceCurrency", IDS_BRAVE_WALLET_PRICE_CURRENCY}, + {"braveWalletBuyWithProvider", IDS_BRAVE_WALLET_BUY_WITH_PROVIDER}, + {"braveWalletAsset", IDS_BRAVE_WALLET_ASSETS}, + {"braveWalletSelected", IDS_BRAVE_WALLET_SELECTED}, + {"braveWalletNoAvailableCurrencies", + IDS_BRAVE_WALLET_NO_AVAILABLE_CURRENCIES}, + {"braveWalletGettingBestPrices", IDS_BRAVE_WALLET_GETTING_BEST_PRICES}, + {"braveWalletBuyAsset", IDS_BRAVE_WALLET_BUY_ASSET}, + {"braveWalletNoProviderFound", IDS_BRAVE_WALLET_NO_PROVIDER_FOUND}, + {"braveWalletTrySearchingForDifferentAsset", + IDS_BRAVE_WALLET_TRY_SEARCHING_FOR_DIFFERENT_ASSET}, + {"braveWalletNoResultsFound", IDS_BRAVE_WALLET_NO_RESULTS_FOUND}, + {"braveWalletTryDifferentKeywords", + IDS_BRAVE_WALLET_TRY_DIFFERENT_KEYWORDS}, {"braveWalletBuyWithSardine", IDS_BRAVE_WALLET_BUY_WITH_SARDINE}, {"braveWalletBuyWithTransak", IDS_BRAVE_WALLET_BUY_WITH_TRANSAK}, {"braveWalletBuyWithStripe", IDS_BRAVE_WALLET_BUY_WITH_STRIPE}, diff --git a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts index f586bf8a3670..509963347182 100644 --- a/components/brave_wallet_ui/common/async/__mocks__/bridge.ts +++ b/components/brave_wallet_ui/common/async/__mocks__/bridge.ts @@ -10,7 +10,12 @@ import { assert } from 'chrome://resources/js/assert.js' import type { AnyAction } from 'redux' // types -import { BraveWallet, CommonNftMetadata } from '../../../constants/types' +import { + BraveWallet, + CommonNftMetadata, + MeldFiatCurrency, + MeldFilter +} from '../../../constants/types' import { WalletActions } from '../../actions' import type WalletApiProxy from '../../wallet_api_proxy' @@ -562,6 +567,26 @@ export class MockedWalletApiProxy { }) } + meldIntegrationService: Partial< + InstanceType + > = { + getFiatCurrencies: async ( + filter: MeldFilter + ): Promise<{ + fiatCurrencies: MeldFiatCurrency[] | null + error: string[] | null + }> => ({ + fiatCurrencies: [ + { + currencyCode: 'usd', + name: 'United States Currency', + symbolImageUrl: '' + } + ], + error: null + }) + } + keyringService: Partial< InstanceType > = { diff --git a/components/brave_wallet_ui/common/constants/local-storage-keys.ts b/components/brave_wallet_ui/common/constants/local-storage-keys.ts index 90b579c100c3..77d665d41271 100644 --- a/components/brave_wallet_ui/common/constants/local-storage-keys.ts +++ b/components/brave_wallet_ui/common/constants/local-storage-keys.ts @@ -34,7 +34,8 @@ export const LOCAL_STORAGE_KEYS = { GROUP_PORTFOLIO_NFTS_BY_COLLECTION: 'GROUP_PORTFOLIO_NFTS_BY_COLLECTION', NFT_COLLECTION_NAMES_REGISTRY: 'NFT_COLLECTION_NAMES_REGISTRY2', FILTERED_OUT_DAPP_NETWORK_KEYS: 'BRAVE_WALLET_FILTERED_OUT_DAPP_NETWORK_KEYS', - FILTERED_OUT_DAPP_CATEGORIES: 'BRAVE_WALLET_FILTERED_OUT_DAPP_CATEGORIES' + FILTERED_OUT_DAPP_CATEGORIES: 'BRAVE_WALLET_FILTERED_OUT_DAPP_CATEGORIES', + HAS_ACCEPTED_PARTNER_TERMS: 'BRAVE_WALLET_HAS_ACCEPTED_PARTNER_TERMS' } as const const LOCAL_STORAGE_KEYS_DEPRECATED = { diff --git a/components/brave_wallet_ui/common/constants/mocks.ts b/components/brave_wallet_ui/common/constants/mocks.ts index 92b4d155d965..3b098fa683d3 100644 --- a/components/brave_wallet_ui/common/constants/mocks.ts +++ b/components/brave_wallet_ui/common/constants/mocks.ts @@ -6,6 +6,10 @@ // types import { BraveWallet, + MeldCountry, + MeldCryptoCurrency, + MeldFiatCurrency, + MeldPaymentMethod, SerializableTransactionInfo, SpotPriceRegistry } from '../../constants/types' @@ -1740,3 +1744,263 @@ export const mockTokenBalanceRegistry: TokenBalancesRegistry = { } } } + +export const mockMeldFiatCurrency: MeldFiatCurrency = { + currencyCode: 'USD', + name: 'United States Dollar', + symbolImageUrl: '' +} + +export const mockMeldFiatCurrencies: MeldFiatCurrency[] = [ + { + 'currencyCode': 'AFN', + 'name': 'Afghani', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/AFN/symbol.png' + }, + { + 'currencyCode': 'DZD', + 'name': 'Algerian Dinar', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/DZD/symbol.png' + }, + { + 'currencyCode': 'ARS', + 'name': 'Argentine Peso', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/ARS/symbol.png' + }, + { + 'currencyCode': 'AMD', + 'name': 'Armenian Dram', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/AMD/symbol.png' + }, + { + 'currencyCode': 'AWG', + 'name': 'Aruban Florin', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/AWG/symbol.png' + }, + { + 'currencyCode': 'AUD', + 'name': 'Australian Dollar', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/AUD/symbol.png' + }, + { + 'currencyCode': 'AZN', + 'name': 'Azerbaijan Manat', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/AZN/symbol.png' + }, + { + 'currencyCode': 'BSD', + 'name': 'Bahamian Dollar', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/BSD/symbol.png' + }, + { + 'currencyCode': 'BHD', + 'name': 'Bahraini Dinar', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/BHD/symbol.png' + }, + { + 'currencyCode': 'THB', + 'name': 'Baht', + 'symbolImageUrl': 'https://images-currency.meld.io/fiat/THB/symbol.png' + } +] + +export const mockMeldCryptoCurrencies: MeldCryptoCurrency[] = [ + { + 'currencyCode': '00', + 'name': '00 Token', + 'chainCode': 'ETH', + 'chainName': 'Ethereum', + 'chainId': '0x1', + 'contractAddress': undefined, + 'symbolImageUrl': 'https://images-currency.meld.io/crypto/00/symbol.png' + }, + { + 'currencyCode': 'ZRX', + 'name': '0x', + 'chainCode': 'ETH', + 'chainName': 'Ethereum', + 'chainId': '0x1', + 'contractAddress': '0xe41d2489571d322189246dafa5ebde1f4699f498', + 'symbolImageUrl': 'https://images-currency.meld.io/crypto/ZRX/symbol.png' + }, + { + 'currencyCode': 'OXD_FTM', + 'name': '0xDAO', + 'chainCode': 'FTM', + 'chainName': 'Fantom', + 'chainId': '0xfa', + 'contractAddress': undefined, + 'symbolImageUrl': + 'https://images-currency.meld.io/crypto/OXD_FTM/symbol.png' + }, + { + 'currencyCode': '1INCH', + 'name': '1inch', + 'chainCode': 'ETH', + 'chainName': 'Ethereum', + 'chainId': '0x1', + 'contractAddress': '0x111111111117dc0aa78b770fa6a738034120c302', + 'symbolImageUrl': 'https://images-currency.meld.io/crypto/1INCH/symbol.png' + }, + { + 'currencyCode': '1INCH_BSC', + 'name': '1inch', + 'chainCode': 'BSC', + 'chainName': 'BNB Smart Chain', + 'chainId': '0x38', + 'contractAddress': '0x111111111117dc0aa78b770fa6a738034120c302', + 'symbolImageUrl': + 'https://images-currency.meld.io/crypto/1INCH_BSC/symbol.png' + } +] + +export const mockMeldCryptoQuotes = [ + { + 'transactionType': 'CRYPTO_PURCHASE', + 'exchangeRate': '3355.431', + 'transactionFee': '4.99', + 'sourceCurrencyCode': 'USD', + 'sourceAmount': '100', + 'sourceAmountWithoutFee': '93.23', + 'fiatAmountWithoutFees': '93.23', + 'totalFee': '6.77', + 'networkFee': '0.78', + 'paymentMethod': 'CREDIT_DEBIT_CARD', + 'destinationCurrencyCode': 'ETH', + 'destinationAmount': '0.02980243', + 'destinationAmountWithoutFees': undefined, + 'customerScore': '20.0', + 'serviceProvider': 'TRANSAK', + 'countryCode': 'US' + } +] + +export const mockServiceProviders = [ + { + 'name': 'Transak', + 'serviceProvider': 'TRANSAK', + 'status': 'LIVE', + 'webSiteUrl': 'https://transak.com', + 'categories': ['CRYPTO_OFFRAMP', 'CRYPTO_ONRAMP'], + 'categoryStatuses': { + 'CRYPTO_OFFRAMP': 'LIVE', + 'CRYPTO_ONRAMP': 'LIVE' + }, + 'logoImages': { + 'darkUrl': 'https://images-serviceprovider.meld.io/TRANSAK/logo_dark.png', + 'darkShortUrl': + 'https://images-serviceprovider.meld.io/TRANSAK/short_logo_dark.png', + 'lightUrl': + 'https://images-serviceprovider.meld.io/TRANSAK/logo_light.png', + 'lightShortUrl': + 'https://images-serviceprovider.meld.io/TRANSAK/short_logo_light.png' + } + } +] + +export const mockMeldCountries = [ + { + 'countryCode': 'AF', + 'name': 'Afghanistan', + 'flagImageUrl': 'https://images-country.meld.io/AF/flag.svg', + 'regions': null + }, + { + 'countryCode': 'AL', + 'name': 'Albania', + 'flagImageUrl': 'https://images-country.meld.io/AL/flag.svg', + 'regions': null + }, + { + 'countryCode': 'DZ', + 'name': 'Algeria', + 'flagImageUrl': 'https://images-country.meld.io/DZ/flag.svg', + 'regions': null + }, + { + 'countryCode': 'AS', + 'name': 'American Samoa', + 'flagImageUrl': 'https://images-country.meld.io/AS/flag.svg', + 'regions': null + }, + { + 'countryCode': 'AD', + 'name': 'Andorra', + 'flagImageUrl': 'https://images-country.meld.io/AD/flag.svg', + 'regions': null + } +] as unknown as MeldCountry[] + +export const mockMeldPaymentMethods = [ + { + 'paymentMethod': 'APPLE_PAY', + 'name': 'Apple Pay', + 'paymentType': 'MOBILE_WALLET', + 'logoImages': { + 'darkUrl': 'https://images-paymentMethod.meld.io/APPLE_PAY/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': + 'https://images-paymentMethod.meld.io/APPLE_PAY/logo_light.png', + 'lightShortUrl': null + } + }, + { + 'paymentMethod': 'CREDIT_DEBIT_CARD', + 'name': 'Credit & Debit Card', + 'paymentType': 'CARD', + 'logoImages': { + 'darkUrl': + 'https://images-paymentMethod.meld.io/CREDIT_DEBIT_CARD/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': + 'https://images-paymentMethod.meld.io/CREDIT_DEBIT_CARD/logo_light.png', + 'lightShortUrl': null + } + }, + { + 'paymentMethod': 'FAST', + 'name': 'FAST', + 'paymentType': 'BANK_TRANSFER', + 'logoImages': { + 'darkUrl': 'https://images-paymentMethod.meld.io/FAST/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': 'https://images-paymentMethod.meld.io/FAST/logo_light.png', + 'lightShortUrl': null + } + }, + { + 'paymentMethod': 'NG_BANK_TRANSFER', + 'name': 'Local Manual Bank Transfer', + 'paymentType': 'BANK_TRANSFER', + 'logoImages': { + 'darkUrl': + 'https://images-paymentMethod.meld.io/NG_BANK_TRANSFER/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': + 'https://images-paymentMethod.meld.io/NG_BANK_TRANSFER/logo_light.png', + 'lightShortUrl': null + } + }, + { + 'paymentMethod': 'SEPA', + 'name': 'SEPA', + 'paymentType': 'BANK_TRANSFER', + 'logoImages': { + 'darkUrl': 'https://images-paymentMethod.meld.io/SEPA/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': 'https://images-paymentMethod.meld.io/SEPA/logo_light.png', + 'lightShortUrl': null + } + }, + { + 'paymentMethod': 'SPEI', + 'name': 'SPEI', + 'paymentType': 'BANK_TRANSFER', + 'logoImages': { + 'darkUrl': 'https://images-paymentMethod.meld.io/SPEI/logo_dark.png', + 'darkShortUrl': null, + 'lightUrl': 'https://images-paymentMethod.meld.io/SPEI/logo_light.png', + 'lightShortUrl': null + } + } +] as unknown as MeldPaymentMethod[] diff --git a/components/brave_wallet_ui/common/hooks/use-multi-chain-buy-assets.ts b/components/brave_wallet_ui/common/hooks/use-multi-chain-buy-assets.ts index f5824ca5fc58..bbbd443573f3 100644 --- a/components/brave_wallet_ui/common/hooks/use-multi-chain-buy-assets.ts +++ b/components/brave_wallet_ui/common/hooks/use-multi-chain-buy-assets.ts @@ -8,24 +8,71 @@ import { skipToken } from '@reduxjs/toolkit/query/react' // types import { BraveWallet } from '../../constants/types' +// utils +import { getAssetSymbol } from '../../utils/meld_utils' +import { loadTimeData } from '../../../common/loadTimeData' + // hooks -import { useGetOnRampAssetsQuery } from '../slices/api.slice' +import { + useGetMeldCryptoCurrenciesQuery, + useGetOnRampAssetsQuery +} from '../slices/api.slice' -export const useIsBuySupported = ( - token?: Pick +export const useFindBuySupportedToken = ( + token?: Pick< + BraveWallet.BlockchainToken, + 'symbol' | 'contractAddress' | 'chainId' + > ) => { + // Computed + const isAndroid = loadTimeData.getBoolean('isAndroid') || false + // queries - const { data: options = undefined } = useGetOnRampAssetsQuery( - token ? undefined : skipToken + const { data: options } = useGetMeldCryptoCurrenciesQuery( + token && !isAndroid ? undefined : skipToken ) - // computed - const isBuySupported = + const { data: androidOptions = undefined } = useGetOnRampAssetsQuery( + token && isAndroid ? undefined : skipToken + ) + + const canBuyOnAndroid = token && - options?.allAssetOptions.some( + isAndroid && + androidOptions?.allAssetOptions.some( (asset) => asset.symbol.toLowerCase() === token.symbol.toLowerCase() ) + // computed + const foundNativeToken = + token && + token.contractAddress === '' && + options?.find( + (asset) => + asset.chainId?.toLowerCase() === token.chainId.toLowerCase() && + getAssetSymbol(asset).toLowerCase() === token.symbol.toLowerCase() + ) + + const foundTokenByContractAddress = + token && + options?.find( + (asset) => + asset.contractAddress?.toLowerCase() === + token.contractAddress.toLowerCase() && + asset.chainId?.toLowerCase() === token.chainId.toLowerCase() + ) + + const foundTokenBySymbol = + token && + options?.find( + (asset) => + getAssetSymbol(asset).toLowerCase() === token.symbol.toLowerCase() + ) + // render - return isBuySupported + return { + foundMeldBuyToken: + foundNativeToken || foundTokenByContractAddress || foundTokenBySymbol, + foundAndroidBuyToken: canBuyOnAndroid ? token : undefined + } } diff --git a/components/brave_wallet_ui/common/slices/api-base.slice.ts b/components/brave_wallet_ui/common/slices/api-base.slice.ts index 4ea6781a0fb6..9a54e03b4892 100644 --- a/components/brave_wallet_ui/common/slices/api-base.slice.ts +++ b/components/brave_wallet_ui/common/slices/api-base.slice.ts @@ -75,7 +75,15 @@ export function createWalletApiBase() { 'PinnableNftIds', 'PendingSignMessageRequests', 'PendingSignMessageErrors', - 'ActiveOrigin' + 'ActiveOrigin', + 'MeldFiatCurrencies', + 'MeldCryptoCurrencies', + 'DefaultCountryCode', + 'MeldCountries', + 'MeldServiceProviders', + 'MeldCryptoQuotes', + 'MeldPaymentMethods', + 'MeldWidget' ], endpoints: ({ mutation, query }) => ({}) }) diff --git a/components/brave_wallet_ui/common/slices/api.slice.ts b/components/brave_wallet_ui/common/slices/api.slice.ts index 7b98cceacb46..6389c439af8b 100644 --- a/components/brave_wallet_ui/common/slices/api.slice.ts +++ b/components/brave_wallet_ui/common/slices/api.slice.ts @@ -49,6 +49,7 @@ import { swapEndpoints } from './endpoints/swap.endpoints' import { encryptionEndpoints } from './endpoints/encryption.endpoints' import { signingEndpoints } from './endpoints/signing.endpoints' import { dappRadarEndpoints } from './endpoints/dapp_radar.endpoints' +import { meldIntegrationEndpoints } from './endpoints/meld_integration.endpoints' export function createWalletApi() { // base to add endpoints to @@ -157,6 +158,8 @@ export function createWalletApi() { .injectEndpoints({ endpoints: signingEndpoints }) // dApp Radar Endpoints .injectEndpoints({ endpoints: dappRadarEndpoints }) + // meld integration endpoints + .injectEndpoints({ endpoints: meldIntegrationEndpoints }) ) } @@ -339,7 +342,14 @@ export const { useUpdateUnapprovedTransactionSpendAllowanceMutation, useUpdateUserAssetVisibleMutation, useUpdateUserTokenMutation, - useValidateUnifiedAddressQuery + useValidateUnifiedAddressQuery, + useGetMeldFiatCurrenciesQuery, + useGetMeldCryptoCurrenciesQuery, + useGetMeldCountriesQuery, + useGetMeldServiceProvidersQuery, + useGetMeldPaymentMethodsQuery, + useGenerateMeldCryptoQuotesMutation, + useCreateMeldBuyWidgetMutation } = walletApi // Derived Data Queries diff --git a/components/brave_wallet_ui/common/slices/endpoints/meld_integration.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/meld_integration.endpoints.ts new file mode 100644 index 000000000000..8db84db742f4 --- /dev/null +++ b/components/brave_wallet_ui/common/slices/endpoints/meld_integration.endpoints.ts @@ -0,0 +1,347 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +// Types +import { + MeldCountry, + MeldCryptoCurrency, + MeldFiatCurrency, + MeldFilter, + MeldCryptoQuote, + MeldServiceProvider, + MeldPaymentMethod, + CryptoWidgetCustomerData, + CryptoBuySessionData, + MeldCryptoWidget +} from '../../../constants/types' + +// Utils +import { handleEndpointError } from '../../../utils/api-utils' +import { getMeldTokensChainId } from '../../../utils/meld_utils' +import { WalletApiEndpointBuilderParams } from '../api-base.slice' + +type GetCryptoQuotesArgs = { + country: string + sourceCurrencyCode: string + destinationCurrencyCode: string + amount: number + account: string | null + paymentMethod: MeldPaymentMethod +} + +type GetPaymentMethodsArg = { + country: string + sourceCurrencyCode: string +} + +type CreateMeldBuyWidgetArgs = { + sessionData: CryptoBuySessionData + customerData: CryptoWidgetCustomerData +} + +const supportedChains = [ + 'BTC', + 'FIL', + 'ZEC', + 'ETH', + 'SOLANA', + 'FTM', + 'BSC', + 'POLYGON', + 'OPTIMISM', + 'AURORA', + 'CELO', + 'ARBITRUM', + 'AVAXC' +] + +export const meldIntegrationEndpoints = ({ + query, + mutation +}: WalletApiEndpointBuilderParams) => { + return { + getMeldFiatCurrencies: query({ + queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + + // get all fiat currencies + const filter: MeldFilter = { + countries: undefined, + fiatCurrencies: undefined, + cryptoCurrencies: undefined, + serviceProviders: undefined, + paymentMethodTypes: undefined, + statuses: undefined, + cryptoChains: undefined + } + const { fiatCurrencies, error } = + await meldIntegrationService.getFiatCurrencies(filter) + + if (error) { + return handleEndpointError( + endpoint, + 'Error getting fiat currencies: ', + error + ) + } + + return { + data: fiatCurrencies || [] + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting fiat currencies: ', + error + ) + } + }, + providesTags: ['MeldFiatCurrencies'] + }), + getMeldCryptoCurrencies: query({ + queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + + // get all crypto currencies + const filter: MeldFilter = { + countries: undefined, + fiatCurrencies: undefined, + cryptoCurrencies: undefined, + serviceProviders: undefined, + paymentMethodTypes: undefined, + statuses: undefined, + cryptoChains: supportedChains.join(',') + } + const { fiatCurrencies: cryptoCurrencies, error } = + await meldIntegrationService.getCryptoCurrencies(filter) + + const tokenList = cryptoCurrencies?.map((token) => { + return { + ...token, + chainId: getMeldTokensChainId(token) + } + }) + + if (error) { + return handleEndpointError( + endpoint, + 'Error getting crypto currencies: ', + error + ) + } + + return { + data: tokenList || [] + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting crypto currencies: ', + error + ) + } + }, + providesTags: ['MeldCryptoCurrencies'] + }), + getMeldCountries: query({ + queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + + // get all countries + const filter: MeldFilter = { + countries: undefined, + fiatCurrencies: undefined, + cryptoCurrencies: undefined, + serviceProviders: undefined, + paymentMethodTypes: undefined, + statuses: undefined, + cryptoChains: undefined + } + const { countries, error } = + await meldIntegrationService.getCountries(filter) + if (error) { + return handleEndpointError( + endpoint, + 'Error getting countries: ', + error + ) + } + + return { + data: countries || [] + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting countries: ', + error + ) + } + }, + providesTags: ['MeldCountries'] + }), + getMeldServiceProviders: query({ + queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + + // get all countries + const filter: MeldFilter = { + countries: undefined, + fiatCurrencies: undefined, + cryptoCurrencies: undefined, + serviceProviders: undefined, + paymentMethodTypes: undefined, + statuses: undefined, + cryptoChains: undefined + } + const { serviceProviders, error } = + await meldIntegrationService.getServiceProviders(filter) + + if (error) { + return handleEndpointError( + endpoint, + 'Error getting countries: ', + error + ) + } + + return { + data: serviceProviders || [] + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting service providers: ', + error + ) + } + }, + providesTags: ['MeldServiceProviders'] + }), + generateMeldCryptoQuotes: mutation< + { + cryptoQuotes: MeldCryptoQuote[] | null + error: string[] | null + }, + GetCryptoQuotesArgs + >({ + queryFn: async (params, { endpoint }, extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + + const { + country, + sourceCurrencyCode, + destinationCurrencyCode, + amount, + account, + paymentMethod + } = params + + const result = await meldIntegrationService.getCryptoQuotes( + country, + sourceCurrencyCode, + destinationCurrencyCode, + amount, + account, + paymentMethod.paymentMethod + ) + + return { + data: result + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting fetching quotes: ', + error + ) + } + }, + invalidatesTags: ['MeldCryptoQuotes'] + }), + getMeldPaymentMethods: query({ + queryFn: async (params, { endpoint }, _extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + const { country, sourceCurrencyCode } = params + const filter: MeldFilter = { + countries: country, + fiatCurrencies: sourceCurrencyCode, + cryptoCurrencies: undefined, + serviceProviders: undefined, + paymentMethodTypes: undefined, + statuses: undefined, + cryptoChains: undefined + } + const { paymentMethods, error } = + await meldIntegrationService.getPaymentMethods(filter) + + if (error) { + return handleEndpointError( + endpoint, + 'Error getting paymentMethods: ', + error + ) + } + + return { + data: paymentMethods || [] + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error getting paymentMethods: ', + error + ) + } + }, + providesTags: ['MeldPaymentMethods'] + }), + createMeldBuyWidget: mutation< + { + widget: MeldCryptoWidget | null + }, + CreateMeldBuyWidgetArgs + >({ + queryFn: async (params, { endpoint }, extraOptions, baseQuery) => { + try { + const { meldIntegrationService } = baseQuery(undefined).data + const { sessionData, customerData } = params + const { widgetData, error } = + await meldIntegrationService.cryptoBuyWidgetCreate( + sessionData, + customerData + ) + + if (error) { + return handleEndpointError( + endpoint, + 'Error in createMeldBuyWidget: ', + error + ) + } + + return { + data: { + widget: widgetData + } + } + } catch (error) { + return handleEndpointError( + endpoint, + 'Error in createMeldBuyWidget: ', + error + ) + } + }, + invalidatesTags: ['MeldWidget'] + }) + } +} diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/page_terms_graphic.svg b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/page_terms_graphic.svg new file mode 100644 index 000000000000..699baff58b8a --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/page_terms_graphic.svg @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/panel_terms_graphic.svg b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/panel_terms_graphic.svg new file mode 100644 index 000000000000..328fe1b0673c --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/assets/panel_terms_graphic.svg @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.stories.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.stories.tsx new file mode 100644 index 000000000000..61b2d26a2848 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.stories.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Components +import { PartnersConsentModal } from './partners_consent_modal' +import { + WalletPageStory // +} from '../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _PartnersConsentModal = () => { + const [isOpen, setIsOpen] = React.useState(true) + + return ( + + setIsOpen(false)} + onContinue={() => setIsOpen(false)} + onCancel={() => setIsOpen(false)} + /> + + ) +} + +export default { + component: _PartnersConsentModal, + title: 'Partners Consent Modal' +} diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.style.ts b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.style.ts new file mode 100644 index 000000000000..dd42a41df04e --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.style.ts @@ -0,0 +1,48 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import Dialog from '@brave/leo/react/dialog' +import { font, spacing, color } from '@brave/leo/tokens/css/variables' + +// Shared Styles +import { WalletButton } from '../../../shared/style' + +export const TermsDialog = styled(Dialog)` + --leo-dialog-backdrop-background: rgba(17, 18, 23, 0.35); + --leo-dialog-backdrop-filter: blur(8px); + --leo-dialog-padding: ${spacing['2Xl']}; +` + +export const Title = styled.h2` + font: ${font.heading.h3}; + margin: 0; + padding: 0; +` + +export const TermsText = styled.p` + font: ${font.default.regular}; + padding: ${spacing['3Xl']} 0; + margin: 0; +` + +export const TermsLabel = styled.span` + font: ${font.default.regular}; + margin: 0; + padding: 0; +` + +export const TermsButton = styled(WalletButton)` + font-family: Poppins; + color: ${color.text.interactive}; + text-decoration: none; + font: ${font.default.semibold}; + background: none; + cursor: pointer; + outline: none; + border: none; + margin: 0px; + padding: 0px; +` diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.tsx new file mode 100644 index 000000000000..cdcf29dff894 --- /dev/null +++ b/components/brave_wallet_ui/components/desktop/popup-modals/partners_consent_modal/partners_consent_modal.tsx @@ -0,0 +1,114 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import Checkbox from '@brave/leo/react/checkbox' +import Button from '@brave/leo/react/button' + +// Utils +import { getLocale, splitStringForTag } from '../../../../../common/locale' + +// Selectors +import { useSafeUISelector } from '../../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../../common/selectors' + +// Assets +import PageTermsGraphic from './assets/page_terms_graphic.svg' +import PanelTermsGraphic from './assets/panel_terms_graphic.svg' + +// Styled Components +import { + TermsText, + TermsDialog, + Title, + TermsLabel, + TermsButton +} from './partners_consent_modal.style' +import { Row } from '../../../shared/style' + +interface PartnerConsentModalProps { + isOpen: boolean + onClose: () => void + onContinue: () => void + onCancel: () => void +} + +export function PartnersConsentModal( + props: Readonly +) { + const { isOpen, onCancel, onClose, onContinue } = props + + // state + const [termsAccepted, setTermsAccepted] = React.useState(false) + + // redux + const isPanel = useSafeUISelector(UISelectors.isPanel) + + const { beforeTag, duringTag } = splitStringForTag( + getLocale('braveWalletMeldTermsOfUse'), + 1 + ) + + const onClickTermsOfUse = () => { + chrome.tabs.create( + { + url: 'https://www.meld.io/terms-of-use' + }, + () => { + if (chrome.runtime.lastError) { + console.error( + 'tabs.create failed: ' + chrome.runtime.lastError.message + ) + } + } + ) + } + + return ( + + {getLocale('braveWalletTransactionsPartner')} + + Terms Graphic + + {getLocale('braveWalletTransactionPartnerConsent')} + setTermsAccepted(e.checked)} + > + + {beforeTag} + {duringTag} + + + + + + + + ) +} diff --git a/components/brave_wallet_ui/components/desktop/views/market/index.tsx b/components/brave_wallet_ui/components/desktop/views/market/index.tsx index deca4403960b..27e787369496 100644 --- a/components/brave_wallet_ui/components/desktop/views/market/index.tsx +++ b/components/brave_wallet_ui/components/desktop/views/market/index.tsx @@ -5,11 +5,13 @@ import * as React from 'react' import { useHistory } from 'react-router' +import { skipToken } from '@reduxjs/toolkit/query/react' // Hooks import { useGetCoinMarketQuery, useGetDefaultFiatCurrencyQuery, + useGetMeldCryptoCurrenciesQuery, useGetOnRampAssetsQuery } from '../../../../common/slices/api.slice' import { @@ -38,10 +40,13 @@ import { UpdateIframeHeightMessage } from '../../../../market/market-ui-messages' import { + makeAndroidFundWalletRoute, makeDepositFundsRoute, makeFundWalletRoute } from '../../../../utils/routes-utils' import { getAssetIdKey } from '../../../../utils/asset-utils' +import { getAssetSymbol } from '../../../../utils/meld_utils' +import { loadTimeData } from '../../../../../common/loadTimeData' const assetsRequestLimit = 250 @@ -57,16 +62,25 @@ export const MarketView = () => { // Hooks const history = useHistory() + // Computed + const isAndroid = loadTimeData.getBoolean('isAndroid') || false + // Queries const { data: defaultFiatCurrency = 'usd' } = useGetDefaultFiatCurrencyQuery() const { data: combinedTokensList } = useGetCombinedTokensListQuery() - const { buyAssets } = useGetOnRampAssetsQuery(undefined, { - selectFromResult: (res) => ({ - isLoading: res.isLoading, - buyAssets: res.data?.allAssetOptions || [] - }) - }) + const { data: buyAssets } = useGetMeldCryptoCurrenciesQuery( + !isAndroid ? undefined : skipToken + ) + const { androidBuyAssets } = useGetOnRampAssetsQuery( + isAndroid ? undefined : skipToken, + { + selectFromResult: (res) => ({ + isLoading: res.isLoading, + androidBuyAssets: res.data?.allAssetOptions || [] + }) + } + ) const { data: allCoins = [], isLoading: isLoadingCoinMarketData } = useGetCoinMarketQuery({ @@ -91,25 +105,33 @@ export const MarketView = () => { case MarketUiCommand.SelectBuy: { const { payload } = message as SelectBuyMessage const symbolLower = payload.symbol.toLowerCase() - const foundTokens = buyAssets.filter( + const foundMeldTokens = buyAssets?.filter( + (t) => getAssetSymbol(t) === symbolLower + ) + const foundAndroidTokens = androidBuyAssets.filter( (t) => t.symbol.toLowerCase() === symbolLower ) - if (foundTokens.length === 1) { + if (isAndroid && foundAndroidTokens.length === 1) { history.push( - makeFundWalletRoute(getAssetIdKey(foundTokens[0]), { + makeAndroidFundWalletRoute(getAssetIdKey(foundAndroidTokens[0]), { searchText: symbolLower }) ) return } - if (foundTokens.length > 1) { + if (isAndroid && foundAndroidTokens.length > 1) { history.push( - makeFundWalletRoute('', { + makeAndroidFundWalletRoute('', { searchText: symbolLower }) ) + return + } + + if (foundMeldTokens) { + history.push(makeFundWalletRoute(foundMeldTokens[0])) } break } @@ -147,7 +169,7 @@ export const MarketView = () => { } } }, - [buyAssets, combinedTokensList, history] + [buyAssets, combinedTokensList, history, androidBuyAssets, isAndroid] ) const onMarketDataFrameLoad = React.useCallback(() => { diff --git a/components/brave_wallet_ui/components/desktop/views/market/market_asset.tsx b/components/brave_wallet_ui/components/desktop/views/market/market_asset.tsx index f80d4376df9c..922fd5633de6 100644 --- a/components/brave_wallet_ui/components/desktop/views/market/market_asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/market/market_asset.tsx @@ -23,6 +23,7 @@ import { import { getAssetIdKey } from '../../../../utils/asset-utils' import { getLocale } from '../../../../../common/locale' import { + makeAndroidFundWalletRoute, makeDepositFundsRoute, makeFundWalletRoute } from '../../../../utils/routes-utils' @@ -58,9 +59,7 @@ import { import { CoinStats } from '../portfolio/components/coin-stats/coin-stats' // Hooks -import { - useIsBuySupported // -} from '../../../../common/hooks/use-multi-chain-buy-assets' +import { useFindBuySupportedToken } from '../../../../common/hooks/use-multi-chain-buy-assets' import { useGetNetworkQuery, useGetPriceHistoryQuery, @@ -203,8 +202,9 @@ export const MarketAsset = () => { ) // custom hooks - const isAssetBuySupported = - useIsBuySupported(selectedAssetFromParams) && !isRewardsToken + const { foundAndroidBuyToken, foundMeldBuyToken } = useFindBuySupportedToken( + selectedAssetFromParams + ) // memos / computed const isLoadingGraphData = @@ -303,32 +303,21 @@ export const MarketAsset = () => { }, [history, selectedAssetFromParams, updateUserAssetVisible]) const onSelectBuy = React.useCallback(() => { - if (foundTokens.length === 1) { + if (selectedAssetFromParams && foundAndroidBuyToken) { history.push( - makeFundWalletRoute(getAssetIdKey(foundTokens[0]), { - searchText: foundTokens[0].symbol - }) + makeAndroidFundWalletRoute(getAssetIdKey(selectedAssetFromParams)) ) return } - - if (foundTokens.length > 1) { - history.push( - makeFundWalletRoute('', { - searchText: foundTokens[0].symbol - }) - ) - return + if (foundMeldBuyToken) { + history.push(makeFundWalletRoute(foundMeldBuyToken)) } - - if (selectedAssetFromParams) { - history.push( - makeFundWalletRoute(getAssetIdKey(selectedAssetFromParams), { - searchText: selectedAssetFromParams.symbol - }) - ) - } - }, [foundTokens, history, selectedAssetFromParams]) + }, [ + history, + foundMeldBuyToken, + selectedAssetFromParams, + foundAndroidBuyToken + ]) const onSelectDeposit = React.useCallback(() => { if (foundTokens.length === 1) { @@ -346,17 +335,8 @@ export const MarketAsset = () => { searchText: foundTokens[0].symbol }) ) - return - } - - if (selectedAssetFromParams) { - history.push( - makeFundWalletRoute('', { - searchText: selectedAssetFromParams.symbol - }) - ) } - }, [foundTokens, history, selectedAssetFromParams]) + }, [foundTokens, history]) // token list & market data needs to load before we can find an asset to // select from the url params @@ -419,7 +399,7 @@ export const MarketAsset = () => { /> - {isAssetBuySupported && ( + {(foundMeldBuyToken || foundAndroidBuyToken) && (
{getLocale('braveWalletBuy')} diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/components/buy-send-swap-deposit-nav/buy-send-swap-deposit-nav.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/components/buy-send-swap-deposit-nav/buy-send-swap-deposit-nav.tsx index dc0d1261506d..057a03f9e50d 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/components/buy-send-swap-deposit-nav/buy-send-swap-deposit-nav.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/components/buy-send-swap-deposit-nav/buy-send-swap-deposit-nav.tsx @@ -7,17 +7,13 @@ import * as React from 'react' import { useHistory } from 'react-router-dom' // Types -import { NavOption, WalletRoutes } from '../../../../../../constants/types' +import { NavOption } from '../../../../../../constants/types' // Hooks import { useOnClickOutside // } from '../../../../../../common/hooks/useOnClickOutside' -// Selectors -import { useSafeUISelector } from '../../../../../../common/hooks/use-safe-selector' -import { UISelectors } from '../../../../../../common/selectors' - // Options import { BuySendSwapDepositOptions // @@ -45,9 +41,6 @@ export const BuySendSwapDepositNav = () => { // Routing const history = useHistory() - // redux - const isPanel = useSafeUISelector(UISelectors.isPanel) - // state const [showMoreMenu, setShowMoreMenu] = React.useState(false) @@ -60,17 +53,9 @@ export const BuySendSwapDepositNav = () => { // methods const onClick = React.useCallback( (option: NavOption) => { - // Redirect to full page view for buy page - // until we have a panel view for that page. - if (option.route === WalletRoutes.FundWalletPageStart && isPanel) { - chrome.tabs.create({ - url: `brave://wallet${option.route}` - }) - } else { - history.push(option.route) - } + history.push(option.route) }, - [history, isPanel] + [history] ) return ( diff --git a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-fungible-asset.tsx b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-fungible-asset.tsx index 9859f131aea4..7fe6042e97b7 100644 --- a/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-fungible-asset.tsx +++ b/components/brave_wallet_ui/components/desktop/views/portfolio/portfolio-fungible-asset.tsx @@ -43,6 +43,7 @@ import { getLocale } from '../../../../../common/locale' import { makeNetworkAsset } from '../../../../options/asset-options' import { isRewardsAssetId } from '../../../../utils/rewards_utils' import { + makeAndroidFundWalletRoute, makeDepositFundsRoute, makeFundWalletRoute } from '../../../../utils/routes-utils' @@ -71,9 +72,7 @@ import { import { useScopedBalanceUpdater // } from '../../../../common/hooks/use-scoped-balance-updater' -import { - useIsBuySupported // -} from '../../../../common/hooks/use-multi-chain-buy-assets' +import { useFindBuySupportedToken } from '../../../../common/hooks/use-multi-chain-buy-assets' import { useGetNetworkQuery, useGetTransactionsQuery, @@ -224,8 +223,9 @@ export const PortfolioFungibleAsset = () => { ) // custom hooks - const isAssetBuySupported = - useIsBuySupported(selectedAssetFromParams) && !isRewardsToken + const { foundAndroidBuyToken, foundMeldBuyToken } = useFindBuySupportedToken( + selectedAssetFromParams + ) // memos /** @@ -410,10 +410,21 @@ export const PortfolioFungibleAsset = () => { ]) const onSelectBuy = React.useCallback(() => { - if (selectedAssetFromParams) { - history.push(makeFundWalletRoute(getAssetIdKey(selectedAssetFromParams))) + if (foundAndroidBuyToken && selectedAssetFromParams) { + history.push( + makeAndroidFundWalletRoute(getAssetIdKey(selectedAssetFromParams)) + ) + return } - }, [history, selectedAssetFromParams]) + if (foundMeldBuyToken) { + history.push(makeFundWalletRoute(foundMeldBuyToken)) + } + }, [ + history, + foundAndroidBuyToken, + foundMeldBuyToken, + selectedAssetFromParams + ]) const onSelectDeposit = React.useCallback(() => { if (selectedAssetFromParams) { @@ -491,7 +502,7 @@ export const PortfolioFungibleAsset = () => { /> - {isAssetBuySupported && ( + {(foundMeldBuyToken || foundAndroidBuyToken) && !isRewardsToken && (
{getLocale('braveWalletBuy')} diff --git a/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx b/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx index 30a45cb0562a..31931a2f59e2 100644 --- a/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx +++ b/components/brave_wallet_ui/components/desktop/wallet-menus/asset-item-menu.tsx @@ -11,7 +11,6 @@ import { BraveWallet, WalletRoutes } from '../../../constants/types' // Queries import { - useGetOnRampAssetsQuery, useUpdateUserAssetVisibleMutation // } from '../../../common/slices/api.slice' @@ -19,11 +18,15 @@ import { import { useMultiChainSellAssets // } from '../../../common/hooks/use-multi-chain-sell-assets' +import { + useFindBuySupportedToken // +} from '../../../common/hooks/use-multi-chain-buy-assets' // Utils import { getLocale } from '../../../../common/locale' import Amount from '../../../utils/amount' import { + makeAndroidFundWalletRoute, makeDepositFundsRoute, makeFundWalletRoute, makeSendRoute, @@ -65,8 +68,6 @@ export const AssetItemMenu = (props: Props) => { const [showSellModal, setShowSellModal] = React.useState(false) // Queries - const { data: { allAssetOptions: allBuyAssetOptions } = {} } = - useGetOnRampAssetsQuery() const [updateUserAssetVisible] = useUpdateUserAssetVisibleMutation() // Hooks @@ -79,21 +80,14 @@ export const AssetItemMenu = (props: Props) => { checkIsAssetSellSupported } = useMultiChainSellAssets() + const { foundAndroidBuyToken, foundMeldBuyToken } = + useFindBuySupportedToken(asset) + // Memos const isAssetsBalanceZero = React.useMemo(() => { return new Amount(assetBalance).isZero() }, [assetBalance]) - const isBuySupported = React.useMemo(() => { - if (!allBuyAssetOptions || isAssetsBalanceZero) { - return false - } - return allBuyAssetOptions.some( - (buyableAsset) => - buyableAsset.symbol.toLowerCase() === asset.symbol.toLowerCase() - ) - }, [asset.symbol, allBuyAssetOptions, isAssetsBalanceZero]) - const isSwapSupported = coinSupportsSwap(asset.coin) && account !== undefined const isSellSupported = React.useMemo(() => { @@ -102,8 +96,14 @@ export const AssetItemMenu = (props: Props) => { // Methods const onClickBuy = React.useCallback(() => { - history.push(makeFundWalletRoute(getAssetIdKey(asset))) - }, [asset, history]) + if (foundAndroidBuyToken) { + history.push(makeAndroidFundWalletRoute(getAssetIdKey(asset))) + return + } + if (foundMeldBuyToken) { + history.push(makeFundWalletRoute(foundMeldBuyToken, account)) + } + }, [foundMeldBuyToken, history, account, foundAndroidBuyToken, asset]) const onClickSend = React.useCallback(() => { if (account) { @@ -149,7 +149,7 @@ export const AssetItemMenu = (props: Props) => { return ( - {isBuySupported && ( + {(foundMeldBuyToken || foundAndroidBuyToken) && ( {getLocale('braveWalletBuy')} diff --git a/components/brave_wallet_ui/components/desktop/wallet-page-wrapper/wallet-page-wrapper.style.ts b/components/brave_wallet_ui/components/desktop/wallet-page-wrapper/wallet-page-wrapper.style.ts index c253974399ab..ae1c7fe44f67 100644 --- a/components/brave_wallet_ui/components/desktop/wallet-page-wrapper/wallet-page-wrapper.style.ts +++ b/components/brave_wallet_ui/components/desktop/wallet-page-wrapper/wallet-page-wrapper.style.ts @@ -17,7 +17,7 @@ import LinesDark from './assets/portfolio_lines_background_dark.svg' // Shared Styles import { Row } from '../../shared/style' -const minCardHeight = 497 +const minCardHeight = 466 export const maxCardWidth = 768 const layoutSmallCardBottom = 67 export const layoutSmallWidth = 1100 diff --git a/components/brave_wallet_ui/components/shared/bottom_sheet/bottom_sheet.style.ts b/components/brave_wallet_ui/components/shared/bottom_sheet/bottom_sheet.style.ts index a15bd7f4f322..e4c3d4f0ae8d 100644 --- a/components/brave_wallet_ui/components/shared/bottom_sheet/bottom_sheet.style.ts +++ b/components/brave_wallet_ui/components/shared/bottom_sheet/bottom_sheet.style.ts @@ -22,7 +22,7 @@ export const Background = styled(Column)<{ isOpen: boolean }>` export const BottomCard = styled(Column)<{ isOpen: boolean }>` position: fixed; - bottom: ${(p) => (p.isOpen ? 0 : -600)}px; + bottom: ${(p) => (p.isOpen ? 0 : -800)}px; transition: all 0.3s ease-in-out; border-radius: 16px 16px 0px 0px; background-color: ${leo.color.container.background}; diff --git a/components/brave_wallet_ui/components/shared/style.tsx b/components/brave_wallet_ui/components/shared/style.tsx index a73a1cece80d..b171ad68380c 100644 --- a/components/brave_wallet_ui/components/shared/style.tsx +++ b/components/brave_wallet_ui/components/shared/style.tsx @@ -143,9 +143,11 @@ export const Row = styled.div< FlexProps & { maxWidth?: CSSProperties['maxWidth'] minWidth?: CSSProperties['minWidth'] + minHeight?: CSSProperties['minHeight'] + height?: '100%' | 'unset' margin?: number | string padding?: number | string - width?: '100%' | 'unset' + width?: CSSProperties['width'] marginBottom?: number | string // https://styled-components.com/docs/api#transient-props $wrap?: boolean @@ -165,6 +167,8 @@ export const Row = styled.div< width: ${(p) => p.width ?? '100%'}; min-width: ${(p) => p.minWidth ?? 'unset'}; max-width: ${(p) => p.maxWidth ?? 'unset'}; + height: ${(p) => p.height ?? 'unset'}; + min-height: ${(p) => p.minHeight ?? 'unset'}; margin: ${(p) => p.margin ?? 'unset'}; ${(p) => p?.marginBottom || p?.marginBottom === 0 diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index 1da3234d5908..cc3cbe2f391b 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -16,6 +16,18 @@ import { // path of generated mojom files. export { BraveWallet } export { Url } from 'gen/url/mojom/url.mojom.m.js' +export { + MeldFiatCurrency, + MeldFilter, + MeldCryptoCurrency, + MeldCountry, + MeldCryptoQuote, + MeldServiceProvider, + MeldPaymentMethod, + MeldCryptoWidget, + CryptoBuySessionData, + CryptoWidgetCustomerData +} from 'gen/brave/components/brave_wallet/common/meld_integration.mojom.m.js' export type NftDropdownOptionId = 'collected' | 'hidden' export type DAppConnectionOptionsType = 'networks' | 'accounts' | 'main' diff --git a/components/brave_wallet_ui/market/market-ui-messages.ts b/components/brave_wallet_ui/market/market-ui-messages.ts index 3364cdc53ea7..2b295431ec36 100644 --- a/components/brave_wallet_ui/market/market-ui-messages.ts +++ b/components/brave_wallet_ui/market/market-ui-messages.ts @@ -8,7 +8,7 @@ // you can obtain one at https://mozilla.org/MPL/2.0/. import { loadTimeData } from '../../common/loadTimeData' -import { BraveWallet } from '../constants/types' +import { BraveWallet, MeldCryptoCurrency } from '../constants/types' import { isComponentInStorybook } from '../utils/string-utils' const marketUiOrigin = loadTimeData.getString('braveWalletMarketUiBridgeUrl') @@ -55,7 +55,7 @@ export type SelectDepositMessage = MarketCommandMessage & { } export type UpdateBuyableAssetsMessage = MarketCommandMessage & { - payload: BraveWallet.BlockchainToken[] + payload: MeldCryptoCurrency[] | undefined } export type UpdateDepositableAssetsMessage = MarketCommandMessage & { diff --git a/components/brave_wallet_ui/market/market.tsx b/components/brave_wallet_ui/market/market.tsx index d0c163b8b397..884430d28eb8 100644 --- a/components/brave_wallet_ui/market/market.tsx +++ b/components/brave_wallet_ui/market/market.tsx @@ -27,6 +27,7 @@ import { BraveWallet, MarketAssetFilterOption, MarketGridColumnTypes, + MeldCryptoCurrency, SortOrder } from '../constants/types' @@ -50,6 +51,7 @@ import { searchCoinMarkets, sortCoinMarkets } from '../utils/coin-market-utils' +import { getAssetSymbol } from '../utils/meld_utils' // Options import { AssetFilterOptions } from '../options/market-data-filter-options' @@ -75,7 +77,7 @@ const App = () => { BraveWallet.CoinMarket[] >([]) const [buyableAssets, setBuyableAssets] = React.useState< - BraveWallet.BlockchainToken[] + MeldCryptoCurrency[] | undefined >([]) const [depositableAssets, setDepositableAssets] = React.useState< BraveWallet.BlockchainToken[] @@ -112,9 +114,12 @@ const App = () => { // Methods const isBuySupported = React.useCallback( (coinMarket: BraveWallet.CoinMarket) => { - return buyableAssets.some( - (asset) => - asset.symbol.toLowerCase() === coinMarket.symbol.toLowerCase() + return ( + buyableAssets?.some( + (asset) => + getAssetSymbol(asset).toLowerCase() === + coinMarket.symbol.toLowerCase() + ) ?? false ) }, [buyableAssets] diff --git a/components/brave_wallet_ui/page/router/unlocked_wallet_routes.tsx b/components/brave_wallet_ui/page/router/unlocked_wallet_routes.tsx index 3c21dd846259..e581e1a18a74 100644 --- a/components/brave_wallet_ui/page/router/unlocked_wallet_routes.tsx +++ b/components/brave_wallet_ui/page/router/unlocked_wallet_routes.tsx @@ -4,12 +4,24 @@ // You can obtain one at https://mozilla.org/MPL/2.0/. import * as React from 'react' -import { Route, Switch } from 'react-router' +import { Prompt, Route, Switch, useHistory } from 'react-router' +import { Location } from 'history' -// constants +// Utils +import { loadTimeData } from '../../../common/loadTimeData' + +// Hooks +import { useLocalStorage } from '../../common/hooks/use_local_storage' + +// Constants +import { + LOCAL_STORAGE_KEYS // +} from '../../common/constants/local-storage-keys' + +// Types import { WalletRoutes } from '../../constants/types' -// components +// Components import { CryptoView } from '../../components/desktop/views/crypto' import { WalletPageLayout } from '../../components/desktop/wallet-page-layout' import { @@ -19,52 +31,111 @@ import { BackupWalletRoutes // } from '../screens/backup-wallet/backup-wallet.routes' import { DepositFundsScreen } from '../screens/fund-wallet/deposit-funds' -import { FundWalletScreen } from '../screens/fund-wallet/fund-wallet' +import { FundWalletScreen } from '../screens/fund-wallet/fund_wallet_v2' +import FundWalletScreenAndroid from '../screens/fund-wallet/fund-wallet' import { SimplePageWrapper } from '../screens/page-screen.styles' import { OnboardingSuccess // } from '../screens/onboarding/onboarding_success/onboarding_success' +import { + PartnersConsentModal // +} from '../../components/desktop/popup-modals/partners_consent_modal/partners_consent_modal' export const UnlockedWalletRoutes = ({ sessionRoute }: { sessionRoute: WalletRoutes | undefined }) => { + // Computed + const isAndroid = loadTimeData.getBoolean('isAndroid') || false + + // State + const [isModalOpen, setModalOpen] = React.useState(false) + const [nextLocation, setNextLocation] = React.useState(null) + const [shouldBlock, setShouldBlock] = React.useState(!isAndroid) + + // Hooks + const history = useHistory() + const [acceptedTerms, setAcceptedTerms] = useLocalStorage( + LOCAL_STORAGE_KEYS.HAS_ACCEPTED_PARTNER_TERMS, + false + ) + + // Methods + const handleAccept = () => { + setAcceptedTerms(true) + setModalOpen(false) + setShouldBlock(false) + if (nextLocation) { + history.push(nextLocation.pathname) + } + } + + const handleDecline = () => { + setModalOpen(false) + setNextLocation(null) + } + + const handleBlockedNavigation = (location: Location) => { + if ( + !isAndroid && + !acceptedTerms && + location.pathname.startsWith(WalletRoutes.FundWalletPageStart) + ) { + setModalOpen(true) + setNextLocation(location) + return false + } + return true + } + // render return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + handleBlockedNavigation(location)} + /> + {}} + onCancel={handleDecline} + onContinue={handleAccept} + /> + + + + + + + + + + + + + + + + + + + + + {isAndroid ? : } + + + + + + + + + + + ) } diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.stories.tsx new file mode 100644 index 000000000000..61cdc3c7486a --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.stories.tsx @@ -0,0 +1,43 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { + mockMeldFiatCurrency // +} from '../../../../../common/constants/mocks' + +// Components +import { AmountButton } from './amount_button' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _AmountButton = () => { + // State + const [amount, setAmount] = React.useState('') + + return ( + + console.log('Open account selection modal')} + /> + + ) +} + +_AmountButton.story = { + name: 'Fund Wallet - Amount Button' +} + +export default { + component: _AmountButton +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.style.ts new file mode 100644 index 000000000000..bbfc1f8ad7f4 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.style.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import Icon from '@brave/leo/react/icon' +import { color, font } from '@brave/leo/tokens/css/variables' + +// Shared Styles +import { Column } from '../../../../../components/shared/style' +import { AmountInput as Input } from '../../../composer_ui/shared_composer.style' +import { + layoutPanelWidth // +} from '../../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' + +export const Wrapper = styled(Column)` + align-items: flex-end; + @media (max-width: ${layoutPanelWidth}px) { + align-items: flex-start; + width: 100%; + } +` + +export const ButtonWrapper = styled(Column)` + align-items: flex-end; + @media (max-width: ${layoutPanelWidth}px) { + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + } +` + +export const CurrencyCode = styled.span` + color: ${color.text.primary}; + font: ${font.heading.h1}; + width: 70px; + text-transform: uppercase; +` + +export const AmountInput = styled(Input).attrs({ + hasError: false, + type: 'number' +})` + color: ${color.text.primary}; + font: ${font.heading.h1}; + text-align: left; + width: 60px; +` + +export const AmountEstimate = styled.span` + color: ${color.text.interactive}; + font: ${font.default.semibold}; + @media (max-width: ${layoutPanelWidth}px) { + font: ${font.default.regular}; + } +` + +export const SwapVerticalIcon = styled(Icon).attrs({ name: 'swap-vertical' })` + --leo-icon-color: ${color.icon.interactive}; + --leo-icon-size: 20px; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.tsx new file mode 100644 index 000000000000..1151aefb8e60 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/amount_button/amount_button.tsx @@ -0,0 +1,85 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Styled Components +import { Row } from '../../../../../components/shared/style' +import { CaretDown, Label, WrapperButton } from '../shared/style' +import { + AmountEstimate, + AmountInput, + CurrencyCode, + SwapVerticalIcon, + Wrapper, + ButtonWrapper +} from './amount_button.style' + +interface SelectAccountProps { + labelText: string + amount?: string + currencyCode?: string + estimatedCryptoAmount?: string + onChange: (amount: string) => void + onClick: () => void +} + +export const AmountButton = ({ + labelText, + currencyCode, + amount, + estimatedCryptoAmount, + onChange, + onClick +}: SelectAccountProps) => { + // Methods + const onInputChange = React.useCallback( + (event: React.ChangeEvent) => { + onChange(event.target.value) + }, + [onChange] + ) + + return ( + + + + + + + + {currencyCode} + + + + + {estimatedCryptoAmount ? ( + + {estimatedCryptoAmount} + + + ) : null} + + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.stories.tsx new file mode 100644 index 000000000000..7d0dfcb16a26 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.stories.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. +import * as React from 'react' + +// Types +import { MeldCryptoQuote } from 'components/brave_wallet_ui/constants/types' + +// Mock Data +import { + mockMeldCryptoQuotes, + mockServiceProviders +} from '../../../../../common/constants/mocks' + +// Components +import { BuyQuote } from './buy_quote' + +export const _BuyQuote = () => { + return ( + + ) +} + +export default { + component: _BuyQuote, + title: 'Fund Wallet - Buy Quote' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.style.ts new file mode 100644 index 000000000000..ccbdce2bd7a9 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.style.ts @@ -0,0 +1,133 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import Icon from '@brave/leo/react/icon' +import Button from '@brave/leo/react/button' +import Label from '@brave/leo/react/label' +import { color, font, spacing } from '@brave/leo/tokens/css/variables' + +// Shared Styles +import { + layoutPanelWidth // +} from '../../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' +import { Column } from '../../../../../components/shared/style' + +export const StyledWrapper = styled.div<{ isOpen?: boolean }>` + display: flex; + flex-direction: column; + padding: ${spacing.l} ${spacing.xl}; + border-radius: 8px; + border: 1px solid + ${(p) => (p.isOpen ? color.button.background : color.divider.subtle)}; + background-color: ${color.container.background}; + width: 100%; +` + +export const ProviderName = styled.p` + color: ${color.text.primary}; + font: ${font.default.semibold}; + text-transform: capitalize; + margin: 0; + padding: 0; +` + +export const Estimate = styled.p` + color: ${color.text.secondary}; + font: ${font.xSmall.regular}; + padding: 0; + margin: 0; +` + +export const ProviderImage = styled.img` + width: 40px; + height: 40px; + margin-right: ${spacing.xl}; +` + +export const PaymentMethodsWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + gap: ${spacing.l}; + padding: 4px 8px; + border-radius: 4px; + background-color: ${color.container.highlight}; +` + +export const PaymentMethodIcon = styled(Icon)` + --leo-icon-size: 20px; + --leo-icon-color: ${color.icon.default}; +` + +export const CaratIcon = styled(Icon)<{ + isOpen?: boolean +}>` + --leo-icon-size: 24px; + --leo-icon-color: ${color.button.background}; + transform: rotate(${(p) => (p.isOpen ? '180deg' : '0deg')}); + transition: transform 0.5s; + margin-left: 8px; +` + +export const WrapperForPadding = styled(Column)` + margin-top: ${spacing.xl}; + padding-left: 56px; + @media (max-width: ${layoutPanelWidth}px) { + padding-left: 0px; + } +` + +export const QuoteDetailsWrapper = styled(Column)` + padding: ${spacing.xl} ${spacing['2Xl']}; + border-radius: 12px; + background-color: ${color.page.background}; +` + +export const QuoteDetailsLabel = styled.p` + color: ${color.text.secondary}; + font: ${font.small.regular}; + color: ${color.text.primary}; + margin: 0; + padding: 0; +` + +export const QuoteDetailsValue = styled.p` + color: ${color.text.primary}; + font: ${font.small.regular}; + color: ${color.text.primary}; + margin: 0; + padding: 0; +` + +export const Divider = styled.div` + height: 1px; + background-color: ${color.divider.subtle}; +` + +export const QuoteTotal = styled.p` + color: ${color.text.primary}; + font: ${font.small.semibold}; + margin: 0; + padding: 0; +` + +export const BuyButton = styled(Button).attrs({ + kind: 'filled' +})` + @media (max-width: ${layoutPanelWidth}px) { + width: 100%; + } +` + +export const BestOptionLabel = styled(Label).attrs({ + mode: 'default', + color: 'green' +})` + --leo-label-padding: 12px; + color: ${color.text.primary}; + text-transform: capitalize; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.tsx new file mode 100644 index 000000000000..f0bce46729ba --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/buy_quote/buy_quote.tsx @@ -0,0 +1,234 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +// Types +import { + MeldCryptoCurrency, + MeldCryptoQuote, + MeldServiceProvider +} from '../../../../../constants/types' + +// Utils +import Amount from '../../../../../utils/amount' +import { toProperCase } from '../../../../../utils/string-utils' +import { getLocale } from '../../../../../../common/locale' +import { getAssetSymbol } from '../../../../../utils/meld_utils' + +// Styled Components +import { + ProviderImage, + ProviderName, + StyledWrapper, + Estimate, + PaymentMethodsWrapper, + PaymentMethodIcon, + CaratIcon, + QuoteDetailsWrapper, + QuoteDetailsLabel, + QuoteDetailsValue, + Divider, + QuoteTotal, + BuyButton, + BestOptionLabel, + WrapperForPadding +} from './buy_quote.style' +import { Column, Row } from '../../../../../components/shared/style' + +interface BuyQuoteProps { + quote: MeldCryptoQuote + serviceProviders: MeldServiceProvider[] + isOpenOverride?: boolean + isBestOption?: boolean + isCreatingWidget: boolean + selectedAsset?: MeldCryptoCurrency + onBuy: (quote: MeldCryptoQuote) => void +} + +export const BuyQuote = ({ + quote, + serviceProviders, + isBestOption, + isCreatingWidget, + selectedAsset, + isOpenOverride, + onBuy +}: BuyQuoteProps) => { + const { + serviceProvider, + sourceCurrencyCode, + sourceAmount, + destinationAmount, + destinationCurrencyCode, + exchangeRate, + sourceAmountWithoutFee, + totalFee, + paymentMethod + } = quote + + // State + const [isOpen, setIsOpen] = React.useState(isOpenOverride ?? false) + + // Computed + const formattedSourceAmount = new Amount(sourceAmount ?? '').formatAsFiat( + sourceCurrencyCode, + 2 + ) + + const assetsSymbol = selectedAsset + ? getAssetSymbol(selectedAsset) + : destinationCurrencyCode + + const formattedCryptoAmount = new Amount( + destinationAmount ?? '' + ).formatAsAsset(5, assetsSymbol) + + const formattedExchangeRate = new Amount(exchangeRate ?? '').formatAsFiat( + '', + 2 + ) + const amountWithoutFees = new Amount( + sourceAmountWithoutFee ?? '' + ).formatAsFiat('', 2) + const formattedTotalFee = new Amount(totalFee ?? '').formatAsFiat('', 2) + const [isCreditCardSupported, isDeditCardSupported] = [ + paymentMethod?.includes('CREDIT'), + paymentMethod?.includes('DEBIT') + ] + const formattedProviderName = toProperCase(serviceProvider ?? '') + const quoteServiceProvider = serviceProviders.find( + (provider) => provider.serviceProvider === serviceProvider + ) + const providerImageUrl = window.matchMedia('(prefers-color-scheme: dark)') + .matches + ? quoteServiceProvider?.logoImages?.darkShortUrl + : quoteServiceProvider?.logoImages?.lightShortUrl + + return ( + + setIsOpen(!isOpen)} + > + + {providerImageUrl ? ( + + ) : null} + + + {formattedProviderName} + + {formattedSourceAmount} = ~{formattedCryptoAmount} + + + + + {isBestOption ? ( + +
+ +
+ {getLocale('braveWalletBestOption')} +
+ ) : null} + + {isDeditCardSupported ? : null} + {isCreditCardSupported ? ( + + ) : null} + + +
+
+ {isOpen ? ( + + + + + + {getLocale('braveWalletExchangeRateWithFees')} + + + ≈ {formattedExchangeRate} {sourceCurrencyCode} /{' '} + {assetsSymbol} + + + + + {getLocale('braveWalletPriceCurrency').replace( + '$1', + sourceCurrencyCode ?? '' + )} + + + ≈ {amountWithoutFees} {sourceCurrencyCode} + + + + + {getLocale('braveWalletFees')} + + + {formattedTotalFee} {sourceCurrencyCode} + + + + + + {getLocale('braveWalletConfirmTransactionTotal')} + + + {formattedSourceAmount} {sourceCurrencyCode} + + + + + onBuy(quote)} + > + {getLocale('braveWalletBuyWithProvider').replace( + '$1', + formattedProviderName + )} +
+ +
+
+
+ ) : null} +
+ ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.stories.tsx new file mode 100644 index 000000000000..46056872fd4f --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.stories.tsx @@ -0,0 +1,35 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { + mockAccounts // +} from '../../../../../stories/mock-data/mock-wallet-accounts' + +// Components +import { SelectAccount } from './select_account' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _SelectAccount = () => { + return ( + + alert('Close was clicked.')} + accounts={mockAccounts} + onSelect={(account) => console.log(account)} + /> + + ) +} + +export default { + component: _SelectAccount, + title: 'Fund Wallet - Select Account' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.style.ts new file mode 100644 index 000000000000..031be37e4443 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.style.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { color, font } from '@brave/leo/tokens/css/variables' + +export const AccountName = styled.span` + font: ${font.default.semibold}; + color: ${color.text.primary}; +` + +export const AccountAddress = styled.span` + font: ${font.xSmall.regular}; + color: ${color.text.secondary}; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.tsx new file mode 100644 index 000000000000..de856ec1507b --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account/select_account.tsx @@ -0,0 +1,153 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { DialogProps } from '@brave/leo/react/dialog' +import { spacing } from '@brave/leo/tokens/css/variables' + +// Selectors +import { + useSafeUISelector // +} from '../../../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../../../common/selectors' + +// Types +import { BraveWallet, MeldCryptoCurrency } from '../../../../../constants/types' + +// Utils +import { reduceAddress } from '../../../../../utils/reduce-address' +import { getLocale } from '../../../../../../common/locale' + +// Components +import { + CreateAccountIcon // +} from '../../../../../components/shared/create-account-icon/create-account-icon' +import { + BottomSheet // +} from '../../../../../components/shared/bottom_sheet/bottom_sheet' + +// Styled Components +import { ContainerButton, Dialog, DialogTitle } from '../shared/style' +import { + Column, + Row, + ScrollableColumn +} from '../../../../../components/shared/style' +import { AccountAddress, AccountName } from './select_account.style' +import { getMeldTokensCoinType } from '../../../../../utils/meld_utils' + +const testnetAccountKeyringIds = [ + BraveWallet.KeyringId.kBitcoin84Testnet, + BraveWallet.KeyringId.kBitcoinHardwareTestnet, + BraveWallet.KeyringId.kBitcoinImportTestnet, + BraveWallet.KeyringId.kFilecoinTestnet, + BraveWallet.KeyringId.kZCashTestnet +] + +interface SelectAccountProps extends DialogProps { + accounts: BraveWallet.AccountInfo[] + selectedAccount?: BraveWallet.AccountInfo + selectedAsset?: MeldCryptoCurrency + isOpen: boolean + onClose: () => void + onSelect: (account: BraveWallet.AccountInfo) => void +} + +interface AccountProps { + account: BraveWallet.AccountInfo + onSelect: (account: BraveWallet.AccountInfo) => void +} + +export const Account = ({ account, onSelect }: AccountProps) => { + return ( + onSelect(account)}> + + + + {account.name} + {reduceAddress(account.address)} + + + + ) +} + +export const SelectAccount = (props: SelectAccountProps) => { + const { accounts, selectedAsset, onSelect, isOpen, onClose, ...rest } = props + + // Selectors + const isPanel = useSafeUISelector(UISelectors.isPanel) + + // Memos + const accountByCoinType = React.useMemo(() => { + if (selectedAsset) { + return accounts.filter( + (account) => + account.accountId.coin === getMeldTokensCoinType(selectedAsset) && + !testnetAccountKeyringIds.includes(account.accountId.keyringId) + ) + } + return accounts + }, [selectedAsset, accounts]) + + const selectAccountContent = React.useMemo(() => { + return ( + <> + + {getLocale('braveWalletSelectAccount')} + + + + {accountByCoinType.map((account) => ( + + ))} + + + ) + }, [accountByCoinType, onSelect, isPanel]) + + if (isPanel) { + return ( + + + {selectAccountContent} + + + ) + } + + return ( + + {selectAccountContent} + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.stories.tsx new file mode 100644 index 000000000000..e3bc3919eabe --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.stories.tsx @@ -0,0 +1,32 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { mockAccount } from '../../../../../common/constants/mocks' + +// Components +import { SelectAccountButton } from './select_account_button' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _SelectAccountButton = () => { + return ( + + console.log('Open account selection modal')} + /> + + ) +} + +export default { + component: _SelectAccountButton, + title: 'Fund Wallet - Select Account Button' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.style.ts new file mode 100644 index 000000000000..ed3cc62f6c7f --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.style.ts @@ -0,0 +1,38 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { color, font, spacing } from '@brave/leo/tokens/css/variables' + +// Shared Styles +import { + layoutPanelWidth // +} from '../../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' + +export const AccountName = styled.h3` + color: ${color.text.primary}; + font: ${font.heading.h3}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 185px; + margin: 0; + padding: 0; +` + +export const AccountAddress = styled.span` + display: flex; + padding: ${spacing.s} ${spacing.m}; + margin-top: ${spacing.m} 0; + align-items: flex-start; + background-color: ${color.page.background}; + border-radius: ${spacing.m}; + color: ${color.text.secondary}; + font: ${font.xSmall.semibold}; + line-height: 16px; + @media (max-width: ${layoutPanelWidth}) { + background-color: transparent; + } +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.tsx new file mode 100644 index 000000000000..668535d778d9 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_account_button/select_account_button.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Types +import { BraveWallet } from '../../../../../constants/types' + +// Hooks +import { + useReceiveAddressQuery // +} from '../../../../../common/slices/api.slice.extra' + +// Components +import { + CreateAccountIcon // +} from '../../../../../components/shared/create-account-icon/create-account-icon' + +// Styled Components +import { Column, Row } from '../../../../../components/shared/style' +import { CaretDown, ControlText, Label, WrapperButton } from '../shared/style' +import { AccountAddress } from './select_account_button.style' + +interface SelectAccountProps { + labelText: string + selectedAccount?: BraveWallet.AccountInfo + onClick: () => void +} + +export const SelectAccountButton = ({ + labelText, + selectedAccount, + onClick +}: SelectAccountProps) => { + const { receiveAddress } = useReceiveAddressQuery(selectedAccount?.accountId) + + return ( + + + + + + + {selectedAccount?.name} + + + + + {receiveAddress} + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.stories.tsx new file mode 100644 index 000000000000..ae4cbc6a5a82 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.stories.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { + mockMeldCryptoCurrencies, + mockMeldFiatCurrencies +} from '../../../../../common/constants/mocks' + +// Types +import { MeldCryptoCurrency } from '../../../../../constants/types' + +// Components +import { SelectAsset } from './select_asset' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _SelectAsset = () => { + // State + const [selectedCurrency, setSelectedCurrency] = React.useState< + MeldCryptoCurrency | undefined + >(undefined) + + return ( + + alert('Close was clicked.')} + assets={mockMeldCryptoCurrencies} + isLoadingAssets={false} + isLoadingSpotPrices={false} + selectedAsset={selectedCurrency} + selectedFiatCurrency={mockMeldFiatCurrencies[0]} + onSelectAsset={(asset) => setSelectedCurrency(asset)} + /> + + ) +} + +export default { + component: _SelectAsset, + title: 'Fund Wallet - Select Asset' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.style.ts new file mode 100644 index 000000000000..301ebf8b528a --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.style.ts @@ -0,0 +1,77 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { font, color } from '@brave/leo/tokens/css/variables' +import ProgressRing from '@brave/leo/react/progressRing' + +// Shared Styles +import { Row } from '../../../../../components/shared/style' + +export const Wrapper = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 999; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +` + +export const AssetImage = styled.img` + width: 32px; + height: 32px; + border-radius: 50%; +` +export const AssetSymbol = styled.span` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + font: ${font.default.semibold}; + color: ${color.container.background}; + background-color: ${color.primary[50]}; +` + +export const AssetName = styled.span` + color: ${color.text.primary}; + font: ${font.default.semibold}; + text-overflow: ellipsis; + text-align: left; + white-space: nowrap; + overflow: hidden; +` + +export const AssetNetwork = styled.span` + color: ${color.text.secondary}; + font: ${font.small.regular}; + white-space: nowrap; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; +` + +export const AssetPrice = styled.span` + display: flex; + color: ${color.text.primary}; + font: ${font.default.regular}; + text-transform: uppercase; +` + +export const Loader = styled(ProgressRing)` + --leo-progressring-size: 22px; +` + +export const AutoSizerStyle = { + width: '100%', + flex: 1 +} + +export const SearchAndNetworkFilterRow = styled(Row)` + background-color: ${color.container.highlight}; + border-radius: 8px; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.tsx new file mode 100644 index 000000000000..7a673cd5f676 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset/select_asset.tsx @@ -0,0 +1,429 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { VariableSizeList as List } from 'react-window' +import AutoSizer from 'react-virtualized-auto-sizer' +import { DialogProps } from '@brave/leo/react/dialog' + +// Selectors +import { + useSafeUISelector // +} from '../../../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../../../common/selectors' + +// Options +import { + AllNetworksOption // +} from '../../../../../options/network-filter-options' + +// Queries +import { + useGetAllKnownNetworksQuery // +} from '../../../../../common/slices/api.slice' + +// Types +import { + BraveWallet, + MeldCryptoCurrency, + MeldFiatCurrency, + SpotPriceRegistry +} from '../../../../../constants/types' + +// Utils +import { + getAssetSymbol, + getTokenPriceFromRegistry, + getAssetIdKey, + getMeldTokensCoinType +} from '../../../../../utils/meld_utils' +import { getLocale } from '../../../../../../common/locale' +import Amount from '../../../../../utils/amount' + +// Components +import { + CreateNetworkIcon // +} from '../../../../../components/shared/create-network-icon' +import { + NetworkFilterSelector // +} from '../../../../../components/desktop/network-filter-selector' +import { + SearchBar // +} from '../../../../../components/shared/search-bar/index' +import { + BottomSheet // +} from '../../../../../components/shared/bottom_sheet/bottom_sheet' + +// Styled Components +import { Column, Row } from '../../../../../components/shared/style' +import { + AssetImage, + AssetName, + AssetNetwork, + AssetPrice, + Loader, + AutoSizerStyle, + SearchAndNetworkFilterRow +} from './select_asset.style' +import { + ContainerButton, + Dialog, + DialogTitle, + ListTitle, + IconsWrapper, + NetworkIconWrapper +} from '../shared/style' + +interface SelectAssetProps extends DialogProps { + assets: MeldCryptoCurrency[] + isLoadingAssets: boolean + isLoadingSpotPrices: boolean + selectedAsset?: MeldCryptoCurrency + selectedFiatCurrency?: MeldFiatCurrency + spotPriceRegistry?: SpotPriceRegistry + isOpen: boolean + onClose: () => void + onSelectAsset: (asset: MeldCryptoCurrency) => void +} + +interface AssetListItemProps { + index: number + style: React.CSSProperties + setSize: (index: number, size: number) => void + asset: MeldCryptoCurrency + isLoadingPrices: boolean + assetPrice?: BraveWallet.AssetPrice + fiatCurrencyCode?: string + onSelect: (currency: MeldCryptoCurrency) => void + network?: BraveWallet.NetworkInfo +} + +const assetItemHeight = 64 + +export const AssetListItem = ({ + index, + style, + setSize, + asset, + isLoadingPrices, + assetPrice, + fiatCurrencyCode, + onSelect, + network +}: AssetListItemProps) => { + const { symbolImageUrl, currencyCode, chainName } = asset + + // Computed + const assetSymbol = getAssetSymbol(asset) + const networkDescription = + currencyCode !== '' + ? getLocale('braveWalletPortfolioAssetNetworkDescription') + .replace('$1', assetSymbol ?? '') + .replace('$2', chainName ?? '') + : chainName + const formattedPrice = assetPrice + ? new Amount(assetPrice.price).formatAsFiat(fiatCurrencyCode ?? '', 4) + : '' + + // Methods + const handleSetSize = React.useCallback( + (ref: HTMLButtonElement | null) => { + if (ref) { + setSize(index, ref.getBoundingClientRect().height) + } + }, + [index, setSize] + ) + + return ( +
+ onSelect(asset)} + ref={handleSetSize} + > + + + + + + + + + {asset.name} + {networkDescription} + + + + {isLoadingPrices ? ( + + ) : ( + {formattedPrice} + )} + + +
+ ) +} + +export const SelectAsset = (props: SelectAssetProps) => { + const { + assets, + selectedAsset, + isLoadingAssets, + isLoadingSpotPrices, + selectedFiatCurrency, + spotPriceRegistry, + onSelectAsset, + isOpen, + onClose, + ...rest + } = props + + // Selectors + const isPanel = useSafeUISelector(UISelectors.isPanel) + + // State + const [searchText, setSearchText] = React.useState('') + const [selectedNetworkFilter, setSelectedNetworkFilter] = + React.useState(AllNetworksOption) + + // Refs + const listRef = React.useRef(null) + const itemSizes = React.useRef( + new Array(assets.length).fill(assetItemHeight) + ) + + // Queries + const { data: networkList = [] } = useGetAllKnownNetworksQuery() + + // Memos + const assetsFilteredByNetwork = React.useMemo(() => { + if (selectedNetworkFilter.chainId === AllNetworksOption.chainId) { + return assets + } + return assets.filter( + (asset) => asset.chainId === selectedNetworkFilter.chainId + ) + }, [selectedNetworkFilter, assets]) + + const searchResults = React.useMemo(() => { + if (searchText === '') return assetsFilteredByNetwork + + return assetsFilteredByNetwork.filter((asset) => { + const assetSymbol = getAssetSymbol(asset).toLowerCase() + const assetName = asset.name?.toLowerCase() ?? '' + return ( + assetName.startsWith(searchText.toLowerCase()) || + assetSymbol.startsWith(searchText.toLowerCase()) || + asset?.contractAddress + ?.toLowerCase() + .startsWith(searchText.toLowerCase()) || + assetName.includes(searchText.toLowerCase()) || + assetSymbol.includes(searchText.toLowerCase()) + ) + }) + }, [assetsFilteredByNetwork, searchText]) + + const networks = React.useMemo(() => { + const allChainIds = assets.map((asset) => asset.chainId) + let reducedChainIds = [...new Set(allChainIds)] + return networkList.filter((network) => + reducedChainIds.includes(network.chainId) + ) + }, [assets, networkList]) + + // Methods + const getListItemKey = (index: number, assets: MeldCryptoCurrency[]) => { + return getAssetIdKey(assets[index]) + } + + const getSize = React.useCallback((index: number) => { + return itemSizes.current[index] || assetItemHeight + }, []) + + const setSize = React.useCallback((index: number, size: number) => { + // Performance: Only update the sizeMap and reset cache if an actual value + // changed + if (itemSizes.current[index] !== size && size > -1) { + itemSizes.current[index] = size + if (listRef.current) { + // Clear cached data and rerender + listRef.current.resetAfterIndex(0) + } + } + }, []) + + const getAssetsNetwork = React.useCallback( + (asset: MeldCryptoCurrency) => { + return networkList.find( + (network) => + network.chainId.toLowerCase() === asset.chainId?.toLowerCase() && + getMeldTokensCoinType(asset) === network.coin + ) + }, + [networkList] + ) + + const updateSearchValue = React.useCallback( + (event: React.ChangeEvent) => { + setSearchText(event.target.value) + }, + [] + ) + + const onSelectNetwork = React.useCallback( + (network: BraveWallet.NetworkInfo) => { + setSelectedNetworkFilter(network) + }, + [] + ) + + // Memos + const selectAssetContent = React.useMemo(() => { + return ( + <> + + {getLocale('braveWalletSelectAsset')} + + + + + + + {isLoadingAssets && ( + + + + )} + {searchResults.length === 0 && !isLoadingAssets ? ( + + {getLocale('braveWalletNoAvailableAssets')} + + ) : ( + <> + + {getLocale('braveWalletAsset')} + ~ {getLocale('braveWalletPrice')} + + + {function ({ + width, + height + }: { + width: number + height: number + }) { + return ( + ( + + )} + /> + ) + }} + + + )} + + + ) + }, [ + getAssetsNetwork, + getSize, + isLoadingAssets, + isLoadingSpotPrices, + networks, + onSelectAsset, + onSelectNetwork, + searchResults, + searchText, + selectedFiatCurrency, + selectedNetworkFilter, + setSize, + spotPriceRegistry, + updateSearchValue + ]) + + if (isPanel) { + return ( + + + {selectAssetContent} + + + ) + } + + return ( + + {selectAssetContent} + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.stories.tsx new file mode 100644 index 000000000000..512313af03af --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.stories.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { + mockMeldCryptoCurrencies // +} from '../../../../../common/constants/mocks' + +// Components +import { SelectAssetButton } from './select_asset_button' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _SelectAssetButton = () => { + return ( + + console.log('Open asset selection modal')} + /> + + ) +} + +export default { + component: _SelectAssetButton, + title: 'Fund Wallet - Select Asset Button' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.tsx new file mode 100644 index 000000000000..29e8bcf76ff0 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_asset_button/select_asset_button.tsx @@ -0,0 +1,99 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { skipToken } from '@reduxjs/toolkit/query' + +// Queries +import { + useGetNetworkQuery // +} from '../../../../../common/slices/api.slice' + +// Types +import { MeldCryptoCurrency } from '../../../../../constants/types' + +// Utils +import { + getAssetSymbol, + getMeldTokensCoinType +} from '../../../../../utils/meld_utils' + +// Components +import { + CreateNetworkIcon // +} from '../../../../../components/shared/create-network-icon' + +// Styled Components +import { Column, Row } from '../../../../../components/shared/style' +import { + AssetIcon, + CaretDown, + ControlText, + Label, + WrapperButton, + IconsWrapper, + NetworkIconWrapper +} from '../shared/style' + +interface SelectAssetButtonProps { + labelText: string + selectedAsset?: MeldCryptoCurrency + onClick: () => void +} + +export const SelectAssetButton = (props: SelectAssetButtonProps) => { + const { labelText, selectedAsset, onClick } = props + + // Queries + const { data: tokensNetwork } = useGetNetworkQuery( + selectedAsset?.chainId + ? { + chainId: selectedAsset.chainId, + coin: getMeldTokensCoinType(selectedAsset) + } + : skipToken + ) + + // Computed + const assetSymbol = selectedAsset ? getAssetSymbol(selectedAsset) : '' + + return ( + + + + + + {selectedAsset && ( + <> + + + {tokensNetwork && ( + + + + )} + + {assetSymbol} + + )} + + + + + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.stories.tsx new file mode 100644 index 000000000000..fec7e32796eb --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.stories.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Mock Data +import { + mockMeldFiatCurrencies // +} from '../../../../../common/constants/mocks' + +// Types +import { MeldFiatCurrency } from '../../../../../constants/types' + +// Components +import { SelectCurrency } from './select_currency' +import { + WalletPageStory // +} from '../../../../../stories/wrappers/wallet-page-story-wrapper' + +export const _SelectCurrency = () => { + // State + const [selectedCurrency, setSelectedCurrency] = React.useState< + MeldFiatCurrency | undefined + >(undefined) + + return ( + + alert('Close was clicked.')} + currencies={mockMeldFiatCurrencies} + selectedCurrency={selectedCurrency} + onSelectCurrency={(currency) => setSelectedCurrency(currency)} + /> + + ) +} + +export default { + component: _SelectCurrency, + title: 'Fund Wallet - Select Currency' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.style.ts new file mode 100644 index 000000000000..31775ef19de6 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.style.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import Input from '@brave/leo/react/input' +import Label from '@brave/leo/react/label' +import { font, color } from '@brave/leo/tokens/css/variables' + +// Shared Styles +import { + layoutPanelWidth // +} from '../../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' + +export const SearchInput = styled(Input).attrs({ + mode: 'filled', + size: window.innerWidth <= layoutPanelWidth ? 'small' : 'normal' +})` + margin-top: 2px; + width: 100%; + padding-bottom: 8px; + @media (max-width: ${layoutPanelWidth}px) { + size: small; + } +` +export const Wrapper = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 999; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +` + +export const CurrencyImage = styled.img` + width: 32px; + height: 32px; + border-radius: 50%; +` +export const CurrencySymbol = styled.span` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + font: ${font.default.semibold}; + color: ${color.container.background}; + background-color: ${color.primary[50]}; +` + +export const CurrencyName = styled.span` + color: ${color.text.primary}; + font: ${font.default.semibold}; + text-overflow: ellipsis; + text-align: left; + white-space: nowrap; + overflow: hidden; +` + +export const CurrencyCode = styled.span` + display: flex; + color: ${color.text.primary}; + font: ${font.default.regular}; + text-transform: uppercase; +` + +export const SelectedLabel = styled(Label).attrs({ + mode: 'default', + color: 'purple' +})` + text-transform: uppercase; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.tsx new file mode 100644 index 000000000000..e6d1b65d3111 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/select_currency/select_currency.tsx @@ -0,0 +1,188 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { DialogProps } from '@brave/leo/react/dialog' +import Icon from '@brave/leo/react/icon' + +// Selectors +import { + useSafeUISelector // +} from '../../../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../../../common/selectors' + +// Types +import { MeldFiatCurrency } from '../../../../../constants/types' + +// Utils +import { getLocale } from '../../../../../../common/locale' + +// Components +import { + BottomSheet // +} from '../../../../../components/shared/bottom_sheet/bottom_sheet' + +// Styled Components +import { + Column, + Row, + ScrollableColumn +} from '../../../../../components/shared/style' +import { + CurrencyImage, + CurrencyName, + CurrencyCode, + SearchInput, + SelectedLabel +} from './select_currency.style' +import { ContainerButton, Dialog, DialogTitle } from '../shared/style' + +interface SelectCurrencyProps extends DialogProps { + currencies: MeldFiatCurrency[] + selectedCurrency?: MeldFiatCurrency + isOpen: boolean + onClose: () => void + onSelectCurrency: (currency: MeldFiatCurrency) => void +} + +export const CurrencyListItem = ({ + currency, + isSelected, + onSelect +}: { + currency: MeldFiatCurrency + isSelected?: boolean + onSelect: (currency: MeldFiatCurrency) => void +}) => { + return ( + onSelect(currency)}> + + + {currency.name} + + + {isSelected && ( + {getLocale('braveWalletSelected')} + )} + {currency.currencyCode} + + + ) +} + +export const SelectCurrency = (props: SelectCurrencyProps) => { + const { + currencies, + selectedCurrency, + onSelectCurrency, + isOpen, + onClose, + ...rest + } = props + + // Selectors + const isPanel = useSafeUISelector(UISelectors.isPanel) + + // State + const [searchText, setSearchText] = React.useState('') + + // Memos + const searchResults = React.useMemo(() => { + if (searchText === '') return currencies + + return currencies.filter((currency) => { + return ( + currency?.name?.toLowerCase().includes(searchText.toLowerCase()) || + currency.currencyCode.toLowerCase().includes(searchText.toLowerCase()) + ) + }) + }, [currencies, searchText]) + + const selectCurrencyContent = React.useMemo(() => { + return ( + <> + + {getLocale('braveWalletSelectCurrency')} + + + setSearchText(e.value)} + > + + + + + {searchResults.length === 0 ? ( + + {getLocale('braveWalletNoAvailableCurrencies')} + + ) : ( + + {searchResults.map((currency) => ( + + ))} + + )} + + + ) + }, [onSelectCurrency, searchResults, selectedCurrency]) + + if (isPanel) { + return ( + + + {selectCurrencyContent} + + + ) + } + + return ( + + {selectCurrencyContent} + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/components/shared/style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/components/shared/style.ts new file mode 100644 index 000000000000..5ee58c9dd4a1 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/components/shared/style.ts @@ -0,0 +1,138 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { color, font } from '@brave/leo/tokens/css/variables' +import Icon from '@brave/leo/react/icon' +import LeoDialog from '@brave/leo/react/dialog' +import LeoDropdown from '@brave/leo/react/dropdown' +import Input from '@brave/leo/react/input' + +// Shared Styles +import { WalletButton } from '../../../../../components/shared/style' +import { + layoutPanelWidth // +} from '../../../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' + +export const Label = styled.label` + color: ${color.text.tertiary}; + font: ${font.default.semibold}; + padding: 8px 0; + margin: 0; +` + +export const AssetSymbol = styled.h3` + color: ${color.text.primary}; + text-align: center; + font: ${font.heading.h3}; + padding: 0; + margin: 0; +` + +export const AssetIcon = styled.img<{ size?: string }>` + width: ${(props) => props.size || '40px'}; + height: auto; + border-radius: 50%; +` + +export const CaretDown = styled(Icon).attrs({ name: 'carat-down' })` + --leo-icon-color: ${color.icon.default}; + --leo-icon-size: 24px; +` + +export const WrapperButton = styled(WalletButton)` + background-color: transparent; + border: none; + cursor: pointer; + padding: 0px; + @media (max-width: ${layoutPanelWidth}px) { + justify-content: space-between; + } +` + +export const ControlText = styled.h3` + color: ${color.text.primary}; + text-align: left; + font: ${font.heading.h3}; + padding: 0; + margin: 0; +` + +export const Dialog = styled(LeoDialog).attrs({ + size: window.innerWidth <= layoutPanelWidth ? 'mobile' : 'normal' +})` + --leo-dialog-backdrop-background: rgba(17, 18, 23, 0.35); + --leo-dialog-backdrop-filter: blur(8px); + --leo-dialog-padding: 16px; + .subtitle { + border: 1px solid red; + margin-bottom: 0; + } +` + +export const DialogTitle = styled.p` + font: ${font.heading.h2}; + color: ${color.text.primary}; + text-align: left; + margin: 0; + @media (max-width: ${layoutPanelWidth}px) { + font: ${font.heading.h3}; + } +` + +export const ContainerButton = styled(WalletButton)` + display: flex; + padding: 16px; + gap: 16px; + width: 100%; + justify-content: flex-start; + align-items: center; + background: transparent; + border: none; + outline: ${color.primary[70]}; + border-radius: 8px; + cursor: pointer; + &:hover { + background: ${color.container.interactive}; + } +` + +export const ListTitle = styled.span` + color: ${color.text.tertiary}; + font: ${font.components.tableheader}; +` + +export const Dropdown = styled(LeoDropdown)` + min-width: 100%; +` + +export const SearchInput = styled(Input).attrs({ + mode: 'filled', + size: window.innerWidth <= layoutPanelWidth ? 'small' : 'normal' +})` + width: 100%; +` + +export const IconsWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + position: relative; +` + +export const NetworkIconWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + position: absolute; + bottom: -3px; + right: -3px; + background-color: ${color.container.background}; + border-radius: 100%; + padding: 2px; + z-index: 3; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/fund-wallet.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/fund-wallet.tsx index 2733854bddf1..3c037dd83a4d 100644 --- a/components/brave_wallet_ui/page/screens/fund-wallet/fund-wallet.tsx +++ b/components/brave_wallet_ui/page/screens/fund-wallet/fund-wallet.tsx @@ -97,7 +97,7 @@ import { import { BuyOptions } from '../../../options/buy-with-options' import { makeFundWalletPurchaseOptionsRoute, - makeFundWalletRoute + makeAndroidFundWalletRoute } from '../../../utils/routes-utils' import { networkSupportsAccount } from '../../../utils/network-utils' @@ -207,7 +207,7 @@ function AssetSelection({ isAndroid }: Props) { selectedCurrency={selectedCurrency} key={assetId} token={asset} - onClick={() => history.push(makeFundWalletRoute(assetId))} + onClick={() => history.push(makeAndroidFundWalletRoute(assetId))} ref={ref} /> ) @@ -395,7 +395,7 @@ function AssetSelection({ isAndroid }: Props) { // save latest form values in router history history.replace( - makeFundWalletRoute(selectedOnRampAssetId, { + makeAndroidFundWalletRoute(selectedOnRampAssetId, { currencyCode: selectedCurrency, buyAmount, // save latest search-box value (if it matches selection name diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.stories.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.stories.tsx new file mode 100644 index 000000000000..39f77433c32b --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.stories.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' + +// Components +import { + WalletPageStory // +} from '../../../stories/wrappers/wallet-page-story-wrapper' +import { FundWalletScreen } from './fund_wallet_v2' + +export const _FundWalletScreen = () => { + return ( + + + + ) +} + +_FundWalletScreen.story = { + name: 'Fund Wallet Screen v2' +} + +export default { + component: _FundWalletScreen, + title: 'Fund Wallet Screen v2' +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.style.ts b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.style.ts new file mode 100644 index 000000000000..4e80f95671dc --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.style.ts @@ -0,0 +1,124 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import { color, font } from '@brave/leo/tokens/css/variables' +import ProgressRing from '@brave/leo/react/progressRing' +import LeoDropdown from '@brave/leo/react/dropdown' +import Icon from '@brave/leo/react/icon' + +// Shared Styles +import { + layoutPanelWidth // +} from '../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper.style' +import { Column, Row } from '../../../components/shared/style' + +export const ContentWrapper = styled(Column)` + @media screen and (max-width: ${layoutPanelWidth}px) { + padding: 8px; + } +` + +export const ControlPanel = styled(Row)` + gap: 16px; + justify-content: space-between; + align-items: flex-start; + overflow: hidden; + flex-wrap: wrap; + padding: 0px 12px; + @media screen and (max-width: ${layoutPanelWidth}px) { + border-radius: 16px; + padding: 8px 20px 20px 20px; + background-color: ${color.container.background}; + justify-content: flex-start; + margin-bottom: 8px; + } +` + +export const ServiceProvidersWrapper = styled(Column)` + flex: 1; + gap: 16px; + @media screen and (max-width: ${layoutPanelWidth}px) { + gap: 8px; + border-radius: 16px; + background-color: ${color.container.background}; + padding: 8px; + } +` + +export const LoaderText = styled.p` + color: ${color.text.primary}; + font: ${font.default.regular}; + text-align: center; +` + +export const Loader = styled(ProgressRing).attrs({ + mode: 'indeterminate' +})` + --leo-progressring-size: 32px; +` + +export const Divider = styled.div` + width: 100%; + height: 1px; + background-color: ${color.divider.subtle}; + margin: 40px 0px 24px 0px; + @media screen and (max-width: ${layoutPanelWidth}px) { + display: none; + } +` + +export const PaymentMethodIcon = styled.img` + width: 20px; + height: 20px; + margin-right: 8px; +` + +export const SearchAndFilterRow = styled(Row)` + gap: 12px; + flex-wrap: wrap; + @media screen and (max-width: ${layoutPanelWidth}px) { + gap: 2px; + } +` + +export const SearchBarWrapper = styled(Row)` + max-width: 200px; + @media screen and (max-width: ${layoutPanelWidth}px) { + max-width: unset; + } +` + +export const DropdownRow = styled(Row)` + width: unset; + gap: 12px; + @media screen and (max-width: ${layoutPanelWidth}px) { + width: 100%; + gap: 4px; + } +` + +export const Dropdown = styled(LeoDropdown).attrs({ + size: window.innerWidth <= layoutPanelWidth ? 'small' : 'normal' +})` + width: unset; + @media screen and (max-width: ${layoutPanelWidth}px) { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + } +` + +export const InfoIconWrapper = styled(Column)` + width: 56px; + height: 56px; + border-radius: 100%; + background-color: ${color.page.background}; +` + +export const InfoIcon = styled(Icon).attrs({ name: 'info-outline' })` + --leo-icon-color: ${color.icon.default}; + --leo-icon-size: 24px; +` diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.tsx b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.tsx new file mode 100644 index 000000000000..f7a277181157 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/fund_wallet_v2.tsx @@ -0,0 +1,392 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import Icon from '@brave/leo/react/icon' + +// Types +import { WalletRoutes } from '../../../constants/types' + +// Hooks +import { useBuy } from './hooks/useBuy' + +// Utils +import { getLocale } from '../../../../common/locale' +import { getAssetSymbol } from '../../../utils/meld_utils' +import { isComponentInStorybook } from '../../../utils/string-utils' + +// Selectors +import { useSafeUISelector } from '../../../common/hooks/use-safe-selector' +import { UISelectors } from '../../../common/selectors' + +// Components +import { + WalletPageWrapper // +} from '../../../components/desktop/wallet-page-wrapper/wallet-page-wrapper' +import { + PageTitleHeader // +} from '../../../components/desktop/card-headers/page-title-header' +import { + PanelActionHeader // +} from '../../../components/desktop/card-headers/panel-action-header' +import { + SelectAssetButton // +} from './components/select_asset_button/select_asset_button' +import { + SelectAccountButton // +} from './components/select_account_button/select_account_button' +import { AmountButton } from './components/amount_button/amount_button' +import { SelectCurrency } from './components/select_currency/select_currency' +import { SelectAccount } from './components/select_account/select_account' +import { SelectAsset } from './components/select_asset/select_asset' +import { BuyQuote } from './components/buy_quote/buy_quote' + +// Styled Components +import { + ContentWrapper, + ControlPanel, + Divider, + Loader, + LoaderText, + ServiceProvidersWrapper, + PaymentMethodIcon, + SearchAndFilterRow, + SearchBarWrapper, + DropdownRow, + Dropdown, + InfoIconWrapper, + InfoIcon +} from './fund_wallet_v2.style' +import { Column, Row, Text } from '../../../components/shared/style' +import { SearchInput } from './components/shared/style' + +interface Props { + isAndroid?: boolean +} + +export const FundWalletScreen = ({ isAndroid }: Props) => { + // State + const [isCurrencyDialogOpen, setIsCurrencyDialogOpen] = React.useState(false) + const [isAssetDialogOpen, setIsAssetDialogOpen] = React.useState(false) + const [isAccountDialogOpen, setIsAccountDialogOpen] = React.useState(false) + + // Hooks + const { + selectedAsset, + selectedCurrency, + selectedAccount, + amount, + isLoadingAssets, + isLoadingSpotPrices, + formattedCryptoEstimate, + spotPriceRegistry, + fiatCurrencies, + accounts, + cryptoCurrencies, + defaultFiatCurrency, + isFetchingQuotes, + quotes, + filteredQuotes, + onSelectToken, + onSelectAccount, + onSelectCurrency, + onSetAmount, + serviceProviders, + selectedCountryCode, + isLoadingPaymentMethods, + isLoadingCountries, + countries, + paymentMethods, + selectedPaymentMethod, + onSelectPaymentMethod, + onSelectCountry, + isCreatingWidget, + onBuy, + searchTerm, + onSearch, + hasQuoteError + } = useBuy() + + // Redux + const isPanel = useSafeUISelector(UISelectors.isPanel) + + // Computed + const selectedCountry = countries?.find( + (country) => + country.countryCode.toLowerCase() === selectedCountryCode.toLowerCase() + ) + const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches + const isStorybook = isComponentInStorybook() + const pageTitle = getLocale('braveWalletBuyAsset').replace( + '$1', + getAssetSymbol(selectedAsset) + ) + const isFetchingFirstTimeQuotes = isFetchingQuotes && quotes?.length === 0 + + return ( + <> + + ) : ( + + ) + } + useDarkBackground={isPanel} + noPadding={isPanel} + noCardPadding={isPanel} + > + + + setIsAssetDialogOpen(true)} + /> + setIsAccountDialogOpen(true)} + /> + { + setIsCurrencyDialogOpen(true) + }} + onChange={onSetAmount} + estimatedCryptoAmount={formattedCryptoEstimate} + /> + + + + + + onSearch(e.value)} + size='small' + disabled={isFetchingFirstTimeQuotes} + > + + + + + onSelectCountry(detail.value as string)} + disabled={isFetchingFirstTimeQuotes || isLoadingCountries} + > +
{selectedCountry?.name}
+ {countries?.map((country) => { + return ( + + {country.name} + + ) + })} +
+ + onSelectPaymentMethod(detail.value as string) + } + disabled={ + isFetchingFirstTimeQuotes || isLoadingPaymentMethods + } + > +
{selectedPaymentMethod.name}
+ {paymentMethods?.map((paymentMethod) => { + const logoUrl = isDarkMode + ? paymentMethod.logoImages?.darkUrl + : paymentMethod.logoImages?.lightUrl + return ( + + + + {paymentMethod.name} + + + ) + })} +
+
+
+ {isFetchingFirstTimeQuotes ? ( + + + + {getLocale('braveWalletGettingBestPrices')} + + + ) : ( + <> + {hasQuoteError ? ( + + + + + + {getLocale('braveWalletNoProviderFound').replace( + '$1', + getAssetSymbol(selectedAsset) + )} + + + {getLocale('braveWalletTrySearchingForDifferentAsset')} + + + ) : ( + <> + {searchTerm !== '' && filteredQuotes.length === 0 ? ( + + + + + + {getLocale('braveWalletNoResultsFound').replace( + '$1', + searchTerm + )} + + + {getLocale('braveWalletTryDifferentKeywords')} + + + ) : ( + + {filteredQuotes?.map((quote, index) => ( + 1 && index === 0 + } + isOpenOverride={index === 0} + isCreatingWidget={isCreatingWidget} + onBuy={onBuy} + selectedAsset={selectedAsset} + /> + ))} + + )} + + )} + + )} +
+
+
+ + { + onSelectToken(asset) + setIsAssetDialogOpen(false) + }} + onClose={() => setIsAssetDialogOpen(false)} + /> + + { + onSelectCurrency(currency) + setIsCurrencyDialogOpen(false) + }} + onClose={() => setIsCurrencyDialogOpen(false)} + /> + + { + onSelectAccount(account) + setIsAccountDialogOpen(false) + }} + selectedAsset={selectedAsset} + onClose={() => setIsAccountDialogOpen(false)} + /> + + ) +} diff --git a/components/brave_wallet_ui/page/screens/fund-wallet/hooks/useBuy.ts b/components/brave_wallet_ui/page/screens/fund-wallet/hooks/useBuy.ts new file mode 100644 index 000000000000..f9508ad83dd4 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/fund-wallet/hooks/useBuy.ts @@ -0,0 +1,535 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import { useState, useEffect, useCallback, useMemo } from 'react' +import { useHistory } from 'react-router' +import { skipToken } from '@reduxjs/toolkit/dist/query' + +// Types +import { + MeldCryptoCurrency, + MeldFiatCurrency, + BraveWallet, + MeldCryptoQuote, + MeldPaymentMethod, + CryptoBuySessionData, + CryptoWidgetCustomerData +} from '../../../../constants/types' + +// Hooks +import { + useDebouncedCallback // +} from '../../swap/hooks/useDebouncedCallback' +import { useQuery } from '../../../../common/hooks/use-query' + +// Queries +import { + useGetDefaultFiatCurrencyQuery, + useGetMeldFiatCurrenciesQuery, + useGetMeldCryptoCurrenciesQuery, + useGetMeldCountriesQuery, + useGenerateMeldCryptoQuotesMutation, + useGetTokenSpotPricesQuery, + useGetMeldServiceProvidersQuery, + useGetMeldPaymentMethodsQuery, + useCreateMeldBuyWidgetMutation +} from '../../../../common/slices/api.slice' +import { + useAccountFromAddressQuery, + useAccountsQuery, + useReceiveAddressQuery +} from '../../../../common/slices/api.slice.extra' + +// Constants +import { querySubscriptionOptions60s } from '../../../../common/slices/constants' + +// Utils +import Amount from '../../../../utils/amount' +import { + getAssetSymbol, + getAssetPriceId, + getMeldTokensCoinType +} from '../../../../utils/meld_utils' +import { makeFundWalletRoute } from '../../../../utils/routes-utils' + +export type BuyParamOverrides = { + country?: string + paymentMethod?: MeldPaymentMethod + sourceCurrencyCode?: string + destinationCurrencyCode?: string + amount?: string + account?: string +} + +const DEFAULT_ASSET: MeldCryptoCurrency = { + 'currencyCode': 'ETH', + 'name': 'Ethereum', + 'chainCode': 'ETH', + 'chainName': 'Ethereum', + 'chainId': '1', + 'contractAddress': '0x0000000000000000000000000000000000000000', + 'symbolImageUrl': 'https://images-currency.meld.io/crypto/ETH/symbol.png' +} + +const DEFAULT_PAYMENT_METHOD: MeldPaymentMethod = { + logoImages: { + darkShortUrl: '', + darkUrl: + 'https://images-paymentMethod.meld.io/CREDIT_DEBIT_CARD/logo_dark.png', + lightShortUrl: '', + lightUrl: + 'https://images-paymentMethod.meld.io/CREDIT_DEBIT_CARD/logo_light.png' + }, + name: 'Credit & Debit Card', + paymentMethod: 'CREDIT_DEBIT_CARD', + paymentType: 'CARD' +} + +const getFirstAccountByCoinType = ( + coin: BraveWallet.CoinType, + accounts: BraveWallet.AccountInfo[] +) => { + return accounts.filter((account) => account.accountId.coin === coin)[0] +} + +export const useBuy = () => { + // Routing + const history = useHistory() + const query = useQuery() + + // Queries + const { data: defaultFiatCurrency = 'USD' } = useGetDefaultFiatCurrencyQuery() + const { data: fiatCurrencies } = useGetMeldFiatCurrenciesQuery() + const { data: meldSupportedBuyAssets, isLoading: isLoadingAssets } = + useGetMeldCryptoCurrenciesQuery() + const { accounts } = useAccountsQuery() + const { data: countries, isLoading: isLoadingCountries } = + useGetMeldCountriesQuery() + const { data: serviceProviders = [], isLoading: isLoadingServiceProvider } = + useGetMeldServiceProvidersQuery() + const { account: accountFromParams } = useAccountFromAddressQuery( + query.get('accountId') ?? undefined + ) + const chainId = query.get('chainId') ?? undefined + const currencyCode = query.get('currencyCode') ?? undefined + + // State + const [selectedCurrency, setSelectedCurrency] = useState< + MeldFiatCurrency | undefined + >(undefined) + const [amount, setAmount] = useState('100') + const [abortController, setAbortController] = useState< + AbortController | undefined + >(undefined) + const [isFetchingQuotes, setIsFetchingQuotes] = useState(false) + const [hasQuoteError, setHasQuoteError] = useState(false) + const [quotes, setQuotes] = useState([]) + const [timeUntilNextQuote, setTimeUntilNextQuote] = useState< + number | undefined + >(undefined) + const [selectedCountryCode, setSelectedCountryCode] = useState('US') + const [selectedPaymentMethod, setSelectedPaymentMethod] = + useState(DEFAULT_PAYMENT_METHOD) + const [isCreatingWidget, setIsCreatingWidget] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + + // Mutations + const [generateQuotes] = useGenerateMeldCryptoQuotesMutation() + const [createMeldBuyWidget] = useCreateMeldBuyWidgetMutation() + const { data: paymentMethods, isLoading: isLoadingPaymentMethods } = + useGetMeldPaymentMethodsQuery( + selectedCountryCode && defaultFiatCurrency + ? { + country: selectedCountryCode, + sourceCurrencyCode: defaultFiatCurrency + } + : skipToken + ) + + // Memos and Queries + const tokenPriceIds: string[] = useMemo(() => { + return meldSupportedBuyAssets?.map((asset) => getAssetPriceId(asset)) ?? [] + }, [meldSupportedBuyAssets]) + + const { data: spotPriceRegistry, isLoading: isLoadingSpotPrices } = + useGetTokenSpotPricesQuery( + tokenPriceIds.length > 0 + ? { + ids: tokenPriceIds, + toCurrency: selectedCurrency?.currencyCode || defaultFiatCurrency + } + : skipToken, + querySubscriptionOptions60s + ) + + const selectedAsset = useMemo(() => { + if (!currencyCode || !meldSupportedBuyAssets || !chainId) { + return DEFAULT_ASSET + } + + return ( + meldSupportedBuyAssets.find( + (asset) => + asset.currencyCode === currencyCode && asset.chainId === chainId + ) ?? DEFAULT_ASSET + ) + }, [meldSupportedBuyAssets, currencyCode, chainId]) + + const selectedAccount = useMemo(() => { + if (!accountFromParams) { + return getFirstAccountByCoinType( + getMeldTokensCoinType(selectedAsset), + accounts + ) + } + return accountFromParams + }, [accountFromParams, accounts, selectedAsset]) + + const selectedAssetSpotPrice = useMemo(() => { + if (selectedAsset && spotPriceRegistry) { + return spotPriceRegistry[getAssetPriceId(selectedAsset)] + } + return undefined + }, [selectedAsset, spotPriceRegistry]) + + const [cryptoEstimate, formattedCryptoEstimate] = useMemo(() => { + if (selectedAssetSpotPrice && selectedAsset) { + const symbol = getAssetSymbol(selectedAsset) + const estimate = new Amount(amount).div(selectedAssetSpotPrice.price) + + return [ + estimate?.toNumber().toString(), + estimate?.formatAsAsset(5, symbol) + ] + } + return ['', ''] + }, [selectedAssetSpotPrice, selectedAsset, amount]) + + const quotesSortedByBestReturn = useMemo(() => { + if (quotes.length === 0) { + return [] + } + return Array.from(quotes).sort(function (a, b) { + return new Amount(b.destinationAmount ?? '0') + .minus(a.destinationAmount ?? '0') + .toNumber() + }) + }, [quotes]) + + const filteredQuotes = useMemo(() => { + if (searchTerm === '') { + return quotesSortedByBestReturn + } + + return quotesSortedByBestReturn.filter( + (quote) => + quote.serviceProvider + ?.toLowerCase() + .startsWith(searchTerm.toLowerCase()) || + quote.serviceProvider?.toLowerCase().includes(searchTerm.toLowerCase()) + ) + }, [quotesSortedByBestReturn, searchTerm]) + + const { receiveAddress } = useReceiveAddressQuery(selectedAccount?.accountId) + + // Methods + const reset = useCallback(() => { + setSelectedCurrency(undefined) + setAmount('') + setHasQuoteError(false) + setQuotes([]) + setTimeUntilNextQuote(undefined) + + if (abortController) { + abortController.abort() + setAbortController(undefined) + } + }, [abortController]) + + const handleQuoteRefreshInternal = useCallback( + async (overrides: BuyParamOverrides) => { + const params = { + country: overrides.country ?? selectedCountryCode, + sourceCurrencyCode: + overrides.sourceCurrencyCode ?? selectedCurrency?.currencyCode, + destinationCurrencyCode: + overrides.destinationCurrencyCode ?? selectedAsset?.currencyCode, + amount: overrides.amount ?? amount, + account: overrides.account ?? selectedAccount.address, + paymentMethod: overrides.paymentMethod ?? selectedPaymentMethod + } + + if ( + !params.sourceCurrencyCode || + !params.destinationCurrencyCode || + !params.account + ) { + return + } + + const amountWrapped = new Amount(params.amount) + const isAmountEmpty = + amountWrapped.isZero() || + amountWrapped.isNaN() || + amountWrapped.isUndefined() + + if (isAmountEmpty) { + return + } + + const controller = new AbortController() + setAbortController(controller) + setIsFetchingQuotes(true) + setHasQuoteError(false) + + let quoteResponse + try { + quoteResponse = await generateQuotes({ + account: selectedAccount.address, + amount: amountWrapped.toNumber(), + country: params.country || 'US', + sourceCurrencyCode: params.sourceCurrencyCode, + destinationCurrencyCode: params.destinationCurrencyCode, + paymentMethod: params.paymentMethod + }).unwrap() + } catch (error) { + console.error('generateQuotes failed', error) + setIsFetchingQuotes(false) + } + + if (controller.signal.aborted) { + setIsFetchingQuotes(false) + setAbortController(undefined) + return + } + + if (quoteResponse?.error) { + console.error('quoteResponse.error', quoteResponse.error) + setHasQuoteError(true) + } + + if (quoteResponse?.cryptoQuotes) { + setQuotes(quoteResponse.cryptoQuotes) + } + + setIsFetchingQuotes(false) + setAbortController(undefined) + setTimeUntilNextQuote(30000) + }, + [ + amount, + selectedCountryCode, + generateQuotes, + selectedAccount?.address, + selectedAsset?.currencyCode, + selectedCurrency?.currencyCode, + selectedPaymentMethod + ] + ) + + const handleQuoteRefresh = useDebouncedCallback( + async (overrides: BuyParamOverrides) => { + await handleQuoteRefreshInternal(overrides) + }, + 700 + ) + + const onSetAmount = useCallback( + async (value: string) => { + setAmount(value) + if (!value) { + setAmount('') + } + + setQuotes([]) + + await handleQuoteRefresh({ + amount: value + }) + }, + [handleQuoteRefresh] + ) + + const onSelectCountry = useCallback( + async (countryCode: string) => { + setSelectedCountryCode(countryCode) + setQuotes([]) + + await handleQuoteRefresh({ + country: countryCode + }) + }, + [handleQuoteRefresh] + ) + + const onSelectPaymentMethod = useCallback( + async (paymentMethod: string) => { + const foundMethod = + paymentMethods?.find( + (method) => method.paymentMethod === paymentMethod + ) ?? DEFAULT_PAYMENT_METHOD + setSelectedPaymentMethod(foundMethod) + setQuotes([]) + + await handleQuoteRefresh({ + paymentMethod: foundMethod + }) + }, + [handleQuoteRefresh, paymentMethods] + ) + + const onSelectCurrency = useCallback( + async (currency: MeldFiatCurrency) => { + setSelectedCurrency(currency) + + await handleQuoteRefresh({ + sourceCurrencyCode: currency.currencyCode + }) + }, + [handleQuoteRefresh] + ) + + const onSelectToken = useCallback( + async (asset: MeldCryptoCurrency) => { + const incomingAssetsCoinType = getMeldTokensCoinType(asset) + const accountToUse = + selectedAccount.accountId.coin !== incomingAssetsCoinType + ? getFirstAccountByCoinType(incomingAssetsCoinType, accounts) + : selectedAccount + history.replace(makeFundWalletRoute(asset, accountToUse)) + setQuotes([]) + + await handleQuoteRefresh({ + destinationCurrencyCode: asset?.currencyCode + }) + }, + [handleQuoteRefresh, history, selectedAccount, accounts] + ) + + const onSelectAccount = useCallback( + (account: BraveWallet.AccountInfo) => { + history.replace(makeFundWalletRoute(selectedAsset, account)) + }, + [selectedAsset, history] + ) + + const onBuy = useCallback( + async (quote: MeldCryptoQuote) => { + if (!quote.serviceProvider || !selectedCurrency) return + + const sessionData: CryptoBuySessionData = { + countryCode: selectedCountryCode, + destinationCurrencyCode: selectedAsset.currencyCode, + paymentMethodType: undefined, + redirectUrl: undefined, + serviceProvider: quote.serviceProvider, + sourceAmount: amount, + sourceCurrencyCode: selectedCurrency.currencyCode, + walletAddress: receiveAddress, + walletTag: undefined, + lockFields: ['walletAddress'] + } + + const customerData: CryptoWidgetCustomerData = { + customer: undefined, + customerId: undefined, + externalCustomerId: undefined, + externalSessionId: undefined + } + + try { + setIsCreatingWidget(true) + const { widget } = await createMeldBuyWidget({ + sessionData, + customerData + }).unwrap() + setIsCreatingWidget(false) + + if (widget) { + const { widgetUrl } = widget + chrome.tabs.create({ url: widgetUrl }) + } + } catch (error) { + console.error('createMeldBuyWidget failed', error) + setIsCreatingWidget(false) + } + }, + [ + amount, + createMeldBuyWidget, + selectedAsset?.currencyCode, + selectedCountryCode, + selectedCurrency, + receiveAddress + ] + ) + + // Effects + useEffect(() => { + if (fiatCurrencies && fiatCurrencies.length > 0 && !selectedCurrency) { + const defaultCurrency = fiatCurrencies.find( + (currency) => + currency.currencyCode.toLowerCase() === + defaultFiatCurrency.toLowerCase() + ) + setSelectedCurrency(defaultCurrency) + } + }, [defaultFiatCurrency, fiatCurrencies, selectedCurrency]) + + // Fetch quotes in intervals + useEffect(() => { + const interval = setInterval(async () => { + if (timeUntilNextQuote && timeUntilNextQuote !== 0) { + setTimeUntilNextQuote(timeUntilNextQuote - 1000) + return + } + if (!isFetchingQuotes) { + await handleQuoteRefresh({}) + } + }, 1000) + return () => { + clearInterval(interval) + } + }, [handleQuoteRefresh, timeUntilNextQuote, isFetchingQuotes]) + + return { + selectedAsset, + selectedCurrency, + selectedAccount, + amount, + isLoadingAssets, + isLoadingSpotPrices, + formattedCryptoEstimate, + spotPriceRegistry, + fiatCurrencies, + cryptoCurrencies: meldSupportedBuyAssets, + countries, + accounts, + defaultFiatCurrency, + isFetchingQuotes, + quotes, + filteredQuotes, + onSelectToken, + onSelectAccount, + onSelectCurrency, + onSetAmount, + serviceProviders, + selectedCountryCode, + isLoadingPaymentMethods, + isLoadingCountries, + paymentMethods, + selectedPaymentMethod, + onSelectCountry, + onSelectPaymentMethod, + onBuy, + isCreatingWidget, + searchTerm, + onSearch: setSearchTerm, + cryptoEstimate, + hasQuoteError, + isLoadingServiceProvider, + reset + } +} diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index 9d3b4884f682..117fe3e8ece1 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -579,7 +579,7 @@ provideStrings({ braveWalletSelectAccount: 'Select account', braveWalletSearchAccount: 'Search accounts', braveWalletSelectNetwork: 'Select network', - braveWalletSelectAsset: 'Select from', + braveWalletSelectAsset: 'Select asset', braveWalletSearchAsset: 'Search coins', braveWalletSelectCurrency: 'Select currency', braveWalletSearchCurrency: 'Search currencies', @@ -668,6 +668,30 @@ provideStrings({ braveWalletBuyDisclaimer: 'Financial and transaction data is processed by our onramp partners. ' + 'Brave does not collect or have access to such data.', + braveWalletTransactionsPartner: 'Transactions partner', + braveWalletTransactionPartnerConsent: + 'Brave Wallet uses Meld.io to help aggregate and surface various ' + + 'crypto providers for your region. We will share information with ' + + 'Meld.io to complete the transaction, including your wallet address ' + + 'and country code. For more information please read Meld’s terms of use.', + braveWalletMeldTermsOfUse: + 'I have read and agree to the $1Meld’s Terms of use$2', + braveWalletBestOption: 'Best Option', + braveWalletExchangeRateWithFees: 'Exchange rate with fees', + braveWalletFees: 'Fees', + braveWalletPriceCurrency: 'Price $1', + braveWalletBuyWithProvider: 'Buy with $1', + braveWalletAsset: 'Asset', + braveWalletSelected: 'Selected', + braveWalletNoAvailableCurrencies: 'No available currencies', + braveWalletGettingBestPrices: 'Getting best prices...', + braveWalletBuyAsset: 'Buy $1', + braveWalletNoProviderFound: 'No providers found for $1', + braveWalletTrySearchingForDifferentAsset: + 'Try searching for a different asset.', + braveWalletNoResultsFound: 'No results found for $1', + braveWalletTryDifferentKeywords: + 'Try using a different keyword or check your spelling.', // Fund Wallet Screen braveWalletFundWalletTitle: diff --git a/components/brave_wallet_ui/utils/coin-market-utils.ts b/components/brave_wallet_ui/utils/coin-market-utils.ts index 756b608d952e..276cdb2682bc 100644 --- a/components/brave_wallet_ui/utils/coin-market-utils.ts +++ b/components/brave_wallet_ui/utils/coin-market-utils.ts @@ -7,8 +7,10 @@ import { BraveWallet, MarketAssetFilterOption, MarketGridColumnTypes, + MeldCryptoCurrency, SortOrder } from '../constants/types' +import { getAssetSymbol } from './meld_utils' export const sortCoinMarkets = ( marketData: BraveWallet.CoinMarket[], @@ -40,18 +42,18 @@ export const searchCoinMarkets = ( export const filterCoinMarkets = ( coins: BraveWallet.CoinMarket[], - tradableAssets: BraveWallet.BlockchainToken[], + tradableAssets: MeldCryptoCurrency[] | undefined, filter: MarketAssetFilterOption ) => { - const tradableAssetsSymbols = tradableAssets.map((asset) => - asset.symbol.toLowerCase() + const tradableAssetsSymbols = tradableAssets?.map((asset) => + getAssetSymbol(asset).toLowerCase() ) if (filter === 'all') { return coins } else if (filter === 'tradable') { return coins.filter((asset) => - tradableAssetsSymbols.includes(asset.symbol.toLowerCase()) + tradableAssetsSymbols?.includes(asset.symbol.toLowerCase()) ) } diff --git a/components/brave_wallet_ui/utils/meld_utils.ts b/components/brave_wallet_ui/utils/meld_utils.ts new file mode 100644 index 000000000000..0e91ea79ad60 --- /dev/null +++ b/components/brave_wallet_ui/utils/meld_utils.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2024 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 https://mozilla.org/MPL/2.0/. + +import { + BraveWallet, + MeldCryptoCurrency, + SpotPriceRegistry +} from '../constants/types' + +export const getAssetSymbol = (asset: MeldCryptoCurrency) => { + return asset.currencyCode.replace(`_${asset.chainCode}`, '') +} + +export const getAssetPriceId = (asset: MeldCryptoCurrency) => { + const isEthereumNetwork = + asset.chainId?.toLowerCase() === BraveWallet.MAINNET_CHAIN_ID.toLowerCase() + if (isEthereumNetwork && asset.contractAddress) { + return asset.contractAddress.toLowerCase() + } + + return getAssetSymbol(asset)?.toLowerCase() ?? '' +} + +export const getTokenPriceFromRegistry = ( + spotPriceRegistry: SpotPriceRegistry, + asset: MeldCryptoCurrency +): BraveWallet.AssetPrice | undefined => { + return spotPriceRegistry[getAssetPriceId(asset)] +} + +export const getAssetIdKey = (asset: MeldCryptoCurrency) => { + return `0x${parseInt(asset.chainId ?? '').toString(16)}-${ + asset.currencyCode + }-${asset.contractAddress}` +} + +export const getMeldTokensCoinType = ( + asset: Pick +) => { + switch (asset.chainCode) { + case 'BTC': + return BraveWallet.CoinType.BTC + case 'FIL': + return BraveWallet.CoinType.FIL + case 'ZEC': + return BraveWallet.CoinType.ZEC + case 'SOLANA': + return BraveWallet.CoinType.SOL + default: + return BraveWallet.CoinType.ETH + } +} + +export const getMeldTokensChainId = ( + asset: Pick +) => { + switch (asset.chainCode) { + case 'BTC': + return BraveWallet.BITCOIN_MAINNET + case 'FIL': + return BraveWallet.FILECOIN_MAINNET + case 'ZEC': + return BraveWallet.Z_CASH_MAINNET + default: + return asset.chainId + } +} diff --git a/components/brave_wallet_ui/utils/routes-utils.ts b/components/brave_wallet_ui/utils/routes-utils.ts index 350df03749ef..7eeba0e3413f 100644 --- a/components/brave_wallet_ui/utils/routes-utils.ts +++ b/components/brave_wallet_ui/utils/routes-utils.ts @@ -11,7 +11,8 @@ import { WalletOrigin, WalletCreationMode, WalletImportMode, - NftDropdownOptionId + NftDropdownOptionId, + MeldCryptoCurrency } from '../constants/types' import { LOCAL_STORAGE_KEYS } from '../common/constants/local-storage-keys' import { SUPPORT_LINKS } from '../common/constants/support_links' @@ -108,6 +109,24 @@ export const makeAccountTransactionRoute = ( } export const makeFundWalletRoute = ( + asset: Pick, + account?: BraveWallet.AccountInfo +) => { + const baseQueryParams = { + currencyCode: asset.currencyCode ?? '', + chainId: asset.chainId ?? '' + } + + const params = new URLSearchParams( + account + ? { ...baseQueryParams, accountId: account.accountId.uniqueKey } + : baseQueryParams + ) + + return `${WalletRoutes.FundWalletPageStart}?${params.toString()}` +} + +export const makeAndroidFundWalletRoute = ( assetId: string, options?: { currencyCode?: string diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 42bd4c8dc577..33db2687d660 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -344,7 +344,7 @@ Select account Search accounts Select network - Select from + Select asset Search coins Amount To @@ -620,6 +620,23 @@ Sell with $1Ramp Buy with Sardine Financial and transaction data is processed by our onramp partners. Brave does not collect or have access to such data. + Transactions partner + Brave Wallet uses Meld.io to help aggregate and surface various crypto providers for your region. We will share information with Meld.io to complete the transaction, including your wallet address and country code. For more information please read Meld’s terms of use. + I have read and agree to the $1Meld's Terms of use$2 + Best Option + Exchange rate with fees + Fees + Price $1USD + Buy with $1Ramp + Asset + Selected + No available currencies + Getting best prices... + Buy $1BAT + No providers found for $1BAT + Try searching for a different asset. + No results found for $1Ramp + Try using a different keyword or check your spelling. Buy with Transak Buy with Link Buy with Coinbase Pay diff --git a/ui/webui/resources/BUILD.gn b/ui/webui/resources/BUILD.gn index f8a601b33b35..07cfee1db513 100644 --- a/ui/webui/resources/BUILD.gn +++ b/ui/webui/resources/BUILD.gn @@ -168,9 +168,9 @@ leo_icons = [ "autoplay-off.svg", "autoplay-on.svg", "backward.svg", + "bank.svg", "bar-chart.svg", "bat-color.svg", - "bank.svg", "bing-color.svg", "bluetooth-off.svg", "bluetooth.svg",