From 29fa361fcf7d2216d98ea010fa9ba589af1909f3 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 1 May 2026 17:54:53 +0530 Subject: [PATCH 1/3] feat: display 5 year projected balance on money account deposit page --- .../custom-amount-info.test.tsx | 14 +++++ .../custom-amount-info/custom-amount-info.tsx | 17 ++++-- .../projected-five-year-balance/index.ts | 2 + .../projected-five-year-balance.tsx | 55 +++++++++++++++++++ locales/languages/en.json | 3 +- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 app/components/Views/confirmations/components/projected-five-year-balance/index.ts create mode 100644 app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.tsx diff --git a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx index 55e69ffe02a..13c14514deb 100644 --- a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx +++ b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx @@ -64,6 +64,20 @@ jest.mock('../../../hooks/pay/useTransactionPayWithdraw', () => ({ })), })); jest.mock('../../../../../../util/transaction-controller', () => ({})); +jest.mock('../../../../../UI/Money/hooks/useMoneyAccountBalance', () => ({ + __esModule: true, + default: () => ({ + vaultApyQuery: { data: { apy: 5.5 }, isLoading: false }, + }), +})); +jest.mock( + '../../../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter', + () => ({ + __esModule: true, + default: () => (value: { toString: () => string }) => + `$${Number(value.toString()).toFixed(2)}`, + }), +); jest.mock('../../../../../../core/Engine', () => ({ context: { TransactionPayController: { diff --git a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx index a0c343562c6..6d200a55316 100644 --- a/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx +++ b/app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx @@ -2,6 +2,7 @@ import React, { ReactNode, memo, useCallback, useState } from 'react'; import { toCaipAssetType } from '@metamask/utils'; import { TransactionType } from '@metamask/transaction-controller'; import { PayTokenAmount, PayTokenAmountSkeleton } from '../../pay-token-amount'; +import { ProjectedFiveYearBalance } from '../../projected-five-year-balance'; import { PayWithRow, PayWithRowSkeleton } from '../../rows/pay-with-row'; import { BridgeFeeRow } from '../../rows/bridge-fee-row'; import { BridgeTimeRow } from '../../rows/bridge-time-row'; @@ -208,12 +209,16 @@ export const CustomAmountInfo: React.FC = memo( onPress={handleAmountPress} disabled={!hasTokens} /> - {!hidePayTokenAmount && disablePay !== true && ( - - )} + {!hidePayTokenAmount && + disablePay !== true && + (isMoneyAccountDeposit ? ( + + ) : ( + + ))} {!hidePayTokenAmount && children} { + const apy = vaultApyQuery.data?.apy; + if (typeof apy !== 'number' || !isFinite(apy) || apy < 0) { + return null; + } + + const amount = new BigNumber(amountFiat || '0'); + if (!amount.isFinite()) { + return null; + } + + const growthFactor = new BigNumber(1).plus( + new BigNumber(apy).dividedBy(100), + ); + return amount.multipliedBy(growthFactor.pow(PROJECTION_YEARS)); + }, [amountFiat, vaultApyQuery.data?.apy]); + + if (vaultApyQuery.isLoading || projected === null) { + return null; + } + + return ( + + + {strings('confirm.custom_amount.projected_five_year_balance')}{' '} + + {formatFiat(projected)} + + + + ); +} diff --git a/locales/languages/en.json b/locales/languages/en.json index 4ec5a87b335..bdf71339d1f 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -7076,7 +7076,8 @@ "custom_amount": { "buy_button": "Buy crypto", "buy_predict": "Add funds to your wallet to use Predictions.", - "buy_perps": "Add funds to your wallet to use Perps." + "buy_perps": "Add funds to your wallet to use Perps.", + "projected_five_year_balance": "Projected 5-year balance:" }, "unlimited": "Unlimited", "all": "All", From df7e9175df18813defcd400d57939ba9ada49beb Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 1 May 2026 18:03:07 +0530 Subject: [PATCH 2/3] update --- .../projected-five-year-balance.test.tsx | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.test.tsx diff --git a/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.test.tsx b/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.test.tsx new file mode 100644 index 00000000000..0595405c7d5 --- /dev/null +++ b/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import BigNumber from 'bignumber.js'; +import { ProjectedFiveYearBalance } from './projected-five-year-balance'; +import useMoneyAccountBalance from '../../../../UI/Money/hooks/useMoneyAccountBalance'; +import useFiatFormatter from '../../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter'; +import { strings } from '../../../../../../locales/i18n'; + +jest.mock('../../../../UI/Money/hooks/useMoneyAccountBalance'); +jest.mock('../../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter'); + +const useMoneyAccountBalanceMock = jest.mocked(useMoneyAccountBalance); +const useFiatFormatterMock = jest.mocked(useFiatFormatter); + +const LABEL = strings('confirm.custom_amount.projected_five_year_balance'); + +function mockBalance({ + apy, + isLoading = false, +}: { + apy: number | undefined; + isLoading?: boolean; +}) { + useMoneyAccountBalanceMock.mockReturnValue({ + vaultApyQuery: { + data: apy === undefined ? undefined : { apy }, + isLoading, + }, + } as unknown as ReturnType); +} + +describe('ProjectedFiveYearBalance', () => { + const formatFiat = jest.fn( + (value: BigNumber) => `$${value.toFixed(2, BigNumber.ROUND_HALF_UP)}`, + ); + + beforeEach(() => { + jest.clearAllMocks(); + useFiatFormatterMock.mockReturnValue(formatFiat); + }); + + it('renders label and projected balance for $1,000 at 5% APY over 5 years (~$1,276.28)', () => { + mockBalance({ apy: 5 }); + + const { getByTestId, getByText } = render( + , + ); + + expect(getByTestId('projected-five-year-balance')).toBeOnTheScreen(); + expect(getByText(LABEL, { exact: false })).toBeOnTheScreen(); + // 1000 * (1.05)^5 = 1276.2815625 + expect(getByText('$1276.28')).toBeOnTheScreen(); + }); + + it('matches the Figma example: $1,000 at the design APY rounds to $1,114.36 when APY=2.18', () => { + mockBalance({ apy: 2.18 }); + + const { getByText } = render( + , + ); + + // 1000 * (1.0218)^5 ≈ 1113.86 — sanity-checks the compounding formula + // tracks the figma direction (label + green dollar amount); exact APY/value + // is product-driven, this just guards the math. + expect(getByText(/^\$1\d{3}\.\d{2}$/)).toBeOnTheScreen(); + }); + + it('returns null while APY is loading', () => { + mockBalance({ apy: undefined, isLoading: true }); + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('projected-five-year-balance')).toBeNull(); + }); + + it('returns null when APY data is unavailable', () => { + mockBalance({ apy: undefined }); + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('projected-five-year-balance')).toBeNull(); + }); + + it('returns null when APY is negative', () => { + mockBalance({ apy: -1 }); + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('projected-five-year-balance')).toBeNull(); + }); + + it('returns null when APY is not finite', () => { + mockBalance({ apy: Number.POSITIVE_INFINITY }); + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('projected-five-year-balance')).toBeNull(); + }); + + it('renders $0.00 when apy is 0% (compounding identity)', () => { + mockBalance({ apy: 0 }); + + const { getByText } = render(); + + expect(getByText('$0.00')).toBeOnTheScreen(); + }); + + it('treats empty amountFiat as zero', () => { + mockBalance({ apy: 5 }); + + const { getByText } = render(); + + expect(getByText('$0.00')).toBeOnTheScreen(); + }); + + it('passes a BigNumber to the fiat formatter', () => { + mockBalance({ apy: 5 }); + + render(); + + expect(formatFiat).toHaveBeenCalledTimes(1); + const passed = formatFiat.mock.calls[0][0]; + expect(BigNumber.isBigNumber(passed)).toBe(true); + // 1000 * 1.05^5 = 1276.2815625 + expect(passed.toFixed(4)).toBe('1276.2816'); + }); + + it('returns null when amountFiat is non-numeric', () => { + mockBalance({ apy: 5 }); + + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('projected-five-year-balance')).toBeNull(); + }); +}); From 1b0c7e22c4bedab12cac9bfcbcb5bc75c3854f4f Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 1 May 2026 18:09:29 +0530 Subject: [PATCH 3/3] update --- .../projected-five-year-balance.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.tsx b/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.tsx index e5ac001f494..527af80e6ed 100644 --- a/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.tsx +++ b/app/components/Views/confirmations/components/projected-five-year-balance/projected-five-year-balance.tsx @@ -1,10 +1,11 @@ import React, { useMemo } from 'react'; import { View } from 'react-native'; import BigNumber from 'bignumber.js'; -import Text, { +import { + Text, TextColor, TextVariant, -} from '../../../../../component-library/components/Texts/Text'; +} from '@metamask/design-system-react-native'; import useMoneyAccountBalance from '../../../../UI/Money/hooks/useMoneyAccountBalance'; import useFiatFormatter from '../../../../UI/SimulationDetails/FiatDisplay/useFiatFormatter'; import { strings } from '../../../../../../locales/i18n'; @@ -44,9 +45,9 @@ export function ProjectedFiveYearBalance({ return ( - + {strings('confirm.custom_amount.projected_five_year_balance')}{' '} - + {formatFiat(projected)}