Skip to content

Commit 4cb0e48

Browse files
committed
feat(MUSD-801): use standard transaction-type icons in Money activity list
Replace the mUSD token avatar in the activity list with a neutral AvatarIcon whose icon is derived per transaction type. Adds an icon resolver to useMoneyTransactionDisplayInfo (moneyActivityTitleKey first, then TransactionType fallback) mapping each type to its MMDS IconName: deposited/added->Add, received->Arrow2Down, converted->Refresh, transferred->SwapHorizontal, card_transaction->Card, sent->Arrow2UpRight.
1 parent ead9094 commit 4cb0e48

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)