Skip to content

Commit 9d915c1

Browse files
Merge branch 'main' into cursor/defi-explore-sites-5eb2
2 parents 6125646 + 34f3d4c commit 9d915c1

34 files changed

Lines changed: 806 additions & 434 deletions

.github/workflows/update-e2e-fixtures.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ jobs:
255255
run: |
256256
IS_TEST='true' NODE_OPTIONS='--experimental-vm-modules' \
257257
yarn detox test -c ios.sim.main.ci --headless \
258-
tests/regression/fixtures/fixture-validation.spec.ts
258+
tests/smoke/fixtures/fixture-validation.spec.ts
259259
env:
260260
PREBUILT_IOS_APP_PATH: artifacts/main-qa-MetaMask.app
261261

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx

Lines changed: 156 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import useMoneyAccountBalance from '../../hooks/useMoneyAccountBalance';
2525
import { selectIsCardholder } from '../../../../../selectors/cardController';
2626
import { getDetectedGeolocation } from '../../../../../reducers/fiatOrders';
2727
import { moneyFormatFiat } from '../../utils/moneyFormatFiat';
28+
import { useMusdConversion } from '../../../Earn/hooks/useMusdConversion';
2829

2930
const mockGoBack = jest.fn();
3031
const mockNavigate = jest.fn();
32+
const mockInitiateCustomConversion = jest.fn();
3133
const mockMoneyFormatFiat = moneyFormatFiat as jest.MockedFunction<
3234
typeof moneyFormatFiat
3335
>;
@@ -72,9 +74,16 @@ jest.mock('../../hooks/useMoneyAccountBalance', () => ({
7274
}));
7375

7476
jest.mock('../../../Earn/hooks/useMusdConversion', () => ({
75-
useMusdConversion: () => ({
76-
initiateCustomConversion: jest.fn(),
77-
}),
77+
useMusdConversion: jest.fn(),
78+
}));
79+
80+
jest.mock('../../../../../core/NavigationService', () => ({
81+
__esModule: true,
82+
default: {
83+
navigation: {
84+
navigate: jest.fn(),
85+
},
86+
},
7887
}));
7988

