Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ export const createStyles = (
alignItems: 'center',
marginTop: 0,
},
subtitleRowSpaced: {
marginTop: 4,
},
subtitleAccountAvatar: {
marginRight: 4,
},
subtitleAccountName: {
flexShrink: 1,
} as TextStyle,
subtitleLeadingIcon: {
height: 16,
justifyContent: 'center',
Expand Down
93 changes: 93 additions & 0 deletions app/components/UI/ActivityListItemRow/ActivityListItemRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useTokensData } from '../../hooks/useTokensData/useTokensData';
const LINEA_MUSD_ADDRESS = '0xaca92e438df0b2401ff60da7e4337b687a2435da';
const LINEA_MUSD_CHECKSUM_ADDRESS =
'0xacA92E438df0B2401fF60dA7E4337B687a2435DA';
const OWNED_ACCOUNT_ADDRESS = '0xAa60919dd0d0964B76620dAaF08bF357e1c9DD73';

const mockState = {
user: {
Expand Down Expand Up @@ -61,6 +62,35 @@ const mockState = {
},
},
},
AccountsController: {
internalAccounts: {
selectedAccount: 'owned-account-1',
accounts: {
'owned-account-1': {
id: 'owned-account-1',
address: OWNED_ACCOUNT_ADDRESS,
metadata: { name: 'Account 2' },
},
},
},
},
AccountTreeController: {
accountTree: {
wallets: {
'entropy:wallet-1': {
id: 'entropy:wallet-1',
metadata: { name: 'Wallet 1' },
groups: {
'entropy:wallet-1/1': {
id: 'entropy:wallet-1/1',
metadata: { name: 'ETH DeFi' },
accounts: ['owned-account-1'],
},
},
},
},
},
},
},
},
};
Expand Down Expand Up @@ -222,6 +252,11 @@ jest.mock('@metamask/design-system-react-native', () => {
severity,
});

const AvatarAccount = ({ address }: { address?: string }) =>
ReactActual.createElement(View, {
testID: `avatar-account-${address ?? 'unknown'}`,
});

const BadgeNetwork = ({ src }: { src?: unknown }) =>
ReactActual.createElement(View, { src });

