diff --git a/android/brave_java_resources.gni b/android/brave_java_resources.gni index 6ba0c11eabe3..71da9d54947c 100644 --- a/android/brave_java_resources.gni +++ b/android/brave_java_resources.gni @@ -169,6 +169,7 @@ brave_java_resources = [ "java/res/drawable-night-xhdpi/ic_cookie_background.png", "java/res/drawable-night-xxhdpi/ic_cookie_background.png", "java/res/drawable-night-xxxhdpi/ic_cookie_background.png", + "java/res/drawable-night/sardine_logo.xml", "java/res/drawable-nodpi/dylan_malval_sea_min.webp", "java/res/drawable-nodpi/eth.png", "java/res/drawable-nodpi/fee_icon.png", @@ -692,6 +693,7 @@ brave_java_resources = [ "java/res/drawable/radio_button_holo_bg.xml", "java/res/drawable/radio_button_selected_bg.xml", "java/res/drawable/radiobutton_background.xml", + "java/res/drawable/ramp_logo.xml", "java/res/drawable/rating_bottomsheet_background.xml", "java/res/drawable/rating_news_button_background.xml", "java/res/drawable/recovery_phrase_bg.xml", @@ -720,6 +722,7 @@ brave_java_resources = [ "java/res/drawable/rounded_top_corners.xml", "java/res/drawable/rounded_wallet_edittext.xml", "java/res/drawable/rounded_white_holo_bg.xml", + "java/res/drawable/sardine_logo.xml", "java/res/drawable/selected_dot.xml", "java/res/drawable/selected_indicator.xml", "java/res/drawable/set_default_rounded_button_disabled.xml", @@ -731,6 +734,7 @@ brave_java_resources = [ "java/res/drawable/tab_selector.xml", "java/res/drawable/tip_amount.xml", "java/res/drawable/toggle_text_color_selector.xml", + "java/res/drawable/transak_logo.xml", "java/res/drawable/transparent_bg_bordered.xml", "java/res/drawable/twitter_button_background.xml", "java/res/drawable/unverified_48_rounded_bg.xml", @@ -769,6 +773,7 @@ brave_java_resources = [ "java/res/layout/activity_network_selector.xml", "java/res/layout/activity_nft_detail.xml", "java/res/layout/activity_onboarding.xml", + "java/res/layout/activity_select_purchase_method.xml", "java/res/layout/activity_split_tunnel.xml", "java/res/layout/activity_welcome_onboarding.xml", "java/res/layout/application_item_layout.xml", diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index d41786059963..8772b4f881b0 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -45,6 +45,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/app/BraveActivity.java", "../../brave/android/java/org/chromium/chrome/browser/app/appmenu/AppMenuIconRowFooter.java", "../../brave/android/java/org/chromium/chrome/browser/app/appmenu/BraveAppMenuPropertiesDelegateImpl.java", + "../../brave/android/java/org/chromium/chrome/browser/app/domain/BuyModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/CryptoModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/CryptoSharedActions.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/CryptoSharedData.java", @@ -92,6 +93,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NetworkSelectorActivity.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/activities/NftDetailActivity.java", + "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/activities/SelectPurchaseMethodActivity.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/AccountSpinnerAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/ApproveTxFragmentPageAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CreateAccountAdapter.java", diff --git a/android/java/AndroidManifest.xml b/android/java/AndroidManifest.xml index dd300b5c4bc8..6274cbd0a5a2 100644 --- a/android/java/AndroidManifest.xml +++ b/android/java/AndroidManifest.xml @@ -86,6 +86,11 @@ android:screenOrientation="sensorPortrait" tools:ignore="LockedOrientationActivity"/> + + { + if (error != null && !error.isEmpty()) { + callback.OnUrlReady(null); + return; + } + callback.OnUrlReady(url); + }); + } + + public void isBuySupported(NetworkInfo selectedNetwork, String assetSymbol, + String contractAddress, String chainId, int[] rampProviders, + Callback1 callback) { + TokenUtils.getBuyTokensFiltered(mBlockchainRegistry, selectedNetwork, + TokenUtils.TokenType.ALL, rampProviders, tokens -> { + callback.call(JavaUtils.includes(tokens, + iToken + -> AssetUtils.Filters.isSameToken( + iToken, assetSymbol, contractAddress, chainId))); + }); + } + + void resetServices(AssetRatioService assetRatioService, BlockchainRegistry blockchainRegistry) { + synchronized (mLock) { + mAssetRatioService = assetRatioService; + mBlockchainRegistry = blockchainRegistry; + } + } + + public interface OnRampCallback { + void OnUrlReady(String url); + } +} diff --git a/android/java/org/chromium/chrome/browser/app/domain/CryptoModel.java b/android/java/org/chromium/chrome/browser/app/domain/CryptoModel.java index 6d5b32bc78c1..9ea9504f2819 100644 --- a/android/java/org/chromium/chrome/browser/app/domain/CryptoModel.java +++ b/android/java/org/chromium/chrome/browser/app/domain/CryptoModel.java @@ -78,6 +78,7 @@ public class CryptoModel { private NetworkModel mNetworkModel; private PortfolioModel mPortfolioModel; + private BuyModel mBuyModel; // Todo: create method to create and return new models for Asset, Account, // TransactionConfirmation, SwapModel, AssetModel, SendModel @@ -134,6 +135,9 @@ public void resetServices(Context context, TxService mTxService, KeyringService mPortfolioModel.resetServices(context, mTxService, mKeyringService, mBlockchainRegistry, mJsonRpcService, mEthTxManagerProxy, mSolanaTxManagerProxy, mBraveWalletService, mAssetRatioService); + if (mBuyModel != null) { + mBuyModel.resetServices(mAssetRatioService, mBlockchainRegistry); + } } init(); } @@ -273,6 +277,13 @@ public NetworkModel getNetworkModel() { return mNetworkModel; } + public BuyModel getBuyModel() { + if (mBuyModel == null) { + mBuyModel = new BuyModel(mAssetRatioService, mBlockchainRegistry); + } + return mBuyModel; + } + public PortfolioModel getPortfolioModel() { return mPortfolioModel; } @@ -324,21 +335,10 @@ public void setAccountInfosFromKeyRingModel( mNetworkModel.setAccountInfosFromKeyRingModel(accountInfosFromKeyRingModel); } - // TODO: Move to BuyModel class - public void isBuySupported(NetworkInfo selectedNetwork, String assetSymbol, - String contractAddress, String chainId, Callback1 callback) { - TokenUtils.getBuyTokensFiltered( - mBlockchainRegistry, selectedNetwork, TokenUtils.TokenType.ALL, tokens -> { - callback.call(JavaUtils.includes(tokens, - iToken - -> AssetUtils.Filters.isSameToken( - iToken, assetSymbol, contractAddress, chainId))); - }); - } - // Clear buy send swap model public void clearBSS() { mSendModel = null; + mBuyModel = null; } /* diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/AssetDetailActivity.java b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/AssetDetailActivity.java index eae9635015de..1f32a5027a9c 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/AssetDetailActivity.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/AssetDetailActivity.java @@ -40,6 +40,7 @@ import org.chromium.brave_wallet.mojom.TransactionInfo; import org.chromium.chrome.R; import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.app.domain.BuyModel; import org.chromium.chrome.browser.app.domain.WalletModel; import org.chromium.chrome.browser.crypto_wallet.BlockchainRegistryFactory; import org.chromium.chrome.browser.crypto_wallet.adapters.WalletCoinAdapter; @@ -506,8 +507,9 @@ private void showHideBuyUi() { LiveDataUtil.observeOnce(mWalletModel.getCryptoModel().getNetworkModel().mDefaultNetwork, selectedNetwork -> { - mWalletModel.getCryptoModel().isBuySupported(selectedNetwork, mAssetSymbol, - mContractAddress, mChainId, isBuyEnabled -> { + mWalletModel.getCryptoModel().getBuyModel().isBuySupported(selectedNetwork, + mAssetSymbol, mContractAddress, mChainId, + BuyModel.SUPPORTED_RAMP_PROVIDERS, isBuyEnabled -> { if (isBuyEnabled) { AndroidUtils.show(mBtnBuy); } else { diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java index 2aeae2a6db91..f447614f6a68 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java @@ -971,18 +971,11 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { }); } } else if (mActivityType == ActivityType.BUY) { - assert mBlockchainRegistry != null; - String symbol = AssetUtils.mapToRampNetworkSymbol(mCurrentBlockchainToken); - mBlockchainRegistry.getBuyUrl(OnRampProvider.RAMP, mCurrentBlockchainToken.chainId, - from, symbol, amount, (url, error) -> { - if (error != null && !error.isEmpty() && Utils.isDebuggable(this)) { - Log.e(TAG, "Could not get buy URL: " + error); - return; - } + Intent selectPurchaseMethodIntent = SelectPurchaseMethodActivity.getIntent(this, + mCurrentBlockchainToken.chainId, from, mCurrentBlockchainToken.symbol, + mCurrentBlockchainToken.contractAddress, amount); + startActivity(selectPurchaseMethodIntent); - TabUtils.openUrlInNewTab(false, url); - TabUtils.bringChromeTabbedActivityToTheTop(this); - }); } else if (mActivityType == ActivityType.SWAP) { if (mCurrentBlockchainToken != null) { String btnText = mBtnBuySendSwap.getText().toString(); diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/SelectPurchaseMethodActivity.java b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/SelectPurchaseMethodActivity.java new file mode 100644 index 000000000000..a1d52fed2147 --- /dev/null +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/SelectPurchaseMethodActivity.java @@ -0,0 +1,156 @@ +/* Copyright (c) 2023 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/. */ + +package org.chromium.chrome.browser.crypto_wallet.activities; + +import static org.chromium.chrome.browser.crypto_wallet.util.Utils.warnWhenError; + +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.annotation.StringRes; +import androidx.appcompat.widget.Toolbar; + +import org.chromium.brave_wallet.mojom.OnRampProvider; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.app.domain.BuyModel; +import org.chromium.chrome.browser.app.domain.WalletModel; +import org.chromium.chrome.browser.util.LiveDataUtil; +import org.chromium.chrome.browser.util.TabUtils; + +public class SelectPurchaseMethodActivity extends BraveWalletBaseActivity { + private static final String CHAIN_ID = "chainId"; + private static final String FROM = "from"; + private static final String ASSET_SYMBOL = "assetSymbol"; + private static final String CONTRACT_ADDRESS = "contractAddress"; + private static final String AMOUNT = "amount"; + + private BuyModel mBuyModel; + private WalletModel mWalletModel; + + private String mChainId; + private String mFrom; + private String mAssetSymbol; + private String mContractAddress; + private String mAmount; + + private ViewGroup mRampNetworkLayout; + private ViewGroup mSardineLayout; + private ViewGroup mTransakLayout; + private Button mRampButton; + private Button mSardineButton; + private Button mTransakButton; + + @Override + protected void triggerLayoutInflation() { + setContentView(R.layout.activity_select_purchase_method); + + Intent intent = getIntent(); + if (intent != null) { + mChainId = intent.getStringExtra(CHAIN_ID); + mFrom = intent.getStringExtra(FROM); + mAssetSymbol = intent.getStringExtra(ASSET_SYMBOL); + mContractAddress = intent.getStringExtra(CONTRACT_ADDRESS); + mAmount = intent.getStringExtra(AMOUNT); + } + + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + mRampNetworkLayout = findViewById(R.id.ramp_network_container); + mSardineLayout = findViewById(R.id.sardine_container); + mTransakLayout = findViewById(R.id.transak_container); + + mRampButton = findViewById(R.id.purchase_method_btn_ramp); + mRampButton.setText(getRampProviderTextButton(R.string.brave_wallet_ramp_network_short)); + mSardineButton = findViewById(R.id.purchase_method_btn_sardine); + mSardineButton.setText(getRampProviderTextButton(R.string.brave_wallet_sardine_title)); + mTransakButton = findViewById(R.id.purchase_method_btn_transak); + mTransakButton.setText(getRampProviderTextButton(R.string.brave_wallet_transak_title)); + + onInitialLayoutInflationComplete(); + } + + @Override + public void finishNativeInitialization() { + super.finishNativeInitialization(); + + BraveActivity activity = BraveActivity.getBraveActivity(); + if (activity != null) { + mWalletModel = activity.getWalletModel(); + if (mWalletModel != null && mWalletModel.getCryptoModel() != null) { + mBuyModel = mWalletModel.getCryptoModel().getBuyModel(); + } + } + if (mWalletModel != null && mBuyModel != null) { + LiveDataUtil.observeOnce( + mWalletModel.getCryptoModel().getNetworkModel().mDefaultNetwork, + selectedNetwork -> { + mBuyModel.isBuySupported(selectedNetwork, mAssetSymbol, mContractAddress, + mChainId, new int[] {OnRampProvider.RAMP}, isBuyEnabled -> { + setupOnRampService(isBuyEnabled, OnRampProvider.RAMP, + mRampNetworkLayout, mRampButton); + }); + + mBuyModel.isBuySupported(selectedNetwork, mAssetSymbol, mContractAddress, + mChainId, new int[] {OnRampProvider.SARDINE}, isBuyEnabled -> { + setupOnRampService(isBuyEnabled, OnRampProvider.SARDINE, + mSardineLayout, mSardineButton); + }); + + mBuyModel.isBuySupported(selectedNetwork, mAssetSymbol, mContractAddress, + mChainId, new int[] {OnRampProvider.TRANSAK}, isBuyEnabled -> { + setupOnRampService(isBuyEnabled, OnRampProvider.TRANSAK, + mTransakLayout, mTransakButton); + }); + }); + } + } + + private String getRampProviderTextButton(@StringRes int providerNameResource) { + return String.format(getString(R.string.brave_wallet_buy_with_ramp_provider), + getString(providerNameResource)); + } + + private void setupOnRampService( + boolean isBuyEnabled, int onRampProvider, ViewGroup onRampLayout, Button onRampButton) { + if (isBuyEnabled && mBuyModel.isAvailable(onRampProvider, getResources())) { + onRampLayout.setVisibility(View.VISIBLE); + mBuyModel.getBuyUrl(onRampProvider, mChainId, mFrom, mAssetSymbol, mAmount, + mContractAddress, + url -> { enableOnRampService(onRampLayout, onRampButton, url); }); + } + } + + private void enableOnRampService( + ViewGroup onRampLayout, Button onRampButton, String onRampUrl) { + if (onRampUrl == null) { + onRampLayout.setVisibility(View.GONE); + } else { + onRampButton.setOnClickListener(v -> { + TabUtils.openUrlInNewTab(false, onRampUrl); + TabUtils.bringChromeTabbedActivityToTheTop(this); + }); + } + } + + public static Intent getIntent(Context context, String chainId, String from, + String rampNetworkSymbol, String contractAddress, String amount) { + Intent intent = new Intent(context, SelectPurchaseMethodActivity.class); + intent.putExtra(CHAIN_ID, chainId); + intent.putExtra(FROM, from); + intent.putExtra(ASSET_SYMBOL, rampNetworkSymbol); + intent.putExtra(CONTRACT_ADDRESS, contractAddress); + intent.putExtra(AMOUNT, amount); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + return intent; + } +} diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java index 87438a7bd539..d7f65d7b8047 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/WalletCoinAdapter.java @@ -241,11 +241,11 @@ public void setWalletCoinAdapterType(AdapterType type) { } public void setWalletListItemModelList(List walletListItemModelList) { - this.walletListItemModelList = walletListItemModelList; + this.walletListItemModelList = removeDuplicates(walletListItemModelList); if (mType == AdapterType.EDIT_VISIBLE_ASSETS_LIST || mType == AdapterType.BUY_ASSETS_LIST || mType == AdapterType.SEND_ASSETS_LIST || mType == AdapterType.SWAP_TO_ASSETS_LIST || mType == AdapterType.SWAP_FROM_ASSETS_LIST) { - walletListItemModelListCopy.addAll(walletListItemModelList); + walletListItemModelListCopy.addAll(this.walletListItemModelList); mCheckedPositions.clear(); } for (int i = 0; i < walletListItemModelListCopy.size(); i++) { @@ -305,6 +305,43 @@ public void updateSelectedNetwork(String title, String subTitle) { } } + // Removing duplicates will allow the recycler viewer to render a clean list without showing the + // same assets multiple times. Currently, the list of available assets is fetched from Core + // API the returns a merged list containing the available assets per ramp provider. + // It's not unusual to have the same asset multiple times with the same contract address all + // upper case from a ramp provider and all lower case from another one. Thus it's important to + // compare the contract addresses ignoring case. + private List removeDuplicates( + List walletListItemModelList) { + List result = new ArrayList<>(); + for (WalletListItemModel item : walletListItemModelList) { + if (item.getBlockchainToken() == null) { + // If blockchain token is null the item can be safely added without any risk + // of duplication. + result.add(item); + continue; + } + String contractAddress = item.getBlockchainToken().contractAddress; + boolean duplicate = false; + for (WalletListItemModel itemResult : result) { + // IMPORTANT: use `equalsIgnoreCase` to detect two contract addresses with different + // capitalization. + if (contractAddress.equalsIgnoreCase( + itemResult.getBlockchainToken().contractAddress)) { + // Duplicated item detected! + duplicate = true; + break; + } + } + // Do not add duplicated item. + if (!duplicate) { + result.add(item); + } + } + + return result; + } + private void updateSelectedNetwork(int selectedAccountPosition) { walletListItemModelList.get(previousSelectedPos).setIsUserSelected(false); notifyItemChanged(previousSelectedPos); diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/EditVisibleAssetsBottomSheetDialogFragment.java b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/EditVisibleAssetsBottomSheetDialogFragment.java index 955a3bada4fa..eeacf9f1e9bc 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/EditVisibleAssetsBottomSheetDialogFragment.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/EditVisibleAssetsBottomSheetDialogFragment.java @@ -49,6 +49,7 @@ import org.chromium.brave_wallet.mojom.NetworkInfo; import org.chromium.chrome.R; import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.app.domain.BuyModel; import org.chromium.chrome.browser.app.domain.WalletModel; import org.chromium.chrome.browser.crypto_wallet.BlockchainRegistryFactory; import org.chromium.chrome.browser.crypto_wallet.activities.BraveWalletBaseActivity; @@ -278,7 +279,7 @@ public void onClick(View clickView) { tokens -> { setUpAssetsList(view, tokens, new BlockchainToken[0]); }); } else if (mType == WalletCoinAdapter.AdapterType.BUY_ASSETS_LIST) { TokenUtils.getBuyTokensFiltered(blockchainRegistry, mSelectedNetwork, - TokenUtils.TokenType.ALL, + TokenUtils.TokenType.ALL, BuyModel.SUPPORTED_RAMP_PROVIDERS, tokens -> { setUpAssetsList(view, tokens, new BlockchainToken[0]); }); } } diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/util/AssetUtils.java b/android/java/org/chromium/chrome/browser/crypto_wallet/util/AssetUtils.java index a4561f2c00ea..373d519d6055 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/util/AssetUtils.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/util/AssetUtils.java @@ -133,21 +133,21 @@ public static String getKeyringForCoinType(int coinType) { return coin; } - public static String mapToRampNetworkSymbol(@NonNull BlockchainToken asset) { - String assetChainId = asset.chainId; - if (asset.symbol.equalsIgnoreCase("bat") + public static String mapToRampNetworkSymbol( + String assetChainId, String assetSymbol, String contractAddress) { + if (assetSymbol.equalsIgnoreCase("bat") && assetChainId.equals(BraveWalletConstants.MAINNET_CHAIN_ID)) { // BAT is the only token on Ethereum Mainnet with a prefix on Ramp.Network return "ETH_BAT"; } else if (assetChainId.equals(BraveWalletConstants.AVALANCHE_MAINNET_CHAIN_ID) - && TextUtils.isEmpty(asset.contractAddress)) { + && TextUtils.isEmpty(contractAddress)) { // AVAX native token has no prefix - return asset.symbol; + return assetSymbol; } else { - String rampNetworkPrefix = getRampNetworkPrefix(asset.chainId); + String rampNetworkPrefix = getRampNetworkPrefix(assetChainId); return TextUtils.isEmpty(rampNetworkPrefix) - ? asset.symbol.toUpperCase(Locale.ENGLISH) - : rampNetworkPrefix + "_" + asset.symbol.toUpperCase(Locale.ENGLISH); + ? assetSymbol.toUpperCase(Locale.ENGLISH) + : rampNetworkPrefix + "_" + assetSymbol.toUpperCase(Locale.ENGLISH); } } diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/util/TokenUtils.java b/android/java/org/chromium/chrome/browser/crypto_wallet/util/TokenUtils.java index 383a20854e60..73fd16a14dc7 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/util/TokenUtils.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/util/TokenUtils.java @@ -120,9 +120,9 @@ public static void getUserOrAllTokensFiltered(BraveWalletService braveWalletServ } public static void getBuyTokensFiltered(BlockchainRegistry blockchainRegistry, - NetworkInfo selectedNetwork, TokenType tokenType, + NetworkInfo selectedNetwork, TokenType tokenType, int[] rampProviders, Callbacks.Callback1 callback) { - blockchainRegistry.getBuyTokens(OnRampProvider.RAMP, selectedNetwork.chainId, tokens -> { + blockchainRegistry.getProvidersBuyTokens(rampProviders, selectedNetwork.chainId, tokens -> { BlockchainToken[] filteredTokens = filterTokens(selectedNetwork, tokens, tokenType, false); Arrays.sort(filteredTokens, blockchainTokenComparatorPerGasOrBatType); diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/util/WalletConstants.java b/android/java/org/chromium/chrome/browser/crypto_wallet/util/WalletConstants.java index f0c27dc90a8c..3b91b4865bb0 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/util/WalletConstants.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/util/WalletConstants.java @@ -10,6 +10,9 @@ public final class WalletConstants { public static final long MILLI_SECOND = 1000; + // USD currency code used by on-ramp providers. + public static final String CURRENCY_CODE_USD = "USD"; + // Android public static final String LINE_SEPARATOR = "line.separator"; diff --git a/android/java/res/drawable-night/sardine_logo.xml b/android/java/res/drawable-night/sardine_logo.xml new file mode 100644 index 000000000000..a1e04837cccc --- /dev/null +++ b/android/java/res/drawable-night/sardine_logo.xml @@ -0,0 +1,9 @@ + + + + diff --git a/android/java/res/drawable/ramp_logo.xml b/android/java/res/drawable/ramp_logo.xml new file mode 100644 index 000000000000..a37ca54bbea7 --- /dev/null +++ b/android/java/res/drawable/ramp_logo.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/android/java/res/drawable/sardine_logo.xml b/android/java/res/drawable/sardine_logo.xml new file mode 100644 index 000000000000..d7aba2dec039 --- /dev/null +++ b/android/java/res/drawable/sardine_logo.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/android/java/res/drawable/transak_logo.xml b/android/java/res/drawable/transak_logo.xml new file mode 100644 index 000000000000..40083a727628 --- /dev/null +++ b/android/java/res/drawable/transak_logo.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/android/java/res/layout/activity_select_purchase_method.xml b/android/java/res/layout/activity_select_purchase_method.xml new file mode 100644 index 000000000000..1b87841bb46d --- /dev/null +++ b/android/java/res/layout/activity_select_purchase_method.xml @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index c17652ccbdfe..f09321608504 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -2041,7 +2041,7 @@ Are you sure you want to do this? Balance: - Continue to Ramp + Select Purchase Method Brave Wallet @@ -2091,6 +2091,30 @@ Are you sure you want to do this? Yes + + Select one of the following options + + + Buy crypto with Visa or Mastercard. + + + Instant buy with your bank account. Lower fees. + + + Ramp.Network + + + Ramp + + + Sardine + + + Transak + + + Buy with %1$s + Clear transaction & nonce info diff --git a/components/brave_wallet/browser/blockchain_registry.cc b/components/brave_wallet/browser/blockchain_registry.cc index 3b6ace7c324a..cd745880f5ba 100644 --- a/components/brave_wallet/browser/blockchain_registry.cc +++ b/components/brave_wallet/browser/blockchain_registry.cc @@ -7,6 +7,7 @@ #include +#include "base/containers/flat_set.h" #include "base/ranges/algorithm.h" #include "base/strings/stringprintf.h" #include "brave/components/brave_wallet/browser/brave_wallet_constants.h" @@ -100,51 +101,62 @@ void BlockchainRegistry::GetAllTokens(const std::string& chain_id, GetAllTokensCallback callback) { const auto key = GetTokenListKey(coin, chain_id); if (!token_list_map_.contains(key)) { - std::move(callback).Run( - std::vector()); + std::move(callback).Run(std::vector()); return; } const auto& tokens = token_list_map_[key]; - std::vector tokens_copy( - tokens.size()); + std::vector tokens_copy(tokens.size()); std::transform( tokens.begin(), tokens.end(), tokens_copy.begin(), - [](const brave_wallet::mojom::BlockchainTokenPtr& current_token) - -> brave_wallet::mojom::BlockchainTokenPtr { - return current_token.Clone(); - }); + [](const mojom::BlockchainTokenPtr& current_token) + -> mojom::BlockchainTokenPtr { return current_token.Clone(); }); std::move(callback).Run(std::move(tokens_copy)); } -void BlockchainRegistry::GetBuyTokens(mojom::OnRampProvider provider, - const std::string& chain_id, - GetBuyTokensCallback callback) { - std::vector blockchain_buy_tokens; - const std::vector* buy_tokens = nullptr; - if (provider == mojom::OnRampProvider::kWyre) - buy_tokens = &GetWyreBuyTokens(); - else if (provider == mojom::OnRampProvider::kRamp) - buy_tokens = &GetRampBuyTokens(); - else if (provider == mojom::OnRampProvider::kSardine) - buy_tokens = &GetSardineBuyTokens(); - else if (provider == mojom::OnRampProvider::kTransak) - buy_tokens = &GetTransakBuyTokens(); - - if (buy_tokens == nullptr) { - std::move(callback).Run(std::move(blockchain_buy_tokens)); - return; - } - - for (const auto& token : *buy_tokens) { - if (token.chain_id != chain_id) { +std::vector BlockchainRegistry::GetBuyTokens( + const std::vector& providers, + const std::string& chain_id) { + std::vector blockchain_buy_tokens; + base::flat_set provider_set(providers.begin(), + providers.end()); + + for (const auto& provider : provider_set) { + const std::vector* buy_tokens = nullptr; + if (provider == mojom::OnRampProvider::kWyre) { + buy_tokens = &GetWyreBuyTokens(); + } else if (provider == mojom::OnRampProvider::kRamp) { + buy_tokens = &GetRampBuyTokens(); + } else if (provider == mojom::OnRampProvider::kSardine) { + buy_tokens = &GetSardineBuyTokens(); + } else if (provider == mojom::OnRampProvider::kTransak) { + buy_tokens = &GetTransakBuyTokens(); + } else { continue; } - blockchain_buy_tokens.push_back( - brave_wallet::mojom::BlockchainToken::New(token)); + for (const auto& token : *buy_tokens) { + if (token.chain_id == chain_id) { + blockchain_buy_tokens.push_back(mojom::BlockchainToken::New(token)); + } + } } - std::move(callback).Run(std::move(blockchain_buy_tokens)); + + return blockchain_buy_tokens; } + +void BlockchainRegistry::GetBuyTokens(mojom::OnRampProvider provider, + const std::string& chain_id, + GetBuyTokensCallback callback) { + std::move(callback).Run(GetBuyTokens({provider}, chain_id)); +} + +void BlockchainRegistry::GetProvidersBuyTokens( + const std::vector& providers, + const std::string& chain_id, + GetProvidersBuyTokensCallback callback) { + std::move(callback).Run(GetBuyTokens(providers, chain_id)); +} + // TODO(muliswilliam) - Remove this function when iOS and Android no longer // depend on it https://github.com/brave/brave-browser/issues/23503 void BlockchainRegistry::GetBuyUrl(mojom::OnRampProvider provider, @@ -183,12 +195,12 @@ void BlockchainRegistry::GetBuyUrl(mojom::OnRampProvider provider, void BlockchainRegistry::GetOnRampCurrencies( GetOnRampCurrenciesCallback callback) { - std::vector currencies; + std::vector currencies; const std::vector* onRampCurrencies = &GetOnRampCurrenciesList(); for (const auto& currency : *onRampCurrencies) { - currencies.push_back(brave_wallet::mojom::OnRampCurrency::New(currency)); + currencies.push_back(mojom::OnRampCurrency::New(currency)); } std::move(callback).Run(std::move(currencies)); } diff --git a/components/brave_wallet/browser/blockchain_registry.h b/components/brave_wallet/browser/blockchain_registry.h index 75363223bdb0..07f376554f1f 100644 --- a/components/brave_wallet/browser/blockchain_registry.h +++ b/components/brave_wallet/browser/blockchain_registry.h @@ -54,6 +54,10 @@ class BlockchainRegistry : public mojom::BlockchainRegistry { void GetBuyTokens(mojom::OnRampProvider provider, const std::string& chain_id, GetBuyTokensCallback callback) override; + void GetProvidersBuyTokens( + const std::vector& providers, + const std::string& chain_id, + GetProvidersBuyTokensCallback callback) override; void GetBuyUrl(mojom::OnRampProvider provider, const std::string& chain_id, const std::string& address, @@ -76,6 +80,9 @@ class BlockchainRegistry : public mojom::BlockchainRegistry { private: mojo::ReceiverSet receivers_; + std::vector GetBuyTokens( + const std::vector& providers, + const std::string& chain_id); }; } // namespace brave_wallet diff --git a/components/brave_wallet/browser/blockchain_registry_unittest.cc b/components/brave_wallet/browser/blockchain_registry_unittest.cc index d4d4dfeb57ec..167d3cfad101 100644 --- a/components/brave_wallet/browser/blockchain_registry_unittest.cc +++ b/components/brave_wallet/browser/blockchain_registry_unittest.cc @@ -14,6 +14,8 @@ #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +#include "brave/components/brave_wallet/browser/brave_wallet_constants.h" + using testing::ElementsAreArray; namespace brave_wallet { @@ -456,6 +458,46 @@ TEST(BlockchainRegistryUnitTest, GetBuyTokens) { run_loop4.Run(); } +TEST(BlockchainRegistryUnitTest, GetProvidersBuyTokens) { + base::test::TaskEnvironment task_environment; + auto* registry = BlockchainRegistry::GetInstance(); + + std::vector buy_tokens; + for (const auto& v : {GetWyreBuyTokens(), GetRampBuyTokens(), + GetSardineBuyTokens(), GetTransakBuyTokens()}) { + buy_tokens.insert(buy_tokens.end(), v.begin(), v.end()); + } + + const char* chains[] = { + mojom::kMainnetChainId, mojom::kPolygonMainnetChainId, + mojom::kAvalancheMainnetChainId, mojom::kBinanceSmartChainMainnetChainId, + mojom::kSolanaMainnet, mojom::kCeloMainnetChainId, + mojom::kArbitrumMainnetChainId}; + + for (auto* chain : chains) { + std::vector expected_tokens; + for (const auto& token : buy_tokens) { + if (token.chain_id == chain) { + expected_tokens.push_back(mojom::BlockchainToken::New(token)); + } + } + + base::RunLoop run_loop; + registry->GetProvidersBuyTokens( + {mojom::OnRampProvider::kWyre, mojom::OnRampProvider::kRamp, + mojom::OnRampProvider::kSardine, mojom::OnRampProvider::kTransak, + mojom::OnRampProvider::kTransak /* test duplicate provider */}, + chain, + base::BindLambdaForTesting( + [&](std::vector token_list) { + EXPECT_EQ(expected_tokens, token_list) << chain; + + run_loop.Quit(); + })); + run_loop.Run(); + } +} + TEST(BlockchainRegistryUnitTest, GetBuyUrlWyre) { base::test::TaskEnvironment task_environment; auto* registry = BlockchainRegistry::GetInstance(); diff --git a/components/brave_wallet/common/brave_wallet.mojom b/components/brave_wallet/common/brave_wallet.mojom index ced671391d60..bf4665f391b8 100644 --- a/components/brave_wallet/common/brave_wallet.mojom +++ b/components/brave_wallet/common/brave_wallet.mojom @@ -670,8 +670,10 @@ interface BlockchainRegistry { GetAllTokens(string chain_id, CoinType coin) => (array tokens); // Below APIs are Ethereum only for the moment. - // Obtains all tokens for the Buy UI + // Obtains all tokens for a single provider for the Buy UI GetBuyTokens(OnRampProvider provider, string chain_id) => (array tokens); + // Obtains all tokens for multiple providers for the Buy UI + GetProvidersBuyTokens(array providers, string chain_id) => (array tokens); // Obtains the URL used for buying assets. GetBuyUrl(OnRampProvider provider, string chain_id, string address, string symbol, string amount) => (string url, string? error);