Skip to content

Commit cd39d7b

Browse files
authored
chore: low return warning alert for bridging (#29171)
<!-- 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** Changes the Low Return tooltip into an alert Banner, and highlights network fee in the quote card <!-- 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? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/29171?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMS-1814 ## **Manual testing steps** 1. Request quotes with a low return 2. Verify that new treatment is shown ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** ![Screenshot 2024-12-17 at 10 47 21 AM](https://github.com/user-attachments/assets/c30a4682-7174-4041-83df-58726127f3c5) ![Screenshot 2024-12-17 at 10 47 46 AM](https://github.com/user-attachments/assets/f4576048-7f41-4677-b205-710421ecd9c6) ### **After** ![Screenshot 2024-12-17 at 10 32 28 AM](https://github.com/user-attachments/assets/c8db2d88-fce9-491e-9949-271b3d9faf96) ## **Pre-merge author checklist** - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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-extension/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.
1 parent b114f88 commit cd39d7b

11 files changed

+137
-116
lines changed

app/_locales/en/messages.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shared/constants/bridge.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
2727
export const METABRIDGE_ETHEREUM_ADDRESS =
2828
'0x0439e60F02a8900a951603950d8D4527f400C3f1';
2929
export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour
30-
export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x times less return than the best quote, ignore it
30+
export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.5; // if a quote returns in x times less return than the best quote, ignore it
3131

3232
export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high';
3333
export const BRIDGE_DEFAULT_SLIPPAGE = 0.5;

ui/ducks/bridge/selectors.test.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@ describe('Bridge selectors', () => {
12121212
).toStrictEqual(false);
12131213
});
12141214

