Skip to content

Commit 6f9a18e

Browse files
authored
feat(MUSD-801): use standard transaction-type icons in Money activity list (#30239)
## **Description** The Money / mUSD account activity list rendered the same mUSD token logo for every row regardless of transaction type. This change replaces that leading element with a neutral circular `AvatarIcon` whose icon is derived from the transaction type, so each row communicates what kind of activity it was, matching the MUSD-801 Figma reference. A pure icon resolver was added to `useMoneyTransactionDisplayInfo` following the same precedence as the existing label resolver (`moneyActivityTitleKey` first, then `TransactionType` fallback). Mapping: deposited/added → `Add`, received → `Arrow2Down`, converted → `Refresh`, transferred → `SwapHorizontal`, card_transaction → `Card`, sent → `Arrow2UpRight`, with `Arrow2Down` as the default. All icons are existing MMDS (`@metamask/design-system-react-native`) icons; no new assets. ## **Changelog** CHANGELOG entry: Updated the Money activity list to show a standard icon for each transaction type ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MUSD-801 ## **Manual testing steps** ```gherkin Feature: Money activity list transaction icons Scenario: user views the Money account activity list Given the user has mUSD activity containing deposit, receive, convert, transfer and card transactions When the user opens the Money account activity list Then each row shows the standard MMDS icon for its transaction type And the icons render correctly in both light and dark themes ``` ## **Screenshots/Recordings** ### **Before** Every activity row showed the mUSD token logo. ### **After** <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/1eda0500-6e5b-47c4-a0b8-b1c6da74342e" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk UI change that swaps the leading activity-row avatar to an icon derived from transaction metadata; primary risk is incorrect/missing icon mapping for some transaction types. > > **Overview** > Updates the Money/mUSD activity list to render a neutral `AvatarIcon` per row instead of always showing the mUSD token logo, including when the optional network badge is displayed. > > Extends `useMoneyTransactionDisplayInfo` to return an `icon: IconName` resolved from `moneyActivityTitleKey` (preferred) or `TransactionType` (fallback) with sensible defaults, and updates tests to cover the new icon mapping and `AvatarIcon` rendering via a new `MoneyActivityItemTestIds.ICON` test id. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4cb0e48. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 9c8f15e commit 6f9a18e

5 files changed

Lines changed: 219 additions & 17 deletions

File tree

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

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type TransactionMeta,
55
TransactionType,
66
} from '@metamask/transaction-controller';
7+
import { IconName } from '@metamask/design-system-react-native';
78
import type { Hex } from '@metamask/utils';
89
import renderWithProvider from '../../../../../util/test/renderWithProvider';
910
import { useMoneyTransactionDisplayInfo } from '../../hooks/useMoneyTransactionDisplayInfo';
@@ -31,13 +32,20 @@ jest.mock('../../../../../util/networks', () => ({
3132
getNetworkImageSource: jest.fn(() => ({ uri: 'network' })),
3233
}));
3334

34-
jest.mock(
35-
'../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken',
36-
() => {
37-
const { View } = jest.requireActual('react-native');
38-
return () => <View testID="mock-avatar-token" />;
39-
},
40-
);
35+
jest.mock('@metamask/design-system-react-native', () => {
36+
const actual = jest.requireActual('@metamask/design-system-react-native');
37+
const { View } = jest.requireActual('react-native');
38+
return {
39+
...actual,
40+
AvatarIcon: ({
41+
iconName,
42+
testID,
43+
}: {
44+
iconName: string;
45+
testID?: string;
46+
}) => <View testID={testID} accessibilityLabel={iconName} />,
47+
};
48+
});
4149

4250
jest.mock(
4351
'../../../../../component-library/components/Badges/BadgeWrapper',
@@ -83,6 +91,7 @@ describe('MoneyActivityItem', () => {
8391
primaryAmount: '+$0.00',
8492
fiatAmount: '$0.00',
8593
isIncoming: true,
94+
icon: IconName.Arrow2Down,
8695
});
8796
});
8897

@@ -107,6 +116,7 @@ describe('MoneyActivityItem', () => {
107116
primaryAmount: '+$0.00',
108117
fiatAmount: '$0.00',
109118
isIncoming: false,
119+
icon: IconName.Arrow2Down,
110120
});
111121

