Skip to content

Commit

Permalink
feat: add cross chain swaps link in extension (#23437)
Browse files Browse the repository at this point in the history
## **Description**

<!--
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?
-->

As part of ongoing monetization efforts, Portfolio primitives are
looking for opportunities to enable new revenue-generating flows. One
area is the swaps page, where we can recommend users navigate to
MetaMask Bridges if they wish to complete a cross-chain swap. Our
hypothesis is that this will allow us to better target users that wish
to complete both a swap and a bridge.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/23437?quickstart=1)

## **Related issues**


[METABRIDGE-846](https://consensyssoftware.atlassian.net/browse/METABRIDGE-846)

## **Manual testing steps**

1. Go to the Swaps page on any network support by *both* Bridges and
Swaps (e.g., Ethereum Mainnet)
2. Verify that the button is available
3. Verify that it links to MetaMask Bridges, with the correct token and
chain selected
- Verify that the MetaMetrics ID is passed as a query parameter when the
page is first loaded
4. Verify that it disappears when you begin to fetch a quote
5. Verify that it doesn't appear at all for networks supported by Swaps
but unsupported by Bridges
- You may need to remove a Swaps-supported network from
`ALLOWED_BRIDGE_CHAIN_IDS`, or add a Swaps Testnet (chain ID 1337) to
MetaMask to test this.

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<img width="403" alt="Screenshot 2024-03-12 at 5 20 17 PM"
src="https://github.com/MetaMask/metamask-extension/assets/44588480/c606c2b8-020d-4c8c-81ce-9b997bb1b8a2">

<!-- [screenshots/recordings] -->

### **After**

<img width="403" alt="Screenshot 2024-03-12 at 6 28 45 PM"
src="https://github.com/MetaMask/metamask-extension/assets/44588480/8bc5d045-fcb7-4d94-a931-5befe23cc406">

<img width="403" alt="Screenshot 2024-03-12 at 6 29 07 PM"
src="https://github.com/MetaMask/metamask-extension/assets/44588480/f73c2622-3520-4b9a-8d5b-2e58c550dbe4">

After clicking the link in either state:

<img width="934" alt="Screenshot 2024-03-12 at 5 18 03 PM"
src="https://github.com/MetaMask/metamask-extension/assets/44588480/b2bea962-a658-452a-9a5a-612ca5320e5b">

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've clearly explained what problem this PR is solving and how it
is solved.
- [x] I've linked related issues
- [x] I've included manual testing steps
- [x] I've included screenshots/recordings if applicable
- [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/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
- [x] I’ve properly set the pull request status:
  - [x] In case it's not yet "ready for review", I've set it to "draft".
- [x] In case it's "ready for review", I've changed it from "draft" to
"non-draft".

## **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.
  • Loading branch information
BZahory authored Mar 13, 2024
1 parent 6e397d0 commit 5349e6a
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 0 deletions.
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json

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

48 changes: 48 additions & 0 deletions ui/pages/swaps/prepare-swap-page/prepare-swap-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
SEVERITIES,
TextVariant,
BLOCK_SIZES,
FontWeight,
} from '../../../helpers/constants/design-system';
import {
fetchQuotesAndSetQuoteState,
Expand Down Expand Up @@ -64,12 +65,15 @@ import {
getTokenList,
isHardwareWallet,
getHardwareWalletType,
getIsBridgeChain,
getMetaMetricsId,
} from '../../../selectors';
import {
getValueFromWeiHex,
hexToDecimal,
} from '../../../../shared/modules/conversion.utils';
import { getURLHostName } from '../../../helpers/utils/util';
import { getPortfolioUrl } from '../../../helpers/utils/portfolio';
import { usePrevious } from '../../../hooks/usePrevious';
import { useTokenTracker } from '../../../hooks/useTokenTracker';
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
Expand Down Expand Up @@ -197,6 +201,8 @@ export default function PrepareSwapPage({
const numberOfAggregators = aggregatorMetadata
? Object.keys(aggregatorMetadata).length
: 0;
const isBridgeChain = useSelector(getIsBridgeChain);
const metaMetricsId = useSelector(getMetaMetricsId);

const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate);
Expand Down Expand Up @@ -713,6 +719,11 @@ export default function PrepareSwapPage({
!swapsErrorKey && !isReviewSwapButtonDisabled && !areQuotesPresent;
const showNotEnoughTokenMessage =
!fromTokenError && balanceError && fromTokenSymbol;
const showCrossChainSwapsLink =
isBridgeChain &&
!showReviewQuote &&
!showQuotesLoadingAnimation &&
!areQuotesPresent;

const tokenVerifiedOn1Source = occurrences === 1;

Expand Down Expand Up @@ -1024,6 +1035,43 @@ export default function PrepareSwapPage({
</div>
</Box>
</div>
{showCrossChainSwapsLink && (
<ButtonLink
endIconName={IconName.Export}
endIconProps={{
size: IconSize.Xs,
}}
variant={TextVariant.bodySm}
marginTop={2}
fontWeight={FontWeight.Normal}
onClick={() => {
const portfolioUrl = getPortfolioUrl(
'bridge',
'ext_bridge_button',
metaMetricsId,
);

global.platform.openTab({
url: `${portfolioUrl}&token=${fromTokenAddress}`,
});

trackEvent({
category: MetaMetricsEventCategory.Swaps,
event: MetaMetricsEventName.BridgeLinkClicked,
properties: {
location: 'Swaps',
text: 'Swap across networks with MetaMask Portfolio',
chain_id: chainId,
token_symbol: fromTokenSymbol,
},
});
}}
target="_blank"
data-testid="prepare-swap-page-cross-chain-swaps-link"
>
{t('crossChainSwapsLink')}
</ButtonLink>
)}
{!showReviewQuote && toTokenIsNotDefault && occurrences < 2 && (
<Box display={DISPLAY.FLEX} marginTop={2}>
<BannerAlert
Expand Down
65 changes: 65 additions & 0 deletions ui/pages/swaps/prepare-swap-page/prepare-swap-page.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,69 @@ describe('PrepareSwapPage', () => {
fireEvent.click(maxLink);
expect(setFromTokenInputValue).toHaveBeenCalled();
});

it('should have the Bridge link enabled if chain id is part of supported chains and there are no quotes', () => {
const mockStore = createSwapsMockStore();
mockStore.metamask.providerConfig = {
chainId: '0x1',
};
mockStore.metamask.swapsState.quotes = [];
const store = configureMockStore(middleware)(mockStore);

const props = createProps();
const { queryByTestId } = renderWithProvider(
<PrepareSwapPage {...props} />,
store,
);
const bridgeButton = queryByTestId(
'prepare-swap-page-cross-chain-swaps-link',
);
expect(bridgeButton).toBeInTheDocument();
expect(bridgeButton).toBeEnabled();
});

it('should not have the Bridge link enabled if chain id is part of supported chains but there are quotes', () => {
const mockStore = createSwapsMockStore();
mockStore.metamask.providerConfig = {
chainId: '0x1',
};
expect(
Object.keys(mockStore.metamask.swapsState.quotes).length,
).toBeDefined();
const store = configureMockStore(middleware)(mockStore);

const props = createProps();
const { queryByTestId } = renderWithProvider(
<PrepareSwapPage {...props} />,
store,
);
const bridgeButton = queryByTestId(
'prepare-swap-page-cross-chain-swaps-link',
);

expect(bridgeButton).toBeNull();
});

it('should not have the Bridge link enabled if there are quotes but chain id is not part of supported chains', () => {
const mockStore = createSwapsMockStore();
mockStore.metamask.providerConfig = {
chainId: '0x539', // swaps testnet
};
expect(
Object.keys(mockStore.metamask.swapsState.quotes).length,
).toBeDefined();

const store = configureMockStore(middleware)(mockStore);

const props = createProps();
const { queryByTestId } = renderWithProvider(
<PrepareSwapPage {...props} />,
store,
);
const bridgeButton = queryByTestId(
'prepare-swap-page-cross-chain-swaps-link',
);

expect(bridgeButton).toBeNull();
});
});

0 comments on commit 5349e6a

Please sign in to comment.