1215-
it('should return isEstimatedReturnLow=true return value is 20% less than sent funds', () => {
1215+
it('should return isEstimatedReturnLow=true return value is 50% less than sent funds', () => {
12161216
const state = createBridgeMockStore({
12171217
featureFlagOverrides: {
12181218
extensionConfig: {
@@ -1228,7 +1228,7 @@ describe('Bridge selectors', () => {
12281228
toToken: { address: zeroAddress(), symbol: 'TEST' },
12291229
fromTokenInputValue: '1',
12301230
fromTokenExchangeRate: 2524.25,
1231-
toTokenExchangeRate: 0.798781,
1231+
toTokenExchangeRate: 0.61,
12321232
},
12331233
bridgeStateOverrides: {
12341234
quotes: mockBridgeQuotesNativeErc20,
@@ -1264,11 +1264,11 @@ describe('Bridge selectors', () => {
12641264
expect(
12651265
getBridgeQuotes(state as never).activeQuote?.adjustedReturn
12661266
.valueInCurrency,
1267-
).toStrictEqual(new BigNumber('16.99676538473491988'));
1267+
).toStrictEqual(new BigNumber('12.38316502627291988'));
12681268
expect(result.isEstimatedReturnLow).toStrictEqual(true);
12691269
});
12701270

1271-
it('should return isEstimatedReturnLow=false when return value is more than 80% of sent funds', () => {
1271+
it('should return isEstimatedReturnLow=false when return value is more than 50% of sent funds', () => {
12721272
const state = createBridgeMockStore({
12731273
featureFlagOverrides: {
12741274
extensionConfig: {
@@ -1283,7 +1283,8 @@ describe('Bridge selectors', () => {
12831283
fromToken: { address: zeroAddress(), symbol: 'ETH' },
12841284
toToken: { address: zeroAddress(), symbol: 'TEST' },
12851285
fromTokenExchangeRate: 2524.25,
1286-
toTokenExchangeRate: 0.998781,
1286+
toTokenExchangeRate: 0.63,
1287+
fromTokenInputValue: 1,
12871288
},
12881289
bridgeStateOverrides: {
12891290
quotes: mockBridgeQuotesNativeErc20,
@@ -1320,7 +1321,7 @@ describe('Bridge selectors', () => {
13201321
expect(
13211322
getBridgeQuotes(state as never).activeQuote?.adjustedReturn
13221323
.valueInCurrency,
1323-
).toStrictEqual(new BigNumber('21.88454578473491988'));
1324+
).toStrictEqual(new BigNumber('12.87194306627291988'));
13241325
expect(result.isEstimatedReturnLow).toStrictEqual(false);
13251326
});
13261327

ui/pages/bridge/prepare/bridge-input-group.tsx

+2-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import { useSelector } from 'react-redux';
33
import { BigNumber } from 'bignumber.js';
44
import { getAddress } from 'ethers/lib/utils';
@@ -7,7 +7,6 @@ import {
77
TextField,
88
TextFieldType,
99
ButtonLink,
10-
PopoverPosition,
1110
Button,
1211
ButtonSize,
1312
} from '../../../components/component-library';
@@ -17,7 +16,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext';
1716
import { getLocale } from '../../../selectors';
1817
import { getCurrentCurrency } from '../../../ducks/metamask/metamask';
1918
import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote';
20-
import { Column, Row, Tooltip } from '../layout';
19+
import { Column, Row } from '../layout';
2120
import {
2221
Display,
2322
FontWeight,
@@ -27,7 +26,6 @@ import {
2726
TextColor,
2827
} from '../../../helpers/constants/design-system';
2928
import { AssetType } from '../../../../shared/constants/transaction';
30-
import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge';
3129
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
3230
import {
3331
getBridgeQuotes,
@@ -87,8 +85,6 @@ export const BridgeInputGroup = ({
8785

8886
const inputRef = useRef<HTMLInputElement | null>(null);
8987

90-
const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true);
91-
9288
useEffect(() => {
9389
if (inputRef.current) {
9490
inputRef.current.value = amountFieldProps?.value?.toString() ?? '';
@@ -189,23 +185,6 @@ export const BridgeInputGroup = ({
189185

190186
<Row justifyContent={JustifyContent.spaceBetween}>
191187
<Row>
192-
{isAmountReadOnly &&
193-
isEstimatedReturnLow &&
194-
isLowReturnTooltipOpen && (
195-
<Tooltip
196-
title={t('lowEstimatedReturnTooltipTitle')}
197-
position={PopoverPosition.TopStart}
198-
isOpen={isLowReturnTooltipOpen}
199-
onClose={() => setIsLowReturnTooltipOpen(false)}
200-
triggerElement={<span />}
201-
flip={false}
202-
offset={[0, 80]}
203-
>
204-
{t('lowEstimatedReturnTooltipMessage', [
205-
BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE * 100,
206-
])}
207-
</Tooltip>
208-
)}
209188
<Text
210189
variant={TextVariant.bodyMd}
211190
fontWeight={FontWeight.Normal}

ui/pages/bridge/prepare/index.scss

+8
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@
7070
border: none;
7171
border-radius: 8px;
7272

73+
.row-with-warning {
74+
padding-left: 16px;
75+
padding-right: 12px;
76+
margin-left: -16px;
77+
max-width: calc(100% + 32px);
78+
width: calc(100% + 32px);
79+
}
80+
7381
[data-theme='light'],
7482
.light {
7583
box-shadow: 0 0 2px 0 #e2e4e9, 0 0 16px 0 rgba(226, 228, 233, 0.16);

ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ const Wrapper = ({ children }) => (
2828
);
2929

3030
const mockFeatureFlags = {
31-
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
32-
destNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
3331
extensionSupport: true,
3432
extensionConfig: {
3533
refreshRate: 30000,
3634
maxRefreshCount: 5,
35+
support: true,
36+
chains: {
37+
'0x1': { isActiveSrc: true, isActiveDest: true },
38+
'0xa': { isActiveSrc: true, isActiveDest: true },
39+
},
3740
},
3841
};
3942
const mockBridgeSlice = {

ui/pages/bridge/prepare/prepare-bridge-page.tsx

+37-5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens';
9191
import { getCurrentKeyring, getLocale } from '../../../selectors';
9292
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
9393
import { SECOND } from '../../../../shared/constants/time';
94+
import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge';
9495
import { BridgeInputGroup } from './bridge-input-group';
9596
import { BridgeCTAButton } from './bridge-cta-button';
9697

@@ -151,10 +152,12 @@ const PrepareBridgePage = () => {
151152

152153
const ticker = useSelector(getNativeCurrency);
153154
const {
155+
isEstimatedReturnLow,
154156
isNoQuotesAvailable,
155157
isInsufficientGasForQuote,
156158
isInsufficientBalance,
157159
} = useSelector(getValidationErrors);
160+
const { quotesRefreshCount } = useSelector(getBridgeQuotes);
158161
const { openBuyCryptoInPdapp } = useRamps();
159162

160163
const { balanceAmount: nativeAssetBalance } = useLatestBalance(
@@ -190,6 +193,10 @@ const PrepareBridgePage = () => {
190193

191194
const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
192195

196+
// Resets the banner visibility when the estimated return is low
197+
const [isLowReturnBannerOpen, setIsLowReturnBannerOpen] = useState(true);
198+
useEffect(() => setIsLowReturnBannerOpen(true), [quotesRefreshCount]);
199+
193200
// Background updates are debounced when the switch button is clicked
194201
// To prevent putting the frontend in an unexpected state, prevent the user
195202
// from switching tokens within the debounce period
@@ -211,16 +218,27 @@ const PrepareBridgePage = () => {
211218
dispatch(resetBridgeState());
212219
}, []);
213220

214-
const scrollRef = useRef<HTMLDivElement>(null);
215-
221+
// Scroll to bottom of the page when banners are shown
222+
const insufficientBalanceBannerRef = useRef<HTMLDivElement>(null);
223+
const isEstimatedReturnLowRef = useRef<HTMLDivElement>(null);
216224
useEffect(() => {
217225
if (isInsufficientGasForQuote(nativeAssetBalance)) {
218-
scrollRef.current?.scrollIntoView({
226+
insufficientBalanceBannerRef.current?.scrollIntoView({
227+
behavior: 'smooth',
228+
block: 'start',
229+
});
230+
}
231+
if (isEstimatedReturnLow) {
232+
isEstimatedReturnLowRef.current?.scrollIntoView({
219233
behavior: 'smooth',
220234
block: 'start',
221235
});
222236
}
223-
}, [isInsufficientGasForQuote(nativeAssetBalance)]);
237+
}, [
238+
isEstimatedReturnLow,
239+
isInsufficientGasForQuote(nativeAssetBalance),
240+
isLowReturnBannerOpen,
241+
]);
224242

225243
const quoteParams = useMemo(
226244
() => ({
@@ -603,12 +621,26 @@ const PrepareBridgePage = () => {
603621
textAlign={TextAlign.Left}
604622
/>
605623
)}
624+
{isEstimatedReturnLow && isLowReturnBannerOpen && (
625+
<BannerAlert
626+
ref={insufficientBalanceBannerRef}
627+
marginInline={4}
628+
marginBottom={3}
629+
title={t('lowEstimatedReturnTooltipTitle')}
630+
severity={BannerAlertSeverity.Warning}
631+
description={t('lowEstimatedReturnTooltipMessage', [
632+
BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE * 100,
633+
])}
634+
textAlign={TextAlign.Left}
635+
onClose={() => setIsLowReturnBannerOpen(false)}
636+
/>
637+
)}
606638
{!isLoading &&
607639
activeQuote &&
608640
!isInsufficientBalance(srcTokenBalance) &&
609641
isInsufficientGasForQuote(nativeAssetBalance) && (
610642
<BannerAlert
611-
ref={scrollRef}
643+
ref={isEstimatedReturnLowRef}
612644
marginInline={4}
613645
marginBottom={3}
614646
title={t('bridgeValidationInsufficientGasTitle', [ticker])}

ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap

+10-26
Original file line numberDiff line numberDiff line change
@@ -95,30 +95,22 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
9595
</div>
9696
</div>
9797
<div
98-
class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
98+
class="mm-box mm-container mm-container--max-width-undefined row-with-warning mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
9999
>
100100
<p
101101
class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative-soft"
102+
style="white-space: nowrap;"
102103
>
103104
Network fees
104105
</p>
105106
<div
106107
class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
107108
>
108109
<p
109-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
110-
>
111-
$2.52
112-
</p>
113-
<p
114-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
115-
>
116-
-
117-
</p>
118-
<p
119-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
110+
class="mm-box mm-text mm-text--body-md"
111+
style="white-space: nowrap; overflow: visible;"
120112
>
121-
$2.52
113+
$2.52 - $2.52
122114
</p>
123115
<span
124116
class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative-soft"
@@ -259,30 +251,22 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
259251
</div>
260252
</div>
261253
<div
262-
class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
254+
class="mm-box mm-container mm-container--max-width-undefined row-with-warning mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
263255
>
264256
<p
265257
class="mm-box mm-text mm-text--body-md-medium mm-box--color-text-alternative-soft"
258+
style="white-space: nowrap;"
266259
>
267260
Network fees
268261
</p>
269262
<div
270263
class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
271264
>
272265
<p
273-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
274-
>
275-
$2.52
276-
</p>
277-
<p
278-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
279-
>
280-
-
281-
</p>
282-
<p
283-
class="mm-box mm-text mm-text--body-md mm-box--color-text-default"
266+
class="mm-box mm-text mm-text--body-md"
267+
style="white-space: nowrap; overflow: visible;"
284268
>
285-
$2.52
269+
$2.52 - $2.52
286270
</p>
287271
<span
288272
class="mm-box mm-icon mm-icon--size-md mm-box--display-inline-block mm-box--color-icon-alternative-soft"

ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
5757
</div>
5858
</header>
5959
<div
60-
class="mm-box mm-container mm-container--max-width-undefined mm-box--padding-4 mm-box--sm:padding-3 mm-box--padding-bottom-1 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
60+
class="mm-box mm-container mm-container--max-width-undefined mm-box--padding-top-3 mm-box--padding-bottom-1 mm-box--padding-inline-4 mm-box--display-flex mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
6161
>
6262
<button
6363
class="mm-box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md-medium mm-box--padding-0 mm-box--padding-right-0 mm-box--padding-left-0 mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-primary-default mm-box--background-color-transparent"

0 commit comments

Comments
 (0)