Skip to content

Commit 85a5904

Browse files
authored
feat: adding dapp swap banner (#37389)
<!-- 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** Adding dapp swap banner. ## **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: ## **Related issues** Fixes: MetaMask/MetaMask-planning#6107 ## **Manual testing steps** 1. Enable dapp swap locally 2. Submit swap 3. Check dapp swap banner ## **Screenshots/Recordings** https://github.com/user-attachments/assets/e45cdef3-3635-4b23-ab50-497d0194d009 ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a Uniswap-gated Dapp Swap comparison banner showing estimated savings, powered by new hook computations and supporting locale strings, with tests and styles. > > - **Confirmations UI**: > - Add `DappSwapComparisonBanner` with toggle (Current vs MetaMask), savings callout, dismiss button, and threshold-based display; gated by `dappSwapMetrics` and origin `https://app.uniswap.org`. > - New styles in `dapp-swap-comparison-banner/index.scss`. > - **Hook Logic (`useDappSwapComparisonInfo`)**: > - Compute and expose `selectedQuoteValueDifference`, `gasDifference`, `tokenAmountDifference`, `destinationTokenSymbol`. > - Rename `bestFilteredQuote` to `selectedQuote`; add memo/metric updates and value comparisons; integrate gas/token USD calculations. > - **Swap Parsing Utils**: > - Replace Permit2 parsing with Seaport (`COMMAND_BYTE_SEAPORT`), keep sweep, and add unwrap WETH handling; refine `getDataFromSwap` and token address collection. > - **i18n**: > - Add `dappSwapAdvantage`, `dappSwapBenefits`, `dappSwapQuoteDetails`, `dappSwapQuoteDifference` to `app/_locales/en*.json`. > - **Tests**: > - Update component test to assert new texts/threshold behavior. > - Expand hook tests with adjusted mock quotes, metrics assertions, and new value-difference expectations. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 16e5664. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent abe5cb3 commit 85a5904

File tree

8 files changed

+330
-55
lines changed

8 files changed

+330
-55
lines changed

app/_locales/en/messages.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/_locales/en_GB/messages.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/pages/confirmations/components/confirm/dapp-swap-comparison-banner/dapp-swap-comparison-banner.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,22 @@ describe('<DappSwapComparisonBanner />', () => {
4545
});
4646

4747
it('renders component without errors', () => {
48+
mockUseDappSwapComparisonInfo.mockReturnValue({
49+
selectedQuoteValueDifference: 0.1,
50+
gasDifference: 0.01,
51+
tokenAmountDifference: 0.01,
52+
destinationTokenSymbol: 'TEST',
53+
} as ReturnType<typeof useDappSwapComparisonInfo>);
4854
const { getByText } = render();
4955
expect(getByText('Current')).toBeInTheDocument();
5056
expect(getByText('Save + Earn')).toBeInTheDocument();
57+
expect(getByText('Save + Earn using MetaMask Swap:')).toBeInTheDocument();
58+
expect(getByText('Save about $0.02')).toBeInTheDocument();
59+
expect(
60+
getByText(
61+
'No additional cost • Priority support • Network fees refunded on failed swaps',
62+
),
63+
).toBeInTheDocument();
5164
});
5265

5366
it('renders undefined for incorrect origin', () => {
@@ -57,8 +70,8 @@ describe('<DappSwapComparisonBanner />', () => {
5770

5871
it('renders undefined if suitable quote is not found', () => {
5972
mockUseDappSwapComparisonInfo.mockReturnValue({
60-
selectedQuote: undefined,
61-
});
73+
selectedQuoteValueDifference: 0.001,
74+
} as ReturnType<typeof useDappSwapComparisonInfo>);
6275
const { container } = render();
6376
expect(container).toBeEmptyDOMElement();
6477
});

ui/pages/confirmations/components/confirm/dapp-swap-comparison-banner/dapp-swap-comparison-banner.tsx

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import React, { useState } from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import {
33
Box,
4+
BoxBackgroundColor,
45
BoxBorderColor,
56
Button,
7+
ButtonIcon,
8+
ButtonIconSize,
69
ButtonSize,
710
ButtonVariant,
11+
IconName,
12+
Text,
813
TextButton,
14+
TextColor,
15+
TextVariant,
916
} from '@metamask/design-system-react';
1017
import { TransactionMeta } from '@metamask/transaction-controller';
1118
import { useSelector } from 'react-redux';
@@ -16,6 +23,7 @@ import { useConfirmContext } from '../../../context/confirm';
1623
import { useDappSwapComparisonInfo } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo';
1724

1825
const DAPP_SWAP_COMPARISON_ORIGIN = 'https://app.uniswap.org';
26+
const DAPP_SWAP_THRESHOLD = 0.01;
1927

2028
const enum SwapType {
2129
Current = 'current',
@@ -57,45 +65,108 @@ const SwapButton = ({
5765

5866
const DappSwapComparisonInner = () => {
5967
const t = useI18nContext();
60-
const { selectedQuote } = useDappSwapComparisonInfo();
68+
const {
69+
selectedQuoteValueDifference,
70+
gasDifference,
71+
tokenAmountDifference,
72+
destinationTokenSymbol,
73+
} = useDappSwapComparisonInfo();
6174
const [selectedSwapType, setSelectedSwapType] = useState<SwapType>(
6275
SwapType.Current,
6376
);
77+
const [showDappSwapComparisonBanner, setShowDappSwapComparisonBanner] =
78+
useState<boolean>(true);
79+
80+
const hideDappSwapComparisonBanner = useCallback(() => {
81+
setShowDappSwapComparisonBanner(false);
82+
}, [setShowDappSwapComparisonBanner]);
6483

6584
if (
6685
process.env.DAPP_SWAP_SHIELD_ENABLED?.toString() !== 'true' ||
67-
!selectedQuote
86+
selectedQuoteValueDifference < DAPP_SWAP_THRESHOLD
6887
) {
6988
return null;
7089
}
7190

91+
const dappTypeSelected = selectedSwapType === SwapType.Current;
92+
7293
return (
73-
<Box
74-
borderColor={BoxBorderColor.BorderMuted}
75-
borderWidth={1}
76-
className="dapp-swap_wrapper"
77-
marginBottom={4}
78-
marginTop={2}
79-
padding={1}
80-
>
81-
<SwapButton
82-
type={
83-
selectedSwapType === SwapType.Current
84-
? SwapButtonType.ButtonType
85-
: SwapButtonType.Text
86-
}
87-
onClick={() => setSelectedSwapType(SwapType.Current)}
88-
label={t('current')}
89-
/>
90-
<SwapButton
91-
type={
92-
selectedSwapType === SwapType.Metamask
93-
? SwapButtonType.ButtonType
94-
: SwapButtonType.Text
95-
}
96-
onClick={() => setSelectedSwapType(SwapType.Metamask)}
97-
label={t('saveAndEarn')}
98-
/>
94+
<Box>
95+
<Box
96+
borderColor={BoxBorderColor.BorderMuted}
97+
borderWidth={1}
98+
className="dapp-swap_wrapper"
99+
marginBottom={4}
100+
marginTop={2}
101+
padding={1}
102+
>
103+
<SwapButton
104+
type={
105+
selectedSwapType === SwapType.Current
106+
? SwapButtonType.ButtonType
107+
: SwapButtonType.Text
108+
}
109+
onClick={() => setSelectedSwapType(SwapType.Current)}
110+
label={t('current')}
111+
/>
112+
<SwapButton
113+
type={
114+
selectedSwapType === SwapType.Metamask
115+
? SwapButtonType.ButtonType
116+
: SwapButtonType.Text
117+
}
118+
onClick={() => setSelectedSwapType(SwapType.Metamask)}
119+
label={t('saveAndEarn')}
120+
/>
121+
</Box>
122+
{showDappSwapComparisonBanner && (
123+
<Box
124+
className="dapp-swap_callout"
125+
backgroundColor={BoxBackgroundColor.BackgroundAlternative}
126+
marginBottom={4}
127+
padding={4}
128+
>
129+
<ButtonIcon
130+
className="dapp-swap_close-button"
131+
iconName={IconName.Close}
132+
size={ButtonIconSize.Sm}
133+
onClick={hideDappSwapComparisonBanner}
134+
ariaLabel="close-dapp-swap-comparison-banner"
135+
/>
136+
{dappTypeSelected && (
137+
<>
138+
<div className="dapp-swap_callout-arrow" />
139+
<Text
140+
className="dapp-swap_callout-text"
141+
color={TextColor.TextDefault}
142+
variant={TextVariant.BodySm}
143+
>
144+
{t('dappSwapAdvantage')}
145+
</Text>
146+
<Text
147+
className="dapp-swap_text-save"
148+
variant={TextVariant.BodySm}
149+
>
150+
{t('dappSwapQuoteDifference', [
151+
`$${(gasDifference + tokenAmountDifference).toFixed(2)}`,
152+
])}
153+
</Text>
154+
</>
155+
)}
156+
{!dappTypeSelected && (
157+
<Text className="dapp-swap_text-save" variant={TextVariant.BodySm}>
158+
{t('dappSwapQuoteDetails', [
159+
`$${gasDifference.toFixed(2)}`,
160+
`$${tokenAmountDifference.toFixed(2)}`,
161+
destinationTokenSymbol?.toUpperCase(),
162+
])}
163+
</Text>
164+
)}
165+
<Text color={TextColor.TextAlternative} variant={TextVariant.BodyXs}>
166+
{t('dappSwapBenefits')}
167+
</Text>
168+
</Box>
169+
)}
99170
</Box>
100171
);
101172
};

ui/pages/confirmations/components/confirm/dapp-swap-comparison-banner/index.scss

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,42 @@ Disabling Stylelint's hex color rule here because the TypeScript migration dashb
2323
text-decoration: none !important;
2424
}
2525
}
26+
27+
&_close-button {
28+
position: absolute;
29+
top: 0;
30+
right: 0;
31+
}
32+
33+
&_callout {
34+
border-radius: 8px;
35+
position: relative;
36+
}
37+
38+
&_callout-text {
39+
margin-bottom: 4px;
40+
}
41+
42+
&_text-save {
43+
color: #c9f570;
44+
margin-bottom: 4px;
45+
}
46+
47+
&_callout-arrow {
48+
position: absolute;
49+
top: -12px;
50+
left: 75%;
51+
transform: translateX(-50%);
52+
width: 0;
53+
height: 0;
54+
}
55+
56+
&_callout-arrow::before {
57+
content: "";
58+
position: absolute;
59+
width: 24px;
60+
height: 12px;
61+
background: var(--color-background-section);
62+
clip-path: polygon(50% 0, 0 100%, 100% 100%);
63+
}
2664
}

ui/pages/confirmations/hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo.test.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ describe('useDappSwapComparisonInfo', () => {
182182
srcChainId: 42161,
183183
destChainId: 42161,
184184
srcTokenAmount: '9913',
185-
destTokenAmount: '9907',
186-
minDestTokenAmount: '9708',
185+
destTokenAmount: '1004000',
186+
minDestTokenAmount: '972870',
187187
walletAddress: '0x178239802520a9C99DCBD791f81326B70298d629',
188188
destWalletAddress: '0x178239802520a9C99DCBD791f81326B70298d629',
189189
bridges: ['okx'],
@@ -197,15 +197,15 @@ describe('useDappSwapComparisonInfo', () => {
197197
from: '0x178239802520a9C99DCBD791f81326B70298d629',
198198
value: '0x0',
199199
data: '',
200-
gasLimit: 63109,
200+
gasLimit: 62000,
201201
},
202202
trade: {
203203
chainId: 42161,
204204
to: '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6',
205205
from: '0x178239802520a9C99DCBD791f81326B70298d629',
206206
value: '0x0',
207207
data: '',
208-
gasLimit: 296174,
208+
gasLimit: 80000,
209209
},
210210
estimatedProcessingTimeInSeconds: 0,
211211
},
@@ -263,9 +263,9 @@ describe('useDappSwapComparisonInfo', () => {
263263
// eslint-disable-next-line @typescript-eslint/naming-convention
264264
swap_mm_from_token_simulated_value_usd: '1',
265265
// eslint-disable-next-line @typescript-eslint/naming-convention
266-
swap_mm_minimum_received_value_usd: '0.000000000000009706097232',
266+
swap_mm_minimum_received_value_usd: '0.00000000000097267931748',
267267
// eslint-disable-next-line @typescript-eslint/naming-convention
268-
swap_mm_network_fee_usd: '0.01393686346576541082',
268+
swap_mm_network_fee_usd: '0.00550828904272868',
269269
// eslint-disable-next-line @typescript-eslint/naming-convention
270270
swap_mm_quote_provider: 'openocean',
271271
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -275,7 +275,7 @@ describe('useDappSwapComparisonInfo', () => {
275275
// eslint-disable-next-line @typescript-eslint/naming-convention
276276
swap_mm_slippage: 2,
277277
// eslint-disable-next-line @typescript-eslint/naming-convention
278-
swap_mm_to_token_simulated_value_usd: '0.000000000000009905058228',
278+
swap_mm_to_token_simulated_value_usd: '0.000000000001003803216',
279279
},
280280
sensitiveProperties: {
281281
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -292,4 +292,33 @@ describe('useDappSwapComparisonInfo', () => {
292292
'f8172040-b3d0-11f0-a882-3f99aa2e9f0c',
293293
);
294294
});
295+
296+
it('return correct values', async () => {
297+
jest.spyOn(Utils, 'fetchTokenExchangeRates').mockResolvedValue({
298+
'0x0000000000000000000000000000000000000000': 4052.27,
299+
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 0.999804,
300+
'0xfdcc3dd6671eab0709a4c0f3f53de9a333d80798': 1,
301+
});
302+
jest.spyOn(TokenUtils, 'fetchAllTokenDetails').mockResolvedValue({
303+
'0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': {
304+
symbol: 'USDC',
305+
decimals: '6',
306+
} as TokenStandAndDetails,
307+
'0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9': {
308+
symbol: 'USDT',
309+
decimals: '6',
310+
} as TokenStandAndDetails,
311+
});
312+
313+
const {
314+
selectedQuoteValueDifference,
315+
gasDifference,
316+
tokenAmountDifference,
317+
destinationTokenSymbol,
318+
} = await runHook();
319+
expect(selectedQuoteValueDifference).toBe(0.012494042894187605);
320+
expect(gasDifference).toBe(0.005686377458187605);
321+
expect(tokenAmountDifference).toBe(0.006807665436);
322+
expect(destinationTokenSymbol).toBe('USDC');
323+
});
295324
});

0 commit comments

Comments
 (0)