diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx index b8962e84d7b8..c3ee970f5bc2 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx @@ -104,6 +104,8 @@ jest.mock( '../../../../../selectors/multichainAccounts/accountTreeController', () => ({ selectAccountToGroupMap: () => ({}), + selectAccountToWalletMap: () => ({}), + selectWalletsMap: () => ({}), selectSelectedAccountGroupWithInternalAccountsAddresses: () => [], selectAccountTreeControllerState: () => ({}), selectAccountGroupWithInternalAccounts: () => [], diff --git a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx index 8dee8b27cdfc..919b8d43ab0e 100644 --- a/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx +++ b/app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, Platform, UIManager } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import I18n, { strings } from '../../../../../../locales/i18n'; @@ -28,18 +28,13 @@ import { selectSourceAmount, selectDestToken, selectSourceToken, - selectDestAddress, - selectIsSwap, } from '../../../../../core/redux/slices/bridge'; -import { selectAccountToGroupMap } from '../../../../../selectors/multichainAccounts/accountTreeController'; -import { selectMultichainAccountsState2Enabled } from '../../../../../selectors/featureFlagController/multichainAccounts'; -import { selectInternalAccounts } from '../../../../../selectors/accountsController'; import { getIntlNumberFormatter } from '../../../../../util/intl'; import { useRewards } from '../../hooks/useRewards'; -import { areAddressesEqual } from '../../../../../util/address'; import RewardsAnimations, { RewardAnimationState, } from '../../../Rewards/components/RewardPointsAnimation'; +import QuoteDetailsRecipientKeyValueRow from '../QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow'; if ( Platform.OS === 'android' && @@ -67,13 +62,6 @@ const QuoteDetailsCard: React.FC = () => { const sourceToken = useSelector(selectSourceToken); const destToken = useSelector(selectDestToken); const sourceAmount = useSelector(selectSourceAmount); - const destAddress = useSelector(selectDestAddress); - const isSwap = useSelector(selectIsSwap); - const internalAccounts = useSelector(selectInternalAccounts); - const accountToGroupMap = useSelector(selectAccountToGroupMap); - const isMultichainAccountsState2Enabled = useSelector( - selectMultichainAccountsState2Enabled, - ); const { estimatedPoints, isLoading: isRewardsLoading, @@ -84,42 +72,12 @@ const QuoteDetailsCard: React.FC = () => { isQuoteLoading, }); - // Get the display name for the destination account - const destinationDisplayName = useMemo(() => { - if (!destAddress) return undefined; - - const internalAccount = internalAccounts.find((account) => - areAddressesEqual(account.address, destAddress), - ); - - if (!internalAccount) return undefined; - - // Use account group name if available, otherwise use account name - if (isMultichainAccountsState2Enabled) { - const accountGroup = accountToGroupMap[internalAccount.id]; - return accountGroup?.metadata.name || internalAccount.metadata.name; - } - - return internalAccount.metadata.name; - }, [ - destAddress, - internalAccounts, - accountToGroupMap, - isMultichainAccountsState2Enabled, - ]); - const handleSlippagePress = () => { navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { screen: Routes.BRIDGE.MODALS.SLIPPAGE_MODAL, }); }; - const handleRecipientPress = () => { - navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { - screen: Routes.BRIDGE.MODALS.RECIPIENT_SELECTOR_MODAL, - }); - }; - // Early return for invalid states if ( !sourceToken?.chainId || @@ -271,44 +229,6 @@ const QuoteDetailsCard: React.FC = () => { }} /> - {!isSwap && ( - - - {destAddress - ? destinationDisplayName || - strings('bridge.external_account') - : strings('bridge.select_recipient')} - - - - ), - }} - /> - )} - {activeQuote?.minToTokenAmount && ( { /> )} + + {/* Estimated Points */} {shouldShowRewardsRow && ( - - - - Recipient - - - + Recipient + - - - + - - Select recipient - - - - - + } + width={16} + /> + diff --git a/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.styles.ts b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.styles.ts new file mode 100644 index 000000000000..8f727e465e10 --- /dev/null +++ b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.styles.ts @@ -0,0 +1,28 @@ +import { StyleSheet } from 'react-native'; + +const createStyles = () => + StyleSheet.create({ + recipientFieldSection: { + flex: 1, + minWidth: 'auto', + width: 'auto', + }, + recipientValueSection: { + flex: 1, + }, + recipientButton: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + gap: 4, + }, + accountNameText: { + flexShrink: 0, + minWidth: 0, + }, + recipientText: { + flexShrink: 1, + }, + }); + +export default createStyles; diff --git a/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.test.tsx b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.test.tsx new file mode 100644 index 000000000000..926761b54b2b --- /dev/null +++ b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.test.tsx @@ -0,0 +1,197 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import QuoteDetailsRecipientKeyValueRow from './QuoteDetailsRecipientKeyValueRow'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { Hex } from '@metamask/utils'; +import { AvatarAccountType } from '../../../../../component-library/components/Avatars/Avatar/variants/AvatarAccount/AvatarAccount.types'; +import Routes from '../../../../../constants/navigation/Routes'; + +const mockNavigate = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: () => ({ + navigate: mockNavigate, + }), +})); + +jest.mock('../../hooks/useRecipientDisplayData', () => ({ + useRecipientDisplayData: jest.fn(), +})); + +jest.mock('../../../../../util/notifications', () => ({ + ...jest.requireActual('../../../../../util/notifications'), + shortenString: jest.fn((address, options) => { + if (!address) return ''; + const start = address.slice(0, options.truncatedStartChars); + const end = address.slice(-options.truncatedEndChars); + return `${start}...${end}`; + }), +})); + +import { useRecipientDisplayData } from '../../hooks/useRecipientDisplayData'; + +const mockUseRecipientDisplayData = + useRecipientDisplayData as jest.MockedFunction< + typeof useRecipientDisplayData + >; + +describe('QuoteDetailsRecipientKeyValueRow', () => { + const mockDestAddress = '0x1234567890123456789012345678901234567890' as Hex; + + // Simplify typing for tests + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const createMockState = (overrides: any = {}): any => ({ + engine: { + backgroundState: { + BridgeController: { + destAddress: mockDestAddress, + }, + KeyringController: { + keyrings: [], + }, + }, + }, + bridge: { + isSwap: false, + destAddress: mockDestAddress, + ...overrides.bridge, + }, + settings: { + avatarAccountType: AvatarAccountType.Maskicon, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: undefined, + }); + }); + + it('returns null when isSwap is true', () => { + const state = createMockState({ + bridge: { + sourceToken: { chainId: 1, symbol: 'ETH' }, + destToken: { chainId: 1, symbol: 'USDC' }, + }, + }); + + const { toJSON } = renderWithProvider( + , + { state }, + ); + + expect(toJSON()).toBeNull(); + }); + + it('renders select recipient text when destAddress is undefined', () => { + const state = createMockState({ + bridge: { destAddress: undefined }, + }); + + const { getByText } = renderWithProvider( + , + { state }, + ); + + expect(getByText('Recipient')).toBeTruthy(); + expect(getByText('Select recipient')).toBeTruthy(); + }); + + it('renders recipient row with internal account display name', () => { + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: 'Account 1', + destinationWalletName: undefined, + destinationAccountAddress: mockDestAddress, + }); + + const state = createMockState(); + + const { getByText, getByTestId } = renderWithProvider( + , + { state }, + ); + + expect(getByText('Recipient')).toBeTruthy(); + expect(getByText('Account 1')).toBeTruthy(); + expect(getByTestId('recipient-selector-button')).toBeTruthy(); + }); + + it('renders recipient row with account display name and wallet name', () => { + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: 'Account 1', + destinationWalletName: 'Wallet 1', + destinationAccountAddress: mockDestAddress, + }); + + const state = createMockState(); + + const { getByText } = renderWithProvider( + , + { state }, + ); + + expect(getByText(/Wallet 1.*Account 1/)).toBeTruthy(); + }); + + it('renders recipient row with shortened external address when no display name', () => { + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: mockDestAddress, + }); + + const state = createMockState(); + + const { getByText } = renderWithProvider( + , + { state }, + ); + + expect(getByText('0x12345...67890')).toBeTruthy(); + }); + + it('navigates to recipient selector modal when button is pressed', () => { + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: 'Account 1', + destinationWalletName: undefined, + destinationAccountAddress: mockDestAddress, + }); + + const state = createMockState(); + + const { getByTestId } = renderWithProvider( + , + { state }, + ); + + const button = getByTestId('recipient-selector-button'); + fireEvent.press(button); + + expect(mockNavigate).toHaveBeenCalledWith(Routes.BRIDGE.MODALS.ROOT, { + screen: Routes.BRIDGE.MODALS.RECIPIENT_SELECTOR_MODAL, + }); + }); + + it('renders select recipient text when destinationAccountAddress is undefined', () => { + mockUseRecipientDisplayData.mockReturnValue({ + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: undefined, + }); + + const state = createMockState(); + + const { getByTestId, getByText, queryByText } = renderWithProvider( + , + { state }, + ); + + expect(getByTestId('recipient-selector-button')).toBeTruthy(); + expect(getByText('Select recipient')).toBeTruthy(); + expect(queryByText('0x12345...67890')).toBeNull(); + }); +}); diff --git a/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.tsx b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.tsx new file mode 100644 index 000000000000..7cf441a5028a --- /dev/null +++ b/app/components/UI/Bridge/components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { strings } from '../../../../../../locales/i18n'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { KeyValueRowStubs } from '../../../../../component-library/components-temp/KeyValueRow'; +import { + Box, + BoxAlignItems, + BoxJustifyContent, +} from '@metamask/design-system-react-native'; +import { useSelector } from 'react-redux'; +import { selectIsSwap } from '../../../../../core/redux/slices/bridge'; +import createStyles from './QuoteDetailsRecipientKeyValueRow.styles'; +import Routes from '../../../../../constants/navigation/Routes'; +import { useNavigation } from '@react-navigation/native'; +import { useRecipientDisplayData } from '../../hooks/useRecipientDisplayData'; +import { shortenString } from '../../../../../util/notifications'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; + +const QuoteDetailsRecipientKeyValueRow = () => { + const styles = createStyles(); + const navigation = useNavigation(); + const isSwap = useSelector(selectIsSwap); + + // Get the display name and wallet name for the destination account + const { + destinationDisplayName, + destinationWalletName, + destinationAccountAddress, + } = useRecipientDisplayData(); + + const handleRecipientPress = () => { + navigation.navigate(Routes.BRIDGE.MODALS.ROOT, { + screen: Routes.BRIDGE.MODALS.RECIPIENT_SELECTOR_MODAL, + }); + }; + + if (isSwap) { + return null; + } + + return ( + + + + {strings('bridge.recipient')} + + + + + {destinationDisplayName ? ( + + {destinationWalletName ? `${destinationWalletName} / ` : ''} + {destinationDisplayName} + + ) : destinationAccountAddress ? ( + + {shortenString(destinationAccountAddress, { + truncatedCharLimit: 15, + truncatedStartChars: 7, + truncatedEndChars: 5, + skipCharacterInEnd: false, + })} + + ) : ( + + {strings('bridge.select_recipient')} + + )} + + + + + ); +}; + +export default QuoteDetailsRecipientKeyValueRow; diff --git a/app/components/UI/Bridge/hooks/useRecipientDisplayData/index.ts b/app/components/UI/Bridge/hooks/useRecipientDisplayData/index.ts new file mode 100644 index 000000000000..9203867df98a --- /dev/null +++ b/app/components/UI/Bridge/hooks/useRecipientDisplayData/index.ts @@ -0,0 +1 @@ +export { useRecipientDisplayData } from './useRecipientDisplayData'; diff --git a/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.test.ts b/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.test.ts new file mode 100644 index 000000000000..41ae86bf8056 --- /dev/null +++ b/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.test.ts @@ -0,0 +1,271 @@ +import '../../_mocks_/initialState'; +import { createBridgeTestState } from '../../testUtils'; +import { useRecipientDisplayData } from './useRecipientDisplayData'; +import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider'; +import { Hex } from '@metamask/utils'; +import { EthAccountType, EthScope } from '@metamask/keyring-api'; +import { AccountWalletType, AccountGroupType } from '@metamask/account-api'; + +const ADDR = '0xabcdef1234567890abcdef1234567890abcdef12' as Hex; +const ADDR_UPPER = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12' as Hex; +const EXTERNAL_ADDR = '0x1111111111111111111111111111111111111111' as Hex; + +const createAccount = (id: string, address: Hex, name: string) => ({ + [id]: { + id, + address, + metadata: { name, lastSelected: 0 }, + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + }, +}); + +const createWallet = ( + walletId: string, + walletName: string, + groupId: string, + groupName: string, + accountIds: string[], +) => ({ + [`${AccountWalletType.Entropy}:${walletId}`]: { + id: `${AccountWalletType.Entropy}:${walletId}`, + type: AccountWalletType.Entropy, + metadata: { name: walletName, entropy: { id: walletId } }, + groups: { + [`${AccountWalletType.Entropy}:${walletId}/${groupId}`]: { + id: `${AccountWalletType.Entropy}:${walletId}/${groupId}`, + type: AccountGroupType.MultichainAccount, + metadata: { + name: groupName, + pinned: false, + hidden: false, + entropy: { groupIndex: Number(groupId) }, + }, + accounts: accountIds, + }, + }, + }, +}); + +const setupMultichainState = ( + accountId: string, + address: Hex, + accountName: string, + options: { + multichainEnabled?: boolean; + groupName?: string; + walletName?: string; + walletsMap?: object; + emptyWalletsMap?: boolean; + } = {}, +) => { + const state = createBridgeTestState({ + bridgeReducerOverrides: { destAddress: address }, + }); + const multichainEnabled = options.multichainEnabled ?? true; + + state.engine = { + ...state.engine, + backgroundState: { + ...state.engine?.backgroundState, + RemoteFeatureFlagController: { + ...state.engine?.backgroundState?.RemoteFeatureFlagController, + remoteFeatureFlags: { + ...state.engine?.backgroundState?.RemoteFeatureFlagController + ?.remoteFeatureFlags, + multichainAccountsState2: multichainEnabled, + }, + }, + AccountsController: { + internalAccounts: { + selectedAccount: accountId, + accounts: createAccount(accountId, address, accountName), + }, + }, + ...(multichainEnabled && { + AccountTreeController: { + accountTree: { + selectedAccountGroup: `${AccountWalletType.Entropy}:wallet1/0`, + wallets: options.emptyWalletsMap + ? {} + : options.walletsMap ?? + createWallet( + 'wallet1', + options.walletName ?? 'Wallet 1', + '0', + options.groupName ?? accountName, + [accountId], + ), + }, + }, + }), + }, + }; + + return state; +}; + +describe('useRecipientDisplayData', () => { + const accountId = 'testAccountId'; + + beforeEach(() => jest.clearAllMocks()); + + // Keep any typing for tests + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderHook = (state: any) => + renderHookWithProvider(() => useRecipientDisplayData(), { state }).result + .current; + + it('returns undefined for all fields when destAddress is not set', () => { + const state = createBridgeTestState({ + bridgeReducerOverrides: { destAddress: undefined }, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: undefined, + }); + }); + + it('returns external address without display name or wallet name', () => { + const state = createBridgeTestState({ + bridgeReducerOverrides: { destAddress: EXTERNAL_ADDR }, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: EXTERNAL_ADDR, + }); + }); + + describe('with multichain disabled', () => { + it('returns account metadata name without wallet name', () => { + const state = setupMultichainState(accountId, ADDR, 'My Account', { + multichainEnabled: false, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'My Account', + destinationWalletName: undefined, + destinationAccountAddress: ADDR, + }); + }); + + it('matches addresses case-insensitively', () => { + const state = setupMultichainState(accountId, ADDR, 'Case Test', { + multichainEnabled: false, + }); + state.bridge.destAddress = ADDR_UPPER; + + const result = renderHook(state); + expect(result.destinationDisplayName).toBe('Case Test'); + expect(result.destinationAccountAddress).toBe(ADDR); + }); + }); + + describe('with multichain enabled', () => { + it('returns account group name and wallet name when available', () => { + const state = setupMultichainState(accountId, ADDR, 'Account', { + groupName: 'Group Name', + walletName: 'My Wallet', + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Group Name', + destinationWalletName: 'My Wallet', + destinationAccountAddress: ADDR, + }); + }); + + it('falls back to account name when account group not found', () => { + const state = setupMultichainState(accountId, ADDR, 'Fallback Account', { + emptyWalletsMap: true, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Fallback Account', + destinationWalletName: undefined, + destinationAccountAddress: ADDR, + }); + }); + + it('includes wallet name with multiple wallets', () => { + const wallets = { + ...createWallet('wallet1', 'First Wallet', '0', 'Group 1', [accountId]), + ...createWallet('wallet2', 'Second Wallet', '0', 'Group 2', [ + 'otherAccount', + ]), + }; + const state = setupMultichainState(accountId, ADDR, 'Account', { + walletsMap: wallets, + groupName: 'Group 1', + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Group 1', + destinationWalletName: 'First Wallet', + destinationAccountAddress: ADDR, + }); + }); + + it('includes wallet name with single wallet', () => { + const state = setupMultichainState(accountId, ADDR, 'Account', { + groupName: 'Group 1', + walletName: 'Only Wallet', + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Group 1', + destinationWalletName: 'Only Wallet', + destinationAccountAddress: ADDR, + }); + }); + + it('returns undefined wallet name when walletId not in accountToWalletMap', () => { + const wallets = { + ...createWallet('wallet1', 'Wallet 1', '0', 'Group 1', []), + ...createWallet('wallet2', 'Wallet 2', '0', 'Group 2', []), + }; + const state = setupMultichainState(accountId, ADDR, 'Account 1', { + walletsMap: wallets, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Account 1', + destinationWalletName: undefined, + destinationAccountAddress: ADDR, + }); + }); + + it('handles empty walletsMap', () => { + const state = setupMultichainState(accountId, ADDR, 'Account', { + emptyWalletsMap: true, + }); + + expect(renderHook(state)).toEqual({ + destinationDisplayName: 'Account', + destinationWalletName: undefined, + destinationAccountAddress: ADDR, + }); + }); + }); + + describe('memoization', () => { + it('returns same object reference when dependencies unchanged', () => { + const state = setupMultichainState(accountId, ADDR, 'Memo Test', { + multichainEnabled: false, + }); + + const { result, rerender } = renderHookWithProvider( + () => useRecipientDisplayData(), + { state }, + ); + const firstResult = result.current; + rerender({ state }); + + expect(result.current).toBe(firstResult); + }); + }); +}); diff --git a/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.ts b/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.ts new file mode 100644 index 000000000000..b81adeb52799 --- /dev/null +++ b/app/components/UI/Bridge/hooks/useRecipientDisplayData/useRecipientDisplayData.ts @@ -0,0 +1,84 @@ +import { useSelector } from 'react-redux'; +import { selectDestAddress } from '../../../../../core/redux/slices/bridge'; +import { selectInternalAccounts } from '../../../../../selectors/accountsController'; +import { + selectAccountToGroupMap, + selectAccountToWalletMap, + selectWalletsMap, +} from '../../../../../selectors/multichainAccounts/accountTreeController'; +import { selectMultichainAccountsState2Enabled } from '../../../../../selectors/featureFlagController/multichainAccounts'; +import { useMemo } from 'react'; +import { areAddressesEqual } from '../../../../../util/address'; + +/** + * Custom hook to retrieve display information for the bridge recipient account. + * + * This hook determines the appropriate display name, wallet name, and address for the + * destination/recipient account in a bridge transaction. It handles both internal accounts + * (accounts within the wallet) and external accounts (addresses not in the wallet). + * + * When multichain accounts state 2 is enabled, it uses account group names and wallet names + * for better organization. + */ +export const useRecipientDisplayData = () => { + const destAddress = useSelector(selectDestAddress); + const internalAccounts = useSelector(selectInternalAccounts); + const accountToGroupMap = useSelector(selectAccountToGroupMap); + const accountToWalletMap = useSelector(selectAccountToWalletMap); + const isMultichainAccountsState2Enabled = useSelector( + selectMultichainAccountsState2Enabled, + ); + const walletsMap = useSelector(selectWalletsMap); + + return useMemo(() => { + if (!destAddress) { + return { + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: undefined, + }; + } + + const internalAccount = internalAccounts.find((account) => + areAddressesEqual(account.address, destAddress), + ); + + if (!internalAccount) { + return { + destinationDisplayName: undefined, + destinationWalletName: undefined, + destinationAccountAddress: destAddress, + }; + } + + let displayName = internalAccount.metadata.name; + let walletName: string | undefined; + + // Use account group name if available, otherwise use account name + if (isMultichainAccountsState2Enabled) { + const accountGroup = accountToGroupMap[internalAccount.id]; + displayName = + accountGroup?.metadata.name || internalAccount.metadata.name; + + if (walletsMap) { + const walletId = accountToWalletMap[internalAccount.id]; + if (walletId && walletsMap[walletId]) { + walletName = walletsMap[walletId].metadata.name; + } + } + } + + return { + destinationDisplayName: displayName, + destinationWalletName: walletName, + destinationAccountAddress: internalAccount.address, + }; + }, [ + destAddress, + internalAccounts, + accountToGroupMap, + isMultichainAccountsState2Enabled, + accountToWalletMap, + walletsMap, + ]); +};