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} + + ); +};