Skip to content

Commit 32146b9

Browse files
committed
feat: money account activity
1 parent 3e17797 commit 32146b9

14 files changed

Lines changed: 773 additions & 16 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
diff a/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx b/app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx (rejected hunks)
2+
@@ -26,7 +26,6 @@ import styleSheet from './MoneyHomeView.styles';
3+
import { useMusdConversionTokens } from '../../../Earn/hooks/useMusdConversionTokens';
4+
import { useMusdConversion } from '../../../Earn/hooks/useMusdConversion';
5+
import { useMoneyAccountTransactions } from '../../hooks/useMoneyAccountTransactions';
6+
-import { showMoneyActivityUnderConstructionAlert } from '../../constants/showMoneyActivityUnderConstructionAlert';
7+
import useMoneyAccountBalance from '../../hooks/useMoneyAccountBalance';
8+
import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController';
9+
import { moneyFormatFiat } from '../../utils/moneyFormatFiat';
10+
@@ -207,9 +206,15 @@ const MoneyHomeView = () => {
11+
}, [navigation]);
12+
const handleActivityHeaderPress = handleViewAllActivityPress;
13+
14+
- const handleActivityItemPress = useCallback(() => {
15+
- showMoneyActivityUnderConstructionAlert();
16+
- }, []);
17+
+ const handleActivityItemPress = useCallback(
18+
+ (transactionId: string) => {
19+
+ navigation.navigate(Routes.MONEY.MODALS.ROOT, {
20+
+ screen: Routes.MONEY.MODALS.TRANSACTION_DETAILS_SHEET,
21+
+ params: { transactionId },
22+
+ });
23+
+ },
24+
+ [navigation],
25+
+ );
26+
27+
const handleOnboardingCtaPress = useCallback(() => {
28+
if (isCardholderWithMilestone) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
diff a/app/components/UI/Money/components/MoneyActivityItem/MoneyActivityItem.tsx b/app/components/UI/Money/components/MoneyActivityItem/MoneyActivityItem.tsx (rejected hunks)
2+
@@ -28,7 +28,7 @@ import { MoneyActivityItemTestIds } from './MoneyActivityItem.testIds';
3+
export interface MoneyActivityItemProps {
4+
tx: TransactionMeta;
5+
moneyAddress: string | undefined;
6+
- onPress?: () => void;
7+
+ onPress?: (transactionId: string) => void;
8+
/** When true, shows the chain network badge on the token avatar. Defaults to false. */
9+
showNetworkBadge?: boolean;
10+
}
11+
@@ -51,6 +51,19 @@ const MoneyActivityItem = ({
12+
[tx.chainId, showNetworkBadge],
13+
);
14+
15+
+ // For ERC-20 source tokens use the token's own image URI.
16+
+ // For native tokens (e.g. ETH) fall back to the source chain's network icon.
17+
+ // If neither is available, fall back to the mUSD icon.
18+
+ const tokenAvatarImageSource = useMemo(() => {
19+
+ if (display.sourceTokenImage) {
20+
+ return { uri: display.sourceTokenImage };
21+
+ }
22+
+ if (display.sourceTokenChainId) {
23+
+ return getNetworkImageSource({ chainId: display.sourceTokenChainId });
24+
+ }
25+
+ return MUSD_TOKEN.imageSource;
26+
+ }, [display.sourceTokenImage, display.sourceTokenChainId]);
27+
+
28+
const amountColor = display.isIncoming
29+
? TextColor.SuccessDefault
30+
: TextColor.TextDefault;
31+
@@ -58,7 +71,7 @@ const MoneyActivityItem = ({
32+
return (
33+
<Pressable
34+
accessibilityRole="button"
35+
- onPress={onPress}
36+
+ onPress={onPress ? () => onPress(tx.id) : undefined}
37+
testID={`${MoneyActivityItemTestIds.ROW}-${tx.id}`}
38+
style={({ pressed }) =>
39+
tw.style(
40+
@@ -81,16 +94,16 @@ const MoneyActivityItem = ({
41+
}
42+
>
43+
<AvatarToken
44+
- name={MUSD_TOKEN.name}
45+
- imageSource={MUSD_TOKEN.imageSource}
46+
+ name={display.sourceTokenSymbol ?? MUSD_TOKEN.name}
47+
+ imageSource={tokenAvatarImageSource}
48+
size={AvatarSize.Lg}
49+
/>
50+
</BadgeWrapper>
51+
) : (
52+
<Box twClassName="self-center">
53+
<AvatarToken
54+
- name={MUSD_TOKEN.name}
55+
- imageSource={MUSD_TOKEN.imageSource}
56+
+ name={display.sourceTokenSymbol ?? MUSD_TOKEN.name}
57+
+ imageSource={tokenAvatarImageSource}
58+
size={AvatarSize.Lg}
59+
/>
60+
</Box>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
diff a/app/components/UI/Money/components/MoneyActivityList/MoneyActivityList.tsx b/app/components/UI/Money/components/MoneyActivityList/MoneyActivityList.tsx (rejected hunks)
2+
@@ -17,7 +17,7 @@ interface MoneyActivityListProps {
3+
moneyAddress?: string;
4+
onViewAllPress?: () => void;
5+
onHeaderPress?: () => void;
6+
- onItemPress?: () => void;
7+
+ onItemPress?: (transactionId: string) => void;
8+
}
9+
10+
const MoneyActivityList = ({

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ const styleSheet = (params: { theme: Theme }) =>
2929
comingSoonTag: {
3030
borderRadius: 8,
3131
},
32+
addressRowContent: {
33+
flex: 1,
34+
flexDirection: 'column',
35+
alignItems: 'flex-start',
36+
gap: 2,
37+
},
38+
addressText: {
39+
color: params.theme.colors.text.alternative,
40+
},
41+
addressActions: {
42+
flexDirection: 'row',
43+
alignItems: 'center',
44+
gap: 4,
45+
},
3246
});
3347

3448
export default styleSheet;

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import {
1111
MUSD_CONVERSION_DEFAULT_CHAIN_ID,
1212
MUSD_TOKEN_ASSET_ID_BY_CHAIN,
1313
} from '../../../Earn/constants/musd';
14+
import { selectPrimaryMoneyAccount } from '../../../../../selectors/moneyAccountController';
15+
import {
16+
selectMoneyShowMoneyAccountAddress,
17+
selectMoneyAccountVaultConfig,
18+
} from '../../../../../selectors/featureFlagController/moneyAccount';
19+
import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
1420

1521
const mockOnCloseBottomSheet = jest.fn((cb?: () => void) => cb?.());
1622
const mockNavigate = jest.fn();
@@ -47,6 +53,28 @@ jest.mock('../../hooks/useMoneyAccount', () => ({
4753
useMoneyAccountDeposit: jest.fn(),
4854
}));
4955

56+
// Selectors added by the "show money account address" feature — mock them so
57+
// the test store doesn't need a fully-hydrated engine.backgroundState.
58+
jest.mock('../../../../../selectors/moneyAccountController', () => ({
59+
selectPrimaryMoneyAccount: jest.fn(),
60+
}));
61+
62+
jest.mock(
63+
'../../../../../selectors/featureFlagController/moneyAccount',
64+
() => ({
65+
selectMoneyShowMoneyAccountAddress: jest.fn(),
66+
selectMoneyAccountVaultConfig: jest.fn(),
67+
}),
68+
);
69+
70+
// Preserve all real networkController exports so downstream selectors that
71+
// run at module init time (e.g. multichainAccounts) still get valid functions;
72+
// only override selectNetworkConfigurations to avoid touching engine state.
73+
jest.mock('../../../../../selectors/networkController', () => ({
74+
...jest.requireActual('../../../../../selectors/networkController'),
75+
selectNetworkConfigurations: jest.fn(),
76+
}));
77+
5078
jest.mock('@metamask/design-system-react-native', () => {
5179
const actual = jest.requireActual('@metamask/design-system-react-native');
5280
const { forwardRef, useImperativeHandle } = jest.requireActual('react');
@@ -93,6 +121,14 @@ describe('MoneyAddMoneySheet', () => {
93121
(useMoneyAccountDeposit as jest.Mock).mockReturnValue({
94122
initiateDeposit: mockInitiateDeposit,
95123
});
124+
125+
// Feature flag off by default → shows "Coming soon" for the receive row.
126+
(selectMoneyShowMoneyAccountAddress as jest.Mock).mockReturnValue(false);
127+
(selectMoneyAccountVaultConfig as jest.Mock).mockReturnValue(undefined);
128+
// No money account address → address UI is hidden.
129+
(selectPrimaryMoneyAccount as jest.Mock).mockReturnValue(undefined);
130+
// Network configurations not needed when vaultConfig is undefined.
131+
(selectNetworkConfigurations as jest.Mock).mockReturnValue({});
96132
});
97133

98134
it('renders all four options', () => {

app/components/UI/Money/components/MoneyAddMoneySheet/MoneyAddMoneySheet.testIds.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ export const MoneyAddMoneySheetTestIds = {
88
MOVE_MUSD_OPTION: 'money-add-money-sheet-move-musd',
99
MOVE_MUSD_DESCRIPTION: 'money-add-money-sheet-move-musd-description',
1010
RECEIVE_EXTERNAL_ROW: 'money-add-money-sheet-receive-external',
11+
RECEIVE_EXTERNAL_ADDRESS: 'money-add-money-sheet-receive-external-address',
12+
COPY_ADDRESS_BUTTON: 'money-add-money-sheet-copy-address',
13+
EXPLORER_BUTTON: 'money-add-money-sheet-explorer',
1114
};

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

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useRef } from 'react';
2-
import { TouchableOpacity, View } from 'react-native';
2+
import { Linking, TouchableOpacity, View } from 'react-native';
33
import { useNavigation } from '@react-navigation/native';
44
import {
55
BottomSheet,
@@ -14,7 +14,15 @@ import {
1414
TextColor,
1515
TextVariant,
1616
} from '@metamask/design-system-react-native';
17+
import Clipboard from '@react-native-clipboard/clipboard';
1718
import Tag from '../../../../../component-library/components/Tags/Tag';
19+
import ButtonIcon, {
20+
ButtonIconSizes,
21+
} from '../../../../../component-library/components/Buttons/ButtonIcon';
22+
import {
23+
IconName as ComponentLibraryIconName,
24+
IconColor as ComponentLibraryIconColor,
25+
} from '../../../../../component-library/components/Icons/Icon';
1826
import { strings } from '../../../../../../locales/i18n';
1927
import { useStyles } from '../../../../../component-library/hooks';
2028
import { useMusdConversionFlowData } from '../../../Earn/hooks/useMusdConversionFlowData';
@@ -25,6 +33,15 @@ import {
2533
} from '../../../Earn/constants/musd';
2634
import { useRampNavigation } from '../../../Ramp/hooks/useRampNavigation';
2735
import { useMoneyAccountDeposit } from '../../hooks/useMoneyAccount';
36+
import { useSelector } from 'react-redux';
37+
import {
38+
selectMoneyShowMoneyAccountAddress,
39+
selectMoneyAccountVaultConfig,
40+
} from '../../../../../selectors/featureFlagController/moneyAccount';
41+
import { selectPrimaryMoneyAccount } from '../../../../../selectors/moneyAccountController';
42+
import { selectNetworkConfigurations } from '../../../../../selectors/networkController';
43+
import { findBlockExplorerUrlForChain } from '../../../../../util/networks';
44+
import { renderShortAddress } from '../../../../../util/address';
2845
import styleSheet from './MoneyAddMoneySheet.styles';
2946
import { MoneyAddMoneySheetTestIds } from './MoneyAddMoneySheet.testIds';
3047

@@ -47,6 +64,19 @@ const MoneyAddMoneySheet: React.FC = () => {
4764
const { goToBuy } = useRampNavigation();
4865
const { initiateDeposit } = useMoneyAccountDeposit();
4966

67+
const showMoneyAccountAddress = useSelector(
68+
selectMoneyShowMoneyAccountAddress,
69+
);
70+
const vaultConfig = useSelector(selectMoneyAccountVaultConfig);
71+
const primaryMoneyAccount = useSelector(selectPrimaryMoneyAccount);
72+
const networkConfigurations = useSelector(selectNetworkConfigurations);
73+
74+
const moneyAccountAddress = primaryMoneyAccount?.address;
75+
76+
const blockExplorerUrl = vaultConfig?.chainId
77+
? findBlockExplorerUrlForChain(vaultConfig.chainId, networkConfigurations)
78+
: undefined;
79+
5080
const closeAndNavigate = useCallback((navigateFn: () => void) => {
5181
sheetRef.current?.onCloseBottomSheet(navigateFn);
5282
}, []);
@@ -88,6 +118,18 @@ const MoneyAddMoneySheet: React.FC = () => {
88118
moveMusdLabel = strings('money.add_money_sheet.move_musd_no_amount');
89119
}
90120

121+
const handleCopyAddress = useCallback(() => {
122+
if (moneyAccountAddress) {
123+
Clipboard.setString(moneyAccountAddress);
124+
}
125+
}, [moneyAccountAddress]);
126+
127+
const handleOpenExplorer = useCallback(() => {
128+
if (blockExplorerUrl && moneyAccountAddress) {
129+
Linking.openURL(`${blockExplorerUrl}/address/${moneyAccountAddress}`);
130+
}
131+
}, [blockExplorerUrl, moneyAccountAddress]);
132+
91133
const options: Option[] = [
92134
{
93135
label: strings('money.add_money_sheet.convert_crypto'),
@@ -163,21 +205,74 @@ const MoneyAddMoneySheet: React.FC = () => {
163205
<Icon
164206
name={IconName.Arrow2Down}
165207
size={IconSize.Lg}
166-
color={IconColor.IconMuted}
208+
color={
209+
showMoneyAccountAddress && moneyAccountAddress
210+
? IconColor.IconDefault
211+
: IconColor.IconMuted
212+
}
167213
/>
168-
<View style={styles.disabledRowContent}>
169-
<Text
170-
variant={TextVariant.BodyMd}
171-
fontWeight={FontWeight.Medium}
172-
color={TextColor.TextAlternative}
173-
>
174-
{strings('money.add_money_sheet.receive_external')}
175-
</Text>
176-
<Tag
177-
label={strings('money.add_money_sheet.coming_soon')}
178-
style={styles.comingSoonTag}
179-
/>
180-
</View>
214+
215+
{/* This is intended to be for development
216+
Until we have implemented the receive by address flow
217+
*/}
218+
219+
{showMoneyAccountAddress && moneyAccountAddress ? (
220+
<>
221+
<View style={styles.addressRowContent}>
222+
<Text
223+
variant={TextVariant.BodyMd}
224+
fontWeight={FontWeight.Medium}
225+
>
226+
{strings('money.add_money_sheet.receive_external')}
227+
</Text>
228+
<Text
229+
variant={TextVariant.BodySm}
230+
color={TextColor.TextAlternative}
231+
testID={MoneyAddMoneySheetTestIds.RECEIVE_EXTERNAL_ADDRESS}
232+
>
233+
{renderShortAddress(moneyAccountAddress)}
234+
</Text>
235+
</View>
236+
<View style={styles.addressActions}>
237+
<ButtonIcon
238+
testID={MoneyAddMoneySheetTestIds.COPY_ADDRESS_BUTTON}
239+
iconName={ComponentLibraryIconName.Copy}
240+
iconColor={ComponentLibraryIconColor.Default}
241+
size={ButtonIconSizes.Lg}
242+
onPress={handleCopyAddress}
243+
accessibilityLabel={strings(
244+
'money.add_money_sheet.copy_address',
245+
)}
246+
/>
247+
{blockExplorerUrl ? (
248+
<ButtonIcon
249+
testID={MoneyAddMoneySheetTestIds.EXPLORER_BUTTON}
250+
iconName={ComponentLibraryIconName.Export}
251+
iconColor={ComponentLibraryIconColor.Default}
252+
size={ButtonIconSizes.Lg}
253+
onPress={handleOpenExplorer}
254+
accessibilityLabel={strings(
255+
'money.add_money_sheet.view_on_explorer',
256+
)}
257+
/>
258+
) : null}
259+
</View>
260+
</>
261+
) : (
262+
<View style={styles.disabledRowContent}>
263+
<Text
264+
variant={TextVariant.BodyMd}
265+
fontWeight={FontWeight.Medium}
266+
color={TextColor.TextAlternative}
267+
>
268+
{strings('money.add_money_sheet.receive_external')}
269+
</Text>
270+
<Tag
271+
label={strings('money.add_money_sheet.coming_soon')}
272+
style={styles.comingSoonTag}
273+
/>
274+
</View>
275+
)}
181276
</View>
182277
</View>
183278
</BottomSheet>

0 commit comments

Comments
 (0)