Skip to content

Commit 96b2e49

Browse files
chore(runway): cherry-pick feat: update recipients UI on swaps (#21634)
- feat: update recipients UI on swaps cp-7.58.0 (#21565) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Render wallet and account name if the recipient is an imported account, else render the truncated wallet address if the recipient is an external account. We also introduced some refactoring to make us scale the `QuestDetailsCard` by extracting UI and business logic to separate components. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Render wallet and account name if the recipient is an imported account, else render the truncated wallet address if the recipient is an external account. ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/SWAPS-3311 ## **Manual testing steps** ```gherkin Scenario: user views bridge quote with wallet and account name Given user is on bridge quote details screen And recipient is set to "Account 1" in "Wallet 1" When bridge quote details are displayed Then recipient row shows "Wallet 1 / Account 1" Scenario: user views bridge quote with external address as recipient Given user is on bridge quote details screen And recipient is set to external address "0x1234567890123456789012345678901234567890" When bridge quote details are displayed Then recipient row shows shortened address "0x12345...67890" Scenario: user opens recipient selector Given user is on bridge quote details screen And recipient is set to "Account 1" When user taps recipient selector button Then recipient selector modal opens Scenario: user views swap quote details Given user is performing a swap (not a bridge) And swap quote details are displayed When bridge recipient row renders Then recipient row is not displayed Scenario: user views bridge quote with no recipient address Given user is on bridge quote details screen And no recipient address is available When bridge quote details are displayed Then recipient row shows "Select recipient" placeholder And no shortened address is displayed ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> <img width="786" height="293" alt="image" src="https://github.com/user-attachments/assets/179c99cb-1f5a-4451-9929-6519e9a60d31" /> <img width="786" height="293" alt="image" src="https://github.com/user-attachments/assets/20409fb5-ca18-40e6-ac0b-92bf9ac02750" /> <img width="786" height="293" alt="image" src="https://github.com/user-attachments/assets/3ac89b42-a9c9-454e-9a60-e89018319fb2" /> ## **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 - [x] 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. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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] > Introduces a dedicated recipient row component powered by a new hook to display wallet/account names or a shortened external address, and wires it into QuoteDetailsCard with updated tests and snapshots. > > - **Bridge UI**: > - **Recipient row extraction**: Move recipient display from `QuoteDetailsCard` to `components/QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow` with dedicated styles. > - **New hook**: Add `hooks/useRecipientDisplayData` to resolve recipient display name, wallet name, and address (supports multichain and external addresses). > - **QuoteDetailsCard**: > - Remove inline recipient logic/selectors and insert `QuoteDetailsRecipientKeyValueRow`. > - Minor import cleanup; no behavioral changes to other rows. > - **Tests**: > - Add tests for `QuoteDetailsRecipientKeyValueRow` and `useRecipientDisplayData`. > - Update `QuoteDetailsCard` test mocks and snapshots to reflect new recipient row structure. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f3be9c4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [2fcd667](2fcd667) Co-authored-by: George Gkasdrogkas <[email protected]>
1 parent 96c3303 commit 96b2e49

File tree

9 files changed

+753
-171
lines changed

9 files changed

+753
-171
lines changed

app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ jest.mock(
104104
'../../../../../selectors/multichainAccounts/accountTreeController',
105105
() => ({
106106
selectAccountToGroupMap: () => ({}),
107+
selectAccountToWalletMap: () => ({}),
108+
selectWalletsMap: () => ({}),
107109
selectSelectedAccountGroupWithInternalAccountsAddresses: () => [],
108110
selectAccountTreeControllerState: () => ({}),
109111
selectAccountGroupWithInternalAccounts: () => [],

app/components/UI/Bridge/components/QuoteDetailsCard/QuoteDetailsCard.tsx

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React from 'react';
22
import { TouchableOpacity, Platform, UIManager } from 'react-native';
33
import { useNavigation } from '@react-navigation/native';
44
import I18n, { strings } from '../../../../../../locales/i18n';
@@ -28,18 +28,13 @@ import {
2828
selectSourceAmount,
2929
selectDestToken,
3030
selectSourceToken,
31-
selectDestAddress,
32-
selectIsSwap,
3331
} from '../../../../../core/redux/slices/bridge';
34-
import { selectAccountToGroupMap } from '../../../../../selectors/multichainAccounts/accountTreeController';
35-
import { selectMultichainAccountsState2Enabled } from '../../../../../selectors/featureFlagController/multichainAccounts';
36-
import { selectInternalAccounts } from '../../../../../selectors/accountsController';
3732
import { getIntlNumberFormatter } from '../../../../../util/intl';
3833
import { useRewards } from '../../hooks/useRewards';
39-
import { areAddressesEqual } from '../../../../../util/address';
4034
import RewardsAnimations, {
4135
RewardAnimationState,
4236
} from '../../../Rewards/components/RewardPointsAnimation';
37+
import QuoteDetailsRecipientKeyValueRow from '../QuoteDetailsRecipientKeyValueRow/QuoteDetailsRecipientKeyValueRow';
4338

4439
if (
4540
Platform.OS === 'android' &&
@@ -67,13 +62,6 @@ const QuoteDetailsCard: React.FC = () => {
6762
const sourceToken = useSelector(selectSourceToken);
6863
const destToken = useSelector(selectDestToken);
6964
const sourceAmount = useSelector(selectSourceAmount);
70-
const destAddress = useSelector(selectDestAddress);
71-
const isSwap = useSelector(selectIsSwap);
72-
const internalAccounts = useSelector(selectInternalAccounts);
73-
const accountToGroupMap = useSelector(selectAccountToGroupMap);
74-
const isMultichainAccountsState2Enabled = useSelector(
75-
selectMultichainAccountsState2Enabled,
76-
);
7765
const {
7866
estimatedPoints,
7967
isLoading: isRewardsLoading,
@@ -84,42 +72,12 @@ const QuoteDetailsCard: React.FC = () => {
8472
isQuoteLoading,
8573
});
8674

87-
// Get the display name for the destination account
88-
const destinationDisplayName = useMemo(() => {
89-
if (!destAddress) return undefined;
90-
91-
const internalAccount = internalAccounts.find((account) =>
92-
areAddressesEqual(account.address, destAddress),
93-
);
94-
95-
if (!internalAccount) return undefined;
96-
97-
// Use account group name if available, otherwise use account name
98-
if (isMultichainAccountsState2Enabled) {
99-
const accountGroup = accountToGroupMap[internalAccount.id];
100-
return accountGroup?.metadata.name || internalAccount.metadata.name;
101-
}
102-
103-
return internalAccount.metadata.name;
104-
}, [
105-
destAddress,
106-
internalAccounts,
107-
accountToGroupMap,
108-
isMultichainAccountsState2Enabled,
109-
]);
110-
11175
const handleSlippagePress = () => {
11276
navigation.navigate(Routes.BRIDGE.MODALS.ROOT, {
11377
screen: Routes.BRIDGE.MODALS.SLIPPAGE_MODAL,
11478
});
11579
};
11680

117-
const handleRecipientPress = () => {
118-
navigation.navigate(Routes.BRIDGE.MODALS.ROOT, {
119-
screen: Routes.BRIDGE.MODALS.RECIPIENT_SELECTOR_MODAL,
120-
});
121-
};
122-
12381
// Early return for invalid states
12482
if (
12583
!sourceToken?.chainId ||
@@ -271,44 +229,6 @@ const QuoteDetailsCard: React.FC = () => {
271229
}}
272230
/>
273231

274-
{!isSwap && (
275-
<KeyValueRow
276-
field={{
277-
label: {
278-
text: strings('bridge.recipient'),
279-
variant: TextVariant.BodyMDMedium,
280-
},
281-
}}
282-
value={{
283-
label: (
284-
<TouchableOpacity
285-
onPress={handleRecipientPress}
286-
activeOpacity={0.6}
287-
testID="recipient-selector-button"
288-
style={styles.slippageButton}
289-
>
290-
<Text
291-
variant={TextVariant.BodyMD}
292-
numberOfLines={1}
293-
ellipsizeMode="tail"
294-
style={styles.recipientText}
295-
>
296-
{destAddress
297-
? destinationDisplayName ||
298-
strings('bridge.external_account')
299-
: strings('bridge.select_recipient')}
300-
</Text>
301-
<Icon
302-
name={IconName.Edit}
303-
size={IconSize.Sm}
304-
color={IconColor.Muted}
305-
/>
306-
</TouchableOpacity>
307-
),
308-
}}
309-
/>
310-
)}
311-
312232
{activeQuote?.minToTokenAmount && (
313233
<KeyValueRow
314234
field={{
@@ -331,6 +251,8 @@ const QuoteDetailsCard: React.FC = () => {
331251
/>
332252
)}
333253

254+
<QuoteDetailsRecipientKeyValueRow />
255+
334256
{/* Estimated Points */}
335257
{shouldShowRewardsRow && (
336258
<KeyValueRow

app/components/UI/Bridge/components/QuoteDetailsCard/__snapshots__/QuoteDetailsCard.test.tsx.snap

Lines changed: 61 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -922,125 +922,97 @@ exports[`QuoteDetailsCard renders initial state 1`] = `
922922
"justifyContent": "space-between",
923923
"overflow": "hidden",
924924
},
925-
[
926-
undefined,
927-
],
925+
undefined,
928926
]
929927
}
930928
>
931929
<View
932930
style={
933-
{
934-
"alignItems": "flex-start",
935-
"flex": 1,
936-
}
931+
[
932+
{
933+
"display": "flex",
934+
},
935+
{
936+
"flex": 1,
937+
"minWidth": "auto",
938+
"width": "auto",
939+
},
940+
]
937941
}
938942
>
939-
<View
943+
<Text
944+
accessibilityRole="text"
940945
style={
941946
{
942-
"alignItems": "center",
943-
"flexDirection": "row",
944-
"gap": 8,
947+
"color": "#121314",
948+
"fontFamily": "Geist Medium",
949+
"fontSize": 16,
950+
"letterSpacing": 0,
951+
"lineHeight": 24,
945952
}
946953
}
947954
>
948-
<View
949-
style={
950-
{
951-
"alignItems": "center",
952-
"flexDirection": "row",
953-
}
954-
}
955-
>
956-
<Text
957-
accessibilityRole="text"
958-
style={
959-
{
960-
"color": "#121314",
961-
"fontFamily": "Geist Medium",
962-
"fontSize": 16,
963-
"letterSpacing": 0,
964-
"lineHeight": 24,
965-
}
966-
}
967-
testID="label"
968-
>
969-
Recipient
970-
</Text>
971-
</View>
972-
</View>
955+
Recipient
956+
</Text>
973957
</View>
974958
<View
975959
style={
976-
{
977-
"alignItems": "flex-end",
978-
"flex": 1,
979-
}
960+
[
961+
{
962+
"alignItems": "flex-end",
963+
"display": "flex",
964+
"justifyContent": "flex-end",
965+
},
966+
{
967+
"flex": 1,
968+
},
969+
]
980970
}
981971
>
982-
<View
972+
<TouchableOpacity
973+
activeOpacity={0.6}
974+
onPress={[Function]}
983975
style={
984976
{
985977
"alignItems": "center",
978+
"flex": 1,
986979
"flexDirection": "row",
987-
"gap": 8,
980+
"gap": 4,
988981
}
989982
}
983+
testID="recipient-selector-button"
990984
>
991-
<View
985+
<Text
986+
accessibilityRole="text"
987+
ellipsizeMode="tail"
988+
numberOfLines={1}
992989
style={
993990
{
994-
"alignItems": "center",
995-
"flexDirection": "row",
991+
"color": "#121314",
992+
"flexShrink": 1,
993+
"fontFamily": "Geist Regular",
994+
"fontSize": 16,
995+
"letterSpacing": 0,
996+
"lineHeight": 24,
996997
}
997998
}
998999
>
999-
<TouchableOpacity
1000-
activeOpacity={0.6}
1001-
onPress={[Function]}
1002-
style={
1003-
{
1004-
"alignItems": "center",
1005-
"flexDirection": "row",
1006-
"gap": 4,
1007-
}
1000+
Select recipient
1001+
</Text>
1002+
<SvgMock
1003+
color="#b7bbc8"
1004+
fill="currentColor"
1005+
height={16}
1006+
name="Edit"
1007+
style={
1008+
{
1009+
"height": 16,
1010+
"width": 16,
10081011
}
1009-
testID="recipient-selector-button"
1010-
>
1011-
<Text
1012-
accessibilityRole="text"
1013-
ellipsizeMode="tail"
1014-
numberOfLines={1}
1015-
style={
1016-
{
1017-
"color": "#121314",
1018-
"flexShrink": 1,
1019-
"fontFamily": "Geist Regular",
1020-
"fontSize": 16,
1021-
"letterSpacing": 0,
1022-
"lineHeight": 24,
1023-
}
1024-
}
1025-
>
1026-
Select recipient
1027-
</Text>
1028-
<SvgMock
1029-
color="#b7bbc8"
1030-
fill="currentColor"
1031-
height={16}
1032-
name="Edit"
1033-
style={
1034-
{
1035-
"height": 16,
1036-
"width": 16,
1037-
}
1038-
}
1039-
width={16}
1040-
/>
1041-
</TouchableOpacity>
1042-
</View>
1043-
</View>
1012+
}
1013+
width={16}
1014+
/>
1015+
</TouchableOpacity>
10441016
</View>
10451017
</View>
10461018
</View>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
const createStyles = () =>
4+
StyleSheet.create({
5+
recipientFieldSection: {
6+
flex: 1,
7+
minWidth: 'auto',
8+
width: 'auto',
9+
},
10+
recipientValueSection: {
11+
flex: 1,
12+
},
13+
recipientButton: {
14+
flexDirection: 'row',
15+
alignItems: 'center',
16+
flex: 1,
17+
gap: 4,
18+
},
19+
accountNameText: {
20+
flexShrink: 0,
21+
minWidth: 0,
22+
},
23+
recipientText: {
24+
flexShrink: 1,
25+
},
26+
});
27+
28+
export default createStyles;

0 commit comments

Comments
 (0)