Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
986f169
Fix: backup/restore for batched swaps
jpuri Nov 14, 2025
f75740a
update
jpuri Nov 14, 2025
60ed1c4
Merge branch 'main' into dapp_switch_fix
jpuri Nov 17, 2025
bb5b33e
update
jpuri Nov 17, 2025
3551fc4
feat: additing more dapp swap metrics
jpuri Nov 17, 2025
82a4107
update
jpuri Nov 17, 2025
7050926
Make min amount dynamic value in tooltip
jpuri Nov 17, 2025
3381053
Merge branch 'main' into tooltip_fix
jpuri Nov 17, 2025
7f2cfc5
update
jpuri Nov 17, 2025
4cb1711
Merge branch 'tooltip_fix' of https://github.com/MetaMask/metamask-ex…
jpuri Nov 17, 2025
e40396c
update
jpuri Nov 17, 2025
0c9e63c
merge
jpuri Nov 17, 2025
d7850f2
Update on dapp-swap ui
jpuri Nov 18, 2025
57c4bcd
update
jpuri Nov 18, 2025
64fd8fe
update
jpuri Nov 18, 2025
8c021cf
Merge branch 'main' into dapp_switch_fix
jpuri Nov 18, 2025
fde6444
Merge branch 'dapp_switch_fix' into dapp_swap_metrics
jpuri Nov 18, 2025
ad034f3
update
jpuri Nov 18, 2025
af809b3
Updating test cases
jpuri Nov 18, 2025
9308918
Adding test coverage
jpuri Nov 18, 2025
7d645d4
update
jpuri Nov 18, 2025
e394827
update
jpuri Nov 18, 2025
c1ea0cb
update
jpuri Nov 18, 2025
1360594
merge
jpuri Nov 18, 2025
2670988
Merge branch 'dapp_swap_metrics' of https://github.com/MetaMask/metam…
jpuri Nov 18, 2025
f702193
Merge branch 'main' into dapp_swap_metrics
jpuri Nov 18, 2025
48692de
merge
jpuri Nov 18, 2025
bdb3336
update
jpuri Nov 18, 2025
6fd10fb
update
jpuri Nov 18, 2025
4e947f5
Merge branch 'main' into dapp_swap_metrics
jpuri Nov 18, 2025
d7508ba
merge
jpuri Nov 18, 2025
cad63cf
Merge branch 'dapp_swap_metrics' into ui_fix
jpuri Nov 18, 2025
684ce33
update
jpuri Nov 18, 2025
bcf8736
Merge branch 'ui_fix' of https://github.com/MetaMask/metamask-extensi…
jpuri Nov 18, 2025
ae5d03d
update
jpuri Nov 18, 2025
f730c38
update
jpuri Nov 18, 2025
f08c704
update
jpuri Nov 18, 2025
a391830
update
jpuri Nov 18, 2025
6df8f91
update
jpuri Nov 18, 2025
85df878
update
jpuri Nov 18, 2025
fc1a9b9
merge
jpuri Nov 19, 2025
7c18d46
Merge branch 'main' into ui_fix
jpuri Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/_locales/en_GB/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions test/data/confirmations/contract-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,43 @@ export const mockSwapConfirmation = {
],
},
};

export const mockBridgeQuotes = [
{
quote: {
requestId:
'0xb7e0cdb746800056208ae5408a1755d9c8c10970c067a5e1fbbe768d2f6f626c',
bridgeId: '0x',
srcChainId: 42161,
destChainId: 42161,
aggregator: 'openocean',
aggregatorType: 'AGG',
srcTokenAmount: '9913',
destTokenAmount: '11104',
minDestTokenAmount: '10881',
walletAddress: '0x178239802520a9C99DCBD791f81326B70298d629',
destWalletAddress: '0x178239802520a9C99DCBD791f81326B70298d629',
bridges: ['0x'],
protocols: ['0x'],
steps: [],
slippage: 2,
},
approval: {
chainId: 42161,
to: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
from: '0x178239802520a9C99DCBD791f81326B70298d629',
value: '0x0',
data: '0x1234567890abcdef',
gasLimit: 63109,
},
trade: {
chainId: 42161,
to: '0x9dDA6Ef3D919c9bC8885D5560999A3640431e8e6',
from: '0x178239802520a9C99DCBD791f81326B70298d629',
value: '0x0',
data: '0x1234567890abcdef',
gasLimit: 596053,
},
estimatedProcessingTimeInSeconds: 0,
},
];
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { QuoteResponse } from '@metamask/bridge-controller';
Expand All @@ -9,20 +10,19 @@ import { renderWithConfirmContextProvider } from '../../../../../../test/lib/con
import { getRemoteFeatureFlags } from '../../../../../selectors/remote-feature-flags';
import { Confirmation } from '../../../types/confirm';
import { useDappSwapComparisonInfo } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo';
import * as SwapCheckHook from '../../../hooks/transactions/dapp-swap-comparison/useSwapCheck';
import * as ConfirmContext from '../../../context/confirm';
import { DappSwapComparisonBanner } from './dapp-swap-comparison-banner';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));

