diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index d41992102745..e2d3c376de31 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -881,6 +881,12 @@
"message": "Buy $1",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
},
+ "buyCrypto": {
+ "message": "Buy crypto"
+ },
+ "buyFirstCrypto": {
+ "message": "Buy your first crypto with a debit or credit card."
+ },
"buyMoreAsset": {
"message": "Buy more $1",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
@@ -1538,6 +1544,9 @@
"deposit": {
"message": "Deposit"
},
+ "depositCrypto": {
+ "message": "Deposit crypto from another account with a wallet address or QR code."
+ },
"deprecatedGoerliNtwrkMsg": {
"message": "Because of updates to the Ethereum system, the Goerli test network will be phased out soon."
},
@@ -2143,6 +2152,12 @@
"genericExplorerView": {
"message": "View account on $1"
},
+ "getStarted": {
+ "message": "Get Started"
+ },
+ "getStartedByFundingWallet": {
+ "message": "Get started by adding some crypto to your wallet."
+ },
"getStartedWithNFTs": {
"message": "Get $1 to buy NFTs",
"description": "$1 is the token symbol"
@@ -2707,6 +2722,9 @@
"link": {
"message": "Link"
},
+ "linkCentralizedExchanges": {
+ "message": "Link your Coinbase or Binance accounts to transfer crypto to MetaMask for free."
+ },
"links": {
"message": "Links"
},
@@ -4220,6 +4238,9 @@
"receive": {
"message": "Receive"
},
+ "receiveCrypto": {
+ "message": "Receive crypto"
+ },
"recipientAddressPlaceholder": {
"message": "Enter public address (0x) or ENS name"
},
@@ -4635,6 +4656,9 @@
"selectEnableDisplayMediaPrivacyPreference": {
"message": "Turn on Display NFT Media"
},
+ "selectFundingMethod": {
+ "message": "Select a funding method"
+ },
"selectHdPath": {
"message": "Select HD path"
},
@@ -6118,6 +6142,9 @@
"transfer": {
"message": "Transfer"
},
+ "transferCrypto": {
+ "message": "Transfer crypto"
+ },
"transferFrom": {
"message": "Transfer from"
},
diff --git a/ui/components/app/assets/asset-list/asset-list.js b/ui/components/app/assets/asset-list/asset-list.js
index 81c70d433dc9..916d86a07c26 100644
--- a/ui/components/app/assets/asset-list/asset-list.js
+++ b/ui/components/app/assets/asset-list/asset-list.js
@@ -35,14 +35,17 @@ import {
DetectedTokensBanner,
TokenListItem,
ImportTokenLink,
+ ReceiveModal,
} from '../../../multichain';
import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance';
import { useIsOriginalNativeTokenSymbol } from '../../../../hooks/useIsOriginalNativeTokenSymbol';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
showPrimaryCurrency,
showSecondaryCurrency,
} from '../../../../../shared/modules/currency-display.utils';
import { roundToDecimalPlacesRemovingExtraZeroes } from '../../../../helpers/utils/util';
+import { FundingMethodModal } from '../../../multichain/funding-method-modal/funding-method-modal';
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import {
RAMPS_CARD_VARIANT_TYPES,
@@ -66,6 +69,7 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
type,
rpcUrl,
);
+ const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const balance = useSelector(getMultichainSelectedAccountCachedBalance);
const balanceIsLoading = !balance;
@@ -101,6 +105,14 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
);
+ const [showFundingMethodModal, setShowFundingMethodModal] = useState(false);
+ const [showReceiveModal, setShowReceiveModal] = useState(false);
+
+ const onClickReceive = () => {
+ setShowFundingMethodModal(false);
+ setShowReceiveModal(true);
+ };
+
const { tokensWithBalances, loading } = useAccountTotalFiatBalance(
selectedAccount,
shouldHideZeroBalanceTokens,
@@ -151,6 +163,9 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
? RAMPS_CARD_VARIANT_TYPES.BTC
: RAMPS_CARD_VARIANT_TYPES.TOKEN
}
+ handleOnClick={
+ isBtc ? undefined : () => setShowFundingMethodModal(true)
+ }
/>
) : null
///: END:ONLY_INCLUDE_IF
@@ -213,6 +228,20 @@ const AssetList = ({ onClickAsset, showTokensLinks }) => {
{showDetectedTokens && (
)}
+ {showReceiveModal && selectedAccount?.address && (
+ setShowReceiveModal(false)}
+ />
+ )}
+ {showFundingMethodModal && (
+ setShowFundingMethodModal(false)}
+ title={t('selectFundingMethod')}
+ onClickReceive={onClickReceive}
+ />
+ )}
>
);
};
diff --git a/ui/components/multichain/funding-method-modal/funding-method-item.tsx b/ui/components/multichain/funding-method-modal/funding-method-item.tsx
new file mode 100644
index 000000000000..85fa9e268ef5
--- /dev/null
+++ b/ui/components/multichain/funding-method-modal/funding-method-item.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Box, Text, Icon, IconName } from '../../component-library';
+import {
+ Display,
+ FlexDirection,
+ TextVariant,
+ TextColor,
+ AlignItems,
+} from '../../../helpers/constants/design-system';
+
+type FundingMethodItemProps = {
+ icon: IconName;
+ title: string;
+ description: string;
+ onClick: () => void;
+};
+
+const FundingMethodItem: React.FC = ({
+ icon,
+ title,
+ description,
+ onClick,
+}) => (
+
+
+
+ {title}
+
+ {description}
+
+
+
+);
+
+export default FundingMethodItem;
diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx
new file mode 100644
index 000000000000..509a4aa60a2a
--- /dev/null
+++ b/ui/components/multichain/funding-method-modal/funding-method-modal.test.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { renderWithProvider } from '../../../../test/jest/rendering';
+import mockState from '../../../../test/data/mock-state.json';
+import useRamps from '../../../hooks/ramps/useRamps/useRamps';
+import { FundingMethodModal } from './funding-method-modal';
+
+jest.mock('../../../hooks/ramps/useRamps/useRamps', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}));
+
+const mockStore = configureMockStore([thunk]);
+
+describe('FundingMethodModal', () => {
+ let store = configureMockStore([thunk])(mockState);
+ let openBuyCryptoInPdapp: jest.Mock<() => void>;
+
+ beforeEach(() => {
+ store = mockStore(mockState);
+ openBuyCryptoInPdapp = jest.fn();
+ (useRamps as jest.Mock).mockReturnValue({ openBuyCryptoInPdapp });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the modal when isOpen is true', () => {
+ const { getByTestId, getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(getByTestId('funding-method-modal')).toBeInTheDocument();
+ expect(getByText('Test Modal')).toBeInTheDocument();
+ });
+
+ it('should not render the modal when isOpen is false', () => {
+ const { queryByTestId } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(queryByTestId('funding-method-modal')).toBeNull();
+ });
+
+ it('should call openBuyCryptoInPdapp when the Buy Crypto item is clicked', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ fireEvent.click(getByText('Buy crypto'));
+ expect(openBuyCryptoInPdapp).toHaveBeenCalled();
+ });
+
+ it('should call onClickReceive when the Receive Crypto item is clicked', () => {
+ const onClickReceive = jest.fn();
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ fireEvent.click(getByText('Receive crypto'));
+ expect(onClickReceive).toHaveBeenCalled();
+ });
+
+ it('should open a new tab with the correct URL when Transfer Crypto item is clicked', () => {
+ global.platform.openTab = jest.fn();
+
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ fireEvent.click(getByText('Transfer crypto'));
+ expect(global.platform.openTab).toHaveBeenCalledWith({
+ url: expect.stringContaining('transfer'),
+ });
+ });
+});
diff --git a/ui/components/multichain/funding-method-modal/funding-method-modal.tsx b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx
new file mode 100644
index 000000000000..47d6ed22c2e8
--- /dev/null
+++ b/ui/components/multichain/funding-method-modal/funding-method-modal.tsx
@@ -0,0 +1,137 @@
+import React, { useCallback, useContext } from 'react';
+import { useSelector } from 'react-redux';
+import { CaipChainId } from '@metamask/utils';
+import {
+ Modal,
+ ModalContent,
+ ModalOverlay,
+ ModalHeader,
+ Text,
+ IconName,
+} from '../../component-library';
+import {
+ TextVariant,
+ TextAlign,
+} from '../../../helpers/constants/design-system';
+import {
+ getMultichainCurrentNetwork,
+ getMultichainDefaultToken,
+} from '../../../selectors/multichain';
+import useRamps, {
+ RampsMetaMaskEntry,
+} from '../../../hooks/ramps/useRamps/useRamps';
+import { getPortfolioUrl } from '../../../helpers/utils/portfolio';
+import {
+ getMetaMetricsId,
+ getParticipateInMetaMetrics,
+ getDataCollectionForMarketing,
+ getSelectedAccount,
+} from '../../../selectors';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { ChainId } from '../../../../shared/constants/network';
+import {
+ MetaMetricsEventCategory,
+ MetaMetricsEventName,
+} from '../../../../shared/constants/metametrics';
+import { MetaMetricsContext } from '../../../contexts/metametrics';
+import FundingMethodItem from './funding-method-item';
+
+type FundingMethodModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ onClickReceive: () => void;
+};
+
+export const FundingMethodModal: React.FC = ({
+ isOpen,
+ onClose,
+ title,
+ onClickReceive,
+}) => {
+ const t = useI18nContext();
+ const trackEvent = useContext(MetaMetricsContext);
+ const { openBuyCryptoInPdapp } = useRamps();
+ const { address: accountAddress } = useSelector(getSelectedAccount);
+ const { chainId } = useSelector(getMultichainCurrentNetwork);
+ const { symbol } = useSelector(getMultichainDefaultToken);
+ const metaMetricsId = useSelector(getMetaMetricsId);
+ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics);
+ const isMarketingEnabled = useSelector(getDataCollectionForMarketing);
+
+ const handleTransferCryptoClick = useCallback(() => {
+ trackEvent({
+ event: MetaMetricsEventName.NavSendButtonClicked,
+ category: MetaMetricsEventCategory.Navigation,
+ properties: {
+ location: RampsMetaMaskEntry?.TokensBanner,
+ text: 'Transfer crypto',
+ chain_id: chainId,
+ token_symbol: symbol,
+ },
+ });
+
+ const url = getPortfolioUrl(
+ 'transfer',
+ 'ext_funding_method_modal',
+ metaMetricsId,
+ isMetaMetricsEnabled,
+ isMarketingEnabled,
+ accountAddress,
+ 'transfer',
+ );
+ global.platform.openTab({ url });
+ }, [
+ metaMetricsId,
+ isMetaMetricsEnabled,
+ isMarketingEnabled,
+ chainId,
+ symbol,
+ accountAddress,
+ ]);
+
+ const handleBuyCryptoClick = useCallback(() => {
+ trackEvent({
+ event: MetaMetricsEventName.NavBuyButtonClicked,
+ category: MetaMetricsEventCategory.Navigation,
+ properties: {
+ location: RampsMetaMaskEntry?.TokensBanner,
+ text: 'Buy crypto',
+ chain_id: chainId,
+ token_symbol: symbol,
+ },
+ });
+ openBuyCryptoInPdapp(chainId as ChainId | CaipChainId);
+ }, [chainId, symbol]);
+
+ return (
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+ );
+};
diff --git a/ui/components/multichain/funding-method-modal/index.scss b/ui/components/multichain/funding-method-modal/index.scss
new file mode 100644
index 000000000000..193b26fc0052
--- /dev/null
+++ b/ui/components/multichain/funding-method-modal/index.scss
@@ -0,0 +1,7 @@
+.funding-method-item {
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--color-background-hover);
+ }
+}
diff --git a/ui/components/multichain/funding-method-modal/index.ts b/ui/components/multichain/funding-method-modal/index.ts
new file mode 100644
index 000000000000..68ac981efe14
--- /dev/null
+++ b/ui/components/multichain/funding-method-modal/index.ts
@@ -0,0 +1 @@
+export { FundingMethodModal } from './funding-method-modal';
diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js
index 1a078ee82c81..5ecc5a2a7d3a 100644
--- a/ui/components/multichain/index.js
+++ b/ui/components/multichain/index.js
@@ -48,5 +48,6 @@ export { NotificationListItemSnap } from './notification-list-item-snap';
export { NotificationsTagCounter } from './notifications-tag-counter';
export { Toast, ToastContainer } from './toast';
export { PermissionDetailsModal } from './permission-details-modal';
+export { ReceiveModal } from './receive-modal';
export { EditNetworksModal } from './edit-networks-modal';
export { EditAccountsModal } from './edit-accounts-modal';
diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss
index 8ab0c6202a5a..2d2d6b3fdef0 100644
--- a/ui/components/multichain/multichain-components.scss
+++ b/ui/components/multichain/multichain-components.scss
@@ -28,6 +28,7 @@
@import 'network-list-menu/select-rpc-url-modal';
@import 'product-tour-popover';
@import 'nft-item';
+@import 'funding-method-modal';
@import 'badge-status';
@import 'import-tokens-modal';
@import 'asset-picker-amount';
diff --git a/ui/components/multichain/ramps-card/ramps-card.js b/ui/components/multichain/ramps-card/ramps-card.js
index cc017421a63e..6d988f8a8bad 100644
--- a/ui/components/multichain/ramps-card/ramps-card.js
+++ b/ui/components/multichain/ramps-card/ramps-card.js
@@ -39,12 +39,14 @@ export const RAMPS_CARD_VARIANTS = {
[RAMPS_CARD_VARIANT_TYPES.TOKEN]: {
illustrationSrc: './images/ramps-card-token-illustration.png',
gradient:
+ // eslint-disable-next-line @metamask/design-tokens/color-no-hex
'linear-gradient(90deg, #0189EC 0%, #4B7AED 35%, #6774EE 58%, #706AF4 80.5%, #7C5BFC 100%)',
title: 'fundYourWallet',
- body: 'fundYourWalletDescription',
+ body: 'getStartedByFundingWallet',
},
[RAMPS_CARD_VARIANT_TYPES.NFT]: {
illustrationSrc: './images/ramps-card-nft-illustration.png',
+ // eslint-disable-next-line @metamask/design-tokens/color-no-hex
gradient: 'linear-gradient(90deg, #F6822D 0%, #F894A7 52%, #ED94FB 92.5%)',
title: 'getStartedWithNFTs',
body: 'getStartedWithNFTsDescription',
@@ -52,6 +54,7 @@ export const RAMPS_CARD_VARIANTS = {
[RAMPS_CARD_VARIANT_TYPES.ACTIVITY]: {
illustrationSrc: './images/ramps-card-activity-illustration.png',
gradient:
+ // eslint-disable-next-line @metamask/design-tokens/color-no-hex
'linear-gradient(90deg, #57C5DC 0%, #06BFDD 49.39%, #35A9C7 100%)',
title: 'startYourJourney',
@@ -60,6 +63,7 @@ export const RAMPS_CARD_VARIANTS = {
[RAMPS_CARD_VARIANT_TYPES.BTC]: {
illustrationSrc: './images/ramps-card-btc-illustration.png',
gradient:
+ // eslint-disable-next-line @metamask/design-tokens/color-no-hex
'linear-gradient(90deg, #017ED9 0%, #446FD9 35%, #5E6AD9 58%, #635ED9 80.5%, #6855D9 92.5%, #6A4FD9 100%)',
title: 'fundYourWallet',
body: 'fundYourWalletDescription',
@@ -73,7 +77,7 @@ const metamaskEntryMap = {
[RAMPS_CARD_VARIANT_TYPES.BTC]: RampsMetaMaskEntry.BtcBanner,
};
-export const RampsCard = ({ variant }) => {
+export const RampsCard = ({ variant, handleOnClick }) => {
const t = useI18nContext();
const { gradient, illustrationSrc, title, body } =
RAMPS_CARD_VARIANTS[variant];
@@ -83,6 +87,8 @@ export const RampsCard = ({ variant }) => {
const { chainId, nickname } = useSelector(getMultichainCurrentNetwork);
const { symbol } = useSelector(getMultichainDefaultToken);
+ const isTokenVariant = variant === RAMPS_CARD_VARIANT_TYPES.TOKEN;
+
useEffect(() => {
trackEvent({
event: MetaMetricsEventName.EmptyBuyBannerDisplayed,
@@ -129,8 +135,11 @@ export const RampsCard = ({ variant }) => {
{t(title, [symbol])}
{t(body, [symbol])}
-
- {t('buyToken', [symbol])}
+
+ {isTokenVariant ? t('getStarted') : t('buyToken', [symbol])}
);
@@ -138,4 +147,5 @@ export const RampsCard = ({ variant }) => {
RampsCard.propTypes = {
variant: PropTypes.oneOf(Object.values(RAMPS_CARD_VARIANT_TYPES)),
+ handleOnClick: PropTypes.oneOfType([PropTypes.func, PropTypes.undefined]),
};
diff --git a/ui/helpers/utils/portfolio.js b/ui/helpers/utils/portfolio.js
index 3aa5d8f592b2..97d6eb528d81 100644
--- a/ui/helpers/utils/portfolio.js
+++ b/ui/helpers/utils/portfolio.js
@@ -4,6 +4,8 @@ export function getPortfolioUrl(
metaMetricsId = '',
metricsEnabled = false,
marketingEnabled = false,
+ accountAddress,
+ tab,
) {
const baseUrl = process.env.PORTFOLIO_URL || '';
const url = new URL(endpoint, baseUrl);
@@ -15,5 +17,13 @@ export function getPortfolioUrl(
url.searchParams.append('metricsEnabled', String(metricsEnabled));
url.searchParams.append('marketingEnabled', String(marketingEnabled));
+ if (accountAddress) {
+ url.searchParams.append('accountAddress', accountAddress);
+ }
+
+ if (tab) {
+ url.searchParams.append('tab', tab);
+ }
+
return url.href;
}