Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import AddFundsBottomSheet from './AddFundsBottomSheet';
import { useOpenSwaps } from '../../hooks/useOpenSwaps';
import useDepositEnabled from '../../../Ramp/Deposit/hooks/useDepositEnabled';
import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork';
import { isBridgeAllowed } from '../../../Bridge/utils';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
Expand All @@ -12,22 +12,23 @@ import { CardFundingToken, FundingStatus } from '../../types';
import { renderScreen } from '../../../../../util/test/renderWithProvider';
import { backgroundState } from '../../../../../util/test/initial-root-state';
import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation';
import useRampsUnifiedV2Enabled from '../../../Ramp/hooks/useRampsUnifiedV2Enabled';
import { CardHomeSelectors } from '../../Views/CardHome/CardHome.testIds';
import { RampsButtonClickData } from '../../../Ramp/hooks/useRampsButtonClickData';

// Mock hooks first - must be hoisted before imports
const mockUseParams = jest.fn();
const mockGoBack = jest.fn();
const mockNavigate = jest.fn();
const mockGoToDeposit = jest.fn();
const mockGoToBuy = jest.fn();

// Mock dependencies
jest.mock('../../../Ramp/hooks/useRampNavigation');
jest.mock('../../hooks/useOpenSwaps', () => ({
useOpenSwaps: jest.fn(),
}));

