diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 039671fa48..9412c5cf5f 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -296,7 +296,7 @@ export function ButtonConfirmed({ } } -export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) { +export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButtonProps) { if (error) { return } else { diff --git a/src/components/Column/index.tsx b/src/components/Column/index.tsx index 009bf7a41e..0de838bc3e 100644 --- a/src/components/Column/index.tsx +++ b/src/components/Column/index.tsx @@ -1,6 +1,5 @@ -import styled, { DefaultTheme } from 'styled-components/macro' - -type Gap = keyof DefaultTheme['grids'] +import styled from 'styled-components/macro' +import { Gap } from 'theme' export const Column = styled.div<{ gap?: Gap diff --git a/src/components/Row/index.tsx b/src/components/Row/index.tsx index 90a92cb6c7..fea6b3220d 100644 --- a/src/components/Row/index.tsx +++ b/src/components/Row/index.tsx @@ -1,7 +1,6 @@ import { Box } from 'rebass/styled-components' -import styled, { DefaultTheme } from 'styled-components/macro' - -type Gap = keyof DefaultTheme['grids'] +import styled from 'styled-components/macro' +import { Gap } from 'theme' // TODO(WEB-3289): // Setting `width: 100%` by default prevents composability in complex flex layouts. @@ -14,7 +13,7 @@ const Row = styled(Box)<{ padding?: string border?: string borderRadius?: string - gap?: string + gap?: Gap | string }>` width: ${({ width }) => width ?? '100%'}; display: flex; diff --git a/src/components/Settings/index.tsx b/src/components/Settings/index.tsx index 1e8075e0a1..ded407eecd 100644 --- a/src/components/Settings/index.tsx +++ b/src/components/Settings/index.tsx @@ -9,6 +9,7 @@ import { useRef } from 'react' import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' import styled from 'styled-components/macro' +import { Divider } from 'theme' import MaxSlippageSettings from './MaxSlippageSettings' import MenuButton from './MenuButton' @@ -40,14 +41,6 @@ const MenuFlyout = styled(AutoColumn)` padding: 1rem; ` -const Divider = styled.div` - width: 100%; - height: 1px; - border-width: 0; - margin: 0; - background-color: ${({ theme }) => theme.backgroundOutline}; -` - export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent }) { const { chainId } = useWeb3React() const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId)) diff --git a/src/components/TransactionConfirmationModal/index.tsx b/src/components/TransactionConfirmationModal/index.tsx index 9e10080493..882af9b1a0 100644 --- a/src/components/TransactionConfirmationModal/index.tsx +++ b/src/components/TransactionConfirmationModal/index.tsx @@ -20,7 +20,7 @@ import { TransactionSummary } from '../AccountDetails/TransactionSummary' import { ButtonLight, ButtonPrimary } from '../Button' import { AutoColumn, ColumnCenter } from '../Column' import Modal from '../Modal' -import { RowBetween, RowFixed } from '../Row' +import Row, { RowBetween, RowFixed } from '../Row' import AnimatedConfirmation from './AnimatedConfirmation' const Wrapper = styled.div` @@ -28,16 +28,12 @@ const Wrapper = styled.div` border-radius: 20px; outline: 1px solid ${({ theme }) => theme.backgroundOutline}; width: 100%; - padding: 1rem; -` -const Section = styled(AutoColumn)<{ inline?: boolean }>` - padding: ${({ inline }) => (inline ? '0' : '0')}; + padding: 16px; ` -const BottomSection = styled(Section)` +const BottomSection = styled(AutoColumn)` border-bottom-left-radius: 20px; border-bottom-right-radius: 20px; - padding-bottom: 10px; ` const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>` @@ -50,6 +46,10 @@ const StyledLogo = styled.img` margin-left: 6px; ` +const ConfirmationModalContentWrapper = styled(AutoColumn)` + padding-bottom: 12px; +` + function ConfirmationPendingContent({ onDismiss, pendingText, @@ -59,8 +59,6 @@ function ConfirmationPendingContent({ pendingText: ReactNode inline?: boolean // not in modal }) { - const theme = useTheme() - return ( @@ -74,15 +72,15 @@ function ConfirmationPendingContent({ - + Waiting for confirmation - - + + {pendingText} - - + + Confirm this transaction in your wallet - + @@ -125,7 +123,7 @@ function TransactionSubmittedContent({ return ( - + {!inline && ( @@ -135,7 +133,7 @@ function TransactionSubmittedContent({ - + Transaction submitted @@ -154,19 +152,19 @@ function TransactionSubmittedContent({ )} - + {inline ? Return : Close} - + {chainId && hash && ( - + View on {chainId === SupportedChainId.MAINNET ? 'Etherscan' : 'Block Explorer'} - + )} - - + + ) } @@ -184,15 +182,15 @@ export function ConfirmationModalContent({ }) { return ( - - - - {title} - + + + + {title} + - + {topContent()} - + {bottomContent && {bottomContent()}} ) @@ -202,7 +200,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React const theme = useTheme() return ( - + Error @@ -213,7 +211,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React {message} - + Dismiss @@ -252,7 +250,7 @@ function L2Content({ return ( - + {!inline && ( @@ -277,7 +275,7 @@ function L2Content({ )} - + {!hash ? ( Confirm transaction in wallet ) : !confirmed ? ( @@ -287,20 +285,20 @@ function L2Content({ ) : ( Error )} - - + + {transaction ? : pendingText} - + {chainId && hash ? ( - + View on Explorer - + ) : ( )} - + {!secondsToConfirm ? ( ) : ( @@ -311,14 +309,14 @@ function L2Content({ )} - + - + {inline ? Return : Close} - + - + ) } diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index 8bdd809f6a..8571a902c3 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -1,9 +1,11 @@ import { Trans } from '@lingui/macro' -import { Trace } from '@uniswap/analytics' -import { InterfaceModalName } from '@uniswap/analytics-events' +import { sendAnalyticsEvent, Trace } from '@uniswap/analytics' +import { InterfaceModalName, SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' import { Percent } from '@uniswap/sdk-core' -import { ReactNode, useCallback, useMemo, useState } from 'react' +import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { InterfaceTrade } from 'state/routing/types' +import { formatSwapPriceUpdatedEventProperties } from 'utils/loggingFormatters' import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' import TransactionConfirmationModal, { @@ -20,21 +22,17 @@ export default function ConfirmSwapModal({ allowedSlippage, onConfirm, onDismiss, - recipient, swapErrorMessage, - isOpen, attemptingTxn, txHash, swapQuoteReceivedDate, fiatValueInput, fiatValueOutput, }: { - isOpen: boolean - trade: InterfaceTrade | undefined + trade: InterfaceTrade originalTrade: InterfaceTrade | undefined attemptingTxn: boolean txHash: string | undefined - recipient: string | null allowedSlippage: Percent onAcceptChanges: () => void onConfirm: () => void @@ -44,35 +42,34 @@ export default function ConfirmSwapModal({ fiatValueInput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean } }) { - // shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed - // and an event triggered by modal closing should be logged. - const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false) const showAcceptChanges = useMemo( - () => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)), + () => Boolean(originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)), [originalTrade, trade] ) + const [lastExecutionPrice, setLastExecutionPrice] = useState(trade?.executionPrice) + const [priceUpdate, setPriceUpdate] = useState() + useEffect(() => { + if (lastExecutionPrice && !trade.executionPrice.equalTo(lastExecutionPrice)) { + setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice)) + setLastExecutionPrice(trade.executionPrice) + } + }, [lastExecutionPrice, setLastExecutionPrice, trade]) + const onModalDismiss = useCallback(() => { - if (isOpen) setShouldLogModalCloseEvent(true) + sendAnalyticsEvent( + SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, + formatSwapPriceUpdatedEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED) + ) onDismiss() - }, [isOpen, onDismiss]) + }, [onDismiss, priceUpdate, trade]) const modalHeader = useCallback(() => { - return trade ? ( - - ) : null - }, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade, shouldLogModalCloseEvent]) + return + }, [allowedSlippage, trade]) const modalBottom = useCallback(() => { - return trade ? ( + return ( - ) : null + ) }, [ + trade, onConfirm, + txHash, + allowedSlippage, showAcceptChanges, swapErrorMessage, - trade, - allowedSlippage, - txHash, swapQuoteReceivedDate, fiatValueInput, fiatValueOutput, + onAcceptChanges, ]) // text to show while loading const pendingText = ( - Swapping {trade?.inputAmount?.toSignificant(6)} {trade?.inputAmount?.currency?.symbol} for{' '} - {trade?.outputAmount?.toSignificant(6)} {trade?.outputAmount?.currency?.symbol} + Swapping {trade.inputAmount.toSignificant(6)} {trade.inputAmount.currency?.symbol} for{' '} + {trade.outputAmount.toSignificant(6)} {trade.outputAmount.currency?.symbol} ) @@ -111,7 +111,7 @@ export default function ConfirmSwapModal({ ) : ( Confirm Swap} + title={Review Swap} onDismiss={onModalDismiss} topContent={modalHeader} bottomContent={modalBottom} @@ -123,13 +123,13 @@ export default function ConfirmSwapModal({ return ( ) diff --git a/src/components/swap/SwapModalFooter.test.tsx b/src/components/swap/SwapModalFooter.test.tsx index 091d06c768..95bf710cbd 100644 --- a/src/components/swap/SwapModalFooter.test.tsx +++ b/src/components/swap/SwapModalFooter.test.tsx @@ -1,27 +1,103 @@ -import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants' -import { render, screen } from 'test-utils/render' +import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants' +import { render, screen, within } from 'test-utils/render' import SwapModalFooter from './SwapModalFooter' -const swapErrorMessage = 'swap error' -const fiatValue = { data: 123, isLoading: false } - describe('SwapModalFooter.tsx', () => { - it('renders with a disabled button with no account', () => { + it('matches base snapshot, test trade exact input', () => { const { asFragment } = render( null} - disabledConfirm - swapErrorMessage={swapErrorMessage} + onConfirm={jest.fn()} + swapErrorMessage={undefined} + disabledConfirm={false} swapQuoteReceivedDate={undefined} - fiatValueInput={fiatValue} - fiatValueOutput={fiatValue} + fiatValueInput={{ + data: undefined, + isLoading: false, + }} + fiatValueOutput={{ + data: undefined, + isLoading: false, + }} + showAcceptChanges={false} + onAcceptChanges={jest.fn()} /> ) expect(asFragment()).toMatchSnapshot() - expect(screen.getByTestId('confirm-swap-button')).toBeDisabled() + + expect( + screen.getByText( + 'The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.' + ) + ).toBeInTheDocument() + expect( + screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.') + ).toBeInTheDocument() + expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument() + }) + + it('shows accept changes section when available', () => { + const mockAcceptChanges = jest.fn() + render( + + ) + const showAcceptChanges = screen.getByTestId('show-accept-changes') + expect(showAcceptChanges).toBeInTheDocument() + expect(within(showAcceptChanges).getByText('Price updated')).toBeVisible() + expect(within(showAcceptChanges).getByText('Accept')).toBeVisible() + }) + + it('test trade exact output, no recipient', () => { + render( + + ) + expect( + screen.getByText( + 'The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.' + ) + ).toBeInTheDocument() + expect( + screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.') + ).toBeInTheDocument() + expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument() }) }) diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index f5c536b3cc..ff254431c6 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -1,105 +1,48 @@ import { Trans } from '@lingui/macro' import { TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Percent } from '@uniswap/sdk-core' +import { formatPriceImpact } from '@uniswap/conedison/format' +import { Percent, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import Column from 'components/Column' +import { MouseoverTooltip } from 'components/Tooltip' import useTransactionDeadline from 'hooks/useTransactionDeadline' -import { - formatPercentInBasisPointsNumber, - formatPercentNumber, - formatToDecimal, - getDurationFromDateMilliseconds, - getDurationUntilTimestampSeconds, - getTokenAddress, -} from 'lib/utils/analytics' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { ReactNode } from 'react' -import { Text } from 'rebass' +import { AlertTriangle } from 'react-feather' import { RouterPreference } from 'state/routing/slice' import { InterfaceTrade } from 'state/routing/types' import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks' -import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries' -import { computeRealizedPriceImpact } from 'utils/prices' +import styled, { useTheme } from 'styled-components/macro' +import { ThemedText } from 'theme' +import { formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers' +import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' +import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters' +import { getPriceImpactWarning } from 'utils/prices' -import { ButtonError } from '../Button' -import { AutoRow } from '../Row' -import { SwapCallbackError } from './styleds' +import { ButtonError, SmallButtonPrimary } from '../Button' +import Row, { AutoRow, RowBetween, RowFixed } from '../Row' +import { SwapCallbackError, SwapShowAcceptChanges } from './styleds' +import { Label } from './SwapModalHeaderAmount' -interface AnalyticsEventProps { - trade: InterfaceTrade - hash: string | undefined - allowedSlippage: Percent - transactionDeadlineSecondsSinceEpoch: number | undefined - isAutoSlippage: boolean - isAutoRouterApi: boolean - swapQuoteReceivedDate: Date | undefined - routes: RoutingDiagramEntry[] - fiatValueInput?: number - fiatValueOutput?: number -} +const DetailsContainer = styled(Column)` + padding: 0 8px; +` -const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => { - const routesEventProperties: Record = { - routes_percentages: [], - routes_protocols: [], - } +const StyledAlertTriangle = styled(AlertTriangle)` + margin-right: 8px; + min-width: 24px; +` - routes.forEach((route, index) => { - routesEventProperties['routes_percentages'].push(formatPercentNumber(route.percent)) - routesEventProperties['routes_protocols'].push(route.protocol) - routesEventProperties[`route_${index}_input_currency_symbols`] = route.path.map( - (pathStep) => pathStep[0].symbol ?? '' - ) - routesEventProperties[`route_${index}_output_currency_symbols`] = route.path.map( - (pathStep) => pathStep[1].symbol ?? '' - ) - routesEventProperties[`route_${index}_input_currency_addresses`] = route.path.map((pathStep) => - getTokenAddress(pathStep[0]) - ) - routesEventProperties[`route_${index}_output_currency_addresses`] = route.path.map((pathStep) => - getTokenAddress(pathStep[1]) - ) - routesEventProperties[`route_${index}_fee_amounts_hundredths_of_bps`] = route.path.map((pathStep) => pathStep[2]) - }) +const ConfirmButton = styled(ButtonError)` + height: 56px; + margin-top: 10px; +` - return routesEventProperties -} - -const formatAnalyticsEventProperties = ({ - trade, - hash, - allowedSlippage, - transactionDeadlineSecondsSinceEpoch, - isAutoSlippage, - isAutoRouterApi, - swapQuoteReceivedDate, - routes, - fiatValueInput, - fiatValueOutput, -}: AnalyticsEventProps) => ({ - estimated_network_fee_usd: trade.gasUseEstimateUSD ?? undefined, - transaction_hash: hash, - transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), - token_in_address: getTokenAddress(trade.inputAmount.currency), - token_out_address: getTokenAddress(trade.outputAmount.currency), - token_in_symbol: trade.inputAmount.currency.symbol, - token_out_symbol: trade.outputAmount.currency.symbol, - token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), - token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), - token_in_amount_usd: fiatValueInput, - token_out_amount_usd: fiatValueOutput, - price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)), - allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), - is_auto_router_api: isAutoRouterApi, - is_auto_slippage: isAutoSlippage, - chain_id: - trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId - ? trade.inputAmount.currency.chainId - : undefined, - duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate - ? getDurationFromDateMilliseconds(swapQuoteReceivedDate) - : undefined, - swap_quote_block_number: trade.blockNumber, - ...formatRoutesEventProperties(routes), -}) +const DetailRowValue = styled(ThemedText.BodySmall)` + text-align: right; + overflow-wrap: break-word; +` export default function SwapModalFooter({ trade, @@ -111,6 +54,8 @@ export default function SwapModalFooter({ swapQuoteReceivedDate, fiatValueInput, fiatValueOutput, + showAcceptChanges, + onAcceptChanges, }: { trade: InterfaceTrade hash: string | undefined @@ -121,46 +66,142 @@ export default function SwapModalFooter({ swapQuoteReceivedDate: Date | undefined fiatValueInput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean } + showAcceptChanges: boolean + onAcceptChanges: () => void }) { const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto' const [routerPreference] = useRouterPreference() const routes = getRoutingDiagramEntries(trade) + const theme = useTheme() + const { chainId } = useWeb3React() + const nativeCurrency = useNativeCurrency(chainId) + + const label = `${trade.executionPrice.baseCurrency?.symbol} ` + const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}` + const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice)) return ( <> - - - + + + + Exchange rate + + {`1 ${labelInverted} = ${formattedPrice ?? '-'} ${label}`} + + + + + + The fee paid to miners who process your transaction. This must be paid in ${nativeCurrency.symbol}. + + } + > + + Network fee + + + {trade.gasUseEstimateUSD ? `~$${trade.gasUseEstimateUSD}` : '-'} + + + + + The impact your trade has on the market price of this pool.}> + + Price impact + + + + {trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'} + + + + + + + The minimum amount you are guaranteed to receive. If the price slips any further, your transaction + will revert. + + ) : ( + + The maximum amount you are guaranteed to spend. If the price slips any further, your transaction + will revert. + + ) + } + > + + {trade.tradeType === TradeType.EXACT_INPUT ? ( + Minimum received + ) : ( + Maximum sent + )} + + + + {trade.tradeType === TradeType.EXACT_INPUT + ? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}` + : `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`} + + + + + {showAcceptChanges ? ( + + + + + + Price updated + + + + Accept + + + + ) : ( + + - - Confirm Swap - - - - {swapErrorMessage ? : null} - + + + Swap + + + + + {swapErrorMessage ? : null} + + )} > ) } diff --git a/src/components/swap/SwapModalHeader.test.tsx b/src/components/swap/SwapModalHeader.test.tsx index 17889002e3..f5df6a0e4b 100644 --- a/src/components/swap/SwapModalHeader.test.tsx +++ b/src/components/swap/SwapModalHeader.test.tsx @@ -1,83 +1,44 @@ -import { sendAnalyticsEvent } from '@uniswap/analytics' -import { - TEST_ALLOWED_SLIPPAGE, - TEST_RECIPIENT_ADDRESS, - TEST_TRADE_EXACT_INPUT, - TEST_TRADE_EXACT_OUTPUT, -} from 'test-utils/constants' -import { render, screen, within } from 'test-utils/render' -import noop from 'utils/noop' +import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format' +import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants' +import { render, screen } from 'test-utils/render' import SwapModalHeader from './SwapModalHeader' -jest.mock('@uniswap/analytics') -const mockSendAnalyticsEvent = sendAnalyticsEvent as jest.MockedFunction - describe('SwapModalHeader.tsx', () => { - let sendAnalyticsEventMock: jest.Mock - - beforeAll(() => { - sendAnalyticsEventMock = jest.fn() - }) - - it('matches base snapshot for test trade with exact input', () => { + it('matches base snapshot, test trade exact input', () => { const { asFragment } = render( - + ) expect(asFragment()).toMatchSnapshot() + expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument() + expect(screen.getByTestId('INPUT-amount')).toHaveTextContent( + `${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.inputAmount, NumberType.TokenTx)} ${ + TEST_TRADE_EXACT_INPUT.inputAmount.currency.symbol ?? '' + }` + ) + expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent( + `${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.outputAmount, NumberType.TokenTx)} ${ + TEST_TRADE_EXACT_INPUT.outputAmount.currency.symbol ?? '' + }` + ) }) - it('shows accept changes section and logs amplitude event', () => { - const setShouldLogModalCloseEventFn = jest.fn() - mockSendAnalyticsEvent.mockImplementation(sendAnalyticsEventMock) - render( - + it('test trade exact output, no recipient', () => { + const { asFragment } = render( + ) - expect(setShouldLogModalCloseEventFn).toHaveBeenCalledWith(false) - const showAcceptChanges = screen.getByTestId('show-accept-changes') - expect(showAcceptChanges).toBeInTheDocument() - expect(within(showAcceptChanges).getByText('Price Updated')).toBeVisible() - expect(within(showAcceptChanges).getByText('Accept')).toBeVisible() - expect(sendAnalyticsEventMock).toHaveBeenCalledTimes(1) - }) - - it('renders correctly for test trade with exact output and no recipient', () => { - const rendered = render( - - ) - expect(rendered.queryByTestId('recipient-info')).toBeNull() + expect(asFragment()).toMatchSnapshot() expect(screen.getByText(/Input is estimated. You will sell at most/i)).toBeInTheDocument() - expect(screen.getByTestId('input-symbol')).toHaveTextContent( - TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? '' + + expect(screen.getByTestId('INPUT-amount')).toHaveTextContent( + `${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.inputAmount, NumberType.TokenTx)} ${ + TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? '' + }` ) - expect(screen.getByTestId('output-symbol')).toHaveTextContent( - TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? '' + expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent( + `${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.outputAmount, NumberType.TokenTx)} ${ + TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? '' + }` ) - expect(screen.getByTestId('input-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.inputAmount.toExact()) - expect(screen.getByTestId('output-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.outputAmount.toExact()) }) }) diff --git a/src/components/swap/SwapModalHeader.tsx b/src/components/swap/SwapModalHeader.tsx index fd5743c755..b36a3d1c1a 100644 --- a/src/components/swap/SwapModalHeader.tsx +++ b/src/components/swap/SwapModalHeader.tsx @@ -1,216 +1,72 @@ import { Trans } from '@lingui/macro' -import { sendAnalyticsEvent } from '@uniswap/analytics' -import { SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' import { Percent, TradeType } from '@uniswap/sdk-core' +import Column, { AutoColumn } from 'components/Column' import { useUSDPrice } from 'hooks/useUSDPrice' -import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' -import { useEffect, useState } from 'react' -import { AlertTriangle, ArrowDown } from 'react-feather' -import { Text } from 'rebass' import { InterfaceTrade } from 'state/routing/types' -import styled, { useTheme } from 'styled-components/macro' +import { Field } from 'state/swap/actions' +import styled from 'styled-components/macro' +import { Divider, ThemedText } from 'theme' -import { ThemedText } from '../../theme' -import { isAddress, shortenAddress } from '../../utils' -import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact' -import { ButtonPrimary } from '../Button' -import { LightCard } from '../Card' -import { AutoColumn } from '../Column' -import { FiatValue } from '../CurrencyInputPanel/FiatValue' -import CurrencyLogo from '../Logo/CurrencyLogo' -import { RowBetween, RowFixed } from '../Row' -import TradePrice from '../swap/TradePrice' -import { AdvancedSwapDetails } from './AdvancedSwapDetails' -import { SwapShowAcceptChanges, TruncatedText } from './styleds' +import { SwapModalHeaderAmount } from './SwapModalHeaderAmount' -const ArrowWrapper = styled.div` - padding: 4px; - border-radius: 12px; - height: 40px; - width: 40px; - position: relative; - margin-top: -18px; - margin-bottom: -18px; - left: calc(50% - 16px); - display: flex; - justify-content: center; - align-items: center; - background-color: ${({ theme }) => theme.backgroundSurface}; - border: 4px solid; - border-color: ${({ theme }) => theme.backgroundModule}; - z-index: 2; +const Rule = styled(Divider)` + margin: 16px 2px 24px 2px; ` -const formatAnalyticsEventProperties = ( - trade: InterfaceTrade, - priceUpdate: number | undefined, - response: SwapPriceUpdateUserResponse -) => ({ - chain_id: - trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId - ? trade.inputAmount.currency.chainId - : undefined, - response, - token_in_symbol: trade.inputAmount.currency.symbol, - token_out_symbol: trade.outputAmount.currency.symbol, - price_update_basis_points: priceUpdate, -}) +const HeaderContainer = styled(AutoColumn)` + margin-top: 16px; +` export default function SwapModalHeader({ trade, - shouldLogModalCloseEvent, - setShouldLogModalCloseEvent, allowedSlippage, - recipient, - showAcceptChanges, - onAcceptChanges, }: { trade: InterfaceTrade - shouldLogModalCloseEvent: boolean - setShouldLogModalCloseEvent: (shouldLog: boolean) => void allowedSlippage: Percent - recipient: string | null - showAcceptChanges: boolean - onAcceptChanges: () => void }) { - const theme = useTheme() - - const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice) - const [priceUpdate, setPriceUpdate] = useState() - const fiatValueInput = useUSDPrice(trade.inputAmount) const fiatValueOutput = useUSDPrice(trade.outputAmount) - useEffect(() => { - if (!trade.executionPrice.equalTo(lastExecutionPrice)) { - setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice)) - setLastExecutionPrice(trade.executionPrice) - } - }, [lastExecutionPrice, setLastExecutionPrice, trade.executionPrice]) - - useEffect(() => { - if (shouldLogModalCloseEvent && showAcceptChanges) { - sendAnalyticsEvent( - SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, - formatAnalyticsEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED) - ) - } - setShouldLogModalCloseEvent(false) - }, [shouldLogModalCloseEvent, showAcceptChanges, setShouldLogModalCloseEvent, trade, priceUpdate]) - return ( - - - - - - - {trade.inputAmount.toSignificant(6)} - - - - - - {trade.inputAmount.currency.symbol} - - - - - - - - - - - - - - - - - {trade.outputAmount.toSignificant(6)} - - - - - - {trade.outputAmount.currency.symbol} - - - - - - - - - - - - - - - - - {showAcceptChanges ? ( - - - - - - Price Updated - - - - Accept - - - - ) : null} - - - {trade.tradeType === TradeType.EXACT_INPUT ? ( - - - Output is estimated. You will receive at least{' '} - - {trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol} - {' '} - or the transaction will revert. - - - ) : ( - - - Input is estimated. You will sell at most{' '} - - {trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol} - {' '} - or the transaction will revert. - - - )} - - {recipient !== null ? ( - - - - Output will be sent to{' '} - {isAddress(recipient) ? shortenAddress(recipient) : recipient} - - - - ) : null} - + + + You pay} + amount={trade.inputAmount} + usdAmount={fiatValueInput.data} + /> + You receive} + amount={trade.outputAmount} + usdAmount={fiatValueOutput.data} + tooltipText={ + trade.tradeType === TradeType.EXACT_INPUT ? ( + + + Output is estimated. You will receive at least{' '} + + {trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol} + {' '} + or the transaction will revert. + + + ) : ( + + + Input is estimated. You will sell at most{' '} + + {trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol} + {' '} + or the transaction will revert. + + + ) + } + /> + + + ) } diff --git a/src/components/swap/SwapModalHeaderAmount.tsx b/src/components/swap/SwapModalHeaderAmount.tsx new file mode 100644 index 0000000000..8f9821b9a4 --- /dev/null +++ b/src/components/swap/SwapModalHeaderAmount.tsx @@ -0,0 +1,68 @@ +import { formatCurrencyAmount, formatNumber, NumberType } from '@uniswap/conedison/format' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import Column from 'components/Column' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import Row from 'components/Row' +import { MouseoverTooltip } from 'components/Tooltip' +import { useWindowSize } from 'hooks/useWindowSize' +import { PropsWithChildren, ReactNode } from 'react' +import { TextProps } from 'rebass' +import { Field } from 'state/swap/actions' +import styled from 'styled-components/macro' +import { BREAKPOINTS, ThemedText } from 'theme' + +const MAX_AMOUNT_STR_LENGTH = 9 + +export const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>` + cursor: ${({ cursor }) => cursor}; + color: ${({ theme }) => theme.textSecondary}; + margin-right: 8px; +` + +const ResponsiveHeadline = ({ children, ...textProps }: PropsWithChildren) => { + const { width } = useWindowSize() + + if (width && width < BREAKPOINTS.xs) { + return {children} + } + + return {children} +} + +interface AmountProps { + field: Field + tooltipText?: ReactNode + label: ReactNode + amount: CurrencyAmount | undefined + usdAmount?: number +} + +export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field }: AmountProps) { + let formattedAmount = formatCurrencyAmount(amount, NumberType.TokenTx) + if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) { + formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount) + } + + return ( + + + + + {label} + + + + + {formattedAmount} {amount?.currency.symbol} + + {usdAmount && ( + + {formatNumber(usdAmount, NumberType.FiatTokenQuantity)} + + )} + + + {amount?.currency && } + + ) +} diff --git a/src/components/swap/__snapshots__/SwapModalFooter.test.tsx.snap b/src/components/swap/__snapshots__/SwapModalFooter.test.tsx.snap index 0151bc74e8..31853b2747 100644 --- a/src/components/swap/__snapshots__/SwapModalFooter.test.tsx.snap +++ b/src/components/swap/__snapshots__/SwapModalFooter.test.tsx.snap @@ -1,7 +1,172 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] = ` +exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = ` + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 8px; +} + +.c2 { + color: #0D111C; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c7 { + display: inline-block; + height: inherit; +} + +.c5 { + color: #7780A0; + margin-right: 8px; +} + +.c8 { + cursor: help; + color: #7780A0; + margin-right: 8px; +} + +.c1 { + padding: 0 8px; +} + +.c6 { + text-align: right; + overflow-wrap: break-word; +} + + + + + + Exchange rate + + + 1 DEF = 1.00 ABC + + + + + + + + + Network fee + + + + + ~$1.00 + + + + + + + + + Price impact + + + + + 105566.373% + + + + + + + + + Minimum received + + + + + 0.00000000000000098 DEF + + + + .c0 { box-sizing: border-box; margin: 0; @@ -58,6 +223,10 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] margin: !important; } +.c7 { + color: #F5F6FC; +} + .c4 { padding: 16px; width: 100%; @@ -146,106 +315,24 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] } .c6 { - background-color: rgba(250,43,57,0.1); - border-radius: 1rem; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - font-size: 0.825rem; - width: 100%; - padding: 3rem 1.25rem 1rem 1rem; - margin-top: -2rem; - color: #FA2B39; - z-index: -1; -} - -.c6 p { - padding: 0; - margin: 0; - font-weight: 500; -} - -.c7 { - background-color: rgba(250,43,57,0.1); - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - margin-right: 12px; - border-radius: 12px; - min-width: 48px; - height: 48px; + height: 56px; + margin-top: 10px; } - Confirm Swap + Swap - - - - - - - - - - swap error - - `; diff --git a/src/components/swap/__snapshots__/SwapModalHeader.test.tsx.snap b/src/components/swap/__snapshots__/SwapModalHeader.test.tsx.snap index dc389885b0..4c6a2ce2b3 100644 --- a/src/components/swap/__snapshots__/SwapModalHeader.test.tsx.snap +++ b/src/components/swap/__snapshots__/SwapModalHeader.test.tsx.snap @@ -1,21 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact input 1`] = ` +exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] = ` - .c1 { - box-sizing: border-box; - margin: 0; - min-width: 0; - padding: 0.75rem 1rem; -} - -.c5 { + .c3 { box-sizing: border-box; margin: 0; min-width: 0; } -.c6 { +.c4 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -26,99 +19,30 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp -webkit-box-align: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c8 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 0px; -} - -.c16 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: flex-end; - -webkit-box-align: flex-end; - -ms-flex-align: flex-end; - align-items: flex-end; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c7 { -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; + gap: 12px; } -.c9 { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; - margin: -0px; -} - -.c24 { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -.c18 { - color: #0D111C; -} - -.c23 { +.c6 { color: #7780A0; } -.c21 { +.c8 { + color: #0D111C; +} + +.c12 { width: 100%; height: 1px; + border-width: 0; + margin: 0; background-color: #D2D9EE; } .c2 { - width: 100%; - padding: 0.75rem 1rem; - border-radius: 16px; -} - -.c19 { - width: 100%; - padding: 1rem; - border-radius: 16px; -} - -.c3 { - border: 1px solid #E8ECFB; - background-color: #F5F6FC; -} - -.c20 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -130,63 +54,39 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; - gap: 12px; + gap: 24px; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; } .c0 { - display: grid; - grid-auto-rows: auto; - grid-row-gap: 4px; -} - -.c4 { display: grid; grid-auto-rows: auto; grid-row-gap: 8px; } -.c25 { - display: grid; - grid-auto-rows: auto; - grid-row-gap: 8px; - justify-items: flex-start; -} - -.c13 { - border-radius: 12px; - border-radius: 12px; - height: 24px; - width: 50%; - width: 50%; - -webkit-animation: fAQEyV 1.5s infinite; - animation: fAQEyV 1.5s infinite; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; - background: linear-gradient( to left,#E8ECFB 25%,#fff 50%,#E8ECFB 75% ); - will-change: background-position; - background-size: 400%; -} - -.c22 { - display: inline-block; - height: inherit; -} - -.c14 { - border-radius: 4px; - width: 4rem; - height: 1rem; -} - -.c12 { - width: 20px; - height: 20px; +.c11 { + width: 36px; + height: 36px; border-radius: 50%; background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px)); box-shadow: 0 0 1px white; } -.c11 { +.c10 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -194,362 +94,334 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp display: flex; } -.c17 { - background-color: transparent; - border: none; - cursor: pointer; +.c7 { + display: inline-block; + height: inherit; +} + +.c9 { + cursor: help; + color: #7780A0; + margin-right: 8px; +} + +.c13 { + margin: 16px 2px 24px 2px; +} + +.c1 { + margin-top: 16px; +} + + + + + + + + + + You pay + + + + + + + <0.00001 ABC + + + + + + + + + + + + + + You receive + + + + + + + <0.00001 DEF + + + + + + + + + + + +`; + +exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 12px; +} + +.c6 { + color: #7780A0; +} + +.c8 { + color: #0D111C; +} + +.c12 { + width: 100%; + height: 1px; + border-width: 0; + margin: 0; + background-color: #D2D9EE; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; -webkit-box-pack: start; -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; - padding: 0; - grid-template-columns: 1fr auto; - grid-gap: 0.25rem; + gap: 24px; +} + +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - text-align: left; - -webkit-flex-wrap: wrap; - -ms-flex-wrap: wrap; - flex-wrap: wrap; - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c0 { + display: grid; + grid-auto-rows: auto; + grid-row-gap: 8px; +} + +.c11 { + width: 36px; + height: 36px; + border-radius: 50%; + background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px)); + box-shadow: 0 0 1px white; } .c10 { - text-overflow: ellipsis; - max-width: 220px; - overflow: hidden; - text-align: right; -} - -.c15 { - padding: 4px; - border-radius: 12px; - height: 40px; - width: 40px; position: relative; - margin-top: -18px; - margin-bottom: -18px; - left: calc(50% - 16px); display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - background-color: #FFFFFF; - border: 4px solid; - border-color: #F5F6FC; - z-index: 2; +} + +.c7 { + display: inline-block; + height: inherit; +} + +.c9 { + cursor: help; + color: #7780A0; + margin-right: 8px; +} + +.c13 { + margin: 16px 2px 24px 2px; +} + +.c1 { + margin-top: 16px; } - 0.000000000000001 - - - - - - - - ABC - - - - - - - - - - - - - - - - - - - - - - 0.000000000000001 - - - - - - - - DEF - - - - - - - - - - - - - - - - 1 DEF = 1.00 ABC - - - - - - - - - - - - Network fee - - - - - ~$1.00 - - - - - - Minimum output + You pay - 0.00000000000000098 DEF + + <0.00001 ABC + + + + + + - Expected output + You receive - 0.000000000000001 DEF - - - - - - Order routing - - - - - Uniswap API - + + <0.00001 GHI - - - - - Output is estimated. You will receive at least - - 0.00000000000000098 DEF - - or the transaction will revert. - - - - - Output will be sent to - - 0x0000...0004 - + + + `; diff --git a/src/components/swap/styleds.tsx b/src/components/swap/styleds.tsx index fa885b2b4b..b648cd6f3a 100644 --- a/src/components/swap/styleds.tsx +++ b/src/components/swap/styleds.tsx @@ -2,7 +2,6 @@ import { SupportedChainId } from 'constants/chains' import { transparentize } from 'polished' import { ReactNode } from 'react' import { AlertTriangle } from 'react-feather' -import { Text } from 'rebass' import styled, { css } from 'styled-components/macro' import { Z_INDEX } from 'theme/zIndex' @@ -64,13 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>` : null} ` -export const TruncatedText = styled(Text)` - text-overflow: ellipsis; - max-width: 220px; - overflow: hidden; - text-align: right; -` - // styles export const Dots = styled.span` &::after { @@ -136,7 +128,7 @@ export function SwapCallbackError({ error }: { error: ReactNode }) { export const SwapShowAcceptChanges = styled(AutoColumn)` background-color: ${({ theme }) => transparentize(0.95, theme.deprecated_primary3)}; color: ${({ theme }) => theme.accentAction}; - padding: 0.5rem; + padding: 12px; border-radius: 12px; margin-top: 8px; ` diff --git a/src/pages/MigrateV2/MigrateV2Pair.tsx b/src/pages/MigrateV2/MigrateV2Pair.tsx index 08bcb4de6c..e3cddc46bf 100644 --- a/src/pages/MigrateV2/MigrateV2Pair.tsx +++ b/src/pages/MigrateV2/MigrateV2Pair.tsx @@ -46,7 +46,7 @@ import useIsArgentWallet from '../../hooks/useIsArgentWallet' import { useTotalSupply } from '../../hooks/useTotalSupply' import { useTokenBalance } from '../../state/connection/hooks' import { TransactionType } from '../../state/transactions/types' -import { BackArrow, ExternalLink, ThemedText } from '../../theme' +import { BackArrowLink, ExternalLink, ThemedText } from '../../theme' import { isAddress } from '../../utils' import { calculateGasMargin } from '../../utils/calculateGasMargin' import { currencyId } from '../../utils/currencyId' @@ -725,7 +725,7 @@ export default function MigrateV2Pair() { - + Migrate V2 Liquidity diff --git a/src/pages/MigrateV2/index.tsx b/src/pages/MigrateV2/index.tsx index 4f4d6999f3..778854b889 100644 --- a/src/pages/MigrateV2/index.tsx +++ b/src/pages/MigrateV2/index.tsx @@ -20,7 +20,7 @@ import { Dots } from '../../components/swap/styleds' import { V2_FACTORY_ADDRESSES } from '../../constants/addresses' import { useTokenBalancesWithLoadingIndicator } from '../../state/connection/hooks' import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks' -import { BackArrow, StyledInternalLink, ThemedText } from '../../theme' +import { BackArrowLink, StyledInternalLink, ThemedText } from '../../theme' import { BodyWrapper } from '../AppBody' function EmptyState({ message }: { message: ReactNode }) { @@ -116,7 +116,7 @@ export default function MigrateV2() { - + Migrate V2 Liquidity diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 8bf94b444e..f16742c32a 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -571,22 +571,22 @@ export function Swap({ showCancel={true} /> - + {trade && showConfirm && ( + + )} diff --git a/src/theme/components/index.tsx b/src/theme/components/index.tsx index 5b9386908e..0018861c45 100644 --- a/src/theme/components/index.tsx +++ b/src/theme/components/index.tsx @@ -467,14 +467,15 @@ export const SpinnerSVG = styled.svg` ${SpinnerCss} ` -const BackArrowLink = styled(StyledInternalLink)` +const BackArrowIcon = styled(ArrowLeft)` color: ${({ theme }) => theme.textPrimary}; ` -export function BackArrow({ to }: { to: string }) { + +export function BackArrowLink({ to }: { to: string }) { return ( - - - + + + ) } @@ -523,3 +524,11 @@ export const GlowEffect = styled.div` export const CautionTriangle = styled(AlertTriangle)` color: ${({ theme }) => theme.accentWarning}; ` + +export const Divider = styled.div` + width: 100%; + height: 1px; + border-width: 0; + margin: 0; + background-color: ${({ theme }) => theme.backgroundOutline}; +` diff --git a/src/theme/components/text.tsx b/src/theme/components/text.tsx index c015a1a7d1..dff987ac63 100644 --- a/src/theme/components/text.tsx +++ b/src/theme/components/text.tsx @@ -52,6 +52,9 @@ export const ThemedText = { MediumHeader(props: TextProps) { return }, + SubHeaderLarge(props: TextProps) { + return + }, SubHeader(props: TextProps) { return }, diff --git a/src/theme/index.tsx b/src/theme/index.tsx index 3d325da3d6..1f44b4dc0e 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -67,15 +67,18 @@ const fonts = { code: 'courier, courier new, serif', } +const gapValues = { + xs: '4px', + sm: '8px', + md: '12px', + lg: '24px', + xl: '32px', +} +export type Gap = keyof typeof gapValues + function getSettings(darkMode: boolean) { return { - grids: { - xs: '4px', - sm: '8px', - md: '12px', - lg: '24px', - xl: '32px', - }, + grids: gapValues, fonts, // shadows @@ -118,7 +121,7 @@ export const ThemedGlobalStyle = createGlobalStyle` background-color: ${({ theme }) => theme.background} !important; } - summary::-webkit-details-marker { + summary::-webkit-details-marker { display:none; } diff --git a/src/utils/loggingFormatters.ts b/src/utils/loggingFormatters.ts new file mode 100644 index 0000000000..c3ebb6d9e5 --- /dev/null +++ b/src/utils/loggingFormatters.ts @@ -0,0 +1,107 @@ +import { SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' +import { Percent } from '@uniswap/sdk-core' +import { + formatPercentInBasisPointsNumber, + formatPercentNumber, + formatToDecimal, + getDurationFromDateMilliseconds, + getDurationUntilTimestampSeconds, + getTokenAddress, +} from 'lib/utils/analytics' +import { InterfaceTrade } from 'state/routing/types' + +import { RoutingDiagramEntry } from './getRoutingDiagramEntries' +import { computeRealizedPriceImpact } from './prices' + +const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => { + const routesEventProperties: Record = { + routes_percentages: [], + routes_protocols: [], + } + + routes.forEach((route, index) => { + routesEventProperties['routes_percentages'].push(formatPercentNumber(route.percent)) + routesEventProperties['routes_protocols'].push(route.protocol) + routesEventProperties[`route_${index}_input_currency_symbols`] = route.path.map( + (pathStep) => pathStep[0].symbol ?? '' + ) + routesEventProperties[`route_${index}_output_currency_symbols`] = route.path.map( + (pathStep) => pathStep[1].symbol ?? '' + ) + routesEventProperties[`route_${index}_input_currency_addresses`] = route.path.map((pathStep) => + getTokenAddress(pathStep[0]) + ) + routesEventProperties[`route_${index}_output_currency_addresses`] = route.path.map((pathStep) => + getTokenAddress(pathStep[1]) + ) + routesEventProperties[`route_${index}_fee_amounts_hundredths_of_bps`] = route.path.map((pathStep) => pathStep[2]) + }) + + return routesEventProperties +} + +export const formatSwapPriceUpdatedEventProperties = ( + trade: InterfaceTrade, + priceUpdate: number | undefined, + response: SwapPriceUpdateUserResponse +) => ({ + chain_id: + trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId + ? trade.inputAmount.currency.chainId + : undefined, + response, + token_in_symbol: trade.inputAmount.currency.symbol, + token_out_symbol: trade.outputAmount.currency.symbol, + price_update_basis_points: priceUpdate, +}) + +interface AnalyticsEventProps { + trade: InterfaceTrade + hash: string | undefined + allowedSlippage: Percent + transactionDeadlineSecondsSinceEpoch: number | undefined + isAutoSlippage: boolean + isAutoRouterApi: boolean + swapQuoteReceivedDate: Date | undefined + routes: RoutingDiagramEntry[] + fiatValueInput?: number + fiatValueOutput?: number +} + +export const formatSwapButtonClickEventProperties = ({ + trade, + hash, + allowedSlippage, + transactionDeadlineSecondsSinceEpoch, + isAutoSlippage, + isAutoRouterApi, + swapQuoteReceivedDate, + routes, + fiatValueInput, + fiatValueOutput, +}: AnalyticsEventProps) => ({ + estimated_network_fee_usd: trade.gasUseEstimateUSD, + transaction_hash: hash, + transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), + token_in_address: trade ? getTokenAddress(trade.inputAmount.currency) : undefined, + token_out_address: trade ? getTokenAddress(trade.outputAmount.currency) : undefined, + token_in_symbol: trade.inputAmount.currency.symbol, + token_out_symbol: trade.outputAmount.currency.symbol, + token_in_amount: trade ? formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals) : undefined, + token_out_amount: trade ? formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals) : undefined, + token_in_amount_usd: fiatValueInput, + token_out_amount_usd: fiatValueOutput, + price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined, + allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), + is_auto_router_api: isAutoRouterApi, + is_auto_slippage: isAutoSlippage, + chain_id: + trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId + ? trade.inputAmount.currency.chainId + : undefined, + duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate + ? getDurationFromDateMilliseconds(swapQuoteReceivedDate) + : undefined, + swap_quote_block_number: trade.blockNumber, + ...formatRoutesEventProperties(routes), +})
- swap error -