112122
const { queryByText } = renderWithProvider(
@@ -135,4 +145,41 @@ describe('MoneyActivityItem', () => {
135145
expect(getByTestId('mock-badge-wrapper')).toBeOnTheScreen();
136146
expect(getByTestId('mock-network-badge')).toBeOnTheScreen();
137147
});
148+
149+
it('renders the AvatarIcon and no longer renders the token avatar', () => {
150+
const { getByTestId, queryByTestId } = renderWithProvider(
151+
<MoneyActivityItem tx={baseTx} moneyAddress="0x1" />,
152+
);
153+
154+
expect(getByTestId(MoneyActivityItemTestIds.ICON)).toBeOnTheScreen();
155+
expect(queryByTestId('mock-avatar-token')).toBeNull();
156+
});
157+
158+
it('renders the AvatarIcon inside the network badge subtree', () => {
159+
const { getByTestId } = renderWithProvider(
160+
<MoneyActivityItem tx={baseTx} moneyAddress="0x1" showNetworkBadge />,
161+
);
162+
163+
expect(getByTestId(MoneyActivityItemTestIds.ICON)).toBeOnTheScreen();
164+
});
165+
166+
it('forwards the icon name from useMoneyTransactionDisplayInfo', () => {
167+
mockUseMoneyTransactionDisplayInfo.mockReturnValue({
168+
label: 'Label',
169+
description: 'Description',
170+
primaryAmount: '+$0.00',
171+
fiatAmount: '$0.00',
172+
isIncoming: true,
173+
icon: IconName.SwapHorizontal,
174+
});
175+
176+
const { getByTestId } = renderWithProvider(
177+
<MoneyActivityItem tx={baseTx} moneyAddress="0x1" />,
178+
);
179+
180+
expect(getByTestId(MoneyActivityItemTestIds.ICON)).toHaveProp(
181+
'accessibilityLabel',
182+
IconName.SwapHorizontal,
183+
);
184+
});
138185
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const MoneyActivityItemTestIds = {
22
ROW: 'money-activity-item-row',
3+
ICON: 'money-activity-item-icon',
34
};

app/components/UI/Money/components/MoneyActivityItem/MoneyActivityItem.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React, { useMemo } from 'react';
22
import { Pressable } from 'react-native';
33
import {
4+
AvatarIcon,
5+
AvatarIconSeverity,
6+
AvatarIconSize,
47
Box,
58
BoxAlignItems,
69
FontWeight,
@@ -11,7 +14,6 @@ import {
1114
import { useTailwind } from '@metamask/design-system-twrnc-preset';
1215
import type { TransactionMeta } from '@metamask/transaction-controller';
1316
import { getNetworkImageSource } from '../../../../../util/networks';
14-
import AvatarToken from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken';
1517
import { AvatarSize } from '../../../../../component-library/components/Avatars/Avatar';
1618
import BadgeWrapper from '../../../../../component-library/components/Badges/BadgeWrapper';
1719
import {
@@ -21,7 +23,6 @@ import {
2123
import Badge, {
2224
BadgeVariant,
2325
} from '../../../../../component-library/components/Badges/Badge';
24-
import { MUSD_TOKEN } from '../../../Earn/constants/musd';
2526
import { useMoneyTransactionDisplayInfo } from '../../hooks/useMoneyTransactionDisplayInfo';
2627
import { MoneyActivityItemTestIds } from './MoneyActivityItem.testIds';
2728

@@ -80,18 +81,20 @@ const MoneyActivityItem = ({
8081
/>
8182
}
8283
>
83-
<AvatarToken
84-
name={MUSD_TOKEN.name}
85-
imageSource={MUSD_TOKEN.imageSource}
86-
size={AvatarSize.Lg}
84+
<AvatarIcon
85+
iconName={display.icon}
86+
severity={AvatarIconSeverity.Neutral}
87+
size={AvatarIconSize.Lg}
88+
testID={MoneyActivityItemTestIds.ICON}
8789
/>
8890
</BadgeWrapper>
8991
) : (
9092
<Box twClassName="self-center">
91-
<AvatarToken
92-
name={MUSD_TOKEN.name}
93-
imageSource={MUSD_TOKEN.imageSource}
94-
size={AvatarSize.Lg}
93+
<AvatarIcon
94+
iconName={display.icon}
95+
severity={AvatarIconSeverity.Neutral}
96+
size={AvatarIconSize.Lg}
97+
testID={MoneyActivityItemTestIds.ICON}
9598
/>
9699
</Box>
97100
)}

app/components/UI/Money/hooks/useMoneyTransactionDisplayInfo.test.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
type TransactionMeta,
33
TransactionType,
44
} from '@metamask/transaction-controller';
5+
import { IconName } from '@metamask/design-system-react-native';
56
import type { Hex } from '@metamask/utils';
67
import { safeToChecksumAddress } from '../../../../util/address';
78
import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
89
import { useMoneyTransactionDisplayInfo } from './useMoneyTransactionDisplayInfo';
910
import { MUSD_TOKEN_ADDRESS } from '../../Earn/constants/musd';
11+
import type { MoneyActivityTitleKey } from '../constants/mockActivityData';
1012

1113
const MOCK_CHAIN: Hex = '0x1';
1214
const checksumToken = safeToChecksumAddress(MUSD_TOKEN_ADDRESS) as string;
@@ -93,4 +95,99 @@ describe('useMoneyTransactionDisplayInfo', () => {
9395
expect(result.current.primaryAmount).toMatch(/1,000\.00/);
9496
expect(result.current.primaryAmount).toContain('mUSD');
9597
});
98+
99+
function renderIcon(tx: TransactionMeta): IconName {
100+
const { result } = renderHookWithProvider(
101+
() => useMoneyTransactionDisplayInfo(tx, undefined),
102+
{
103+
state: {
104+
engine: {
105+
backgroundState: {
106+
CurrencyRateController: {
107+
currentCurrency: 'usd',
108+
currencyRates: {
109+
ETH: {
110+
conversionRate: 3000,
111+
usdConversionRate: 3000,
112+
conversionDate: null,
113+
},
114+
},
115+
},
116+
TokenRatesController: tokenMarketState(1 / 3000),
117+
},
118+
},
119+
},
120+
},
121+
);
122+
return result.current.icon;
123+
}
124+
125+
function txWithTitleKey(key: MoneyActivityTitleKey): TransactionMeta {
126+
return {
127+
...baseTx,
128+
moneyActivityTitleKey: key,
129+
} as unknown as TransactionMeta;
130+
}
131+
132+
function txWithType(type: TransactionType | undefined): TransactionMeta {
133+
return {
134+
...baseTx,
135+
type,
136+
} as unknown as TransactionMeta;
137+
}
138+
139+
describe('icon', () => {
140+
it.each<[MoneyActivityTitleKey, IconName]>([
141+
['added', IconName.Add],
142+
['deposited', IconName.Add],
143+
['received', IconName.Arrow2Down],
144+
['converted', IconName.Refresh],
145+
['transferred', IconName.SwapHorizontal],
146+
['card_transaction', IconName.Card],
147+
['sent', IconName.Arrow2UpRight],
148+
])('maps title key "%s" to %s', (key, expected) => {
149+
expect(renderIcon(txWithTitleKey(key))).toBe(expected);
150+
});
151+
152+
it.each<[TransactionType, IconName]>([
153+
[TransactionType.moneyAccountDeposit, IconName.Add],
154+
[TransactionType.incoming, IconName.Arrow2Down],
155+
[TransactionType.musdConversion, IconName.Refresh],
156+
[TransactionType.moneyAccountWithdraw, IconName.SwapHorizontal],
157+
[TransactionType.simpleSend, IconName.Arrow2UpRight],
158+
])('falls back to type "%s" mapping %s', (type, expected) => {
159+
expect(renderIcon(txWithType(type))).toBe(expected);
160+
});
161+
162+
it('defaults to Arrow2Down when type is undefined and no title key', () => {
163+
expect(renderIcon(txWithType(undefined))).toBe(IconName.Arrow2Down);
164+
});
165+
166+
it('defaults to Arrow2Down for an unmapped transaction type', () => {
167+
expect(renderIcon(txWithType(TransactionType.contractInteraction))).toBe(
168+
IconName.Arrow2Down,
169+
);
170+
});
171+
172+
it('disambiguates moneyAccountWithdraw (no title key) to SwapHorizontal', () => {
173+
expect(renderIcon(txWithType(TransactionType.moneyAccountWithdraw))).toBe(
174+
IconName.SwapHorizontal,
175+
);
176+
});
177+
178+
it('disambiguates simpleSend (no title key) to Arrow2UpRight', () => {
179+
expect(renderIcon(txWithType(TransactionType.simpleSend))).toBe(
180+
IconName.Arrow2UpRight,
181+
);
182+
});
183+
184+
it('prefers the title key over the transaction type', () => {
185+
const tx = {
186+
...baseTx,
187+
type: TransactionType.simpleSend,
188+
moneyActivityTitleKey: 'received',
189+
} as unknown as TransactionMeta;
190+
expect(renderIcon(tx)).toBe(IconName.Arrow2Down);
191+
});
192+
});
96193
});

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type TransactionMeta,
55
TransactionType,
66
} from '@metamask/transaction-controller';
7+
import { IconName } from '@metamask/design-system-react-native';
78
import { strings } from '../../../../../locales/i18n';
89
import {
910
selectCurrencyRates,
@@ -26,6 +27,7 @@ export interface MoneyTransactionDisplayInfo {
2627
primaryAmount: string;
2728
fiatAmount: string;
2829
isIncoming: boolean;
30+
icon: IconName;
2931
}
3032

3133
function titleKeyToLabel(key: MoneyActivityTitleKey): string {
@@ -80,6 +82,57 @@ function getLabel(tx: TransactionMeta): string {
8082
return getLabelForTransactionType(tx.type);
8183
}
8284

85+
function titleKeyToIcon(key: MoneyActivityTitleKey): IconName {
86+
switch (key) {
87+
case 'added':
88+
return IconName.Add;
89+
case 'deposited':
90+
return IconName.Add;
91+
case 'received':
92+
return IconName.Arrow2Down;
93+
case 'card_transaction':
94+
return IconName.Card;
95+
case 'converted':
96+
return IconName.Refresh;
97+
case 'sent':
98+
return IconName.Arrow2UpRight;
99+
case 'transferred':
100+
return IconName.SwapHorizontal;
101+
default:
102+
return IconName.Arrow2Down;
103+
}
104+
}
105+
106+
function getIconForTransactionType(
107+
type: TransactionType | undefined,
108+
): IconName {
109+
if (!type) {
110+
return IconName.Arrow2Down;
111+
}
112+
switch (type) {
113+
case TransactionType.moneyAccountDeposit:
114+
return IconName.Add;
115+
case TransactionType.incoming:
116+
return IconName.Arrow2Down;
117+
case TransactionType.musdConversion:
118+
return IconName.Refresh;
119+
case TransactionType.moneyAccountWithdraw:
120+
return IconName.SwapHorizontal;
121+
case TransactionType.simpleSend:
122+
return IconName.Arrow2UpRight;
123+
default:
124+
return IconName.Arrow2Down;
125+
}
126+
}
127+
128+
function getIcon(tx: TransactionMeta): IconName {
129+
const extended = tx as MoneyActivityTransactionMeta;
130+
if (extended.moneyActivityTitleKey) {
131+
return titleKeyToIcon(extended.moneyActivityTitleKey);
132+
}
133+
return getIconForTransactionType(tx.type);
134+
}
135+
83136
/**
84137
* Derives display strings for a Money activity row backed by {@link TransactionMeta}.
85138
*/
@@ -104,6 +157,7 @@ export function useMoneyTransactionDisplayInfo(
104157
tokenMarketData,
105158
),
106159
isIncoming: isIncomingMoneyTransactionMeta(tx),
160+
icon: getIcon(tx),
107161
}),
108162
[tx, subtitle, currentCurrency, currencyRates, tokenMarketData],
109163
);

0 commit comments

Comments
 (0)