From 03c5f584388dedbe3d4793e31179b5511a8026c7 Mon Sep 17 00:00:00 2001 From: hunty Date: Wed, 19 Feb 2025 12:02:31 -0600 Subject: [PATCH] feat: MMS-1868 new quote card and story (#30303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** New quote card design for Solana cross-chain swaps. integration blocked by dependencies, separate integration PR will come in the future, probably next week, to add it to the prepare-bridge page. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/30303?quickstart=1) ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MMS-1868 (item no. 3) ## **Manual testing steps** Not relevant. Only on storybook for now. Component is not rendered anywhere in the app. ## **Screenshots/Recordings** ### **Before** ### **After** ## **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** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. --- .../multichain-bridge-quote-card.stories.tsx | 101 ++++++ .../quotes/multichain-bridge-quote-card.tsx | 312 ++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx create mode 100644 ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx new file mode 100644 index 000000000000..79616e27271a --- /dev/null +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.stories.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import configureStore from '../../../store/store'; +import { MultichainBridgeQuoteCard } from './multichain-bridge-quote-card'; +import { createBridgeMockStore } from '../../../../test/jest/mock-store'; +import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json'; + +const storybook = { + title: 'Pages/Bridge/MultichainBridgeQuoteCard', + component: MultichainBridgeQuoteCard, +}; + +const Container = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +export const DefaultStory = () => { + return ( + + + + ); +}; +DefaultStory.storyName = 'Default'; +DefaultStory.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export const WithDestinationAddress = () => { + return ( + + + + ); +}; +WithDestinationAddress.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export const WithLowEstimatedReturn = () => { + return ( + + + + ); +}; +WithLowEstimatedReturn.decorators = [ + (story) => ( + + {story()} + + ), +]; + +export default storybook; diff --git a/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx new file mode 100644 index 000000000000..87767298f014 --- /dev/null +++ b/ui/pages/bridge/quotes/multichain-bridge-quote-card.tsx @@ -0,0 +1,312 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + Text, + PopoverPosition, + IconName, + ButtonLink, + Icon, + IconSize, + AvatarNetwork, + AvatarNetworkSize, +} from '../../../components/component-library'; +import { + getBridgeQuotes, + getFromChain, + getToChain, + getValidationErrors, +} from '../../../ducks/bridge/selectors'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + formatCurrencyAmount, + formatTokenAmount, + formatEtaInMinutes, +} from '../utils/quote'; +import { + getCurrentCurrency, + getNativeCurrency, +} from '../../../ducks/metamask/metamask'; +import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker'; +import { useRequestProperties } from '../../../hooks/bridge/events/useRequestProperties'; +import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties'; +import { useQuoteProperties } from '../../../hooks/bridge/events/useQuoteProperties'; +import { MetaMetricsEventName } from '../../../../shared/constants/metametrics'; +import { + AlignItems, + BackgroundColor, + BlockSize, + IconColor, + JustifyContent, + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import { Row, Column, Tooltip } from '../layout'; +import { + BRIDGE_MM_FEE_RATE, + NETWORK_TO_SHORT_NETWORK_NAME_MAP, +} from '../../../../shared/constants/bridge'; +import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network'; +import { decimalToHex } from '../../../../shared/modules/conversion.utils'; +import { TERMS_OF_USE_LINK } from '../../../../shared/constants/terms'; +import { getIntlLocale } from '../../../ducks/locale/locale'; +import { shortenString } from '../../../helpers/utils/util'; +import { BridgeQuotesModal } from './bridge-quotes-modal'; + +type MultichainBridgeQuoteCardProps = { + destinationAddress?: string; +}; + +export const MultichainBridgeQuoteCard = ({ + destinationAddress, +}: MultichainBridgeQuoteCardProps) => { + const t = useI18nContext(); + const { activeQuote } = useSelector(getBridgeQuotes); + const currency = useSelector(getCurrentCurrency); + const ticker = useSelector(getNativeCurrency); + const { isEstimatedReturnLow } = useSelector(getValidationErrors); + + const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker(); + const { quoteRequestProperties } = useRequestProperties(); + const requestMetadataProperties = useRequestMetadataProperties(); + const quoteListProperties = useQuoteProperties(); + + const fromChain = useSelector(getFromChain); + const toChain = useSelector(getToChain); + const locale = useSelector(getIntlLocale); + + const [showAllQuotes, setShowAllQuotes] = useState(false); + const [shouldShowNetworkFeesInGasToken, setShouldShowNetworkFeesInGasToken] = + useState(false); + + return ( + <> + setShowAllQuotes(false)} + /> + {activeQuote ? ( + + + + {t('bestPrice')} + + {t('howQuotesWorkExplanation', [BRIDGE_MM_FEE_RATE])} + + + + { + quoteRequestProperties && + requestMetadataProperties && + quoteListProperties && + trackCrossChainSwapsEvent({ + event: MetaMetricsEventName.AllQuotesOpened, + properties: { + ...quoteRequestProperties, + ...requestMetadataProperties, + ...quoteListProperties, + }, + }); + setShowAllQuotes(true); + }} + > + {t('moreQuotes')} + + + + + + + + + { + NETWORK_TO_SHORT_NETWORK_NAME_MAP[ + `0x${decimalToHex( + activeQuote.quote.srcChainId, + )}` as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP + ] + } + + + + + { + NETWORK_TO_SHORT_NETWORK_NAME_MAP[ + `0x${decimalToHex( + activeQuote.quote.destChainId, + )}` as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP + ] + } + + + {destinationAddress && ( + + {shortenString(destinationAddress)} + + )} + + + + + + + {shouldShowNetworkFeesInGasToken + ? `${ + activeQuote.totalNetworkFee?.valueInCurrency + ? formatTokenAmount( + locale, + activeQuote.totalNetworkFee?.amount, + ) + : undefined + } - ${ + activeQuote.totalMaxNetworkFee?.valueInCurrency + ? formatTokenAmount( + locale, + activeQuote.totalMaxNetworkFee?.amount, + ticker, + ) + : undefined + }` + : `${ + formatCurrencyAmount( + activeQuote.totalNetworkFee?.valueInCurrency, + currency, + 2, + ) ?? + formatTokenAmount( + locale, + activeQuote.totalNetworkFee?.amount, + ) + } - ${ + formatCurrencyAmount( + activeQuote.totalMaxNetworkFee?.valueInCurrency, + currency, + 2, + ) ?? + formatTokenAmount( + locale, + activeQuote.totalMaxNetworkFee?.amount, + ticker, + ) + }`} + + + setShouldShowNetworkFeesInGasToken( + !shouldShowNetworkFeesInGasToken, + ) + } + /> + + + + + + {t('bridgeTimingMinutes', [ + formatEtaInMinutes( + activeQuote.estimatedProcessingTimeInSeconds, + ), + ])} + + + + + + + {t('rateIncludesMMFee', [BRIDGE_MM_FEE_RATE])} + + + {t('bridgeTerms')} + + + + + ) : null} + + ); +};