8089
jest.mock('../../utils/moneyFormatFiat', () => ({
@@ -98,6 +107,8 @@ const mockUseMoneyAccountTransactions = jest.mocked(
98107
useMoneyAccountTransactions,
99108
);
100109

110+
const mockUseMusdConversion = jest.mocked(useMusdConversion);
111+
101112
const mockUseMoneyAccountBalance = jest.mocked(useMoneyAccountBalance);
102113

103114
jest.mock(
@@ -119,13 +130,22 @@ jest.mock('../../../../../component-library/components/Badges/Badge', () => ({
119130
}));
120131

121132
jest.mock('../../components/MoneyActivityItem/MoneyActivityItem', () => {
122-
const { View, Text } = jest.requireActual('react-native');
133+
const { TouchableOpacity, Text } = jest.requireActual('react-native');
123134
return {
124135
__esModule: true,
125-
default: ({ tx }: { tx: { id: string } }) => (
126-
<View testID={`money-activity-item-${tx.id}`}>
136+
default: ({
137+
tx,
138+
onPress,
139+
}: {
140+
tx: { id: string };
141+
onPress?: () => void;
142+
}) => (
143+
<TouchableOpacity
144+
testID={`money-activity-item-${tx.id}`}
145+
onPress={onPress}
146+
>
127147
<Text>{tx.id}</Text>
128-
</View>
148+
</TouchableOpacity>
129149
),
130150
};
131151
});
@@ -134,12 +154,21 @@ jest.mock('@react-native-masked-view/masked-view', () => 'MaskedView');
134154
jest.mock('../../../../UI/AssetOverview/Balance/Balance', () => ({
135155
NetworkBadgeSource: jest.fn(() => null),
136156
}));
157+
jest.mock('../../../../../util/Logger', () => ({
158+
__esModule: true,
159+
default: { error: jest.fn() },
160+
}));
137161

138162
describe('MoneyHomeView', () => {
139163
beforeEach(() => {
140164
jest.clearAllMocks();
141165
global.alert = jest.fn();
142166

167+
mockInitiateCustomConversion.mockResolvedValue(undefined);
168+
mockUseMusdConversion.mockReturnValue({
169+
initiateCustomConversion: mockInitiateCustomConversion,
170+
} as unknown as ReturnType<typeof useMusdConversion>);
171+
143172
mockSelectIsCardholder.mockReturnValue(false);
144173
mockGetDetectedGeolocation.mockReturnValue('US');
145174

@@ -339,22 +368,31 @@ describe('MoneyHomeView', () => {
339368

340369
expect(mockNavigate).toHaveBeenCalledWith(Routes.MONEY.MODALS.ROOT, {
341370
screen: Routes.MONEY.MODALS.EARNINGS_INFO_SHEET,
342-
params: { apy: 5 },
343371
});
344372
});
345373

346-
describe('projected earnings', () => {
347-
it('passes the formatted projected earnings to MoneyEarnings', () => {
374+
describe('monthly and yearly earnings', () => {
375+
it('passes the formatted monthly earnings to MoneyEarnings', () => {
348376
mockMoneyFormatFiat.mockReturnValue('$0.12');
349377

350378
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
351379

352-
expect(
353-
getByTestId(MoneyEarningsTestIds.PROJECTED_VALUE),
354-
).toHaveTextContent('$0.12');
380+
expect(getByTestId(MoneyEarningsTestIds.MONTHLY_VALUE)).toHaveTextContent(
381+
'$0.12',
382+
);
383+
});
384+
385+
it('passes the formatted yearly earnings to MoneyEarnings', () => {
386+
mockMoneyFormatFiat.mockReturnValue('$0.12');
387+
388+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
389+
390+
expect(getByTestId(MoneyEarningsTestIds.YEARLY_VALUE)).toHaveTextContent(
391+
'$0.12',
392+
);
355393
});
356394

357-
it('displays the zero-formatted value for projected earnings when totalFiatRaw is absent', () => {
395+
it('displays the zero-formatted value for monthly earnings when totalFiatRaw is absent', () => {
358396
mockMoneyFormatFiat.mockReturnValue('$0.00');
359397
mockUseMoneyAccountBalance.mockReturnValue({
360398
totalFiatFormatted: undefined,
@@ -373,9 +411,9 @@ describe('MoneyHomeView', () => {
373411

374412
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
375413

376-
expect(
377-
getByTestId(MoneyEarningsTestIds.PROJECTED_VALUE),
378-
).toHaveTextContent('$0.00');
414+
expect(getByTestId(MoneyEarningsTestIds.MONTHLY_VALUE)).toHaveTextContent(
415+
'$0.00',
416+
);
379417
});
380418
});
381419

@@ -599,6 +637,107 @@ describe('MoneyHomeView', () => {
599637
screen: Routes.MONEY.MODALS.ADD_MONEY_SHEET,
600638
});
601639
});
640+
641+
it('navigates to Asset details when the mUSD token row is pressed', () => {
642+
const NavigationService = jest.requireMock(
643+
'../../../../../core/NavigationService',
644+
).default;
645+
646+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
647+
648+
fireEvent.press(getByTestId(MoneyMusdTokenRowTestIds.CONTAINER));
649+
650+
expect(NavigationService.navigation.navigate).toHaveBeenCalledWith(
651+
'Asset',
652+
expect.objectContaining({ source: expect.any(String) }),
653+
);
654+
});
655+
656+
it('navigates to HowItWorks when its section header is pressed', () => {
657+
const { getByText } = renderWithProvider(<MoneyHomeView />);
658+
659+
fireEvent.press(getByText(strings('money.how_it_works.title')));
660+
661+
expect(mockNavigate).toHaveBeenCalledWith(Routes.MONEY.HOW_IT_WORKS);
662+
});
663+
664+
it('opens the Learn more URL when Learn more is pressed', () => {
665+
const { Linking } = jest.requireMock('react-native');
666+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
667+
668+
fireEvent.press(getByTestId(MoneyWhatYouGetTestIds.LEARN_MORE_BUTTON));
669+
670+
expect(Linking.openURL).toHaveBeenCalledWith(
671+
expect.stringContaining('http'),
672+
);
673+
});
674+
});
675+
676+
describe('filled state navigation handlers', () => {
677+
it('navigates to Potential Earnings when View all is pressed on potential earnings section', () => {
678+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
679+
680+
fireEvent.press(
681+
getByTestId(MoneyPotentialEarningsTestIds.VIEW_ALL_BUTTON),
682+
);
683+
684+
expect(mockNavigate).toHaveBeenCalledWith(
685+
Routes.MONEY.POTENTIAL_EARNINGS,
686+
);
687+
});
688+
689+
it('initiates a custom conversion when a token Convert button is pressed', async () => {
690+
const { getByText } = renderWithProvider(<MoneyHomeView />);
691+
692+
fireEvent.press(getByText(strings('money.potential_earnings.convert')));
693+
694+
expect(mockInitiateCustomConversion).toHaveBeenCalledWith(
695+
expect.objectContaining({
696+
preferredPaymentToken: expect.objectContaining({
697+
address: mockConversionTokens[0].address,
698+
}),
699+
navigationStack: Routes.MONEY.ROOT,
700+
}),
701+
);
702+
});
703+
704+
it('logs an error when initiateCustomConversion rejects', async () => {
705+
mockInitiateCustomConversion.mockRejectedValueOnce(
706+
new Error('network failure'),
707+
);
708+
const Logger = jest.requireMock('../../../../../util/Logger');
709+
710+
const { getByText } = renderWithProvider(<MoneyHomeView />);
711+
712+
fireEvent.press(getByText(strings('money.potential_earnings.convert')));
713+
714+
await Promise.resolve();
715+
716+
expect(Logger.default.error).toHaveBeenCalledWith(
717+
expect.any(Error),
718+
expect.objectContaining({
719+
message: expect.stringContaining('MoneyHomeView'),
720+
}),
721+
);
722+
});
723+
724+
it('triggers the under-construction alert when an activity item is pressed', () => {
725+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
726+
727+
fireEvent.press(getByTestId('money-activity-item-padded-0'));
728+
729+
expect(global.alert).toHaveBeenCalled();
730+
});
731+
});
732+
733+
describe('card upsell mode — Get Now handler', () => {
734+
it('navigates to Card root when the Get Now card row is pressed', () => {
735+
const { getByTestId } = renderWithProvider(<MoneyHomeView />);
736+
737+
fireEvent.press(getByTestId(MoneyMetaMaskCardTestIds.VIRTUAL_CARD_ROW));
738+
739+
expect(mockNavigate).toHaveBeenCalledWith(Routes.CARD.ROOT);
740+
});
602741
});
603742

604743
describe('Metal card geolocation gating', () => {

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,28 @@ const MoneyHomeView = () => {
8686
[currentCurrency],
8787
);
8888

89-
const projectedEarnings = useMemo(() => {
89+
const monthlyEarnings = useMemo(() => {
9090
if (!totalFiatRaw || !apyPercent) return formattedZero;
9191
const balance = new BigNumber(totalFiatRaw);
9292
if (balance.isZero() || balance.isNaN()) return formattedZero;
93-
const earnings = calculateProjectedEarnings(balance.toNumber(), apyPercent);
93+
const earnings = calculateProjectedEarnings(
94+
balance.toNumber(),
95+
apyPercent,
96+
1 / 12,
97+
);
98+
if (!Number.isFinite(earnings)) return formattedZero;
99+
return moneyFormatFiat(new BigNumber(earnings), currentCurrency);
100+
}, [totalFiatRaw, apyPercent, currentCurrency, formattedZero]);
101+
102+
const yearlyEarnings = useMemo(() => {
103+
if (!totalFiatRaw || !apyPercent) return formattedZero;
104+
const balance = new BigNumber(totalFiatRaw);
105+
if (balance.isZero() || balance.isNaN()) return formattedZero;
106+
const earnings = calculateProjectedEarnings(
107+
balance.toNumber(),
108+
apyPercent,
109+
1,
110+
);
94111
if (!Number.isFinite(earnings)) return formattedZero;
95112
return moneyFormatFiat(new BigNumber(earnings), currentCurrency);
96113
}, [totalFiatRaw, apyPercent, currentCurrency, formattedZero]);
@@ -141,9 +158,8 @@ const MoneyHomeView = () => {
141158
const handleEarningsInfoPress = useCallback(() => {
142159
navigation.navigate(Routes.MONEY.MODALS.ROOT, {
143160
screen: Routes.MONEY.MODALS.EARNINGS_INFO_SHEET,
144-
params: { apy: apyPercent },
145161
});
146-
}, [navigation, apyPercent]);
162+
}, [navigation]);
147163

148164
const handleMusdRowPress = useCallback(() => {
149165
NavigationService.navigation.navigate('Asset', {
@@ -248,8 +264,8 @@ const MoneyHomeView = () => {
248264
/>
249265
<Divider />
250266
<MoneyEarnings
251-
lifetimeEarnings={formattedZero}
252-
projectedEarnings={projectedEarnings}
267+
monthlyEarnings={monthlyEarnings}
268+
yearlyEarnings={yearlyEarnings}
253269
isLoading={vaultApyQuery.isLoading || isAggregatedBalanceLoading}
254270
onInfoPress={handleEarningsInfoPress}
255271
/>
@@ -285,7 +301,6 @@ const MoneyHomeView = () => {
285301
<MoneyPotentialEarnings
286302
tokens={conversionTokens}
287303
apy={apyPercent}
288-
condensed={isMilestone}
289304
onTokenPress={handleTokenConvertPress}
290305
onViewAllPress={handleEarnCryptoPress}
291306
onHeaderPress={handleEarnCryptoPress}

0 commit comments

Comments
 (0)