jest.mock('../../../Ramp/Deposit/hooks/useDepositEnabled', () => ({
jest.mock('../../../Ramp/Aggregator/hooks/useRampNetwork', () => ({
__esModule: true,
default: jest.fn(),
}));
Expand All @@ -47,7 +48,7 @@ jest.mock('../../../../../util/networks', () => ({
jest.mock('../../../../../util/trace', () => ({
trace: jest.fn(),
TraceName: {
LoadDepositExperience: 'LoadDepositExperience',
LoadRampExperience: 'LoadRampExperience',
},
}));

Expand Down Expand Up @@ -148,12 +149,12 @@ describe('AddFundsBottomSheet', () => {
});

(useRampNavigation as jest.Mock).mockReturnValue({
goToDeposit: mockGoToDeposit,
goToBuy: mockGoToBuy,
});

(useDepositEnabled as jest.Mock).mockReturnValue({
isDepositEnabled: true,
});
(useRampNetwork as jest.Mock).mockReturnValue([true, true]);

(useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(true);

(isBridgeAllowed as jest.Mock).mockReturnValue(true);

Expand All @@ -175,18 +176,16 @@ describe('AddFundsBottomSheet', () => {
expect(getByText('Fund with crypto')).toBeOnTheScreen();
});

it('renders with only swap option when deposit is disabled', () => {
(useDepositEnabled as jest.Mock).mockReturnValue({
isDepositEnabled: false,
});
it('renders with only swap option when unified buy v2 is disabled', () => {
(useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(false);

const { getByText, queryByText } = setupComponent();

expect(getByText('Fund with crypto')).toBeOnTheScreen();
expect(queryByText('Fund with cash')).not.toBeOnTheScreen();
});

it('renders with only deposit option when swaps are not allowed', () => {
it('renders with only cash option when swaps are not allowed', () => {
(isBridgeAllowed as jest.Mock).mockReturnValue(false);

const { getByText, queryByText } = setupComponent();
Expand All @@ -196,9 +195,8 @@ describe('AddFundsBottomSheet', () => {
});

it('renders with no options when both are disabled', () => {
(useDepositEnabled as jest.Mock).mockReturnValue({
isDepositEnabled: false,
});
(useRampsUnifiedV2Enabled as jest.Mock).mockReturnValue(false);
(useRampNetwork as jest.Mock).mockReturnValue([false, false]);
(isBridgeAllowed as jest.Mock).mockReturnValue(false);

const { getByText, queryByText } = setupComponent();
Expand All @@ -223,7 +221,7 @@ describe('AddFundsBottomSheet', () => {
expect(getByText('Swap tokens into USDC on Linea')).toBeTruthy();
});

it('handles deposit option press correctly', () => {
it('handles fund with cash option press correctly', () => {
const { getByText } = setupComponent();

fireEvent.press(getByText('Fund with cash'));
Expand All @@ -239,7 +237,7 @@ describe('AddFundsBottomSheet', () => {
button_text: 'Fund with cash',
location: 'CardHome',
chain_id_destination: '59144',
ramp_type: 'DEPOSIT',
ramp_type: 'UNIFIED_BUY_2',
ramp_routing: undefined,
is_authenticated: false,
preferred_provider: undefined,
Expand All @@ -248,7 +246,7 @@ describe('AddFundsBottomSheet', () => {
);
expect(mockTrackEvent).toHaveBeenCalled();
expect(trace).toHaveBeenCalledWith({
name: TraceName.LoadDepositExperience,
name: TraceName.LoadRampExperience,
});
});

Expand Down Expand Up @@ -321,12 +319,15 @@ describe('AddFundsBottomSheet', () => {
expect(getByText('Swap tokens into ETH on Linea')).toBeTruthy();
});

it('navigates to deposit route when deposit callback is executed', () => {
it('navigates to unified buy when fund with cash callback is executed', () => {
const { getByText } = setupComponent();

fireEvent.press(getByText('Fund with cash'));

expect(mockGoToDeposit).toHaveBeenCalled();
expect(mockGoToBuy).toHaveBeenCalledWith(
{ assetId: 'eip155:59144/erc20:0x456' },
{ buyFlowOrigin: 'cardHome' },
);
});

it('renders component correctly', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { View } from 'react-native';
import { CardFundingToken } from '../../types';
import AppConstants from '../../../../../core/AppConstants';
import { isBridgeAllowed } from '../../../Bridge/utils';
import useDepositEnabled from '../../../Ramp/Deposit/hooks/useDepositEnabled';
import useRampNetwork from '../../../Ramp/Aggregator/hooks/useRampNetwork';
import { getDecimalChainId } from '../../../../../util/networks';
import { trace, TraceName } from '../../../../../util/trace';
import { useOpenSwaps } from '../../hooks/useOpenSwaps';
Expand All @@ -32,6 +32,7 @@ import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { strings } from '../../../../../../locales/i18n';
import { CardHomeSelectors } from '../../Views/CardHome/CardHome.testIds';
import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation';
import { cardFundingTokenToRampIntent } from '../../util/cardFundingTokenToRampIntent';
import { safeFormatChainIdToHex } from '../../util/safeFormatChainIdToHex';
import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders';
import { useRampsButtonClickData } from '../../../Ramp/hooks/useRampsButtonClickData';
Expand All @@ -57,17 +58,18 @@ const AddFundsBottomSheet: React.FC = () => {
const sheetRef = useRef<BottomSheetRef>(null);
const { priorityToken } = useParams<AddFundsModalNavigationDetails>();

const { isDepositEnabled } = useDepositEnabled();
const [isNetworkRampSupported] = useRampNetwork();
const theme = useTheme();
const styles = createStyles(theme);
const { openSwaps } = useOpenSwaps({
priorityToken,
});
const { trackEvent, createEventBuilder } = useAnalytics();
const rampGeodetectedRegion = useSelector(getDetectedGeolocation);
const { goToDeposit } = useRampNavigation();
const { goToBuy } = useRampNavigation();
const buttonClickData = useRampsButtonClickData();
const isV2UnifiedEnabled = useRampsUnifiedV2Enabled();
const isCashFundingEnabled = isV2UnifiedEnabled && isNetworkRampSupported;

const closeBottomSheetAndNavigate = useCallback(
(navigateFunc: () => void) => {
Expand All @@ -83,9 +85,11 @@ const AddFundsBottomSheet: React.FC = () => {
});
}, [priorityToken, openSwaps, closeBottomSheetAndNavigate]);

const openDeposit = useCallback(() => {
const openUnifiedBuy = useCallback(() => {
closeBottomSheetAndNavigate(() => {
goToDeposit();
goToBuy(cardFundingTokenToRampIntent(priorityToken), {
buyFlowOrigin: 'cardHome',
});
});
trackEvent(
createEventBuilder(
Expand All @@ -99,7 +103,7 @@ const AddFundsBottomSheet: React.FC = () => {
button_text: 'Fund with cash',
location: 'CardHome',
chain_id_destination: getDecimalChainId(priorityToken?.caipChainId),
ramp_type: isV2UnifiedEnabled ? 'UNIFIED_BUY_2' : 'DEPOSIT',
ramp_type: 'UNIFIED_BUY_2',
region: rampGeodetectedRegion,
ramp_routing: buttonClickData.ramp_routing,
is_authenticated: buttonClickData.is_authenticated,
Expand All @@ -110,27 +114,26 @@ const AddFundsBottomSheet: React.FC = () => {
);

trace({
name: TraceName.LoadDepositExperience,
name: TraceName.LoadRampExperience,
});
}, [
rampGeodetectedRegion,
closeBottomSheetAndNavigate,
goToDeposit,
goToBuy,
trackEvent,
createEventBuilder,
priorityToken,
buttonClickData,
isV2UnifiedEnabled,
]);

const options = [
{
label: strings('card.add_funds_bottomsheet.deposit'),
description: strings('card.add_funds_bottomsheet.deposit_description'),
icon: IconName.Bank,
onPress: openDeposit,
onPress: openUnifiedBuy,
testID: CardHomeSelectors.ADD_FUNDS_BOTTOM_SHEET_DEPOSIT_OPTION,
enabled: isDepositEnabled,
enabled: isCashFundingEnabled,
},
{
label: strings('card.add_funds_bottomsheet.swap'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FundingStatus } from '../types';
import { cardFundingTokenToRampIntent } from './cardFundingTokenToRampIntent';

describe('cardFundingTokenToRampIntent', () => {
it('returns erc20 assetId when token has an address', () => {
const intent = cardFundingTokenToRampIntent({
address: '0xABC',
symbol: 'USDC',
decimals: 6,
name: 'USD Coin',
caipChainId: 'eip155:1',
fundingStatus: FundingStatus.Enabled,
spendableBalance: '0',
});

expect(intent).toEqual({ assetId: 'eip155:1/erc20:0xabc' });
});

it('returns empty intent when token has no address', () => {
const intent = cardFundingTokenToRampIntent({
address: null,
symbol: 'ETH',
decimals: 18,
name: 'Ether',
caipChainId: 'eip155:1',
fundingStatus: FundingStatus.Enabled,
spendableBalance: '0',
});

expect(intent).toEqual({});
});

it('returns empty intent when token is undefined', () => {
expect(cardFundingTokenToRampIntent(undefined)).toEqual({});
});
});
18 changes: 18 additions & 0 deletions app/components/UI/Card/util/cardFundingTokenToRampIntent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { CardFundingToken } from '../types';
import type { RampIntent } from '../../Ramp/types';

/**
* Maps a MetaMask Card priority funding token to a ramps {@link RampIntent}
* for unified buy (UB2). ERC-20 tokens use `eip155:…/erc20:0x…`. When
* `address` is missing, returns an empty intent so `goToBuy` opens token selection.
*/
export function cardFundingTokenToRampIntent(
token: CardFundingToken | undefined,
): RampIntent {
if (!token?.address) {
return {};
}

const assetId = `${token.caipChainId}/erc20:${token.address.toLowerCase()}`;
return { assetId };
}
2 changes: 1 addition & 1 deletion app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function isBailedOrderStatus(
* - 'homeTokenList': Home → (token list with Buy buttons) → Buy
* - undefined: Home → Buy → Token Selection → BuildQuote (standard flow)
*/
export type BuyFlowOrigin = 'tokenInfo' | 'homeTokenList';
export type BuyFlowOrigin = 'tokenInfo' | 'homeTokenList' | 'cardHome';

export interface BuildQuoteParams {
assetId?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useNavigation, type ParamListBase } from '@react-navigation/native';
import type { StackNavigationProp } from '@react-navigation/stack';
import {
BottomSheet,
type BottomSheetRef,
Expand Down Expand Up @@ -44,7 +45,7 @@ export const createTokenNotAvailableModalNavigationDetails =
function TokenNotAvailableModal() {
const { trackEvent, createEventBuilder } = useAnalytics();
const { assetId, buyFlowOrigin } = useParams<TokenNotAvailableModalParams>();
const navigation = useNavigation();
const navigation = useNavigation<StackNavigationProp<ParamListBase>>();
const sheetRef = useRef<BottomSheetRef>(null);
const { styles } = useStyles(styleSheet, {});

Expand Down Expand Up @@ -82,6 +83,10 @@ function TokenNotAvailableModal() {
} else if (buyFlowOrigin === 'homeTokenList') {
// Home token list buy flow: return to home screen
navigation.navigate(Routes.WALLET.HOME as never);
} else if (buyFlowOrigin === 'cardHome') {
navigation.navigate(Routes.CARD.ROOT, {
screen: Routes.CARD.HOME,
});
} else {
navigation.navigate(Routes.RAMP.TOKEN_SELECTION, {
screen: Routes.RAMP.TOKEN_SELECTION,
Expand Down Expand Up @@ -145,6 +150,10 @@ function TokenNotAvailableModal() {
} else if (buyFlowOrigin === 'homeTokenList') {
// Home token list buy flow: return to home screen
navigation.navigate(Routes.WALLET.HOME as never);
} else if (buyFlowOrigin === 'cardHome') {
navigation.navigate(Routes.CARD.ROOT, {
screen: Routes.CARD.HOME,
});
} else {
navigation.navigate(Routes.RAMP.TOKEN_SELECTION, {
screen: Routes.RAMP.TOKEN_SELECTION,
Expand Down
Loading
Loading