feat: x rollout cleanup (#7582)

* feat: cleanup post x rollout

* feat: remove feature flag

* fix: remove more unused styled components

* fix: delete deprecated value from redux store

* fix: lint

* fix: remove userOptedOutOfUniswapX

* fix: migrate verion in edge case, add test
This commit is contained in:
eddie 2023-11-16 13:59:36 -08:00 committed by GitHub
parent 0f4ca592f2
commit 5ded55e061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 172 additions and 495 deletions

@ -120,10 +120,7 @@ describe('Swap with fees', () => {
describe('UniswapX swaps', () => {
it('displays UniswapX fee in UI', () => {
cy.visit('/swap', {
featureFlags: [
{ name: FeatureFlag.feesEnabled, value: true },
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
],
featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }],
})
// Intercept the trade quote

@ -1,6 +1,5 @@
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { FeatureFlag } from 'featureFlags'
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
@ -45,9 +44,7 @@ function stubSwapTxReceipt() {
describe('UniswapX Toggle', () => {
beforeEach(() => {
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: true }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('displays uniswapx ui when setting is on', () => {
@ -69,9 +66,7 @@ describe('UniswapX Orders', () => {
stubSwapTxReceipt()
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: true }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('can swap exact-in trades using uniswapX', () => {
@ -159,9 +154,7 @@ describe('UniswapX Eth Input', () => {
stubSwapTxReceipt()
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: true }],
})
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`)
})
it('can swap using uniswapX with ETH as input', () => {
@ -247,9 +240,7 @@ describe('UniswapX activity history', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: true }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('can view UniswapX order status progress in activity', () => {

@ -14,7 +14,6 @@ import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
import { useProgressIndicatorV2Flag } from 'featureFlags/flags/progressIndicatorV2'
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabledFlag } from 'featureFlags/flags/useFees'
import { useUpdateAtom } from 'jotai/utils'
@ -324,12 +323,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.uniswapXSyntheticQuote}
label="Force synthetic quotes for UniswapX"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useUniswapXDefaultEnabledFlag()}
featureFlag={FeatureFlag.uniswapXDefaultEnabled}
label="Enable UniswapX by default"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="Info Site Migration">
<FeatureFlagOption

@ -20,7 +20,7 @@ const ReferenceElement = styled.div`
height: inherit;
`
export const Arrow = styled.div`
const Arrow = styled.div`
width: 8px;
height: 8px;
z-index: 9998;

@ -3,11 +3,8 @@ import Column from 'components/Column'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { RowBetween, RowFixed } from 'components/Row'
import Toggle from 'components/Toggle'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/types'
import { useRouterPreference, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { updateDisabledUniswapX, updateOptedOutOfUniswapX } from 'state/user/reducer'
import { useRouterPreference } from 'state/user/hooks'
import styled from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components'
@ -22,12 +19,6 @@ const InlineLink = styled(ThemedText.BodySmall)`
export default function RouterPreferenceSettings() {
const [routerPreference, setRouterPreference] = useRouterPreference()
const dispatch = useAppDispatch()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const isUniswapXOverrideEnabled = isUniswapXDefaultEnabled && !userOptedOutOfUniswapX
const uniswapXInEffect = routerPreference === RouterPreference.X || isUniswapXOverrideEnabled
return (
<RowBetween gap="sm">
@ -46,21 +37,9 @@ export default function RouterPreferenceSettings() {
</RowFixed>
<Toggle
id="toggle-uniswap-x-button"
// If UniswapX-by-default is enabled we need to render this as active even if routerPreference === RouterPreference.API
// because we're going to default to the UniswapX quote.
// If the user manually toggles it off, this doesn't apply.
isActive={uniswapXInEffect}
isActive={routerPreference === RouterPreference.X}
toggle={() => {
if (uniswapXInEffect) {
if (isUniswapXDefaultEnabled) {
// We need to remember if a opts out of UniswapX, so we don't request UniswapX quotes.
dispatch(updateOptedOutOfUniswapX({ optedOutOfUniswapX: true }))
} else {
// We need to remember if a user disables Uniswap X, so we don't show the opt-in flow again.
dispatch(updateDisabledUniswapX({ disabledUniswapX: true }))
}
}
setRouterPreference(uniswapXInEffect ? RouterPreference.API : RouterPreference.X)
setRouterPreference(routerPreference === RouterPreference.X ? RouterPreference.API : RouterPreference.X)
}}
/>
</RowBetween>

@ -4,7 +4,6 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components'
import { Z_INDEX } from 'theme/zIndex'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { AutoColumn } from '../Column'
export const PageWrapper = styled.div`
@ -61,109 +60,6 @@ const SwapWrapperInner = styled.div`
padding-top: 12px;
`
export const UniswapPopoverContainer = styled.div`
padding: 18px;
color: ${({ theme }) => theme.neutral1};
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: ${({ theme }) => theme.surface1};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.surface3};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
position: relative;
overflow: hidden;
`
const springDownKeyframes = `@keyframes spring-down {
0% { transform: translateY(-80px); }
25% { transform: translateY(4px); }
50% { transform: translateY(-1px); }
75% { transform: translateY(0px); }
100% { transform: translateY(0px); }
}`
const backUpKeyframes = `@keyframes back-up {
0% { transform: translateY(0px); }
100% { transform: translateY(-80px); }
}`
export const UniswapXShine = (props: any) => {
const isDarkMode = useIsDarkMode()
return <UniswapXShineInner {...props} style={{ opacity: isDarkMode ? 0.15 : 0.05, ...props.style }} />
}
const UniswapXShineInner = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
background: linear-gradient(130deg, transparent 20%, ${({ theme }) => theme.accent1}, transparent 80%);
opacity: 0.15;
`
// overflow hidden to hide the SwapMustacheShadow
export const SwapOptInSmallContainer = styled.div<{ visible: boolean }>`
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
overflow: hidden;
margin-top: -14px;
transform: translateY(${({ visible }) => (visible ? 0 : -80)}px);
transition: all ease 400ms;
animation: ${({ visible }) => (visible ? `spring-down 900ms ease forwards` : 'back-up 200ms ease forwards')};
${springDownKeyframes}
${backUpKeyframes}
`
export const UniswapXOptInLargeContainerPositioner = styled.div`
position: absolute;
top: 211px;
right: ${-320 - 15}px;
width: 320px;
align-items: center;
min-height: 170px;
display: flex;
pointer-events: none;
`
export const UniswapXOptInLargeContainer = styled.div<{ visible: boolean }>`
opacity: ${({ visible }) => (visible ? 1 : 0)};
transform: ${({ visible }) => `translateY(${visible ? 0 : -6}px)`};
transition: all ease-in 300ms;
transition-delay: ${({ visible }) => (visible ? '350ms' : '0')};
pointer-events: ${({ visible }) => (visible ? 'auto' : 'none')};
`
export const SwapMustache = styled.main`
position: relative;
background: ${({ theme }) => theme.surface1};
border-radius: 16px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border: 1px solid ${({ theme }) => theme.surface3};
border-top-width: 0;
padding: 18px;
padding-top: calc(12px + 18px);
z-index: 0;
transition: transform 250ms ease;
`
export const SwapMustacheShadow = styled.main`
position: absolute;
top: 0;
left: 0;
border-radius: 16px;
height: 100%;
width: 100%;
transform: translateY(-100%);
box-shadow: 0 0 20px 20px ${({ theme }) => theme.surface2};
background: red;
`
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
border-radius: 12px;
height: 40px;

@ -1,9 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXDefaultEnabledFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXDefaultEnabled)
}
export function useUniswapXDefaultEnabled(): boolean {
return useUniswapXDefaultEnabledFlag() === BaseVariant.Enabled
}

@ -16,7 +16,6 @@ export enum FeatureFlag {
infoTDP = 'info_tdp',
infoPoolPage = 'info_pool_page',
infoLiveViews = 'info_live_views',
uniswapXDefaultEnabled = 'uniswapx_default_enabled',
quickRouteMainnet = 'enable_quick_route_mainnet',
progressIndicatorV2 = 'progress_indicator_v2',
feesEnabled = 'fees_enabled',

@ -1,12 +1,10 @@
import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabled } from 'featureFlags/flags/useFees'
import { useMemo } from 'react'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types'
import { currencyAddressForSwapQuote } from 'state/routing/utils'
import { useUserDisabledUniswapX, useUserOptedOutOfUniswapX } from 'state/user/hooks'
/**
* Returns query arguments for the Routing API query or undefined if the
@ -29,9 +27,6 @@ export function useRoutingAPIArguments({
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}): GetQuoteArgs | SkipToken {
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const userDisabledUniswapX = useUserDisabledUniswapX()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const feesEnabled = useFeesEnabled()
// Don't enable fee logic if this is a quote for pricing
@ -56,23 +51,8 @@ export function useRoutingAPIArguments({
tradeType,
needsWrapIfUniswapX: tokenIn.isNative,
uniswapXForceSyntheticQuotes,
userDisabledUniswapX,
userOptedOutOfUniswapX,
isUniswapXDefaultEnabled,
sendPortionEnabled,
},
[
account,
amount,
routerPreference,
tokenIn,
tokenOut,
tradeType,
uniswapXForceSyntheticQuotes,
userDisabledUniswapX,
userOptedOutOfUniswapX,
isUniswapXDefaultEnabled,
sendPortionEnabled,
]
[account, amount, routerPreference, tokenIn, tokenOut, tradeType, uniswapXForceSyntheticQuotes, sendPortionEnabled]
)
}

@ -5,8 +5,7 @@ import ErrorBoundary from 'components/ErrorBoundary'
import Loader from 'components/Icons/LoadingSpinner'
import NavBar, { PageTabs } from 'components/NavBar'
import { UK_BANNER_HEIGHT, UK_BANNER_HEIGHT_MD, UK_BANNER_HEIGHT_SM, UkBanner } from 'components/NavBar/UkBanner'
import { FeatureFlag, useFeatureFlagsIsLoaded } from 'featureFlags'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useFeatureFlagsIsLoaded } from 'featureFlags'
import { useAtom } from 'jotai'
import { useBag } from 'nft/hooks/useBag'
import { lazy, Suspense, useEffect, useLayoutEffect, useMemo, useState } from 'react'
@ -14,9 +13,8 @@ import { Navigate, Route, Routes, useLocation, useSearchParams } from 'react-rou
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import { useAppSelector } from 'state/hooks'
import { AppState } from 'state/reducer'
import { RouterPreference } from 'state/routing/types'
import { useRouterPreference, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { StatsigProvider, StatsigUser, useGate } from 'statsig-react'
import { useRouterPreference } from 'state/user/hooks'
import { StatsigProvider, StatsigUser } from 'statsig-react'
import styled from 'styled-components'
import DarkModeQueryParamReader from 'theme/components/DarkModeQueryParamReader'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
@ -212,9 +210,6 @@ function UserPropertyUpdater() {
const isDarkMode = useIsDarkMode()
const [routerPreference] = useRouterPreference()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const { isLoading: isUniswapXDefaultLoading } = useGate(FeatureFlag.uniswapXDefaultEnabled)
const rehydrated = useAppSelector((state) => state._persist.rehydrated)
useEffect(() => {
@ -248,22 +243,8 @@ function UserPropertyUpdater() {
}, [isDarkMode])
useEffect(() => {
if (isUniswapXDefaultLoading || !rehydrated) return
// If we're not in the transition period to UniswapX opt-out, set the router preference to whatever is specified.
if (!isUniswapXDefaultEnabled) {
if (!rehydrated) return
user.set(CustomUserProperties.ROUTER_PREFERENCE, routerPreference)
return
}
// In the transition period, override the stored API preference to UniswapX if the user hasn't opted out.
if (routerPreference === RouterPreference.API && !userOptedOutOfUniswapX) {
user.set(CustomUserProperties.ROUTER_PREFERENCE, RouterPreference.X)
return
}
// Otherwise, the user has opted out or their preference is UniswapX/client, so set the preference to whatever is specified.
user.set(CustomUserProperties.ROUTER_PREFERENCE, routerPreference)
}, [routerPreference, isUniswapXDefaultEnabled, userOptedOutOfUniswapX, isUniswapXDefaultLoading, rehydrated])
}, [routerPreference, rehydrated])
return null
}

@ -1,207 +0,0 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace } from 'analytics'
import Column from 'components/Column'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { Arrow } from 'components/Popover'
import UniswapXRouterLabel from 'components/RouterLabel/UniswapXRouterLabel'
import Row from 'components/Row'
import {
SwapMustache,
SwapMustacheShadow,
SwapOptInSmallContainer,
UniswapPopoverContainer,
UniswapXOptInLargeContainer,
UniswapXOptInLargeContainerPositioner,
UniswapXShine,
} from 'components/swap/styled'
import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
import { PropsWithChildren, useRef, useState } from 'react'
import { X } from 'react-feather'
import { useLocation } from 'react-router-dom'
import { Text } from 'rebass'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
import { SwapInfo } from 'state/swap/hooks'
import { useRouterPreference, useUserDisabledUniswapX } from 'state/user/hooks'
import { updateDisabledUniswapX } from 'state/user/reducer'
import styled from 'styled-components'
import { ThemedText } from 'theme/components'
export const UniswapXOptIn = (props: { swapInfo: SwapInfo; isSmall: boolean }) => {
const {
trade: { trade },
allowedSlippage,
} = props.swapInfo
const userDisabledUniswapX = useUserDisabledUniswapX()
const isOnClassic = Boolean(trade && isClassicTrade(trade) && trade.isUniswapXBetter && !userDisabledUniswapX)
const [hasEverShown, setHasEverShown] = useState(false)
if (isOnClassic && !hasEverShown) {
setHasEverShown(true)
}
// avoid some work if never needed to show
if (!hasEverShown) {
return null
}
return (
<Trace
shouldLogImpression
name="UniswapX Opt In Impression"
properties={trade ? formatCommonPropertiesForTrade(trade, allowedSlippage) : undefined}
>
<OptInContents isOnClassic={isOnClassic} {...props} />
</Trace>
)
}
const OptInContents = ({
swapInfo,
isOnClassic,
isSmall,
}: {
swapInfo: SwapInfo
isOnClassic: boolean
isSmall: boolean
}) => {
const {
trade: { trade },
allowedSlippage,
} = swapInfo
const [, setRouterPreference] = useRouterPreference()
const dispatch = useAppDispatch()
const [showYoureIn, setShowYoureIn] = useState(false)
const isVisible = isOnClassic
const location = useLocation()
const tryItNowElement = (
<ThemedText.BodySecondary
color="accent1"
fontSize={14}
fontWeight="500"
onClick={() => {
// slight delay before hiding
setTimeout(() => {
setShowYoureIn(true)
setTimeout(() => {
setShowYoureIn(false)
}, 5000)
}, 200)
if (!trade) return
sendAnalyticsEvent('UniswapX Opt In Toggled', {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
new_preference: RouterPreference.X,
})
setRouterPreference(RouterPreference.X)
}}
style={{
cursor: 'pointer',
}}
>
Try it now
</ThemedText.BodySecondary>
)
const containerRef = useRef<HTMLDivElement>()
if (isSmall || location.pathname.includes('/tokens/')) {
return (
<SwapOptInSmallContainer ref={containerRef as any} visible={isVisible}>
<SwapMustache>
<UniswapXShine />
<SwapMustacheShadow />
<Row justify="space-between" align="center" flexWrap="wrap">
<Text fontSize={14} fontWeight={485} lineHeight="20px">
<Trans>Try gas free swaps with the</Trans>
<br />
<UniswapXBrandMark fontWeight="bold" style={{ transform: `translateY(1px)`, margin: '0 2px' }} />{' '}
<Trans>Beta</Trans>
</Text>
{tryItNowElement}
</Row>
</SwapMustache>
</SwapOptInSmallContainer>
)
}
return (
<>
{/* first popover: intro */}
<UniswapXOptInPopover shiny visible={isVisible && !showYoureIn}>
<CloseIcon
size={18}
onClick={() => {
if (!trade) return
sendAnalyticsEvent('UniswapX Opt In Toggled', {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
new_preference: RouterPreference.API,
})
setRouterPreference(RouterPreference.API)
dispatch(updateDisabledUniswapX({ disabledUniswapX: true }))
}}
/>
<Column>
<Text fontSize={14} fontWeight={485} lineHeight="20px">
<Trans>Try the</Trans>{' '}
<UniswapXBrandMark fontWeight="bold" style={{ transform: `translateY(2px)`, margin: '0 1px' }} />{' '}
<Trans>Beta</Trans>
<ul style={{ margin: '5px 0 12px 24px', lineHeight: '24px', padding: 0 }}>
<li>
<Trans>Gas free swaps</Trans>
</li>
<li>
<Trans>MEV protection</Trans>
</li>
<li>
<Trans>Better prices and more liquidity</Trans>
</li>
</ul>
</Text>
</Column>
{tryItNowElement}
</UniswapXOptInPopover>
{/* second popover: you're in! */}
<UniswapXOptInPopover visible={showYoureIn}>
<UniswapXRouterLabel disableTextGradient>
<Text fontSize={14} fontWeight={535} lineHeight="20px">
<Trans>You&apos;re in!</Trans>
</Text>
</UniswapXRouterLabel>
<ThemedText.BodySecondary style={{ marginTop: 8 }} fontSize={14}>
<Trans>You can turn it off at anytime in settings</Trans>
</ThemedText.BodySecondary>
</UniswapXOptInPopover>
</>
)
}
const UniswapXOptInPopover = (props: PropsWithChildren<{ visible: boolean; shiny?: boolean }>) => {
return (
// positioner ensures no matter the height of the inner content
// it sits at the same position from the top of the swap area
<UniswapXOptInLargeContainerPositioner>
<UniswapXOptInLargeContainer visible={props.visible}>
<Arrow className="arrow-right" style={{ position: 'absolute', bottom: '50%', left: -3.5, zIndex: 100 }} />
<UniswapPopoverContainer>
{props.shiny && <UniswapXShine style={{ zIndex: 0 }} />}
{props.children}
</UniswapPopoverContainer>
</UniswapXOptInLargeContainer>
</UniswapXOptInLargeContainerPositioner>
)
}
const CloseIcon = styled(X)`
color: ${({ theme }) => theme.neutral3};
cursor: pointer;
position: absolute;
top: 14px;
right: 14px;
`

@ -33,7 +33,6 @@ import { useConnectionReady } from 'connection/eagerlyConnect'
import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain, isSupportedChain } from 'constants/chains'
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useCurrency, useDefaultActiveTokens } from 'hooks/Tokens'
import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
@ -64,10 +63,8 @@ import { maxAmountSpend } from 'utils/maxAmountSpend'
import { computeRealizedPriceImpact, warningSeverity } from 'utils/prices'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import { useScreenSize } from '../../hooks/useScreenSize'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { OutputTaxTooltipBody } from './TaxTooltipBody'
import { UniswapXOptIn } from './UniswapXOptIn'
export const ArrowContainer = styled.div`
display: inline-flex;
@ -597,9 +594,7 @@ export function Swap({
const inputCurrency = currencies[Field.INPUT] ?? undefined
const switchChain = useSwitchChain()
const switchingChain = useAppSelector((state) => state.wallets.switchingChain)
const showOptInSmall = !useScreenSize().navSearchInputVisible
const isDark = useIsDarkMode()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const [currentTab, setCurrentTab] = useState<SwapTab>(SwapTab.Swap)
@ -832,7 +827,6 @@ export function Swap({
)}
</div>
</AutoColumn>
{!showOptInSmall && !isUniswapXDefaultEnabled && <UniswapXOptIn isSmall={false} swapInfo={swapInfo} />}
</>
)
@ -849,7 +843,6 @@ export function Swap({
/>
{/* todo: build Limit UI */}
{currentTab === SwapTab.Swap ? swapElement : undefined}
{showOptInSmall && !isUniswapXDefaultEnabled && <UniswapXOptIn isSmall swapInfo={swapInfo} />}
</SwapWrapper>
)
}

@ -14,7 +14,7 @@ const defaultState = {
user: {},
_persist: {
rehydrated: true,
version: 4,
version: 5,
},
application: {
chainId: null,

@ -6,6 +6,7 @@ import { migration1 } from './migrations/1'
import { migration2 } from './migrations/2'
import { migration3 } from './migrations/3'
import { migration4 } from './migrations/4'
import { migration5 } from './migrations/5'
import { legacyLocalStorageMigration } from './migrations/legacy'
/**
@ -23,6 +24,7 @@ export const migrations: MigrationManifest = {
2: migration2,
3: migration3,
4: migration4,
5: migration5,
}
// We use a custom migration function for the initial state, because redux-persist

@ -0,0 +1,98 @@
import { createMigrate } from 'redux-persist'
import { RouterPreference } from 'state/routing/types'
import { SlippageTolerance } from 'state/user/types'
import { migration1 } from './1'
import { migration2 } from './2'
import { migration3 } from './3'
import { migration4 } from './4'
import { migration5, PersistAppStateV5 } from './5'
const previousState: PersistAppStateV5 = {
user: {
userRouterPreference: RouterPreference.API,
optedOutOfUniswapX: false,
disabledUniswapX: false,
userLocale: null,
userHideClosedPositions: false,
userSlippageTolerance: SlippageTolerance.Auto,
userSlippageToleranceHasBeenMigratedToAuto: true,
userDeadline: 1800,
tokens: {},
pairs: {},
timestamp: Date.now(),
hideAndroidAnnouncementBanner: false,
},
_persist: {
version: 4,
rehydrated: true,
},
}
describe('migration to v5', () => {
it('should migrate users who currently have `API` router preference', async () => {
const migrator = createMigrate(
{
1: migration1,
2: migration2,
3: migration3,
4: migration4,
5: migration5,
},
{ debug: false }
)
const result: any = await migrator(previousState, 5)
expect(result?.user?.userRouterPreference).toEqual(RouterPreference.X)
expect(result?.user?.disabledUniswapX).toBeUndefined()
expect(result?.user?.optedOutOfUniswapX).toBeUndefined()
expect(result?._persist.version).toEqual(5)
})
it('should not migrate routerPreference if user disabled during rollout', async () => {
const migrator = createMigrate(
{
1: migration1,
2: migration2,
3: migration3,
4: migration4,
5: migration5,
},
{ debug: false }
)
const result: any = await migrator(
{
...previousState,
user: {
...previousState.user,
optedOutOfUniswapX: true,
},
} as PersistAppStateV5,
5
)
expect(result?.user?.userRouterPreference).toEqual(RouterPreference.API)
expect(result?.user?.optedOutOfUniswapX).toBeUndefined()
expect(result?._persist.version).toEqual(5)
})
it('should not migrate user if user does not exist', async () => {
const migrator = createMigrate(
{
1: migration1,
2: migration2,
3: migration3,
4: migration4,
5: migration5,
},
{ debug: false }
)
const result: any = await migrator(
{
...previousState,
user: undefined,
} as PersistAppStateV5,
5
)
expect(result?.user).toBeUndefined()
expect(result?._persist.version).toEqual(5)
})
})

43
src/state/migrations/5.ts Normal file

@ -0,0 +1,43 @@
import { PersistState } from 'redux-persist'
import { RouterPreference } from 'state/routing/types'
import { UserState } from 'state/user/reducer'
export type PersistAppStateV5 = {
_persist: PersistState
} & { user?: UserState & { disabledUniswapX?: boolean; optedOutOfUniswapX?: boolean } }
/**
* Migration to migrate users to UniswapX by default.
*/
export const migration5 = (state: PersistAppStateV5 | undefined) => {
if (!state) return state
// Remove a previously-persisted variable
if (state?.user && 'disabledUniswapX' in state.user) {
delete state.user['disabledUniswapX']
}
const userOptedOutOfUniswapX = state?.user?.optedOutOfUniswapX
if (state?.user && 'optedOutOfUniswapX' in state.user) {
delete state.user['optedOutOfUniswapX']
}
// If the the user has previously disabled UniswapX *during the opt-out rollout period*, we respect that preference.
if (state?.user && !userOptedOutOfUniswapX) {
return {
...state,
user: {
...state.user,
userRouterPreference: RouterPreference.X,
},
_persist: {
...state._persist,
version: 5,
},
}
}
return {
...state,
_persist: {
...state._persist,
version: 5,
},
}
}

@ -44,7 +44,7 @@ export type AppState = ReturnType<typeof appReducer>
const persistConfig: PersistConfig<AppState> = {
key: 'interface',
version: 4, // see migrations.ts for more details about this version
version: 5, // see migrations.ts for more details about this version
storage: localForage.createInstance({
name: 'redux',
}),

@ -89,8 +89,6 @@ interface ExpectedUserState {
timestamp: number
hideAndroidAnnouncementBanner: boolean
showSurveyPopup?: boolean
disabledUniswapX?: boolean
optedOutOfUniswapX?: boolean
originCountry?: string
}

@ -63,9 +63,9 @@ function getRoutingAPIConfig(args: GetQuoteArgs): RoutingConfig {
if (
// If the user has opted out of UniswapX during the opt-out transition period, we should respect that preference and only request classic quotes.
(args.userOptedOutOfUniswapX && routerPreference !== RouterPreference.X) ||
!isUniswapXSupportedChain(tokenInChainId) ||
routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE
routerPreference === RouterPreference.API ||
routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ||
!isUniswapXSupportedChain(tokenInChainId)
) {
return [classic]
}

@ -43,11 +43,6 @@ export interface GetQuoteArgs {
tradeType: TradeType
needsWrapIfUniswapX: boolean
uniswapXForceSyntheticQuotes: boolean
// legacy field indicating the user disabled UniswapX during the opt-in period, or dismissed the UniswapX opt-in modal.
userDisabledUniswapX: boolean
// temporary field indicating the user disabled UniswapX during the transition to the opt-out model
userOptedOutOfUniswapX: boolean
isUniswapXDefaultEnabled: boolean
sendPortionEnabled: boolean
}
@ -197,7 +192,6 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
approveInfo: ApproveInfo
gasUseEstimateUSD?: number // gas estimate for swaps
blockNumber: string | null | undefined
isUniswapXBetter: boolean | undefined
requestId: string | undefined
quoteMethod: QuoteMethod
swapFee: SwapFeeInfo | undefined
@ -205,7 +199,6 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
constructor({
gasUseEstimateUSD,
blockNumber,
isUniswapXBetter,
requestId,
quoteMethod,
approveInfo,
@ -215,7 +208,6 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
gasUseEstimateUSD?: number
totalGasUseEstimateUSD?: number
blockNumber?: string | null
isUniswapXBetter?: boolean
requestId?: string
quoteMethod: QuoteMethod
approveInfo: ApproveInfo
@ -240,7 +232,6 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
super(routes)
this.blockNumber = blockNumber
this.gasUseEstimateUSD = gasUseEstimateUSD
this.isUniswapXBetter = isUniswapXBetter
this.requestId = requestId
this.quoteMethod = quoteMethod
this.approveInfo = approveInfo

@ -3,13 +3,12 @@ import { renderHook } from '@testing-library/react'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { USDC_MAINNET } from 'constants/tokens'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabled } from 'featureFlags/flags/useFees'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import ms from 'ms'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types'
import { useRouterPreference, useUserDisabledUniswapX, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { useRouterPreference } from 'state/user/hooks'
import { ETH_MAINNET } from 'test-utils/constants'
import { mocked } from 'test-utils/mocked'
@ -29,16 +28,12 @@ jest.mock('./slice', () => {
})
jest.mock('state/user/hooks')
jest.mock('featureFlags/flags/uniswapXUseSyntheticQuote')
jest.mock('featureFlags/flags/uniswapXDefault')
jest.mock('featureFlags/flags/useFees')
beforeEach(() => {
mocked(useIsWindowVisible).mockReturnValue(true)
mocked(useRouterPreference).mockReturnValue([RouterPreference.API, () => undefined])
mocked(useUniswapXSyntheticQuoteEnabled).mockReturnValue(false)
mocked(useUserDisabledUniswapX).mockReturnValue(false)
mocked(useUserOptedOutOfUniswapX).mockReturnValue(false)
mocked(useUniswapXDefaultEnabled).mockReturnValue(false)
mocked(useFeesEnabled).mockReturnValue(true)
// @ts-ignore we dont use the response from this hook in useRoutingAPITrade so fine to mock as undefined
mocked(useGetQuoteQuery).mockReturnValue(undefined)
@ -66,9 +61,6 @@ const MOCK_ARGS: GetQuoteArgs = {
tradeType: TradeType.EXACT_INPUT,
needsWrapIfUniswapX: USDCAmount.currency.isNative,
uniswapXForceSyntheticQuotes: false,
userDisabledUniswapX: false,
userOptedOutOfUniswapX: false,
isUniswapXDefaultEnabled: false,
sendPortionEnabled: true,
}

@ -219,18 +219,14 @@ export async function transformQuoteToTrade(
data: URAQuoteResponse,
quoteMethod: QuoteMethod
): Promise<TradeResult> {
const { tradeType, needsWrapIfUniswapX, routerPreference, account, amount, isUniswapXDefaultEnabled } = args
const { tradeType, needsWrapIfUniswapX, routerPreference, account, amount } = args
const showUniswapXTrade = data.routing === URAQuoteType.DUTCH_LIMIT && routerPreference === RouterPreference.X
const showUniswapXTrade =
data.routing === URAQuoteType.DUTCH_LIMIT &&
(routerPreference === RouterPreference.X || (isUniswapXDefaultEnabled && routerPreference === RouterPreference.API))
const [currencyIn, currencyOut] = getTradeCurrencies(args, showUniswapXTrade)
const { gasUseEstimateUSD, blockNumber, routes, gasUseEstimate, swapFee } = getClassicTradeDetails(args, data)
// If the top-level URA quote type is DUTCH_LIMIT, then UniswapX is better for the user
const isUniswapXBetter = data.routing === URAQuoteType.DUTCH_LIMIT
// Some sus javascript float math but it's ok because its just an estimate for display purposes
const usdCostPerGas = gasUseEstimateUSD && gasUseEstimate ? gasUseEstimateUSD / gasUseEstimate : undefined
@ -267,15 +263,14 @@ export async function transformQuoteToTrade(
gasUseEstimateUSD,
approveInfo,
blockNumber,
isUniswapXBetter,
requestId: data.quote.requestId,
quoteMethod,
swapFee,
})
// During the opt-in period, only return UniswapX quotes if the user has turned on the setting,
// even if it is the better quote.
if (isUniswapXBetter && (routerPreference === RouterPreference.X || isUniswapXDefaultEnabled)) {
// If the top-level URA quote type is DUTCH_LIMIT, then UniswapX is better for the user
const isUniswapXBetter = data.routing === URAQuoteType.DUTCH_LIMIT
if (isUniswapXBetter) {
const orderInfo = toDutchOrderInfo(data.quote.orderInfo)
const swapFee = getSwapFee(data.quote)
const wrapInfo = await getWrapInfo(needsWrapIfUniswapX, account, currencyIn.chainId, amount, usdCostPerGas)

@ -77,7 +77,7 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
}
export type SwapInfo = {
type SwapInfo = {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
inputTax: Percent

@ -5,13 +5,7 @@ import store from 'state'
import { RouterPreference } from 'state/routing/types'
import { renderHook } from 'test-utils/render'
import {
deserializeToken,
serializeToken,
useRouterPreference,
useUserDisabledUniswapX,
useUserSlippageTolerance,
} from './hooks'
import { deserializeToken, serializeToken, useRouterPreference, useUserSlippageTolerance } from './hooks'
import { updateUserSlippageTolerance } from './reducer'
import { SlippageTolerance } from './types'
@ -81,12 +75,3 @@ describe('useRouterPreference', () => {
expect(routerPreference).toBe(RouterPreference.X)
})
})
describe('useUserDisabledUniswapX', () => {
it('returns `false` by default', () => {
const {
result: { current: disabledUniswapX },
} = renderHook(() => useUserDisabledUniswapX())
expect(disabledUniswapX).toBe(false)
})
})

@ -217,14 +217,6 @@ export function useHideAndroidAnnouncementBanner(): [boolean, () => void] {
return [hideAndroidAnnouncementBanner, toggleHideAndroidAnnouncementBanner]
}
export function useUserDisabledUniswapX(): boolean {
return useAppSelector((state) => state.user.disabledUniswapX) ?? false
}
export function useUserOptedOutOfUniswapX(): boolean {
return useAppSelector((state) => state.user.optedOutOfUniswapX) ?? false
}
/**
* Given two tokens return the liquidity token that represents its liquidity shares
* @param tokenA one of the two tokens

@ -48,10 +48,6 @@ export interface UserState {
timestamp: number
hideAndroidAnnouncementBanner: boolean
// legacy field indicating the user disabled UniswapX during the opt-in period, or dismissed the UniswapX opt-in modal.
disabledUniswapX?: boolean
// temporary field indicating the user disabled UniswapX during the transition to the opt-out model
optedOutOfUniswapX?: boolean
// undefined means has not gone through A/B split yet
showSurveyPopup?: boolean
@ -111,12 +107,6 @@ const userSlice = createSlice({
updateHideAndroidAnnouncementBanner(state, action) {
state.hideAndroidAnnouncementBanner = action.payload.hideAndroidAnnouncementBanner
},
updateDisabledUniswapX(state, action) {
state.disabledUniswapX = action.payload.disabledUniswapX
},
updateOptedOutOfUniswapX(state, action) {
state.optedOutOfUniswapX = action.payload.optedOutOfUniswapX
},
addSerializedToken(state, { payload: { serializedToken } }) {
if (!state.tokens) {
state.tokens = {}
@ -153,7 +143,5 @@ export const {
updateUserLocale,
updateUserSlippageTolerance,
updateHideAndroidAnnouncementBanner,
updateDisabledUniswapX,
updateOptedOutOfUniswapX,
} = userSlice.actions
export default userSlice.reducer