Skip to content

Commit 9f071af

Browse files
authored
Merge branch 'main' into price-alerts-price-percent
2 parents a928ada + 5d9466a commit 9f071af

40 files changed

Lines changed: 2186 additions & 837 deletions

app/components/UI/Money/components/MoneyAddMoneySheet/MoneyAddMoneySheet.pendingTransaction.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ jest.mock('../../../../../selectors/tokenBalancesController', () => ({
163163
jest.mock('../../hooks/useMoneyAnalytics', () => ({
164164
useMoneyAnalytics: jest.fn(),
165165
}));
166+
jest.mock('../../../Ramp/hooks/useRampNavigation', () => ({
167+
useRampNavigation: () => ({ goToBuy: jest.fn(() => Promise.resolve()) }),
168+
}));
166169
jest.mock('../../../../../selectors/preferencesController', () => ({
167170
...jest.requireActual('../../../../../selectors/preferencesController'),
168171
selectPrivacyMode: jest.fn(() => false),

app/components/UI/Money/components/MoneyAddMoneySheet/MoneyAddMoneySheet.test.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ const mockOnCloseBottomSheet = jest.fn((cb?: () => void) => cb?.());
3333
const mockNavigate = jest.fn();
3434
const mockGoBack = jest.fn();
3535
const mockInitiateDeposit = jest.fn(() => Promise.resolve());
36+
const mockGoToBuy = jest.fn(() => Promise.resolve());
37+
38+
jest.mock('../../../Ramp/hooks/useRampNavigation', () => ({
39+
useRampNavigation: () => ({ goToBuy: mockGoToBuy }),
40+
}));
3641

3742
jest.mock('@react-navigation/native', () => {
3843
const actualReactNavigation = jest.requireActual('@react-navigation/native');
@@ -202,7 +207,7 @@ describe('MoneyAddMoneySheet', () => {
202207
expect(getByText('mUSD')).toBeOnTheScreen();
203208
});
204209

205-
it('shows the move-mUSD row disabled with the "Add mUSD" label when the selected EVM account has no mUSD tokens or fiat balance', () => {
210+
it('shows the move-mUSD row with the "Add mUSD" label and routes to the Ramps buy flow when the selected EVM account has no mUSD tokens or fiat balance', () => {
206211
(useMusdBalance as jest.Mock).mockReturnValue({
207212
fiatBalanceAggregated: undefined,
208213
fiatBalanceAggregatedFormatted: '$0.00',
@@ -222,11 +227,14 @@ describe('MoneyAddMoneySheet', () => {
222227

223228
fireEvent.press(getByTestId(MoneyAddMoneySheetTestIds.MOVE_MUSD_OPTION));
224229

225-
expect(mockOnCloseBottomSheet).not.toHaveBeenCalled();
230+
expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1);
231+
expect(mockGoToBuy).toHaveBeenCalledWith({
232+
assetId: MUSD_TOKEN_ASSET_ID_BY_CHAIN[MUSD_CONVERSION_DEFAULT_CHAIN_ID],
233+
});
226234
expect(mockInitiateDeposit).not.toHaveBeenCalled();
227235
});
228236

229-
it('shows the move-mUSD row disabled with the "Add mUSD" label when the selected EVM account mUSD fiat balance is zero', () => {
237+
it('shows the move-mUSD row with the "Add mUSD" label and routes to the Ramps buy flow when the selected EVM account mUSD fiat balance is zero', () => {
230238
(useMusdBalance as jest.Mock).mockReturnValue({
231239
fiatBalanceAggregated: '0',
232240
fiatBalanceAggregatedFormatted: '$0.00',
@@ -246,7 +254,10 @@ describe('MoneyAddMoneySheet', () => {
246254

247255
fireEvent.press(getByTestId(MoneyAddMoneySheetTestIds.MOVE_MUSD_OPTION));
248256

249-
expect(mockOnCloseBottomSheet).not.toHaveBeenCalled();
257+
expect(mockOnCloseBottomSheet).toHaveBeenCalledTimes(1);
258+
expect(mockGoToBuy).toHaveBeenCalledWith({
259+
assetId: MUSD_TOKEN_ASSET_ID_BY_CHAIN[MUSD_CONVERSION_DEFAULT_CHAIN_ID],
260+
});
250261
expect(mockInitiateDeposit).not.toHaveBeenCalled();
251262
});
252263

@@ -590,5 +601,24 @@ describe('MoneyAddMoneySheet', () => {
590601
redirect_target: SCREEN_NAMES.MONEY_DEPOSIT,
591602
});
592603
});
604+
605+
it('calls trackSurfaceClicked with the RAMPS_BUY redirect target when "Add mUSD" is pressed with no mUSD balance', () => {
606+
(useMusdBalance as jest.Mock).mockReturnValue({
607+
fiatBalanceAggregated: '0',
608+
fiatBalanceAggregatedFormatted: '$0.00',
609+
hasMusdBalanceOnAnyChain: false,
610+
tokenBalanceAggregated: '0',
611+
tokenBalanceByChain: {},
612+
});
613+
614+
const { getByTestId } = renderWithProvider(<MoneyAddMoneySheet />);
615+
616+
fireEvent.press(getByTestId(MoneyAddMoneySheetTestIds.MOVE_MUSD_OPTION));
617+
618+
expect(mockTrackSurfaceClicked).toHaveBeenCalledWith({
619+
component_name: COMPONENT_NAMES.MONEY_ADD_MONEY_SHEET_MOVE_MUSD,
620+
redirect_target: SCREEN_NAMES.RAMPS_BUY,
621+
});
622+
});
593623
});
594624
});

app/components/UI/Money/components/MoneyAddMoneySheet/MoneyAddMoneySheet.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import {
2929
MUSD_CONVERSION_DEFAULT_CHAIN_ID,
3030
MUSD_TOKEN,
3131
MUSD_TOKEN_ADDRESS_BY_CHAIN,
32+
MUSD_TOKEN_ASSET_ID_BY_CHAIN,
3233
} from '../../../Earn/constants/musd';
34+
import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation';
35+
import Logger from '../../../../../util/Logger';
3336
import {
3437
useMoneyAccountDeposit,
3538
type InitiateDepositOptions,
@@ -68,6 +71,7 @@ const MoneyAddMoneySheet: React.FC = () => {
6871
tokenBalanceByChain,
6972
} = useMusdBalance();
7073
const { initiateDeposit } = useMoneyAccountDeposit();
74+
const { goToBuy } = useRampNavigation();
7175
const { enabledTransactionTypes } = useMMPayFiatConfig();
7276
const hasAnyCryptoBalance = useSelector(selectHasAnyNonZeroTokenBalance);
7377
const transactions = useSelector(selectTransactions);
@@ -164,7 +168,34 @@ const MoneyAddMoneySheet: React.FC = () => {
164168
startDeposit({ autoSelectFiatPayment: true, intent: 'card' });
165169
}, [startDeposit, trackSurfaceClicked]);
166170

171+
const parsedMusdFiat = Number(fiatBalanceAggregated);
172+
const hasParsedFiatBalance =
173+
Number.isFinite(parsedMusdFiat) && parsedMusdFiat > 0;
174+
const hasMusdBalance = hasMusdBalanceOnAnyChain || hasParsedFiatBalance;
175+
167176
const handleMoveMusd = useCallback(() => {
177+
// With no mUSD anywhere there is nothing to move, so the row routes to
178+
// Ramps to buy mUSD instead.
179+
if (!hasMusdBalance) {
180+
trackSurfaceClicked({
181+
component_name: COMPONENT_NAMES.MONEY_ADD_MONEY_SHEET_MOVE_MUSD,
182+
redirect_target: SCREEN_NAMES.RAMPS_BUY,
183+
});
184+
185+
sheetRef.current?.onCloseBottomSheet(() => {
186+
goToBuy({
187+
assetId:
188+
MUSD_TOKEN_ASSET_ID_BY_CHAIN[MUSD_CONVERSION_DEFAULT_CHAIN_ID],
189+
}).catch((error) => {
190+
Logger.error(
191+
error as Error,
192+
'[MoneyAddMoneySheet] Failed to open the Ramps buy flow for mUSD',
193+
);
194+
});
195+
});
196+
return;
197+
}
198+
168199
let sourceChainId: Hex = MUSD_CONVERSION_DEFAULT_CHAIN_ID;
169200
let bestBalance = new BigNumber(0);
170201
for (const [chainId, balance] of Object.entries(
@@ -189,12 +220,13 @@ const MoneyAddMoneySheet: React.FC = () => {
189220
chainId: sourceChainId,
190221
},
191222
});
192-
}, [startDeposit, tokenBalanceByChain, trackSurfaceClicked]);
193-
194-
const parsedMusdFiat = Number(fiatBalanceAggregated);
195-
const hasParsedFiatBalance =
196-
Number.isFinite(parsedMusdFiat) && parsedMusdFiat > 0;
197-
const hasMusdBalance = hasMusdBalanceOnAnyChain || hasParsedFiatBalance;
223+
}, [
224+
goToBuy,
225+
hasMusdBalance,
226+
startDeposit,
227+
tokenBalanceByChain,
228+
trackSurfaceClicked,
229+
]);
198230

199231
const moveMusdAmount = useMemo(
200232
() => moneyFormatUsd(new BigNumber(tokenBalanceAggregated)),
@@ -236,7 +268,6 @@ const MoneyAddMoneySheet: React.FC = () => {
236268
icon: IconName.Add,
237269
onPress: handleMoveMusd,
238270
testID: MoneyAddMoneySheetTestIds.MOVE_MUSD_OPTION,
239-
disabled: !hasMusdBalance,
240271
},
241272
{
242273
label: strings('money.add_money_sheet.bank_account'),

app/components/UI/Money/constants/moneyEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export enum SCREEN_NAMES {
1717
MONEY_ACTIVITY_DETAILS = 'money_activity_details',
1818
MONEY_POTENTIAL_EARNINGS = 'money_potential_earnings',
1919
MONEY_FIRST_TIME_DEPOSIT = 'money_first_time_deposit',
20+
RAMPS_BUY = 'ramps_buy',
2021
}
2122

2223
export enum BOTTOM_SHEET_NAMES {

app/components/UI/Money/hooks/useMoneyTransactionStatus.test.ts

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './useMoneyAccount';
2121
import { shouldShowMoneyFirstTimeDepositAnimation } from '../utils/firstTimeDeposit';
2222
import { getMemoizedInternalAccountByAddress } from '../../../../selectors/accountsController';
23+
import { selectCurrentCurrency } from '../../../../selectors/currencyRateController';
2324
import { selectAccountToGroupMap } from '../../../../selectors/multichainAccounts/accountTreeController';
2425
import Routes from '../../../../constants/navigation/Routes';
2526
import NavigationService from '../../../../core/NavigationService/NavigationService';
@@ -683,7 +684,7 @@ describe('useMoneyTransactionStatus', () => {
683684
});
684685
});
685686

686-
it('confirmed → success toast with decoded fiat amount', () => {
687+
it('confirmed → success toast with decoded dollar amount', () => {
687688
const { confirmedHandler } = renderAndGetHandlers();
688689

689690
confirmedHandler(
@@ -699,8 +700,7 @@ describe('useMoneyTransactionStatus', () => {
699700

700701
expect(depositSuccessFn).toHaveBeenCalledTimes(1);
701702
const params = depositSuccessFn.mock.calls[0][0];
702-
expect(params.amountFiat).toContain('mUSD');
703-
expect(params.amountFiat).toContain('12.34');
703+
expect(params.amountFiat).toBe('$12.34');
704704
});
705705

706706
it('failed → deposit failed toast', () => {
@@ -1508,37 +1508,82 @@ describe('useMoneyTransactionStatus', () => {
15081508
});
15091509

15101510
describe('formatMusdAmountForToast', () => {
1511-
it('falls back to mUSD format when no fiat rate is available', () => {
1512-
expect(formatMusdAmountForToast(BigInt(1_000_000))).toBe('1.00 mUSD');
1513-
expect(formatMusdAmountForToast(BigInt(123_456))).toBe('0.12 mUSD');
1511+
it('formats the decoded mUSD amount as US dollars', () => {
1512+
expect(formatMusdAmountForToast(BigInt(1_000_000))).toBe('$1.00');
1513+
expect(formatMusdAmountForToast(BigInt(123_456))).toBe('$0.12');
1514+
expect(formatMusdAmountForToast(BigInt(5_000_000))).toBe('$5.00');
15141515
});
1516+
});
1517+
1518+
describe('USD formatting regardless of preferred currency', () => {
1519+
beforeEach(() => {
1520+
jest.mocked(selectCurrentCurrency).mockReturnValue('EUR');
1521+
});
1522+
1523+
afterEach(() => {
1524+
jest.mocked(selectCurrentCurrency).mockReturnValue('usd');
1525+
});
1526+
1527+
it('confirmed deposit → success amount is dollar-formatted when the preferred currency is EUR', () => {
1528+
const { confirmedHandler } = renderAndGetHandlers();
15151529

1516-
it('formats as fiat when token market data, currency rates and network config resolve', () => {
1517-
const tokenRatesMock = jest.requireMock(
1518-
'../../../../selectors/tokenRatesController',
1530+
confirmedHandler(
1531+
buildTxMeta({
1532+
type: TransactionType.moneyAccountDeposit,
1533+
status: TransactionStatus.confirmed,
1534+
txParams: {
1535+
from: '0x0',
1536+
data: encodeDepositData(BigInt(12_340_000)),
1537+
},
1538+
}),
15191539
);
1520-
const currencyRatesMock = jest.requireMock(
1521-
'../../../../selectors/currencyRateController',
1540+
1541+
expect(depositSuccessFn).toHaveBeenCalledTimes(1);
1542+
const amountFiat = depositSuccessFn.mock.calls[0][0].amountFiat;
1543+
expect(amountFiat).toMatch(/^\$/);
1544+
expect(amountFiat).not.toContain('€');
1545+
});
1546+
1547+
it('confirmed withdraw → success amount is dollar-formatted when the preferred currency is EUR', () => {
1548+
const { confirmedHandler } = renderAndGetHandlers();
1549+
1550+
confirmedHandler(
1551+
buildTxMeta({
1552+
type: TransactionType.moneyAccountWithdraw,
1553+
status: TransactionStatus.confirmed,
1554+
txParams: {
1555+
from: '0x0',
1556+
data: encodeWithdrawData(BigInt(50_000_000)),
1557+
},
1558+
}),
15221559
);
1523-
const networkConfigMock = jest.requireMock(
1524-
'../../../../selectors/networkController',
1560+
1561+
expect(withdrawSuccessFn).toHaveBeenCalledTimes(1);
1562+
const amountFiat = withdrawSuccessFn.mock.calls[0][0].amountFiat;
1563+
expect(amountFiat).toMatch(/^\$/);
1564+
expect(amountFiat).not.toContain('€');
1565+
});
1566+
1567+
it('confirmed perps send → success amount is dollar-formatted when the preferred currency is EUR', () => {
1568+
const { confirmedHandler } = renderAndGetHandlers();
1569+
1570+
confirmedHandler(
1571+
buildTxMeta({
1572+
id: 'eur-send-tx',
1573+
type: TransactionType.perpsDeposit,
1574+
status: TransactionStatus.confirmed,
1575+
metamaskPay: {
1576+
tokenAddress: MUSD_ADDRESS,
1577+
chainId: CHAIN_IDS.MONAD,
1578+
targetFiat: '100',
1579+
},
1580+
} as unknown as Partial<TransactionMeta>),
15251581
);
1526-
tokenRatesMock.selectTokenMarketData.mockReturnValueOnce({
1527-
'0x1': {
1528-
'0xacA92E438df0B2401fF60dA7E4337B687a2435DA': { price: 1 },
1529-
},
1530-
});
1531-
currencyRatesMock.selectCurrencyRates.mockReturnValueOnce({
1532-
ETH: { conversionRate: 2 },
1533-
});
1534-
networkConfigMock.selectNetworkConfigurations.mockReturnValueOnce({
1535-
'0x1': { nativeCurrency: 'ETH' },
1536-
});
1537-
currencyRatesMock.selectCurrentCurrency.mockReturnValueOnce('usd');
15381582

1539-
const formatted = formatMusdAmountForToast(BigInt(5_000_000));
1540-
expect(formatted).not.toContain('mUSD');
1541-
expect(formatted).toMatch(/10/);
1583+
expect(sendSuccessFn).toHaveBeenCalledTimes(1);
1584+
const amountFiat = sendSuccessFn.mock.calls[0][0].amountFiat;
1585+
expect(amountFiat).toMatch(/^\$/);
1586+
expect(amountFiat).not.toContain('€');
15421587
});
15431588
});
15441589

app/components/UI/Money/hooks/useMoneyTransactionStatus.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
CHAIN_IDS,
32
TransactionMeta,
43
TransactionStatus,
54
TransactionType,
@@ -15,24 +14,14 @@ import Logger from '../../../../util/Logger';
1514
import { fromTokenMinimalUnitString } from '../../../../util/number/bigint';
1615
import { strings } from '../../../../../locales/i18n';
1716
import { store } from '../../../../store';
18-
import {
19-
selectCurrencyRates,
20-
selectCurrentCurrency,
21-
} from '../../../../selectors/currencyRateController';
22-
import { selectNetworkConfigurations } from '../../../../selectors/networkController';
2317
import { getMemoizedInternalAccountByAddress } from '../../../../selectors/accountsController';
2418
import { selectAccountToGroupMap } from '../../../../selectors/multichainAccounts/accountTreeController';
25-
import { selectTokenMarketData } from '../../../../selectors/tokenRatesController';
26-
import {
27-
renderShortAddress,
28-
toChecksumAddress,
29-
} from '../../../../util/address';
19+
import { renderShortAddress } from '../../../../util/address';
3020
import {
3121
MUSD_DECIMALS,
32-
MUSD_TOKEN_ADDRESS_BY_CHAIN,
3322
TOAST_TRACKING_CLEANUP_DELAY_MS,
3423
} from '../../Earn/constants/musd';
35-
import { moneyFormatFiat } from '../utils/moneyFormatFiat';
24+
import { moneyFormatUsd } from '../utils/moneyFormatFiat';
3625
import { TELLER_ABI } from '../utils/moneyAccountTransactions';
3726
import {
3827
isMoneyAccountTx,
@@ -117,50 +106,17 @@ function decodeTellerAmount(
117106
return undefined;
118107
}
119108

120-
function getMusdFiatRate(): BigNumber | undefined {
121-
const state = store.getState();
122-
const tokenMarketData = selectTokenMarketData(state);
123-
const currencyRates = selectCurrencyRates(state);
124-
const networkConfigurations = selectNetworkConfigurations(state);
125-
126-
const musdAddress = MUSD_TOKEN_ADDRESS_BY_CHAIN[CHAIN_IDS.MAINNET];
127-
if (!musdAddress) return undefined;
128-
129-
const checksumAddress = toChecksumAddress(musdAddress);
130-
const chainConfig = networkConfigurations?.[CHAIN_IDS.MAINNET];
131-
const nativeCurrency = chainConfig?.nativeCurrency;
132-
const conversionRate = nativeCurrency
133-
? currencyRates?.[nativeCurrency]?.conversionRate
134-
: undefined;
135-
136-
const priceInNativeCurrency =
137-
tokenMarketData?.[CHAIN_IDS.MAINNET]?.[checksumAddress]?.price ??
138-
tokenMarketData?.[CHAIN_IDS.MAINNET]?.[musdAddress]?.price;
139-
140-
if (!conversionRate || priceInNativeCurrency === undefined) return undefined;
141-
return new BigNumber(priceInNativeCurrency).times(conversionRate);
142-
}
143-
144109
export function formatMusdAmountForToast(amountWei: bigint): string {
145110
const musdDecimal = new BigNumber(
146111
fromTokenMinimalUnitString(amountWei.toString(), MUSD_DECIMALS),
147112
);
148-
const rate = getMusdFiatRate();
149-
const currentCurrency = selectCurrentCurrency(store.getState());
150-
151-
if (!rate || !currentCurrency) {
152-
return `${musdDecimal.toFixed(2)} mUSD`;
153-
}
154-
return moneyFormatFiat(musdDecimal.times(rate), currentCurrency);
113+
return moneyFormatUsd(musdDecimal);
155114
}
156115

157116
function formatMetamaskPayFiat(value: unknown): string | undefined {
158117
const fiat = Number(value);
159118
if (Number.isNaN(fiat) || fiat <= 0) return undefined;
160-
return moneyFormatFiat(
161-
new BigNumber(fiat),
162-
selectCurrentCurrency(store.getState()),
163-
);
119+
return moneyFormatUsd(new BigNumber(fiat));
164120
}
165121

166122
function navigateToMoneyTransactionDetails(transactionId: string) {

0 commit comments

Comments
 (0)