Expand Down Expand Up @@ -249,6 +284,13 @@ jest.mock('@metamask/design-system-react-native', () => {
AvatarIcon,
AvatarIconSeverity: { Neutral: 'neutral', Danger: 'danger' },
AvatarIconSize: { Xs: 'xs', Sm: 'sm', Md: 'md', Lg: 'lg', Xl: 'xl' },
AvatarAccount,
AvatarAccountVariant: {
Jazzicon: 'Jazzicon',
Blockies: 'Blockies',
Maskicon: 'Maskicon',
},
AvatarBaseSize: { Xs: 'xs', Sm: 'sm', Md: 'md', Lg: 'lg', Xl: 'xl' },
BadgeNetwork,
BadgeWrapper,
BadgeWrapperPosition: { BottomRight: 'BottomRight' },
Expand Down Expand Up @@ -455,6 +497,57 @@ describe('ActivityListItemRow — row content', () => {
expect(getByTestId('avatar-token-USDC')).toBeOnTheScreen();
});

it('renders the account name in the subtitle when sending to an owned account', () => {
const item = makeItem({
type: 'send',
status: 'success',
// Lowercased on purpose: the owned-account match is case-insensitive.
to: OWNED_ACCOUNT_ADDRESS.toLowerCase(),
token: {
amount: '10',
symbol: 'USDC',
direction: 'out',
},
});
const { getByTestId } = render(
<ActivityListItemRow item={item} index={0} />,
);

expect(getByTestId('activity-subtitle-0xabc').props.children).toBe('To: ');
expect(
getByTestId('activity-subtitle-account-name-0xabc').props.children,
).toBe('ETH DeFi');
expect(
getByTestId('activity-subtitle-account-avatar-0xabc'),
).toBeOnTheScreen();
});

it('renders the account name in the subtitle when receiving from an owned account', () => {
const item = makeItem({
type: 'receive',
status: 'success',
from: OWNED_ACCOUNT_ADDRESS,
token: {
amount: '10',
symbol: 'USDC',
direction: 'in',
},
});
const { getByTestId } = render(
<ActivityListItemRow item={item} index={0} />,
);

expect(getByTestId('activity-subtitle-0xabc').props.children).toBe(
'From: ',
);
expect(
getByTestId('activity-subtitle-account-name-0xabc').props.children,
).toBe('ETH DeFi');
expect(
getByTestId('activity-subtitle-account-avatar-0xabc'),
).toBeOnTheScreen();
});

it('shows "Send cancelled" and hides the amount for a cancelled send', () => {
const item = makeItem({
type: 'send',
Expand Down
7 changes: 7 additions & 0 deletions app/components/UI/ActivityListItemRow/ActivityListItemRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PendingActivityListItemRow } from './PendingActivityListItemRow';
import { resolveTransactionIconName } from './resolveIconType';
import { useActivityListItemRowContent } from './useActivityListItemRowContent';
import { useNftActivityImage } from './useNftActivityImage';
import { useSubtitleAccountParts } from './useSubtitleAccountParts';
import type { ActivityListItemRowProps } from './ActivityListItemRow.types';
import {
isPerpsOrderKind,
Expand Down Expand Up @@ -68,6 +69,11 @@ function ResolvedActivityListItemRow({

const nftImageUrl = useNftActivityImage(item);
const styles = createStyles(colors, typography);
const subtitleParts = useSubtitleAccountParts(
content,
styles,
item.hash ?? index,
);
const isFailed = item.status === 'failed';
const fallbackIconName = resolveTransactionIconName(item.type);
const networkImageSource = isSingleNetworkDomainKind(item.type)
Expand Down Expand Up @@ -105,6 +111,7 @@ function ResolvedActivityListItemRow({
secondaryAmount={content.secondaryAmount}
styles={styles}
subtitle={content.subtitle}
subtitleParts={subtitleParts}
title={titleOverride ?? content.title}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface ActivityListItemRowProps
export interface ActivityListItemRowContent {
title: string;
subtitle?: string;
subtitleAccount?: { prefix: string; address: string; name: string };
primaryToken?: TokenAmount;
secondaryToken?: TokenAmount;
primaryAmount?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
} from '../../../util/activity-adapters';
import type { ActivityListItemRowStyles } from './ActivityListItemRow.styles';

export function ActivityListItemRowLayout({

Check failure on line 10 in app/components/UI/ActivityListItemRow/ActivityListItemRowLayout.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9n7Hk1h2ZDL1BPFW_C&open=AZ9n7Hk1h2ZDL1BPFW_C&pullRequest=33390
avatar,
footer,
index,
Expand All @@ -23,6 +23,7 @@
styles,
subtitle,
subtitleLeadingAccessory,
subtitleParts,
title,
titleAccessory,
}: {
Expand All @@ -41,6 +42,7 @@
styles: ActivityListItemRowStyles;
subtitle?: string;
subtitleLeadingAccessory?: React.ReactNode;
subtitleParts?: { pre: string; avatar: React.ReactNode; name: string };
title: string;
titleAccessory?: React.ReactNode;
}) {
Expand Down Expand Up @@ -68,28 +70,51 @@
) : (
titleText
);
const subtitleNode = subtitle ? (
const subtitleNode = subtitleParts ? (
<View style={[styles.subtitleRow, styles.subtitleRowSpaced]}>
{subtitleLeadingAccessory}
<Text
numberOfLines={1}
style={[styles.subtitleText, styles.statusText]}
testID={`activity-subtitle-${testIdSuffix}`}
>
{subtitleParts.pre}
</Text>
{subtitleParts.avatar}
<Text
numberOfLines={1}
style={[
styles.subtitleText,
styles.statusText,
styles.subtitleAccountName,
]}
testID={`activity-subtitle-account-name-${testIdSuffix}`}
>
{subtitleParts.name}
</Text>
</View>
) : subtitle ? (
subtitleLeadingAccessory ? (
<View style={styles.subtitleRow}>
{subtitleLeadingAccessory}
<Text
numberOfLines={1}
style={styles.subtitleText}
testID={`activity-subtitle-${testIdSuffix}`}
>
{subtitle}
</Text>
</View>
) : (
<Text
numberOfLines={1}
style={styles.subtitleText}
testID={`activity-subtitle-${testIdSuffix}`}
>
{subtitle}
</Text>
)
) : undefined;

Check warning on line 117 in app/components/UI/ActivityListItemRow/ActivityListItemRowLayout.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9n7Hk1h2ZDL1BPFW_D&open=AZ9n7Hk1h2ZDL1BPFW_D&pullRequest=33390
const primaryAmountNode = primaryAmount ? (
<Text
numberOfLines={1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ActivityListItemRowIcon } from './ActivityListItemRowIcon';
import { ActivityListItemRowLayout } from './ActivityListItemRowLayout';
import { resolveTransactionIconName } from './resolveIconType';
import { useActivityListItemRowContent } from './useActivityListItemRowContent';
import { useSubtitleAccountParts } from './useSubtitleAccountParts';
import type { ActivityListItemRowProps } from './ActivityListItemRow.types';

export function PendingActivityListItemRow({
Expand Down Expand Up @@ -53,6 +54,19 @@ export function PendingActivityListItemRow({
: content.subtitle
: undefined;

const subtitleAccountParts = useSubtitleAccountParts(
content,
styles,
testIdSuffix,
);
const subtitleParts =
subtitleAccountParts && isQueued
? {
...subtitleAccountParts,
pre: `${strings('transaction.queued')} • ${subtitleAccountParts.pre}`,
}
: subtitleAccountParts;

const titleAccessory = isQueued ? undefined : (
<View style={styles.titleSpinner}>
<PendingSpinner testID={`activity-pending-spinner-${testIdSuffix}`} />
Expand Down Expand Up @@ -89,6 +103,7 @@ export function PendingActivityListItemRow({
styles={styles}
subtitle={subtitle}
subtitleLeadingAccessory={subtitleLeadingAccessory}
subtitleParts={subtitleParts}
title={titleOverride ?? content.title}
titleAccessory={titleAccessory}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
} from '../../../util/number/bigint';
// eslint-disable-next-line import-x/no-restricted-paths -- TODO(ADR-0020): route-isolation backlog
import { getPerpsDisplaySymbol } from '@metamask/perps-controller';
import { useAccountNames } from '../../hooks/DisplayName/useAccountNames';
import { NameType } from '../Name/Name.types';
import type { ActivityListItemRowContent } from './ActivityListItemRow.types';
import {
ACTIVITY_FALLBACK_TITLE_RESOLVERS,
Expand Down Expand Up @@ -466,6 +468,7 @@
function resolveCoreContent(
item: ActivityListItem,
bridgeHistoryItem?: BridgeHistoryItem,
counterpartyName?: string,
): Omit<
ActivityListItemRowContent,
'avatarTokens' | 'primaryAmount' | 'secondaryAmount'
Expand All @@ -483,6 +486,10 @@
const cancelledLabel =
item.type === 'receive' ? 'Receive cancelled' : 'Send cancelled';
const subtitlePrefix = item.type === 'receive' ? 'From' : 'To';
const counterpartyLabel =
counterpartyName ||
shortAddress(address) ||
strings('transactions.unavailable');

return {
title: statusTitle(item, {
Expand All @@ -491,7 +498,16 @@
failed: failedLabel,
cancelled: cancelledLabel,
}),
subtitle: `${subtitlePrefix}: ${shortAddress(address) ?? strings('transactions.unavailable')}`,
subtitle: `${subtitlePrefix}: ${counterpartyLabel}`,
...(counterpartyName && address
? {
subtitleAccount: {
prefix: subtitlePrefix,
address,
name: counterpartyName,
},
}
: {}),
primaryToken: token,
};
}
Expand Down Expand Up @@ -1154,7 +1170,28 @@
}
const lendingTokenData = useTokensData(lendingAssetIds);

const content = resolveCoreContent(item, bridgeHistoryItem);
// The send/receive subtitle names the counterparty. Resolve it against the
// user's own accounts so transfers between their accounts read as
// "To: <account name>" instead of a hex address.
const counterpartyAddress =
item.type === 'send' || item.type === 'receive'
? item.type === 'receive'
? item.data.from
: item.data.to

Check warning on line 1180 in app/components/UI/ActivityListItemRow/useActivityListItemRowContent.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=MetaMask_metamask-mobile&issues=AZ9n7HJph2ZDL1BPFW_B&open=AZ9n7HJph2ZDL1BPFW_B&pullRequest=33390
: undefined;
const counterpartyName = useAccountNames(
counterpartyAddress
? [
{
value: counterpartyAddress,
variation: networkChainId ?? '',
type: NameType.EthereumAddress,
},
]
: [],
)[0];

const content = resolveCoreContent(item, bridgeHistoryItem, counterpartyName);

let basePrimaryToken: TokenAmount | undefined;
if (isSpendingCap) {
Expand Down
47 changes: 47 additions & 0 deletions app/components/UI/ActivityListItemRow/useSubtitleAccountParts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { View } from 'react-native';
import { useSelector } from 'react-redux';
import {
AvatarAccount,
AvatarBaseSize,
} from '@metamask/design-system-react-native';
import { getAvatarAccountVariant } from '../../../component-library/components-temp/MultichainAccounts/avatarAccountVariant';
import { selectAvatarAccountType } from '../../../selectors/settings';
import type { ActivityListItemRowContent } from './ActivityListItemRow.types';
import type { ActivityListItemRowStyles } from './ActivityListItemRow.styles';

/**
* Builds the structured "To: <avatar> Name" subtitle for rows whose
* counterparty resolved to one of the user's own accounts, using the same
* account avatar the Accounts list shows.
*/
export function useSubtitleAccountParts(
content: ActivityListItemRowContent,
styles: ActivityListItemRowStyles,
testIdSuffix: string | number | undefined,
): { pre: string; avatar: React.ReactNode; name: string } | undefined {
const avatarVariant = getAvatarAccountVariant(
useSelector(selectAvatarAccountType),
);
const account = content.subtitleAccount;
if (!account) {
return undefined;
}

return {
pre: `${account.prefix}: `,
avatar: (
<View
style={styles.subtitleAccountAvatar}
testID={`activity-subtitle-account-avatar-${testIdSuffix}`}
>
<AvatarAccount
address={account.address}
variant={avatarVariant}
size={AvatarBaseSize.Xs}
/>
</View>
),
name: account.name,
};
}
Loading