const mockUpdateTransaction = jest.fn();
jest.mock('../../../../../store/actions', () => ({
...jest.requireActual('../../../../../store/actions'),
updateTransaction: () => mockUpdateTransaction,
}));
jest.mock(
'../../../../../components/app/alert-system/contexts/alertMetricsContext',
() => ({
useAlertMetrics: jest.fn(() => ({
trackInlineAlertClicked: jest.fn(),
trackAlertRender: jest.fn(),
trackAlertActionClicked: jest.fn(),
})),
}),
);

jest.mock(
'../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo',
Expand All @@ -33,6 +33,18 @@ jest.mock(
}),
);

const mockCaptureDappSwapComparisonDisplayProperties = jest.fn();
jest.mock(
'../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonMetrics',
() => ({
useDappSwapComparisonMetrics: jest.fn(() => ({
captureSwapSubmit: jest.fn(),
captureDappSwapComparisonDisplayProperties:
mockCaptureDappSwapComparisonDisplayProperties,
})),
}),
);

jest.mock('../../../../../selectors/remote-feature-flags');

const quote = {
Expand Down Expand Up @@ -91,6 +103,7 @@ describe('<DappSwapComparisonBanner />', () => {
const mockUseDappSwapComparisonInfo = jest.mocked(useDappSwapComparisonInfo);

beforeEach(() => {
mockCaptureDappSwapComparisonDisplayProperties.mockClear();
mockGetRemoteFeatureFlags.mockReturnValue({
dappSwapMetrics: { enabled: true },
dappSwapUi: { enabled: true, threshold: 0.01 },
Expand Down Expand Up @@ -127,7 +140,13 @@ describe('<DappSwapComparisonBanner />', () => {
expect(container).toBeEmptyDOMElement();
});

it('call function to update confirmation when user clicks on Metamask Swap button', () => {
it('call function to update quote swap when user clicks on Metamask Swap button', () => {
const mockSetQuoteSelectedForMMSwap = jest.fn();
jest.spyOn(ConfirmContext, 'useConfirmContext').mockReturnValue({
currentConfirmation: mockSwapConfirmation,
setQuoteSelectedForMMSwap: mockSetQuoteSelectedForMMSwap,
} as unknown as ReturnType<typeof ConfirmContext.useConfirmContext>);

mockUseDappSwapComparisonInfo.mockReturnValue({
selectedQuote: quote as unknown as QuoteResponse,
selectedQuoteValueDifference: 0.1,
Expand All @@ -138,13 +157,29 @@ describe('<DappSwapComparisonBanner />', () => {
const { getByText } = render();
const quoteSwapButton = getByText('Metamask Swap');
fireEvent.click(quoteSwapButton);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockSetQuoteSelectedForMMSwap).toHaveBeenCalledTimes(1);
expect(
mockCaptureDappSwapComparisonDisplayProperties,
).toHaveBeenCalledTimes(2);
expect(
mockCaptureDappSwapComparisonDisplayProperties,
).toHaveBeenNthCalledWith(1, {
swap_mm_cta_displayed: 'true',
});
expect(
mockCaptureDappSwapComparisonDisplayProperties,
).toHaveBeenNthCalledWith(2, {
swap_mm_opened: 'true',
});
});

it('call function to update confirmation when user clicks on Market rate button', () => {
jest.spyOn(SwapCheckHook, 'useSwapCheck').mockReturnValue({
isQuotedSwap: true,
});
it('call function to update quote swap clicks on Market rate button', () => {
const mockSetQuoteSelectedForMMSwap = jest.fn();
jest.spyOn(ConfirmContext, 'useConfirmContext').mockReturnValue({
currentConfirmation: mockSwapConfirmation,
setQuoteSelectedForMMSwap: mockSetQuoteSelectedForMMSwap,
} as unknown as ReturnType<typeof ConfirmContext.useConfirmContext>);

mockUseDappSwapComparisonInfo.mockReturnValue({
selectedQuote: quote as unknown as QuoteResponse,
selectedQuoteValueDifference: 0.1,
Expand All @@ -155,6 +190,14 @@ describe('<DappSwapComparisonBanner />', () => {
const { getByText } = render();
const quoteSwapButton = getByText('Market rate');
fireEvent.click(quoteSwapButton);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockSetQuoteSelectedForMMSwap).toHaveBeenCalledTimes(1);
expect(
mockCaptureDappSwapComparisonDisplayProperties,
).toHaveBeenCalledTimes(1);
expect(
mockCaptureDappSwapComparisonDisplayProperties,
).toHaveBeenNthCalledWith(1, {
swap_mm_cta_displayed: 'true',
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
/* eslint-disable @typescript-eslint/naming-convention */
import React, { useCallback, useEffect, useState } from 'react';
import {
Box,
BoxBackgroundColor,
Expand All @@ -14,23 +15,19 @@ import {
TextColor,
TextVariant,
} from '@metamask/design-system-react';
import {
BatchTransaction,
TransactionMeta,
} from '@metamask/transaction-controller';
import { QuoteResponse, TxData } from '@metamask/bridge-controller';
import { toHex } from '@metamask/controller-utils';
import { useDispatch, useSelector } from 'react-redux';
import { QuoteResponse } from '@metamask/bridge-controller';
import { useSelector } from 'react-redux';

import { getRemoteFeatureFlags } from '../../../../../selectors/remote-feature-flags';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
import { updateTransaction } from '../../../../../store/actions';
import { useConfirmContext } from '../../../context/confirm';
import { useDappSwapComparisonInfo } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonInfo';
import { useDappSwapComparisonMetrics } from '../../../hooks/transactions/dapp-swap-comparison/useDappSwapComparisonMetrics';
import { useSwapCheck } from '../../../hooks/transactions/dapp-swap-comparison/useSwapCheck';
import { QuoteSwapSimulationDetails } from '../../transactions/quote-swap-simulation-details/quote-swap-simulation-details';
import { ConfirmInfoSection } from '../../../../../components/app/confirm/info/row/section';
import { NetworkRow } from '../info/shared/network-row/network-row';

const DAPP_SWAP_COMPARISON_ORIGIN = 'https://app.uniswap.org';
const TEST_DAPP_ORIGIN = 'https://metamask.github.io';
const DAPP_SWAP_THRESHOLD = 0.01;

type DappSwapUiFlag = {
Expand Down Expand Up @@ -83,10 +80,6 @@ const SwapButton = ({

const DappSwapComparisonInner = () => {
const t = useI18nContext();
const [
batchedDappSwapNestedTransactions,
setBatchedDappSwapNestedTransactions,
] = useState<BatchTransaction[] | undefined>();
const {
fiatRates,
gasDifference,
Expand All @@ -96,15 +89,13 @@ const DappSwapComparisonInner = () => {
sourceTokenAmount,
tokenAmountDifference,
tokenDetails,
} = useDappSwapComparisonInfo(batchedDappSwapNestedTransactions);

const dispatch = useDispatch();
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
} = useDappSwapComparisonInfo();
const { captureDappSwapComparisonDisplayProperties } =
useDappSwapComparisonMetrics();
const { dappSwapUi } = useSelector(getRemoteFeatureFlags) as {
dappSwapUi: DappSwapUiFlag;
};

// update selectedSwapType depending on data
const { setQuoteSelectedForMMSwap } = useConfirmContext();
const [selectedSwapType, setSelectedSwapType] = useState<SwapType>(
SwapType.Current,
);
Expand All @@ -113,66 +104,48 @@ const DappSwapComparisonInner = () => {

const hideDappSwapComparisonBanner = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setShowDappSwapComparisonBanner(false);
},
[setShowDappSwapComparisonBanner],
);

const updateSwapToCurrent = useCallback(() => {
setQuoteSelectedForMMSwap(undefined);
setSelectedSwapType(SwapType.Current);
if (currentConfirmation.txParamsOriginal) {
dispatch(
updateTransaction(
{
...currentConfirmation,
txParams: currentConfirmation.txParamsOriginal,
batchTransactions: undefined,
nestedTransactions: batchedDappSwapNestedTransactions,
},
false,
),
);
}
}, [currentConfirmation, dispatch, setSelectedSwapType]);
}, [setQuoteSelectedForMMSwap, setSelectedSwapType]);

const updateSwapToSelectedQuote = useCallback(() => {
setQuoteSelectedForMMSwap(selectedQuote);
captureDappSwapComparisonDisplayProperties({
swap_mm_opened: 'true',
});
setSelectedSwapType(SwapType.Metamask);
setShowDappSwapComparisonBanner(false);
const { value, gasLimit, data, to } = selectedQuote?.trade as TxData;
dispatch(
updateTransaction(
{
...currentConfirmation,
txParams: {
...currentConfirmation.txParams,
value,
to,
gas: toHex(gasLimit ?? 0),
data,
},
txParamsOriginal: currentConfirmation.txParams,
batchTransactions: [selectedQuote?.approval as BatchTransaction],
nestedTransactions: undefined,
},
false,
),
);
}, [
currentConfirmation,
dispatch,
setBatchedDappSwapNestedTransactions,
captureDappSwapComparisonDisplayProperties,
setQuoteSelectedForMMSwap,
setSelectedSwapType,
setShowDappSwapComparisonBanner,
selectedQuote,
]);

if (
!dappSwapUi?.enabled ||
selectedQuoteValueDifference <
(dappSwapUi?.threshold ?? DAPP_SWAP_THRESHOLD)
) {
const swapComparisonDisplayed =
dappSwapUi?.enabled &&
selectedQuoteValueDifference >=
(dappSwapUi?.threshold ?? DAPP_SWAP_THRESHOLD);

useEffect(() => {
let dappSwapComparisonDisplayed = false;
if (swapComparisonDisplayed) {
dappSwapComparisonDisplayed = true;
}
captureDappSwapComparisonDisplayProperties({
swap_mm_cta_displayed: dappSwapComparisonDisplayed.toString(),
});
}, [captureDappSwapComparisonDisplayProperties, swapComparisonDisplayed]);

if (!swapComparisonDisplayed) {
return null;
}

Expand Down Expand Up @@ -248,28 +221,31 @@ const DappSwapComparisonInner = () => {
</Box>
)}
{selectedSwapType === SwapType.Metamask && (
<QuoteSwapSimulationDetails
fiatRates={fiatRates}
quote={selectedQuote as QuoteResponse}
tokenDetails={tokenDetails}
sourceTokenAmount={sourceTokenAmount}
tokenAmountDifference={tokenAmountDifference}
minDestTokenAmountInUSD={minDestTokenAmountInUSD}
/>
<>
<QuoteSwapSimulationDetails
fiatRates={fiatRates}
quote={selectedQuote as QuoteResponse}
tokenDetails={tokenDetails}
sourceTokenAmount={sourceTokenAmount}
tokenAmountDifference={tokenAmountDifference}
minDestTokenAmountInUSD={minDestTokenAmountInUSD}
/>
<ConfirmInfoSection data-testid="transaction-details-section">
<NetworkRow />
</ConfirmInfoSection>
</>
)}
</Box>
);
};

export const DappSwapComparisonBanner = () => {
const { currentConfirmation: transactionMeta } =
useConfirmContext<TransactionMeta>();
const { dappSwapMetrics } = useSelector(getRemoteFeatureFlags);
const { isSwapToBeCompared } = useSwapCheck();

const dappSwapMetricsEnabled =
(dappSwapMetrics as { enabled: boolean })?.enabled === true &&
(transactionMeta.origin === DAPP_SWAP_COMPARISON_ORIGIN ||
transactionMeta.origin === TEST_DAPP_ORIGIN);
isSwapToBeCompared;

if (!dappSwapMetricsEnabled) {
return null;
Expand Down
Loading
Loading