Compare commits

...

14 Commits

Author SHA1 Message Date
aballerr
4529e3cc88 fix: failing cypress test (#5715)
* fix failing cypress test for 404 page
2022-12-16 16:49:23 -05:00
Mike Grabowski
4d47470f33 feat: not found page (#5708)
* chore: save

* save

* chore: finish

* chore: Fix link

* chore: remove div

* chore: tweaaks

* chore: tweaks
2022-12-16 11:21:29 -05:00
eddie
aedc020646 fix: use render function for SearchBar placeholder translation (#5710)
* fix: use render function for SearchBar placeholder translation

* fix: use render function for SearchBar placeholder translation

* fix: correct clsx usage
2022-12-16 11:07:42 -05:00
Zach Pomerantz
fd8085722e fix: mark permit not syncing if not permitted (#5706)
* fix: mark permit not syncing if not permitted

* fix: clarify naming

* fix: show approval when loading
2022-12-15 13:09:39 -08:00
Mike Grabowski
a60a85db54 fix: layout padding/margin & overflow (#5707)
* chore: fix

* chore: tweaks
2022-12-15 15:34:39 -05:00
eddie
ad2472eac6 fix: correct color for selected token in CurrencySearchModal (#5705)
Co-authored-by: Eddie Dugan <eddie.dugan@UniswapdieDugan.localdomain>
2022-12-15 15:14:16 -05:00
eddie
f4d4acacae fix: constrain width on token details back button (#5703)
Co-authored-by: Eddie Dugan <eddie.dugan@UniswapdieDugan.localdomain>
2022-12-15 14:46:04 -05:00
lynn
a5d7af192c fix: Web 1610 token details another funky state chart re prices (#5685)
* initial commit

* fixes

* move msg loc depending on display price avail

* fred copywriting + jordan comments changes

* fix build errors

Co-authored-by: cartcrom <cartergcromer@gmail.com>
2022-12-15 14:23:03 -05:00
Zach Pomerantz
21a2863ae3 build: default flags but maintain togglability (#5702)
fix: default flags but maintain togglability
2022-12-15 10:56:06 -08:00
Zach Pomerantz
1f871d4e73 build: upgrade widget to 2.22.11 (#5701) 2022-12-15 10:49:31 -08:00
Vignesh Mohankumar
3690936aff chore: remove landing page flag (#5673) 2022-12-15 13:48:50 -05:00
Vignesh Mohankumar
e95e2321b4 fix: used sticky position for landing page content (#5699)
* fix: update padding to 80px on mobile landing

* try 100

* try 120

* 140

* try sticky
2022-12-15 13:29:55 -05:00
Zach Pomerantz
8b1bf09ff1 fix: await syncing allowance to update permit state (#5689)
* fix: await syncing allowance to update permit state

* fix: clarify isSyncing on Permit2

* fix: further clarify isApprovalSyncing
2022-12-15 09:48:18 -08:00
aballerr
6383e9e4bf fix: reverting some changes to wallet dropdown (#5694)
reverting some changes to wallet dropdown cypress tests
2022-12-15 12:15:44 -05:00
35 changed files with 618 additions and 417 deletions

View File

@@ -3,8 +3,8 @@ describe('Redirect', () => {
cy.visit('/create-proposal')
cy.url().should('match', /\/vote\/create-proposal/)
})
it('should redirect to /swap when visiting nonexist url', () => {
it('should redirect to /not-found when visiting nonexist url', () => {
cy.visit('/none-exist-url')
cy.url().should('match', /\/swap/)
cy.url().should('match', /\/not-found/)
})
})

View File

@@ -1,7 +1,7 @@
import { getTestSelector } from '../utils'
describe.skip('Wallet Dropdown', () => {
beforeEach(() => {
describe('Wallet Dropdown', () => {
before(() => {
cy.visit('/pool')
})
@@ -12,7 +12,6 @@ describe.skip('Wallet Dropdown', () => {
})
it('should select a language', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
@@ -22,24 +21,20 @@ describe.skip('Wallet Dropdown', () => {
})
it('should be able to view transactions', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-transactions')).click()
cy.get(getTestSelector('wallet-empty-transaction-text')).should('exist')
cy.get(getTestSelector('wallet-back')).click()
})
it('should change the theme when not connected', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Dark theme').should('exist')
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
})
it('should select a language when not connected', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
@@ -49,8 +44,6 @@ describe.skip('Wallet Dropdown', () => {
})
it('should open the wallet connect modal from the drop down when not connected', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-connect-wallet')).click()
cy.get(getTestSelector('wallet-modal')).should('exist')
cy.get(getTestSelector('wallet-modal-close')).click()

View File

@@ -156,7 +156,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "2.22.10",
"@uniswap/widgets": "2.22.11",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -79,6 +79,13 @@ export const ButtonPrimary = styled(BaseButton)`
}
`
export const SmallButtonPrimary = styled(ButtonPrimary)`
width: auto;
font-size: 16px;
padding: 10px 16px;
border-radius: 12px;
`
export const ButtonLight = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.accentAction};

View File

@@ -1,6 +1,7 @@
import { Group } from '@visx/group'
import { LinePath } from '@visx/shape'
import { easeCubicInOut } from 'd3'
import { easeSinOut } from 'd3'
import ms from 'ms.macro'
import React from 'react'
import { useEffect, useRef, useState } from 'react'
import { animated, useSpring } from 'react-spring'
@@ -11,8 +12,8 @@ import { LineChartProps } from './LineChart'
type AnimatedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'>
const config = {
duration: 800,
easing: easeCubicInOut,
duration: ms`0.8s`,
easing: easeSinOut,
}
// code reference: https://airbnb.io/visx/lineradial
@@ -91,4 +92,4 @@ function AnimatedInLineChart<T>({
)
}
export default AnimatedInLineChart
export default React.memo(AnimatedInLineChart) as typeof AnimatedInLineChart

View File

@@ -0,0 +1,86 @@
import { Group } from '@visx/group'
import { LinePath } from '@visx/shape'
import { easeCubicInOut } from 'd3'
import React from 'react'
import { useEffect, useRef, useState } from 'react'
import { animated, useSpring } from 'react-spring'
import { useTheme } from 'styled-components/macro'
import { LineChartProps } from './LineChart'
type FadedInLineChartProps<T> = Omit<LineChartProps<T>, 'height' | 'width' | 'children'> & { dashed?: boolean }
const config = {
duration: 3000,
easing: easeCubicInOut,
}
// code reference: https://airbnb.io/visx/lineradial
function FadedInLineChart<T>({
data,
getX,
getY,
marginTop,
curve,
color,
strokeWidth,
dashed,
}: FadedInLineChartProps<T>) {
const lineRef = useRef<SVGPathElement>(null)
const [lineLength, setLineLength] = useState(0)
const [shouldAnimate, setShouldAnimate] = useState(false)
const [hasAnimatedIn, setHasAnimatedIn] = useState(false)
const spring = useSpring({
frame: shouldAnimate ? 0 : 1,
config,
onRest: () => {
setShouldAnimate(false)
setHasAnimatedIn(true)
},
})
// We need to check to see after the "invisble" line has been drawn
// what the length is to be able to animate in the line for the first time
// This will run on each render to see if there is a new line length
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (lineRef.current) {
const length = lineRef.current.getTotalLength()
if (length !== lineLength) {
setLineLength(length)
}
if (length > 0 && !shouldAnimate && !hasAnimatedIn) {
setShouldAnimate(true)
}
}
})
const theme = useTheme()
const lineColor = color ?? theme.accentAction
return (
<Group top={marginTop}>
<LinePath curve={curve} x={getX} y={getY}>
{({ path }) => {
const d = path(data) || ''
return (
<>
<animated.path
d={d}
ref={lineRef}
strokeWidth={strokeWidth}
strokeOpacity={hasAnimatedIn ? 1 : spring.frame.to((v) => 1 - v)}
fill="none"
stroke={lineColor}
strokeDasharray={dashed ? '4,4' : undefined}
/>
</>
)
}}
</LinePath>
</Group>
)
}
export default React.memo(FadedInLineChart) as typeof FadedInLineChart

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import * as Sentry from '@sentry/react'
import { ButtonLight, ButtonPrimary } from 'components/Button'
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
import { ChevronUpIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react'
@@ -23,13 +23,6 @@ const BodyWrapper = styled.div<{ margin?: string }>`
padding: 1rem;
`
const SmallButtonPrimary = styled(ButtonPrimary)`
width: auto;
font-size: 16px;
padding: 10px 16px;
border-radius: 12px;
`
const SmallButtonLight = styled(ButtonLight)`
font-size: 16px;
padding: 10px 16px;

View File

@@ -1,5 +1,4 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@@ -203,12 +202,6 @@ export default function FeatureFlagModal() {
<X size={24} />
</CloseButton>
</Header>
<FeatureFlagOption
variant={LandingPageVariant}
value={useLandingPageFlag()}
featureFlag={FeatureFlag.landingPage}
label="Landing page"
/>
<FeatureFlagOption
variant={Permit2Variant}
value={usePermit2Flag()}

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, SectionName } from '@uniswap/analytics-events'
import clsx from 'clsx'
@@ -156,9 +156,9 @@ export const SearchBar = () => {
>
<Row
className={clsx(
` ${styles.nftSearchBar} ${!isOpen && !isMobile && magicalGradientOnHover} ${
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
} `
styles.nftSearchBar,
!isOpen && !isMobile && magicalGradientOnHover,
isMobileOrTablet && (isOpen ? styles.visible : styles.hidden)
)}
borderRadius={isOpen || isMobileOrTablet ? undefined : '12'}
borderTopRightRadius={isOpen && !isMobile ? '12' : undefined}
@@ -182,18 +182,23 @@ export const SearchBar = () => {
element={ElementName.NAVBAR_SEARCH_INPUT}
properties={{ ...trace }}
>
<Box
as="input"
placeholder={placeholderText}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
!isOpen && toggleOpen()
setSearchValue(event.target.value)
}}
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
value={searchValue}
ref={inputRef}
width="full"
<Trans
id={placeholderText}
render={({ translation }) => (
<Box
as="input"
placeholder={translation as string}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
!isOpen && toggleOpen()
setSearchValue(event.target.value)
}}
onBlur={() => sendAnalyticsEvent(EventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)}
className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`}
value={searchValue}
ref={inputRef}
width="full"
/>
)}
/>
</TraceEvent>
{!isOpen && <KeyShortCut>/</KeyShortCut>}

View File

@@ -9,14 +9,12 @@ import { AutoColumn } from '../Column'
import ClaimPopup from './ClaimPopup'
import PopupItem from './PopupItem'
const MobilePopupWrapper = styled.div<{ height: string | number }>`
const MobilePopupWrapper = styled.div`
position: relative;
max-width: 100%;
height: ${({ height }) => height};
margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
margin: 0 auto;
display: none;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: block;
padding-top: 20px;
@@ -74,16 +72,18 @@ export default function Popups() {
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</FixedPopupColumn>
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner>
{activePopups // reverse so new items up front
.slice(0)
.reverse()
.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</MobilePopupInner>
</MobilePopupWrapper>
{activePopups?.length > 0 && (
<MobilePopupWrapper>
<MobilePopupInner>
{activePopups // reverse so new items up front
.slice(0)
.reverse()
.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</MobilePopupInner>
</MobilePopupWrapper>
)}
</>
)
}

View File

@@ -18,7 +18,7 @@ const MobileWrapper = styled(AutoColumn)`
`
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? theme.accentAction : theme.backgroundOutline)};
border: 1px solid ${({ theme, disable }) => (disable ? theme.accentActive : theme.backgroundOutline)};
border-radius: 16px;
display: flex;
padding: 6px;
@@ -30,8 +30,8 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
background-color: ${({ theme }) => theme.hoverDefault};
}
color: ${({ theme, disable }) => disable && theme.accentAction};
background-color: ${({ theme, disable }) => disable && theme.accentActionSoft};
color: ${({ theme, disable }) => disable && theme.accentActive};
background-color: ${({ theme, disable }) => disable && theme.accentActiveSoft};
`
const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({

View File

@@ -11,6 +11,7 @@ export const BreadcrumbNavLink = styled(Link)`
text-decoration: none;
margin-bottom: 16px;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
width: fit-content;
&:hover {
color: ${({ theme }) => theme.textTertiary};

View File

@@ -4,11 +4,11 @@ import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { isPricePoint, PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import { startTransition, Suspense, useMemo, useState } from 'react'
import { pageTimePeriodAtom } from 'pages/TokenDetails'
import { startTransition, Suspense, useMemo } from 'react'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { filterTimeAtom } from '../state'
import PriceChart from './PriceChart'
import { PriceChart } from './PriceChart'
import TimePeriodSelector from './TimeSelector'
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined {
@@ -58,7 +58,7 @@ function Chart({
}) {
const prices = usePreloadedTokenPriceQuery(priceQueryReference)
// Initializes time period to global & maintain separate time period for subsequent changes
const [timePeriod, setTimePeriod] = useState(useAtomValue(filterTimeAtom))
const timePeriod = useAtomValue(pageTimePeriodAtom)
return (
<ChartContainer>
@@ -69,7 +69,6 @@ function Chart({
currentTimePeriod={timePeriod}
onTimeChange={(t: TimePeriod) => {
startTransition(() => refetchTokenPrices(t))
setTimePeriod(t)
}}
/>
</ChartContainer>

View File

@@ -1,11 +1,11 @@
import { Trans } from '@lingui/macro'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { AxisBottom, TickFormatter } from '@visx/axis'
import { localPoint } from '@visx/event'
import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import FadedInLineChart from 'components/Charts/FadeInLineChart'
import { bisect, curveCardinal, NumberValue, scaleLinear, timeDay, timeHour, timeMinute, timeMonth } from 'd3'
import { PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
@@ -13,6 +13,8 @@ import { useActiveLocale } from 'hooks/useActiveLocale'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { textFadeIn } from 'theme/styles'
import {
dayHourFormatter,
hourFormatter,
@@ -21,6 +23,7 @@ import {
monthYearDayFormatter,
weekFormatter,
} from 'utils/formatChartTimes'
import { formatDollar } from 'utils/formatNumbers'
const DATA_EMPTY = { value: 0, timestamp: 0 }
@@ -68,11 +71,19 @@ export const DeltaText = styled.span<{ delta: number | undefined }>`
const ChartHeader = styled.div`
position: absolute;
${textFadeIn};
animation-duration: ${({ theme }) => theme.transition.duration.medium};
`
export const TokenPrice = styled.span`
font-size: 36px;
line-height: 44px;
`
const MissingPrice = styled(TokenPrice)`
font-size: 24px;
line-height: 44px;
color: ${({ theme }) => theme.textTertiary};
`
const DeltaContainer = styled.div`
height: 16px;
display: flex;
@@ -84,6 +95,29 @@ export const ArrowCell = styled.div`
display: flex;
`
function fixChart(prices: PricePoint[] | undefined | null) {
if (!prices) return { prices: null, blanks: [] }
const fixedChart: PricePoint[] = []
const blanks: PricePoint[][] = []
let lastValue: PricePoint | undefined = undefined
for (let i = 0; i < prices.length; i++) {
if (prices[i].value !== 0) {
if (fixedChart.length === 0 && i !== 0) {
blanks.push([{ ...prices[0], value: prices[i].value }, prices[i]])
}
lastValue = prices[i]
fixedChart.push(prices[i])
}
}
if (lastValue && lastValue !== prices[prices.length - 1]) {
blanks.push([lastValue, { ...prices[prices.length - 1], value: lastValue.value }])
}
return { prices: fixedChart, blanks }
}
const margin = { top: 100, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
@@ -94,24 +128,30 @@ interface PriceChartProps {
timePeriod: TimePeriod
}
function formatDisplayPrice(value: number) {
const str = value.toFixed(9)
const [digits, decimals] = str.split('.')
// Displays longer string for numbers < $2 to show changes in both stablecoins & small values
if (digits === '0' || digits === '1')
return `$${digits + '.' + decimals.substring(0, 2) + decimals.substring(2).replace(/0+$/, '')}`
return formatUSDPrice(value)
}
function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
export function PriceChart({ width, height, prices: originalPrices, timePeriod }: PriceChartProps) {
const locale = useActiveLocale()
const theme = useTheme()
const { prices, blanks } = useMemo(
() => (originalPrices && originalPrices.length > 0 ? fixChart(originalPrices) : { prices: null, blanks: [] }),
[originalPrices]
)
const chartAvailable = !!prices && prices.length > 0
const missingPricesMessage = !chartAvailable ? (
prices?.length === 0 ? (
<>
<Trans>Missing price data due to recently low trading volume on Uniswap v3</Trans>
</>
) : (
<Trans>Missing chart data</Trans>
)
) : null
// first price point on the x-axis of the current time period's chart
const startingPrice = prices?.[0] ?? DATA_EMPTY
const startingPrice = originalPrices?.[0] ?? DATA_EMPTY
// last price point on the x-axis of the current time period's chart
const endingPrice = prices?.[prices.length - 1] ?? DATA_EMPTY
const endingPrice = originalPrices?.[originalPrices.length - 1] ?? DATA_EMPTY
const [displayPrice, setDisplayPrice] = useState(startingPrice)
// set display price to ending price when prices have changed.
@@ -133,9 +173,9 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const rdScale = useMemo(
() =>
scaleLinear()
.domain(getPriceBounds(prices ?? []))
.domain(getPriceBounds(originalPrices ?? []))
.range([graphInnerHeight, 0]),
[prices, graphInnerHeight]
[originalPrices, graphInnerHeight]
)
function tickFormat(
@@ -221,7 +261,6 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const arrow = getDeltaArrow(delta)
const crosshairEdgeMax = width * 0.85
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
const hasData = prices && prices.length > 0
/*
* Default curve doesn't look good for the HOUR chart.
@@ -233,27 +272,27 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const getX = useMemo(() => (p: PricePoint) => timeScale(p.timestamp), [timeScale])
const getY = useMemo(() => (p: PricePoint) => rdScale(p.value), [rdScale])
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
return (
<>
<ChartHeader>
<TokenPrice>{formatDisplayPrice(displayPrice.value)}</TokenPrice>
<DeltaContainer>
<ArrowCell>{arrow}</ArrowCell>
<DeltaText delta={delta}>{formattedDelta}</DeltaText>
</DeltaContainer>
{displayPrice.value ? (
<>
<TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
<DeltaContainer>
{formattedDelta}
<ArrowCell>{arrow}</ArrowCell>
</DeltaContainer>
</>
) : (
<>
<MissingPrice>Price Unavailable</MissingPrice>
<ThemedText.Caption style={{ color: theme.textTertiary }}>{missingPricesMessage}</ThemedText.Caption>
</>
)}
</ChartHeader>
{!hasData ? (
<MissingPriceChart
width={width}
height={graphHeight}
message={
prices?.length === 0 ? (
<Trans>This token doesn&apos;t have chart data because it hasn&apos;t been traded on Uniswap v3</Trans>
) : (
<Trans>Missing chart data</Trans>
)
}
/>
{!chartAvailable ? (
<MissingPriceChart width={width} height={graphHeight} message={!!displayPrice.value && missingPricesMessage} />
) : (
<svg width={width} height={graphHeight} style={{ minWidth: '100%' }}>
<AnimatedInLineChart
@@ -264,6 +303,19 @@ function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
curve={curve}
strokeWidth={2}
/>
{blanks.map((blank, index) => (
<FadedInLineChart
key={index}
data={blank}
getX={getX}
getY={getY}
marginTop={margin.top}
curve={curve}
strokeWidth={2}
color={theme.textTertiary}
dashed
/>
))}
{crosshair !== null ? (
<g>
<AxisBottom
@@ -354,9 +406,7 @@ const StyledMissingChart = styled.svg`
font-weight: 400;
}
`
const chartBottomPadding = 15
function MissingPriceChart({ width, height, message }: { width: number; height: number; message: ReactNode }) {
const theme = useTheme()
const midPoint = height / 2 + 45
@@ -369,18 +419,10 @@ function MissingPriceChart({ width, height, message }: { width: number; height:
fill="transparent"
strokeWidth="2"
/>
<TrendingUp stroke={theme.textTertiary} x={0} size={12} y={height - chartBottomPadding - 10} />
{message && <TrendingUp stroke={theme.textTertiary} x={0} size={12} y={height - chartBottomPadding - 10} />}
<text y={height - chartBottomPadding} x="20" fill={theme.textTertiary}>
{message || <Trans>Missing chart data</Trans>}
{message}
</text>
<path
d={`M 0 ${height - 1}, ${width} ${height - 1}`}
stroke={theme.backgroundOutline}
fill="transparent"
strokeWidth="1"
/>
</StyledMissingChart>
)
}
export default PriceChart

View File

@@ -72,6 +72,8 @@ export const TokenInfoContainer = styled.div`
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
${textFadeIn};
animation-duration: ${({ theme }) => theme.transition.duration.medium};
`
export const TokenNameCell = styled.div`
display: flex;
@@ -79,7 +81,6 @@ export const TokenNameCell = styled.div`
font-size: 20px;
line-height: 28px;
align-items: center;
${textFadeIn}
`
/* Loading state bubbles */
const DetailBubble = styled(LoadingBubble)`

View File

@@ -12,7 +12,6 @@ export const PageWrapper = styled.div`
padding: 68px 8px 0px;
max-width: 480px;
width: 100%;
height: 100vh;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
padding-top: 48px;

View File

@@ -1,5 +1,4 @@
export enum FeatureFlag {
traceJsonRpc = 'traceJsonRpc',
landingPage = 'landingPage',
permit2 = 'permit2',
}

View File

@@ -1,7 +0,0 @@
import { BaseVariant } from '../index'
export function useLandingPageFlag(): BaseVariant {
return BaseVariant.Enabled
}
export { BaseVariant as LandingPageVariant }

View File

@@ -55,12 +55,13 @@ export enum BaseVariant {
Enabled = 'enabled',
}
export function useBaseFlag(flag: string): BaseVariant {
export function useBaseFlag(flag: string, defaultValue = BaseVariant.Control): BaseVariant {
switch (useFeatureFlagsContext().flags[flag]) {
case 'enabled':
return BaseVariant.Enabled
case 'control':
default:
return BaseVariant.Control
default:
return defaultValue
}
}

View File

@@ -5,22 +5,30 @@ import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useHasPendingApproval } from 'state/transactions/hooks'
import { ApproveTransactionInfo } from 'state/transactions/types'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'
enum SyncState {
PENDING,
SYNCING,
SYNCED,
}
export enum PermitState {
INVALID,
LOADING,
PERMIT_NEEDED,
PERMITTED,
APPROVAL_OR_PERMIT_NEEDED,
APPROVAL_LOADING,
APPROVED_AND_PERMITTED,
}
export interface Permit {
state: PermitState
signature?: PermitSignature
callback?: (sPendingApproval: boolean) => Promise<{
callback?: () => Promise<{
response: ContractTransaction
info: ApproveTransactionInfo
} | void>
@@ -28,7 +36,7 @@ export interface Permit {
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
const { account } = useWeb3React()
const tokenAllowance = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const isAllowed = useMemo(
() => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)),
@@ -71,32 +79,62 @@ export default function usePermit(amount?: CurrencyAmount<Token>, spender?: stri
true
)
const callback = useCallback(
async (isPendingApproval: boolean) => {
let info
if (!isAllowed && !isPendingApproval) {
info = await updateTokenAllowance()
}
if (!isPermitted && !isSigned) {
await updatePermitAllowance()
}
return info
},
[isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance]
)
// Permit2 should be marked syncing from the time approval is submitted (pending) until it is
// synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval.
const [syncState, setSyncState] = useState(SyncState.SYNCED)
const isApprovalLoading = syncState !== SyncState.SYNCED
const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS)
useEffect(() => {
if (hasPendingApproval) {
setSyncState(SyncState.PENDING)
} else {
setSyncState((state) => {
if (state === SyncState.PENDING && isApprovalSyncing) {
return SyncState.SYNCING
} else if (state === SyncState.SYNCING && !isApprovalSyncing) {
return SyncState.SYNCED
} else {
return state
}
})
}
}, [hasPendingApproval, isApprovalSyncing])
const callback = useCallback(async () => {
let info
if (!isAllowed && !hasPendingApproval) {
info = await updateTokenAllowance()
}
if (!isPermitted && !isSigned) {
await updatePermitAllowance()
}
return info
}, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance])
return useMemo(() => {
if (!amount) {
return { state: PermitState.INVALID }
} else if (!tokenAllowance || !permitAllowance) {
return { state: PermitState.LOADING }
} else if (isAllowed) {
if (isPermitted) {
return { state: PermitState.PERMITTED }
} else if (isSigned) {
return { state: PermitState.PERMITTED, signature }
} else if (!(isPermitted || isSigned)) {
return { state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback }
} else if (!isAllowed) {
return {
state: isApprovalLoading ? PermitState.APPROVAL_LOADING : PermitState.APPROVAL_OR_PERMIT_NEEDED,
callback,
}
} else {
return { state: PermitState.APPROVED_AND_PERMITTED, signature: isPermitted ? undefined : signature }
}
return { state: PermitState.PERMIT_NEEDED, callback }
}, [amount, callback, isAllowed, isPermitted, isSigned, permitAllowance, signature, tokenAllowance])
}, [
amount,
callback,
isAllowed,
isApprovalLoading,
isPermitted,
isSigned,
permitAllowance,
signature,
tokenAllowance,
])
}

View File

@@ -8,16 +8,23 @@ import { calculateGasMargin } from 'utils/calculateGasMargin'
import { useTokenContract } from './useContract'
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): CurrencyAmount<Token> | undefined {
export function useTokenAllowance(
token?: Token,
owner?: string,
spender?: string
): {
tokenAllowance: CurrencyAmount<Token> | undefined
isSyncing: boolean
} {
const contract = useTokenContract(token?.address, false)
const inputs = useMemo(() => [owner, spender], [owner, spender])
const allowance = useSingleCallResult(contract, 'allowance', inputs).result
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
return useMemo(
() => (token && allowance ? CurrencyAmount.fromRawAmount(token, allowance.toString()) : undefined),
[token, allowance]
)
return useMemo(() => {
const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
return { tokenAllowance, isSyncing }
}, [isSyncing, result, token])
}
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {

View File

@@ -25,22 +25,22 @@ function useApprovalStateForSpender(
const { account } = useWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const { tokenAllowance } = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useIsPendingApproval(token, spender)
return useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
if (!tokenAllowance) return ApprovalState.UNKNOWN
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove)
// amountToApprove will be defined if tokenAllowance is
return tokenAllowance.lessThan(amountToApprove)
? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
}, [amountToApprove, pendingApproval, spender, tokenAllowance])
}
export function useApproval(

View File

@@ -4,7 +4,6 @@ import Loader from 'components/Loader'
import { MenuDropdown } from 'components/NavBar/MenuDropdown'
import TopLevelModals from 'components/TopLevelModals'
import { useFeatureFlagsIsLoaded } from 'featureFlags'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { Box } from 'nft/components/Box'
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
@@ -37,6 +36,7 @@ import Manage from './Earn/Manage'
import Landing from './Landing'
import MigrateV2 from './MigrateV2'
import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
import NotFound from './NotFound'
import Pool from './Pool'
import { PositionPage } from './Pool/PositionPage'
import PoolV2 from './Pool/v2'
@@ -65,29 +65,19 @@ initializeAnalytics(ANALYTICS_DUMMY_KEY, OriginApplication.INTERFACE, {
isProductionEnv: isProductionEnv(),
})
const AppWrapper = styled.div`
display: flex;
flex-flow: column;
align-items: flex-start;
height: 100%;
`
const BodyWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 72px 0px 0px 0px;
min-height: 100vh;
padding: ${({ theme }) => theme.navHeight}px 0px 5rem 0px;
align-items: center;
flex: 1;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
padding: 52px 0px 16px 0px;
`};
`
const MobileBottomBar = styled.div`
z-index: ${Z_INDEX.sticky};
position: sticky;
position: fixed;
display: flex;
bottom: 0;
right: 0;
@@ -115,10 +105,6 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
z-index: ${Z_INDEX.sticky};
`
const Marginer = styled.div`
margin-top: 5rem;
`
function getCurrentPageFromLocation(locationPathname: string): PageName | undefined {
switch (true) {
case locationPathname.startsWith('/swap'):
@@ -202,134 +188,131 @@ export default function App() {
const isHeaderTransparent = !scrolledState
const landingPageFlag = useLandingPageFlag()
return (
<ErrorBoundary>
<DarkModeQueryParamReader />
<ApeModeQueryParamReader />
<AppWrapper>
<Trace page={currentPage}>
<HeaderWrapper transparent={isHeaderTransparent}>
<NavBar />
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Polling />
<TopLevelModals />
<Suspense fallback={<Loader />}>
{isLoaded ? (
<Routes>
{landingPageFlag === LandingPageVariant.Enabled && <Route path="/" element={<Landing />} />}
<Route path="tokens" element={<Tokens />}>
<Route path=":chainName" />
</Route>
<Route path="tokens/:chainName/:tokenAddress" element={<TokenDetails />} />
<Route
path="vote/*"
element={
<Suspense fallback={<LazyLoadSpinner />}>
<Vote />
</Suspense>
}
/>
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
<Route path="claim" element={<OpenClaimAddressModalAndRedirectToSwap />} />
<Route path="uni" element={<Earn />} />
<Route path="uni/:currencyIdA/:currencyIdB" element={<Manage />} />
<Trace page={currentPage}>
<HeaderWrapper transparent={isHeaderTransparent}>
<NavBar />
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Polling />
<TopLevelModals />
<Suspense fallback={<Loader />}>
{isLoaded ? (
<Routes>
<Route path="/" element={<Landing />} />
<Route path="send" element={<RedirectPathToSwapOnly />} />
<Route path="swap" element={<Swap />} />
<Route path="tokens" element={<Tokens />}>
<Route path=":chainName" />
</Route>
<Route path="tokens/:chainName/:tokenAddress" element={<TokenDetails />} />
<Route
path="vote/*"
element={
<Suspense fallback={<LazyLoadSpinner />}>
<Vote />
</Suspense>
}
/>
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
<Route path="claim" element={<OpenClaimAddressModalAndRedirectToSwap />} />
<Route path="uni" element={<Earn />} />
<Route path="uni/:currencyIdA/:currencyIdB" element={<Manage />} />
<Route path="pool/v2/find" element={<PoolFinder />} />
<Route path="pool/v2" element={<PoolV2 />} />
<Route path="pool" element={<Pool />} />
<Route path="pool/:tokenId" element={<PositionPage />} />
<Route path="send" element={<RedirectPathToSwapOnly />} />
<Route path="swap" element={<Swap />} />
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
</Route>
<Route path="add" element={<RedirectDuplicateTokenIds />}>
{/* this is workaround since react-router-dom v6 doesn't support optional parameters any more */}
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
</Route>
<Route path="pool/v2/find" element={<PoolFinder />} />
<Route path="pool/v2" element={<PoolV2 />} />
<Route path="pool" element={<Pool />} />
<Route path="pool/:tokenId" element={<PositionPage />} />
<Route path="increase" element={<AddLiquidity />}>
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount/:tokenId" />
</Route>
<Route path="add/v2" element={<RedirectDuplicateTokenIdsV2 />}>
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
</Route>
<Route path="add" element={<RedirectDuplicateTokenIds />}>
{/* this is workaround since react-router-dom v6 doesn't support optional parameters any more */}
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
</Route>
<Route path="remove/v2/:currencyIdA/:currencyIdB" element={<RemoveLiquidity />} />
<Route path="remove/:tokenId" element={<RemoveLiquidityV3 />} />
<Route path="increase" element={<AddLiquidity />}>
<Route path=":currencyIdA" />
<Route path=":currencyIdA/:currencyIdB" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount" />
<Route path=":currencyIdA/:currencyIdB/:feeAmount/:tokenId" />
</Route>
<Route path="migrate/v2" element={<MigrateV2 />} />
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} />
<Route path="remove/v2/:currencyIdA/:currencyIdB" element={<RemoveLiquidity />} />
<Route path="remove/:tokenId" element={<RemoveLiquidityV3 />} />
<Route path="about" element={<About />} />
<Route path="migrate/v2" element={<MigrateV2 />} />
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} />
<Route path="*" element={<RedirectPathToSwapOnly />} />
<Route path="about" element={<About />} />
<Route
path="/nfts"
element={
// TODO: replace loading state during Apollo migration
<Suspense fallback={null}>
<NftExplore />
</Suspense>
}
/>
<Route
path="/nfts/asset/:contractAddress/:tokenId"
element={
<Suspense fallback={<AssetDetailsLoading />}>
<Asset />
</Suspense>
}
/>
<Route
path="/nfts/profile"
element={
<Suspense fallback={<ProfilePageLoadingSkeleton />}>
<Profile />
</Suspense>
}
/>
<Route
path="/nfts/collection/:contractAddress"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
<Route
path="/nfts/collection/:contractAddress/activity"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
</Routes>
) : (
<Loader />
)}
</Suspense>
<Marginer />
</BodyWrapper>
<MobileBottomBar>
<PageTabs />
<Box marginY="4">
<MenuDropdown />
</Box>
</MobileBottomBar>
</Trace>
</AppWrapper>
<Route
path="/nfts"
element={
// TODO: replace loading state during Apollo migration
<Suspense fallback={null}>
<NftExplore />
</Suspense>
}
/>
<Route
path="/nfts/asset/:contractAddress/:tokenId"
element={
<Suspense fallback={<AssetDetailsLoading />}>
<Asset />
</Suspense>
}
/>
<Route
path="/nfts/profile"
element={
<Suspense fallback={<ProfilePageLoadingSkeleton />}>
<Profile />
</Suspense>
}
/>
<Route
path="/nfts/collection/:contractAddress"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
<Route
path="/nfts/collection/:contractAddress/activity"
element={
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
}
/>
<Route path="*" element={<Navigate to="/not-found" replace />} />
<Route path="/not-found" element={<NotFound />} />
</Routes>
) : (
<Loader />
)}
</Suspense>
</BodyWrapper>
<MobileBottomBar>
<PageTabs />
<Box marginY="4">
<MenuDropdown />
</Box>
</MobileBottomBar>
</Trace>
</ErrorBoundary>
)
}

View File

@@ -1,7 +1,6 @@
import { Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events'
import { BaseButton } from 'components/Button'
import { LandingPageVariant, useLandingPageFlag } from 'featureFlags/flags/landingPage'
import Swap from 'pages/Swap'
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
@@ -54,17 +53,17 @@ const ContentContainer = styled.div<{ isDarkMode: boolean }>`
align-items: center;
width: 100%;
max-width: min(720px, 90%);
position: absolute;
position: sticky;
bottom: 0;
z-index: ${Z_INDEX.dropdown};
padding: 32px 0 64px;
padding: 32px 0 80px;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
* {
pointer-events: auto;
}
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
@media screen and (min-width: ${BREAKPOINTS.md}px) {
padding: 64px 0;
}
`
@@ -170,21 +169,14 @@ export default function Landing() {
const location = useLocation()
const isOpen = location.pathname === '/'
const landingPageFlag = useLandingPageFlag()
useEffect(() => {
if (landingPageFlag) {
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'auto'
}
}
document.body.style.overflow = 'hidden'
return () => {
// need to have a return so the hook doesn't throw.
document.body.style.overflow = 'auto'
}
}, [landingPageFlag])
}, [])
if (landingPageFlag === LandingPageVariant.Control || !isOpen) return null
if (!isOpen) return null
return (
<Trace page={PageName.LANDING_PAGE} shouldLogImpression>

View File

@@ -0,0 +1,65 @@
import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { SmallButtonPrimary } from 'components/Button'
import { useIsMobile } from 'nft/hooks'
import { Link } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import darkImage from '../../assets/images/404-page-dark.png'
import lightImage from '../../assets/images/404-page-light.png'
const Image = styled.img`
max-width: 510px;
width: 100%;
padding: 0 75px;
`
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`
const Header = styled(Container)`
gap: 30px;
`
const PageWrapper = styled(Container)`
flex: 1;
justify-content: center;
gap: 50px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
justify-content: space-between;
padding-top: 64px;
}
`
export default function NotFound() {
const isDarkMode = useIsDarkMode()
const isMobile = useIsMobile()
const Title = isMobile ? ThemedText.LargeHeader : ThemedText.Hero
const Paragraph = isMobile ? ThemedText.HeadlineMedium : ThemedText.HeadlineLarge
return (
<PageWrapper>
<Trace page="404-page" shouldLogImpression>
<Header>
<Container>
<Title>404</Title>
<Paragraph color="textSecondary">
<Trans>Page not found!</Trans>
</Paragraph>
</Container>
<Image src={isDarkMode ? darkImage : lightImage} alt="Liluni" />
</Header>
<SmallButtonPrimary as={Link} to="/">
<Trans>Oops, take me back to Swap</Trans>
</SmallButtonPrimary>
</Trace>
</PageWrapper>
)
}

View File

@@ -165,7 +165,7 @@ function WrongNetworkCard() {
const theme = useTheme()
return (
<div style={{ height: '100vh' }}>
<>
<PageWrapper>
<AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" style={{ width: '100%' }}>
@@ -189,7 +189,7 @@ function WrongNetworkCard() {
</AutoColumn>
</PageWrapper>
<SwitchLocaleLink />
</div>
</>
)
}
@@ -263,86 +263,84 @@ export default function Pool() {
return (
<Trace page={PageName.POOL_PAGE} shouldLogImpression>
<div style={{ height: '100vh' }}>
<PageWrapper>
<AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" style={{ width: '100%' }}>
<TitleRow padding="0">
<ThemedText.LargeHeader>
<Trans>Pools</Trans>
</ThemedText.LargeHeader>
<ButtonRow>
{showV2Features && (
<PoolMenu
menuItems={menuItems}
flyoutAlignment={FlyoutAlignment.LEFT}
ToggleUI={(props: any) => (
<MoreOptionsButton {...props}>
<MoreOptionsText>
<Trans>More</Trans>
<ChevronDown size={15} />
</MoreOptionsText>
</MoreOptionsButton>
)}
/>
)}
<ResponsiveButtonPrimary data-cy="join-pool-button" id="join-pool-button" as={Link} to="/add/ETH">
+ <Trans>New Position</Trans>
</ResponsiveButtonPrimary>
</ButtonRow>
</TitleRow>
<MainContentWrapper>
{positionsLoading ? (
<PositionsLoadingPlaceholder />
) : filteredPositions && closedPositions && filteredPositions.length > 0 ? (
<PositionList
positions={filteredPositions}
setUserHideClosedPositions={setUserHideClosedPositions}
userHideClosedPositions={userHideClosedPositions}
<PageWrapper>
<AutoColumn gap="lg" justify="center">
<AutoColumn gap="lg" style={{ width: '100%' }}>
<TitleRow padding="0">
<ThemedText.LargeHeader>
<Trans>Pools</Trans>
</ThemedText.LargeHeader>
<ButtonRow>
{showV2Features && (
<PoolMenu
menuItems={menuItems}
flyoutAlignment={FlyoutAlignment.LEFT}
ToggleUI={(props: any) => (
<MoreOptionsButton {...props}>
<MoreOptionsText>
<Trans>More</Trans>
<ChevronDown size={15} />
</MoreOptionsText>
</MoreOptionsButton>
)}
/>
) : (
<ErrorContainer>
<ThemedText.DeprecatedBody color={theme.textTertiary} textAlign="center">
<InboxIcon strokeWidth={1} style={{ marginTop: '2em' }} />
<div>
<Trans>Your active V3 liquidity positions will appear here.</Trans>
</div>
</ThemedText.DeprecatedBody>
{!showConnectAWallet && closedPositions.length > 0 && (
<ButtonText
style={{ marginTop: '.5rem' }}
onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}
>
<Trans>Show closed positions</Trans>
</ButtonText>
)}
{showConnectAWallet && (
<TraceEvent
events={[BrowserEvent.onClick]}
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: false }}
element={ElementName.CONNECT_WALLET_BUTTON}
>
<ButtonPrimary
style={{ marginTop: '2em', marginBottom: '2em', padding: '8px 16px' }}
onClick={toggleWalletModal}
>
<Trans>Connect a wallet</Trans>
</ButtonPrimary>
</TraceEvent>
)}
</ErrorContainer>
)}
</MainContentWrapper>
<HideSmall>
<CTACards />
</HideSmall>
</AutoColumn>
<ResponsiveButtonPrimary data-cy="join-pool-button" id="join-pool-button" as={Link} to="/add/ETH">
+ <Trans>New Position</Trans>
</ResponsiveButtonPrimary>
</ButtonRow>
</TitleRow>
<MainContentWrapper>
{positionsLoading ? (
<PositionsLoadingPlaceholder />
) : filteredPositions && closedPositions && filteredPositions.length > 0 ? (
<PositionList
positions={filteredPositions}
setUserHideClosedPositions={setUserHideClosedPositions}
userHideClosedPositions={userHideClosedPositions}
/>
) : (
<ErrorContainer>
<ThemedText.DeprecatedBody color={theme.textTertiary} textAlign="center">
<InboxIcon strokeWidth={1} style={{ marginTop: '2em' }} />
<div>
<Trans>Your active V3 liquidity positions will appear here.</Trans>
</div>
</ThemedText.DeprecatedBody>
{!showConnectAWallet && closedPositions.length > 0 && (
<ButtonText
style={{ marginTop: '.5rem' }}
onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}
>
<Trans>Show closed positions</Trans>
</ButtonText>
)}
{showConnectAWallet && (
<TraceEvent
events={[BrowserEvent.onClick]}
name={EventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: false }}
element={ElementName.CONNECT_WALLET_BUTTON}
>
<ButtonPrimary
style={{ marginTop: '2em', marginBottom: '2em', padding: '8px 16px' }}
onClick={toggleWalletModal}
>
<Trans>Connect a wallet</Trans>
</ButtonPrimary>
</TraceEvent>
)}
</ErrorContainer>
)}
</MainContentWrapper>
<HideSmall>
<CTACards />
</HideSmall>
</AutoColumn>
</PageWrapper>
<SwitchLocaleLink />
</div>
</AutoColumn>
</PageWrapper>
<SwitchLocaleLink />
</Trace>
)
}

View File

@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName, PageName, SectionName } from '@uniswap/analytics-events'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
@@ -28,7 +27,7 @@ import { Text } from 'rebass'
import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade } from 'state/routing/types'
import { TradeState } from 'state/routing/types'
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
import { useTransactionAdder } from 'state/transactions/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
@@ -300,14 +299,14 @@ export default function Swap({ className }: { className?: string }) {
permit2Enabled ? maximumAmountIn : undefined,
permit2Enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
)
const isApprovalLoading = permit.state === PermitState.APPROVAL_LOADING
const [isPermitPending, setIsPermitPending] = useState(false)
const [isPermitFailed, setIsPermitFailed] = useState(false)
const addTransaction = useTransactionAdder()
const isApprovalPending = useHasPendingApproval(maximumAmountIn?.currency, PERMIT2_ADDRESS)
const updatePermit = useCallback(async () => {
setIsPermitPending(true)
try {
const approval = await permit.callback?.(isApprovalPending)
const approval = await permit.callback?.()
if (approval) {
sendAnalyticsEvent(EventName.APPROVE_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
@@ -325,14 +324,7 @@ export default function Swap({ className }: { className?: string }) {
} finally {
setIsPermitPending(false)
}
}, [
addTransaction,
chainId,
isApprovalPending,
maximumAmountIn?.currency.address,
maximumAmountIn?.currency.symbol,
permit,
])
}, [addTransaction, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, permit])
// check whether the user has approved the router on the input token
const [approvalState, approveCallback] = useApproveCallbackFromTrade(
@@ -794,10 +786,12 @@ export default function Swap({ className }: { className?: string }) {
</ButtonError>
</AutoColumn>
</AutoRow>
) : isValid && permit.state === PermitState.PERMIT_NEEDED ? (
) : isValid &&
(permit.state === PermitState.APPROVAL_OR_PERMIT_NEEDED ||
permit.state === PermitState.APPROVAL_LOADING) ? (
<ButtonYellow
onClick={updatePermit}
disabled={isPermitPending || isApprovalPending}
disabled={isPermitPending || isApprovalLoading}
style={{ gap: 14 }}
>
{isPermitPending ? (
@@ -814,7 +808,7 @@ export default function Swap({ className }: { className?: string }) {
<Trans>Approval failed. Try again.</Trans>
</ThemedText.SubHeader>
</>
) : isApprovalPending ? (
) : isApprovalLoading ? (
<>
<Loader size="20px" stroke={theme.accentWarning} />
<ThemedText.SubHeader color="accentWarning">

View File

@@ -1,21 +1,23 @@
import { filterTimeAtom } from 'components/Tokens/state'
import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { TokenQuery, tokenQuery } from 'graphql/data/Token'
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { Suspense, useCallback, useEffect, useMemo } from 'react'
import { useQueryLoader } from 'react-relay'
import { useParams } from 'react-router-dom'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
export default function TokenDetailsPage() {
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const chain = validateUrlChainParam(chainName)
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const isNative = tokenAddress === NATIVE_CHAIN_ID
const timePeriod = useAtomValue(filterTimeAtom)
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
const [contract, duration] = useMemo(
() => [
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain },
@@ -35,8 +37,9 @@ export default function TokenDetailsPage() {
const refetchTokenPrices = useCallback(
(t: TimePeriod) => {
loadPriceQuery({ contract, duration: toHistoryDuration(t) })
setTimePeriod(t)
},
[contract, loadPriceQuery]
[contract, loadPriceQuery, setTimePeriod]
)
if (!tokenQueryReference) {

View File

@@ -31,12 +31,18 @@ export const ThemedText = {
HeadlineSmall(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={20} lineHeight="28px" color="textPrimary" {...props} />
},
HeadlineMedium(props: TextProps) {
return <TextWrapper fontWeight={500} fontSize={28} color="textPrimary" {...props} />
},
HeadlineLarge(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={36} lineHeight="44px" color="textPrimary" {...props} />
return <TextWrapper fontWeight={600} fontSize={36} lineHeight="36px" color="textPrimary" {...props} />
},
LargeHeader(props: TextProps) {
return <TextWrapper fontWeight={400} fontSize={36} color="textPrimary" {...props} />
},
Hero(props: TextProps) {
return <TextWrapper fontWeight={500} fontSize={48} color="textPrimary" {...props} />
},
Link(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} />
},

View File

@@ -91,7 +91,8 @@ function getSettings(darkMode: boolean) {
}
}
function getTheme(darkMode: boolean) {
// eslint-disable-next-line import/no-unused-modules -- used in styled.d.ts
export function getTheme(darkMode: boolean) {
return {
darkMode,
...(darkMode ? darkTheme : lightTheme),

View File

@@ -7,6 +7,7 @@ export function isTestEnv(): boolean {
}
export function isStagingEnv(): boolean {
// NB: This is set in vercel builds.
return Boolean(process.env.REACT_APP_STAGING)
}

View File

@@ -4438,10 +4438,10 @@
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@2.22.10":
version "2.22.10"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.10.tgz#3b4fbe3ca607c8b096aae58bd6e6d4188af7ddd5"
integrity sha512-wFw68p9fiVt06rernsWWqlB3lccgfyCuHTGNFT2NPBQ1lnpxudpa6MKlsMih5gaWiZM1pT8jd3LYbgCX9OeOTw==
"@uniswap/widgets@2.22.11":
version "2.22.11"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.22.11.tgz#d64179e58d3923af1b80f63bd8fb44804fc047bd"
integrity sha512-eBG7L/inLLHY2+cLKFsqqCK8rhoIzHWsUDm0glpLUyqs6NiZssoKHgIPWzjT2cNwupCYol08ruKM+JR1wBVdTQ==
dependencies:
"@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1"