Skip to content

Commit e97f7d8

Browse files
feat: allow removal of down to 2 tokens
1 parent 61e5d8a commit e97f7d8

4 files changed

Lines changed: 117 additions & 3 deletions

File tree

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReview.test.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Routes from '../../../../../constants/navigation/Routes';
99

1010
const mockNavigate = jest.fn();
1111
const mockDispatch = jest.fn();
12-
const mockSelectedTokens: BridgeToken[] = [
12+
const defaultSelectedTokens: BridgeToken[] = [
1313
{
1414
address: '0x1111111111111111111111111111111111111111',
1515
chainId: '0x1' as Hex,
@@ -25,6 +25,13 @@ const mockSelectedTokens: BridgeToken[] = [
2525
balance: '154.297',
2626
},
2727
];
28+
const thirdSelectedToken: BridgeToken = {
29+
address: '0x3333333333333333333333333333333333333333',
30+
chainId: '0x1' as Hex,
31+
decimals: 18,
32+
symbol: 'LINK',
33+
balance: '42.123',
34+
};
2835
const usdcToken: BridgeToken = {
2936
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
3037
chainId: '0x1' as Hex,
@@ -39,6 +46,7 @@ const musdToken: BridgeToken = {
3946
symbol: 'MUSD',
4047
image: 'musd-image-url',
4148
};
49+
let mockSelectedTokens: BridgeToken[] = defaultSelectedTokens;
4250
let mockSelectedDestinationToken: BridgeToken | undefined = usdcToken;
4351
let mockDestinationTokens: BridgeToken[] = [usdcToken];
4452
let mockBatchSellSlippages: Partial<Record<CaipAssetType, string | undefined>> =
@@ -61,6 +69,10 @@ jest.mock('../../../../../core/redux/slices/bridge', () => ({
6169
type: 'bridge/setBatchSellDestToken',
6270
payload: token,
6371
})),
72+
setBatchSellSourceTokens: jest.fn((tokens: BridgeToken[]) => ({
73+
type: 'bridge/setBatchSellSourceTokens',
74+
payload: tokens,
75+
})),
6476
setBatchSellTokenSlippages: jest.fn(
6577
(slippage: Partial<Record<CaipAssetType, string | undefined>>) => ({
6678
type: 'bridge/setBatchSellTokenSlippages',
@@ -80,11 +92,15 @@ jest.mock('./BatchSellReviewTokenRow', () => {
8092

8193
return {
8294
BatchSellReviewTokenRow: ({
95+
isRemoveTokenDisabled,
96+
onRemovePress,
8397
onSlippagePress,
8498
percent,
8599
token,
86100
tokenKey,
87101
}: {
102+
isRemoveTokenDisabled?: boolean;
103+
onRemovePress: (token: BridgeToken) => void;
88104
onSlippagePress: (token: BridgeToken) => void;
89105
percent: number;
90106
token: BridgeToken;
@@ -99,13 +115,22 @@ jest.mock('./BatchSellReviewTokenRow', () => {
99115
onPress: () => onSlippagePress(token),
100116
testID: `batch-sell-review-customize-button-${tokenKey}`,
101117
}),
118+
ReactActual.createElement(Pressable, {
119+
accessibilityState: { disabled: Boolean(isRemoveTokenDisabled) },
120+
disabled: isRemoveTokenDisabled,
121+
onPress: isRemoveTokenDisabled
122+
? undefined
123+
: () => onRemovePress(token),
124+
testID: `batch-sell-review-remove-button-${tokenKey}`,
125+
}),
102126
),
103127
};
104128
});
105129

106130
describe('BatchSellReview', () => {
107131
beforeEach(() => {
108132
jest.clearAllMocks();
133+
mockSelectedTokens = defaultSelectedTokens;
109134
mockSelectedDestinationToken = usdcToken;
110135
mockDestinationTokens = [usdcToken];
111136
mockBatchSellSlippages = {};
@@ -208,4 +233,37 @@ describe('BatchSellReview', () => {
208233
payload: usdcToken,
209234
});
210235
});
236+
237+
it('removes a token when more than two source tokens are selected', () => {
238+
mockSelectedTokens = [...defaultSelectedTokens, thirdSelectedToken];
239+
const { getByTestId } = render(<BatchSellReview />);
240+
241+
mockDispatch.mockClear();
242+
fireEvent.press(
243+
getByTestId(
244+
`${BatchSellReviewSelectorsIDs.REMOVE_BUTTON}-0x1:0x2222222222222222222222222222222222222222`,
245+
),
246+
);
247+
248+
expect(mockDispatch).toHaveBeenCalledWith({
249+
type: 'bridge/setBatchSellSourceTokens',
250+
payload: [defaultSelectedTokens[0], thirdSelectedToken],
251+
});
252+
});
253+
254+
it('disables token removal when two source tokens are selected', () => {
255+
const { getByTestId } = render(<BatchSellReview />);
256+
const removeButton = getByTestId(
257+
`${BatchSellReviewSelectorsIDs.REMOVE_BUTTON}-0x1:0x2222222222222222222222222222222222222222`,
258+
);
259+
260+
mockDispatch.mockClear();
261+
fireEvent.press(removeButton);
262+
263+
expect(removeButton.props.accessibilityState.disabled).toBe(true);
264+
expect(mockDispatch).not.toHaveBeenCalledWith({
265+
type: 'bridge/setBatchSellSourceTokens',
266+
payload: expect.any(Array),
267+
});
268+
});
211269
});

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReview.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
selectBatchSellDestStablecoins,
3535
selectBatchSellSourceTokens,
3636
setBatchSellDestToken,
37+
setBatchSellSourceTokens,
3738
setBatchSellTokenSlippages,
3839
} from '../../../../../core/redux/slices/bridge';
3940
import { RootState } from '../../../../../reducers';
@@ -101,6 +102,7 @@ export function BatchSellReview() {
101102
);
102103
const selectedDestinationToken = useSelector(selectBatchSellDestToken);
103104
const batchSellSlippages = useSelector(selectBatchSellSlippages);
105+
const isRemoveTokenDisabled = selectedTokens.length <= 2;
104106
const [percentsByTokenKey, setPercentsByTokenKey] = useState<
105107
Record<string, number>
106108
>({});
@@ -182,6 +184,20 @@ export function BatchSellReview() {
182184
[navigation, selectedDestinationToken?.chainId],
183185
);
184186

187+
const handleRemoveToken = useCallback(
188+
(tokenToRemove: BridgeToken) => {
189+
if (isRemoveTokenDisabled) return;
190+
191+
const tokenKeyToRemove = getTokenKey(tokenToRemove);
192+
const remainingTokens = selectedTokens.filter(
193+
(token) => getTokenKey(token) !== tokenKeyToRemove,
194+
);
195+
196+
dispatch(setBatchSellSourceTokens(remainingTokens));
197+
},
198+
[dispatch, isRemoveTokenDisabled, selectedTokens],
199+
);
200+
185201
return (
186202
<SafeAreaView
187203
style={tw.style('flex-1 bg-default')}
@@ -274,6 +290,8 @@ export function BatchSellReview() {
274290
percent={percentsByTokenKey[tokenKey] ?? DEFAULT_PERCENT}
275291
onPercentChange={handlePercentChange}
276292
onSlippagePress={handleSlippagePress}
293+
onRemovePress={handleRemoveToken}
294+
isRemoveTokenDisabled={isRemoveTokenDisabled}
277295
/>
278296
);
279297
})}

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReviewTokenRow.test.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,24 @@ jest.mock('@metamask/design-system-react-native', () => {
4040
BoxFlexDirection: { Row: 'row' },
4141
ButtonIcon: ({
4242
accessibilityLabel,
43+
isDisabled,
4344
onPress,
4445
testID,
4546
}: {
4647
accessibilityLabel?: string;
48+
isDisabled?: boolean;
4749
onPress?: () => void;
4850
testID?: string;
4951
}) =>
5052
ReactActual.createElement(
5153
RNPressable,
52-
{ accessibilityLabel, onPress, testID },
54+
{
55+
accessibilityLabel,
56+
accessibilityState: { disabled: Boolean(isDisabled) },
57+
disabled: isDisabled,
58+
onPress: isDisabled ? undefined : onPress,
59+
testID,
60+
},
5361
null,
5462
),
5563
ButtonIconSize: { Md: 'md' },
@@ -186,4 +194,25 @@ describe('BatchSellReviewTokenRow', () => {
186194
expect(mockOnSlippagePress).toHaveBeenCalledWith(mockToken);
187195
expect(mockOnRemovePress).toHaveBeenCalledWith(mockToken);
188196
});
197+
198+
it('disables remove presses', () => {
199+
const { getByTestId } = render(
200+
<BatchSellReviewTokenRow
201+
token={mockToken}
202+
tokenKey={mockTokenKey}
203+
percent={100}
204+
onPercentChange={mockOnPercentChange}
205+
onRemovePress={mockOnRemovePress}
206+
isRemoveTokenDisabled
207+
/>,
208+
);
209+
const removeButton = getByTestId(
210+
`${BatchSellReviewSelectorsIDs.REMOVE_BUTTON}-${mockTokenKey}`,
211+
);
212+
213+
fireEvent.press(removeButton);
214+
215+
expect(removeButton.props.accessibilityState.disabled).toBe(true);
216+
expect(mockOnRemovePress).not.toHaveBeenCalled();
217+
});
189218
});

app/components/UI/Bridge/Views/BatchSellReview/BatchSellReviewTokenRow.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface BatchSellReviewTokenRowProps {
3030
onPercentChange: (tokenKey: string, percent: number) => void;
3131
onSlippagePress?: (token: BridgeToken) => void;
3232
onRemovePress?: (token: BridgeToken) => void;
33+
isRemoveTokenDisabled?: boolean;
3334
}
3435

3536
function getTokenBalanceText(token: BridgeToken, percent: number) {
@@ -47,6 +48,7 @@ export function BatchSellReviewTokenRow({
4748
onPercentChange,
4849
onSlippagePress,
4950
onRemovePress,
51+
isRemoveTokenDisabled = false,
5052
}: BatchSellReviewTokenRowProps) {
5153
const tw = useTailwind();
5254
const balanceText = useMemo(
@@ -61,6 +63,12 @@ export function BatchSellReviewTokenRow({
6163
[onPercentChange, tokenKey],
6264
);
6365

66+
const handleRemovePress = useCallback(() => {
67+
if (isRemoveTokenDisabled) return;
68+
69+
onRemovePress?.(token);
70+
}, [isRemoveTokenDisabled, onRemovePress, token]);
71+
6472
return (
6573
<Box
6674
testID={`${BatchSellReviewSelectorsIDs.TOKEN_ROW}-${tokenKey}`}
@@ -114,7 +122,8 @@ export function BatchSellReviewTokenRow({
114122
accessibilityLabel={strings('bridge.batch_sell_remove_token', {
115123
tokenSymbol: token.symbol,
116124
})}
117-
onPress={() => onRemovePress?.(token)}
125+
isDisabled={isRemoveTokenDisabled}
126+
onPress={handleRemovePress}
118127
testID={`${BatchSellReviewSelectorsIDs.REMOVE_BUTTON}-${tokenKey}`}
119128
/>
120129
</Box>

0 commit comments

Comments
 (0)