Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a286e5b114 | ||
|
|
62361647e0 | ||
|
|
deee278439 | ||
|
|
6340deb201 | ||
|
|
28b154ebe8 | ||
|
|
3bde2165f4 | ||
|
|
78c8fd2359 | ||
|
|
c378752910 | ||
|
|
0bf7b92013 | ||
|
|
283479f76e | ||
|
|
d3c30e2f6b | ||
|
|
32d226f78e | ||
|
|
96744505c0 | ||
|
|
97236033d4 | ||
|
|
86e62dc4b9 | ||
|
|
e584a5fa36 | ||
|
|
332ef6e6c8 | ||
|
|
8cbd111e65 | ||
|
|
55ffcbd465 | ||
|
|
404775e86d | ||
|
|
0ae9fe28a2 | ||
|
|
89c0caae43 | ||
|
|
c8086e3c76 | ||
|
|
1c2842e5a0 | ||
|
|
a2c6d3f475 | ||
|
|
841ea7f8a1 | ||
|
|
804692b114 | ||
|
|
6282298d13 | ||
|
|
7a5b855097 | ||
|
|
c9908748cf | ||
|
|
79b77deee1 | ||
|
|
a554af6670 | ||
|
|
1843f214b1 | ||
|
|
3e0788092e | ||
|
|
d14c49df0d | ||
|
|
c098ad1ffe | ||
|
|
48114ef51d | ||
|
|
cb7132ee17 | ||
|
|
0fa4859a09 | ||
|
|
f8bb5046f0 | ||
|
|
7d1589d1df | ||
|
|
26b603cc2e | ||
|
|
ece68a0ec7 | ||
|
|
fd212477ce | ||
|
|
a16d2387cc | ||
|
|
cae56ec385 | ||
|
|
d16b3473e0 | ||
|
|
f66f249dba | ||
|
|
08afd888d0 | ||
|
|
b427be2673 | ||
|
|
f753a5e325 | ||
|
|
46d9d8e3df | ||
|
|
680d3a3f26 | ||
|
|
e4c625ee71 |
6
.env
@@ -1,7 +1,11 @@
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
# These API keys are intentionally public. Please do not report them - thank you for your concern.
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_AWS_API_REGION="us-east-2"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||
ESLINT_NO_DEV_ERRORS=true
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
it('loads swap page', () => {
|
||||
cy.get('#swap-page')
|
||||
it('shows landing page when no selectedWallet', () => {
|
||||
cy.visit('/', { noWallet: true })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('redirects to swap page when selectedWallet is INJECTED', () => {
|
||||
cy.visit('/', { selectedWallet: 'INJECTED' })
|
||||
cy.get('#swap-page')
|
||||
cy.url().should('include', '/swap')
|
||||
cy.screenshot()
|
||||
})
|
||||
|
||||
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
|
||||
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
})
|
||||
|
||||
it('shows landing page when the unicorn icon in nav is selected', () => {
|
||||
cy.get(getTestSelector('uniswap-logo')).click()
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
})
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.get('#pool-nav-link').click()
|
||||
cy.url().should('include', '/pool')
|
||||
|
||||
@@ -59,6 +59,5 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('nft-no-nfts-selected')).should('exist')
|
||||
cy.get(getTestSelector('nft-bag-close-icon')).click()
|
||||
cy.get(getTestSelector('nft-explore-nfts-button')).click()
|
||||
cy.get(getTestSelector('nft-welcome-modal')).should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ declare global {
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
selectedWallet?: string
|
||||
noWallet?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +40,12 @@ Cypress.Commands.overwrite(
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
win.localStorage.clear()
|
||||
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
|
||||
|
||||
const userState = {
|
||||
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
|
||||
fiatOnrampDismissed: true,
|
||||
}
|
||||
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
|
||||
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
|
||||
@@ -138,8 +138,8 @@
|
||||
"@types/react-relay": "^13.0.2",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.2.0",
|
||||
"@uniswap/analytics-events": "1.3.1",
|
||||
"@uniswap/conedison": "^1.1.0",
|
||||
"@uniswap/analytics-events": "^1.5.0",
|
||||
"@uniswap/conedison": "^1.1.1",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
|
||||
21
src/assets/svg/fiat_mask.svg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
89
src/components/About/AboutFooter.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { BookOpen, Globe, Heart, Twitter } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 48px;
|
||||
max-width: 1440px;
|
||||
`
|
||||
|
||||
const FooterLinks = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
const FooterLink = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
svg {
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const Copyright = styled.span`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
export const AboutFooter = () => {
|
||||
return (
|
||||
<Footer>
|
||||
<FooterLinks>
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.SUPPORT_LINK}>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://support.uniswap.org">
|
||||
<Globe /> Support
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.TWITTER_LINK}>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://twitter.com/uniswap">
|
||||
<Twitter /> Twitter
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.BLOG_LINK}>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://uniswap.org/blog">
|
||||
<BookOpen /> Blog
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.CAREERS_LINK}>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://boards.greenhouse.io/uniswaplabs">
|
||||
<Heart /> Careers
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
</FooterLinks>
|
||||
<Copyright>© {new Date().getFullYear()} Uniswap Labs</Copyright>
|
||||
</Footer>
|
||||
)
|
||||
}
|
||||
150
src/components/About/Card.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, EventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
export enum CardType {
|
||||
Primary = 'Primary',
|
||||
Secondary = 'Secondary',
|
||||
}
|
||||
|
||||
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
|
||||
display: flex;
|
||||
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
|
||||
isDarkMode
|
||||
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
|
||||
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
|
||||
}`
|
||||
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
|
||||
background-size: auto 100%;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: border-box;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
padding: 24px;
|
||||
height: 212px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
|
||||
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
padding: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const TitleRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const CardTitle = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 600;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const getCardDescriptionColor = (type: CardType, theme: DefaultTheme) => {
|
||||
switch (type) {
|
||||
case CardType.Secondary:
|
||||
return theme.textSecondary
|
||||
default:
|
||||
return theme.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
const CardDescription = styled.div<{ type: CardType }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme, type }) => getCardDescriptionColor(type, theme)};
|
||||
padding: 0 40px 0 0;
|
||||
max-width: 480px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
max-width: 480px;
|
||||
}
|
||||
`
|
||||
|
||||
const CardCTA = styled(CardDescription)`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-weight: 500;
|
||||
margin: 24px 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const Card = ({
|
||||
type = CardType.Primary,
|
||||
title,
|
||||
description,
|
||||
cta,
|
||||
to,
|
||||
external,
|
||||
backgroundImgSrc,
|
||||
icon,
|
||||
elementName,
|
||||
}: {
|
||||
type?: CardType
|
||||
title: string
|
||||
description: string
|
||||
cta?: string
|
||||
to: string
|
||||
external?: boolean
|
||||
backgroundImgSrc?: string
|
||||
icon?: React.ReactNode
|
||||
elementName?: string
|
||||
}) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={elementName}>
|
||||
<StyledCard
|
||||
type={type}
|
||||
as={external ? 'a' : Link}
|
||||
to={external ? undefined : to}
|
||||
href={external ? to : undefined}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopenener noreferrer' : undefined}
|
||||
isDarkMode={isDarkMode}
|
||||
backgroundImgSrc={backgroundImgSrc}
|
||||
>
|
||||
<TitleRow>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{icon}
|
||||
</TitleRow>
|
||||
<CardDescription type={type}>
|
||||
{description}
|
||||
<CardCTA type={type}>{cta}</CardCTA>
|
||||
</CardDescription>
|
||||
</StyledCard>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
106
src/components/About/ProtocolBanner.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { ButtonEmpty } from 'components/Button'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import meshSrc from './images/Mesh.png'
|
||||
|
||||
const DARK_MODE_GRADIENT = 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)'
|
||||
|
||||
const Banner = styled.div<{ isDarkMode: boolean }>`
|
||||
height: 340px;
|
||||
width: 100%;
|
||||
border-radius: 32px;
|
||||
max-width: 1440px;
|
||||
margin: 80px 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32px 48px;
|
||||
|
||||
box-shadow: 0px 10px 24px rgba(51, 53, 72, 0.04);
|
||||
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? `url(${meshSrc}), ${DARK_MODE_GRADIENT}`
|
||||
: `url(${meshSrc}), linear-gradient(93.06deg, #FF00C7 2.66%, #FF9FFB 98.99%);`};
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
height: 140px;
|
||||
flex-direction: row;
|
||||
}
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
color: white;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const HeaderText = styled.div`
|
||||
font-weight: 700;
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const DescriptionText = styled.div`
|
||||
margin: 10px 10px 0 0;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
`
|
||||
|
||||
const BannerButtonContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
width: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const BannerButton = styled(ButtonEmpty)`
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
`
|
||||
|
||||
const ProtocolBanner = () => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<Banner isDarkMode={isDarkMode}>
|
||||
<TextContainer>
|
||||
<HeaderText>Powered by the Uniswap Protocol</HeaderText>
|
||||
<DescriptionText>
|
||||
The leading decentralized crypto trading protocol, governed by a global community.
|
||||
</DescriptionText>
|
||||
</TextContainer>
|
||||
<BannerButtonContainer>
|
||||
<BannerButton width="200px" as="a" href="https://uniswap.org" rel="noopener noreferrer" target="_blank">
|
||||
Learn more
|
||||
</BannerButton>
|
||||
</BannerButtonContainer>
|
||||
</Banner>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProtocolBanner
|
||||
71
src/components/About/constants.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { ElementName } from '@uniswap/analytics-events'
|
||||
import { DollarSign, Terminal } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
|
||||
import darkArrowImgSrc from './images/aboutArrowDark.png'
|
||||
import lightArrowImgSrc from './images/aboutArrowLight.png'
|
||||
import darkDollarImgSrc from './images/aboutDollarDark.png'
|
||||
import darkTerminalImgSrc from './images/aboutTerminalDark.png'
|
||||
import nftCardImgSrc from './images/nftCard.png'
|
||||
import swapCardImgSrc from './images/swapCard.png'
|
||||
|
||||
export const MAIN_CARDS = [
|
||||
{
|
||||
to: '/swap',
|
||||
title: 'Swap tokens',
|
||||
description: 'Buy, sell, and explore tokens on Ethereum, Polygon, Optimism, and more.',
|
||||
cta: 'Trade Tokens',
|
||||
darkBackgroundImgSrc: swapCardImgSrc,
|
||||
lightBackgroundImgSrc: swapCardImgSrc,
|
||||
elementName: ElementName.ABOUT_PAGE_SWAP_CARD,
|
||||
},
|
||||
{
|
||||
to: '/nfts',
|
||||
title: 'Trade NFTs',
|
||||
description: 'Buy and sell NFTs across marketplaces to find more listings at better prices.',
|
||||
cta: 'Explore NFTs',
|
||||
darkBackgroundImgSrc: nftCardImgSrc,
|
||||
lightBackgroundImgSrc: nftCardImgSrc,
|
||||
elementName: ElementName.ABOUT_PAGE_NFTS_CARD,
|
||||
},
|
||||
]
|
||||
|
||||
const StyledCardLogo = styled.img`
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
max-height: 48px;
|
||||
max-width: 48px;
|
||||
`
|
||||
|
||||
export const MORE_CARDS = [
|
||||
{
|
||||
to: 'https://support.uniswap.org/hc/en-us/articles/11306574799117-How-to-use-Moon-Pay-on-the-Uniswap-web-app-',
|
||||
external: true,
|
||||
title: 'Buy crypto',
|
||||
description: 'Buy crypto with your credit card or bank account at the best rates.',
|
||||
lightIcon: <DollarSign color={lightTheme.textTertiary} size={48} />,
|
||||
darkIcon: <StyledCardLogo src={darkDollarImgSrc} alt="Earn" />,
|
||||
cta: 'Buy now',
|
||||
elementName: ElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
|
||||
},
|
||||
{
|
||||
to: '/pool',
|
||||
title: 'Earn',
|
||||
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
|
||||
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
|
||||
darkIcon: <StyledCardLogo src={darkArrowImgSrc} alt="Analytics" />,
|
||||
cta: 'Provide liquidity',
|
||||
elementName: ElementName.ABOUT_PAGE_EARN_CARD,
|
||||
},
|
||||
{
|
||||
to: 'https://docs.uniswap.org',
|
||||
external: true,
|
||||
title: 'Build dApps',
|
||||
description: 'Build apps and tools on the largest DeFi protocol on Ethereum.',
|
||||
lightIcon: <Terminal color={lightTheme.textTertiary} size={48} />,
|
||||
darkIcon: <StyledCardLogo src={darkTerminalImgSrc} alt="Developers" />,
|
||||
cta: 'Developer docs',
|
||||
elementName: ElementName.ABOUT_PAGE_DEV_DOCS_CARD,
|
||||
},
|
||||
]
|
||||
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 379 KiB |
BIN
src/components/About/images/Mesh.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/components/About/images/aboutArrowDark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/components/About/images/aboutArrowLight.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
src/components/About/images/aboutDollarDark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/components/About/images/aboutTerminalDark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/components/About/images/nftCard.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/components/About/images/swapCard.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
13
src/components/Button/LoadingButtonSpinner.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SpinnerSVG } from 'theme'
|
||||
|
||||
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
|
||||
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
opacity="0.1"
|
||||
d="M18.8334 10.0003C18.8334 14.6027 15.1025 18.3337 10.5001 18.3337C5.89771 18.3337 2.16675 14.6027 2.16675 10.0003C2.16675 5.39795 5.89771 1.66699 10.5001 1.66699C15.1025 1.66699 18.8334 5.39795 18.8334 10.0003ZM4.66675 10.0003C4.66675 13.222 7.27842 15.8337 10.5001 15.8337C13.7217 15.8337 16.3334 13.222 16.3334 10.0003C16.3334 6.77867 13.7217 4.16699 10.5001 4.16699C7.27842 4.16699 4.66675 6.77867 4.66675 10.0003Z"
|
||||
/>
|
||||
<path d="M17.5834 10.0003C18.2738 10.0003 18.843 9.4376 18.7398 8.755C18.6392 8.0891 18.458 7.43633 18.1991 6.8113C17.7803 5.80025 17.1665 4.88159 16.3926 4.10777C15.6188 3.33395 14.7002 2.72012 13.6891 2.30133C13.0641 2.04243 12.4113 1.86121 11.7454 1.76057C11.0628 1.6574 10.5001 2.22664 10.5001 2.91699C10.5001 3.60735 11.066 4.15361 11.7405 4.30041C12.0789 4.37406 12.4109 4.47786 12.7324 4.61103C13.4401 4.90418 14.0832 5.33386 14.6249 5.87554C15.1665 6.41721 15.5962 7.06027 15.8894 7.76801C16.0225 8.08949 16.1264 8.42147 16.2 8.75986C16.3468 9.43443 16.8931 10.0003 17.5834 10.0003Z" />
|
||||
</SpinnerSVG>
|
||||
)
|
||||
|
||||
export default ButtonLoadingSpinner
|
||||
@@ -5,16 +5,31 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
|
||||
|
||||
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
|
||||
|
||||
export const BaseButton = styled(RebassButton)<
|
||||
{
|
||||
padding?: string
|
||||
width?: string
|
||||
$borderRadius?: string
|
||||
altDisabledStyle?: boolean
|
||||
} & ButtonProps
|
||||
>`
|
||||
const ButtonOverlay = styled.div`
|
||||
background-color: transparent;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 150ms ease background-color;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
type BaseButtonProps = {
|
||||
padding?: string
|
||||
width?: string
|
||||
$borderRadius?: string
|
||||
altDisabledStyle?: boolean
|
||||
} & ButtonProps
|
||||
|
||||
export const BaseButton = styled(RebassButton)<BaseButtonProps>`
|
||||
padding: ${({ padding }) => padding ?? '16px'};
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
font-weight: 500;
|
||||
@@ -86,7 +101,7 @@ export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
export const ButtonLight = styled(BaseButton)`
|
||||
const BaseButtonLight = styled(BaseButton)`
|
||||
background-color: ${({ theme }) => theme.accentActionSoft};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
font-size: 20px;
|
||||
@@ -103,6 +118,19 @@ export const ButtonLight = styled(BaseButton)`
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
|
||||
}
|
||||
|
||||
:hover {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
}
|
||||
|
||||
:active {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
}
|
||||
}
|
||||
|
||||
:disabled {
|
||||
opacity: 0.4;
|
||||
:hover {
|
||||
@@ -369,18 +397,6 @@ export function ButtonRadioChecked({ active = false, children, ...rest }: { acti
|
||||
}
|
||||
}
|
||||
|
||||
const ButtonOverlay = styled.div`
|
||||
background-color: transparent;
|
||||
bottom: 0;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 150ms ease background-color;
|
||||
width: 100%;
|
||||
`
|
||||
export enum ButtonSize {
|
||||
small,
|
||||
medium,
|
||||
@@ -395,7 +411,7 @@ export enum ButtonEmphasis {
|
||||
warning,
|
||||
destructive,
|
||||
}
|
||||
interface BaseButtonProps {
|
||||
interface BaseThemeButtonProps {
|
||||
size: ButtonSize
|
||||
emphasis: ButtonEmphasis
|
||||
}
|
||||
@@ -474,7 +490,7 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
|
||||
}
|
||||
}
|
||||
|
||||
const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
const BaseThemeButton = styled.button<BaseThemeButtonProps>`
|
||||
align-items: center;
|
||||
background-color: ${pickThemeButtonBackgroundColor};
|
||||
border-radius: 16px;
|
||||
@@ -491,16 +507,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
padding: ${pickThemeButtonPadding};
|
||||
position: relative;
|
||||
transition: 150ms ease opacity;
|
||||
user-select: none;
|
||||
|
||||
:active {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
}
|
||||
}
|
||||
:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
:focus {
|
||||
${ButtonOverlay} {
|
||||
background-color: ${({ theme }) => theme.stateOverlayPressed};
|
||||
@@ -511,9 +524,20 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
}
|
||||
:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
:disabled:active,
|
||||
:disabled:focus,
|
||||
:disabled:hover {
|
||||
${ButtonOverlay} {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}
|
||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
|
||||
|
||||
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||
return (
|
||||
@@ -523,3 +547,12 @@ export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||
</BaseThemeButton>
|
||||
)
|
||||
}
|
||||
|
||||
export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => {
|
||||
return (
|
||||
<BaseButtonLight {...rest}>
|
||||
<ButtonOverlay />
|
||||
{children}
|
||||
</BaseButtonLight>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
@@ -208,6 +209,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.permit2}
|
||||
label="Permit 2 / Universal Router"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFiatOnrampFlag()}
|
||||
featureFlag={FeatureFlag.fiatOnramp}
|
||||
label="Fiat on-ramp"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
147
src/components/FiatOnrampAnnouncement/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useToggleWalletDropdown } from 'state/application/hooks'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||
import { dismissFiatOnramp } from 'state/user/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
const Arrow = styled.div`
|
||||
top: -4px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
width: 16px;
|
||||
|
||||
::before {
|
||||
background: hsl(315.75, 93%, 83%);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
width: 16px;
|
||||
}
|
||||
`
|
||||
const ArrowWrapper = styled.div`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 90%;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
min-height: 92px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
right: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled(X)`
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
`
|
||||
const Wrapper = styled.button`
|
||||
background: radial-gradient(105% 250% at 100% 5%, hsla(318, 95%, 85%) 1%, hsla(331, 80%, 75%, 0.1) 84%),
|
||||
linear-gradient(180deg, hsla(296, 92%, 67%, 0.5) 0%, hsla(313, 96%, 60%, 0.5) 130%);
|
||||
background-color: hsla(297, 93%, 68%, 1);
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-align: start;
|
||||
max-width: 320px;
|
||||
min-height: 92px;
|
||||
width: 100%;
|
||||
|
||||
:before {
|
||||
background-image: url(${fiatMaskUrl});
|
||||
background-repeat: no-repeat;
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: -154px; // roughly width of fiat mask image
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
const Header = styled(ThemedText.SubHeader)`
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 12px 12px 4px;
|
||||
position: relative;
|
||||
`
|
||||
const Body = styled(ThemedText.BodySmall)`
|
||||
color: white;
|
||||
margin: 0 12px 12px 12px !important;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const MAX_RENDER_COUNT = 3
|
||||
|
||||
export function FiatOnrampAnnouncement() {
|
||||
const { account } = useWeb3React()
|
||||
const [acks, acknowledge] = useFiatOnrampAck()
|
||||
const fiatOnrampDismissed = useAppSelector((state) => state.user.fiatOnrampDismissed)
|
||||
|
||||
useEffect(() => {
|
||||
acknowledge({ renderCount: acks?.renderCount + 1 })
|
||||
// The dependency list is empty so this is only run once on mount
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(dismissFiatOnramp())
|
||||
}, [dispatch])
|
||||
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const handleClick = useCallback(() => {
|
||||
toggleWalletDropdown()
|
||||
acknowledge({ user: true })
|
||||
}, [acknowledge, toggleWalletDropdown])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
const openModal = useAppSelector((state) => state.application.openModal)
|
||||
|
||||
if (
|
||||
!account ||
|
||||
acks?.user ||
|
||||
fiatOnrampFlag === BaseVariant.Control ||
|
||||
fiatOnrampDismissed ||
|
||||
acks?.renderCount >= MAX_RENDER_COUNT ||
|
||||
isMobile ||
|
||||
openModal !== null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ArrowWrapper>
|
||||
<Arrow />
|
||||
<CloseIcon onClick={handleClose} />
|
||||
<Wrapper onClick={handleClick}>
|
||||
<Header>
|
||||
<Trans>Buy crypto</Trans>
|
||||
</Header>
|
||||
<Body>
|
||||
<Trans>Get tokens at the best prices in web3 on Uniswap, powered by Moonpay.</Trans>
|
||||
</Body>
|
||||
</Wrapper>
|
||||
</ArrowWrapper>
|
||||
)
|
||||
}
|
||||
143
src/components/FiatOnrampModal/index.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { CustomLightSpinner, ThemedText } from 'theme'
|
||||
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0;
|
||||
min-height: 720px;
|
||||
min-width: 375px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ErrorText = styled(ThemedText.BodyPrimary)`
|
||||
color: ${({ theme }) => theme.accentFailure};
|
||||
margin: auto !important;
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
`
|
||||
const StyledIframe = styled.iframe`
|
||||
background-color: ${({ theme }) => theme.white};
|
||||
border-radius: 12px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: calc(100% - 16px);
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: calc(100% - 16px);
|
||||
`
|
||||
const StyledSpinner = styled(CustomLightSpinner)`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`
|
||||
|
||||
const MOONPAY_SUPPORTED_CURRENCY_CODES = [
|
||||
'eth',
|
||||
'eth_arbitrum',
|
||||
'eth_optimism',
|
||||
'eth_polygon',
|
||||
'weth',
|
||||
'wbtc',
|
||||
'matic_polygon',
|
||||
'polygon',
|
||||
'usdc_arbitrum',
|
||||
'usdc_optimism',
|
||||
'usdc_polygon',
|
||||
]
|
||||
|
||||
export default function FiatOnrampModal() {
|
||||
const { account } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
const closeModal = useCloseModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
|
||||
|
||||
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchSignedIframeUrl = useCallback(async () => {
|
||||
if (!account) {
|
||||
setError('Please connect an account before making a purchase.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
|
||||
const res = await fetch(signedIframeUrlFetchEndpoint, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
colorCode: theme.accentAction,
|
||||
defaultCurrencyCode: 'eth',
|
||||
redirectUrl: 'https://app.uniswap.org/#/swap',
|
||||
walletAddresses: JSON.stringify(
|
||||
MOONPAY_SUPPORTED_CURRENCY_CODES.reduce(
|
||||
(acc, currencyCode) => ({
|
||||
...acc,
|
||||
[currencyCode]: account,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
),
|
||||
}),
|
||||
})
|
||||
const { url } = await res.json()
|
||||
setSignedIframeUrl(url)
|
||||
} catch (e) {
|
||||
console.log('there was an error fetching the link', e)
|
||||
setError(e.toString())
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [account, theme.accentAction])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignedIframeUrl()
|
||||
}, [fetchSignedIframeUrl])
|
||||
|
||||
return (
|
||||
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
|
||||
<Wrapper data-testid="fiat-onramp-modal">
|
||||
{error ? (
|
||||
<>
|
||||
<ThemedText.MediumHeader>
|
||||
<Trans>Moonpay Fiat On-ramp iframe</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<ErrorText>
|
||||
<Trans>something went wrong!</Trans>
|
||||
<br />
|
||||
{error}
|
||||
</ErrorText>
|
||||
</>
|
||||
) : loading ? (
|
||||
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
|
||||
) : (
|
||||
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
|
||||
)}
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: ${Z_INDEX.modalBackdrop};
|
||||
background-color: transparent;
|
||||
@@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
|
||||
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
|
||||
justify-content: center;
|
||||
|
||||
background-color: ${({ theme }) => theme.backgroundScrim};
|
||||
@@ -89,7 +89,7 @@ interface ModalProps {
|
||||
maxWidth?: number
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
scrollOverlay?: boolean
|
||||
$scrollOverlay?: boolean
|
||||
hideBorder?: boolean
|
||||
isBottomSheet?: boolean
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default function Modal({
|
||||
initialFocusRef,
|
||||
children,
|
||||
onSwipe = onDismiss,
|
||||
scrollOverlay,
|
||||
$scrollOverlay,
|
||||
isBottomSheet = isMobile,
|
||||
hideBorder = false,
|
||||
}: ModalProps) {
|
||||
@@ -136,7 +136,7 @@ export default function Modal({
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
unstable_lockFocusAcrossFrames={false}
|
||||
scrollOverlay={scrollOverlay}
|
||||
$scrollOverlay={$scrollOverlay}
|
||||
>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
@@ -149,7 +149,7 @@ export default function Modal({
|
||||
$minHeight={minHeight}
|
||||
$maxHeight={maxHeight}
|
||||
$isBottomSheet={isBottomSheet}
|
||||
$scrollOverlay={scrollOverlay}
|
||||
$scrollOverlay={$scrollOverlay}
|
||||
$hideBorder={hideBorder}
|
||||
$maxWidth={maxWidth}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { ReactNode, useReducer, useRef } from 'react'
|
||||
import { NavLink, NavLinkProps } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
import { useToggleModal } from '../../state/application/hooks'
|
||||
@@ -50,8 +51,13 @@ const PrimaryMenuRow = ({
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
|
||||
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
|
||||
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
|
||||
}
|
||||
|
||||
PrimaryMenuRow.Text = PrimaryMenuRowText
|
||||
@@ -115,7 +121,6 @@ export const MenuDropdown = () => {
|
||||
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
|
||||
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
|
||||
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const baseNavDropdown = style([
|
||||
borderWidth: '1px',
|
||||
paddingBottom: '8',
|
||||
paddingTop: '8',
|
||||
zIndex: '2',
|
||||
}),
|
||||
{
|
||||
boxShadow: '0px 4px 12px 0px #00000026',
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { Box, BoxProps } from 'nft/components/Box'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { ForwardedRef, forwardRef } from 'react'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import * as styles from './NavDropdown.css'
|
||||
|
||||
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
|
||||
const isMobile = useIsMobile()
|
||||
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
style={{ zIndex: Z_INDEX.modal }}
|
||||
className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
NavDropdown.displayName = 'NavDropdown'
|
||||
|
||||
@@ -90,9 +90,13 @@ const Navbar = () => {
|
||||
<UniIcon
|
||||
width="48"
|
||||
height="48"
|
||||
data-testid="uniswap-logo"
|
||||
className={styles.logo}
|
||||
onClick={() => {
|
||||
navigate('/')
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: '?intro=true',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RowFixed } from 'components/Row'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import useGasPrice from 'hooks/useGasPrice'
|
||||
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||
import JSBI from 'jsbi'
|
||||
@@ -120,6 +121,7 @@ export default function Polling() {
|
||||
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
|
||||
const blockTime = useCurrentBlockTimestamp()
|
||||
const isNftPage = useIsNftPage()
|
||||
const isLandingPage = useIsLandingPage()
|
||||
|
||||
const ethGasPrice = useGasPrice()
|
||||
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
|
||||
@@ -154,7 +156,7 @@ export default function Polling() {
|
||||
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
|
||||
}, [blockNumber, chainId])
|
||||
|
||||
if (isNftPage) {
|
||||
if (isNftPage || isLandingPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
const Wrapper = styled(AutoColumn)`
|
||||
margin-right: 8px;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const Grouping = styled(AutoColumn)`
|
||||
width: fit-content;
|
||||
padding: 4px;
|
||||
/* background-color: ${({ theme }) => theme.backgroundInteractive}; */
|
||||
border-radius: 16px;
|
||||
`
|
||||
|
||||
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: ${({ theme, confirmed, disabled }) =>
|
||||
disabled ? theme.deprecated_bg3 : confirmed ? theme.accentSuccess : theme.accentAction};
|
||||
border-radius: 50%;
|
||||
color: ${({ theme, disabled }) => (disabled ? theme.textTertiary : theme.textPrimary)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 8px;
|
||||
font-size: 16px;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const CircleRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
interface ProgressCirclesProps {
|
||||
steps: boolean[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on array of steps, create a step counter of circles.
|
||||
* A circle can be enabled, disabled, or confirmed. States are derived
|
||||
* from previous step.
|
||||
*
|
||||
* An extra circle is added to represent the ability to swap, add, or remove.
|
||||
* This step will never be marked as complete (because no 'txn done' state in body ui).
|
||||
*
|
||||
* @param steps array of booleans where true means step is complete
|
||||
*/
|
||||
export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Wrapper justify="center" {...rest}>
|
||||
<Grouping>
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<CircleRow key={i}>
|
||||
<Circle confirmed={step} disabled={disabled || (!steps[i - 1] && i !== 0)}>
|
||||
{step ? '✓' : i + 1 + '.'}
|
||||
</Circle>
|
||||
<ThemedText.DeprecatedMain color={theme.deprecated_text4}>|</ThemedText.DeprecatedMain>
|
||||
</CircleRow>
|
||||
)
|
||||
})}
|
||||
<Circle disabled={disabled || !steps[steps.length - 1]}>{steps.length + 1 + '.'}</Circle>
|
||||
</Grouping>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -4,4 +4,4 @@ export const LARGE_MEDIA_BREAKPOINT = '840px'
|
||||
export const MEDIUM_MEDIA_BREAKPOINT = '720px'
|
||||
export const SMALL_MEDIA_BREAKPOINT = '540px'
|
||||
export const MOBILE_MEDIA_BREAKPOINT = '420px'
|
||||
export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
|
||||
// export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
|
||||
import { lazy } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
@@ -17,21 +18,18 @@ export default function TopLevelModals() {
|
||||
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
|
||||
const { account } = useWeb3React()
|
||||
const location = useLocation()
|
||||
const pageShowsNftPromoBanner =
|
||||
location.pathname.startsWith('/swap') ||
|
||||
location.pathname.startsWith('/tokens') ||
|
||||
location.pathname.startsWith('/pool')
|
||||
useAccountRiskCheck(account)
|
||||
const open = Boolean(blockedAccountModalOpen && account)
|
||||
const accountBlocked = Boolean(blockedAccountModalOpen && account)
|
||||
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={open} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
|
||||
<Bag />
|
||||
<TransactionCompleteModal />
|
||||
<AirdropModal />
|
||||
{pageShowsNftPromoBanner && <NftExploreBanner />}
|
||||
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
|
||||
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
|
||||
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : attemptingTxn ? (
|
||||
|
||||
@@ -1,33 +1,69 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import ms from 'ms.macro'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Copy, ExternalLink, Power } from 'react-feather'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import styled, { css, keyframes } from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
|
||||
import { shortenAddress } from '../../nft/utils/address'
|
||||
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
|
||||
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
|
||||
const BuyCryptoButtonBorderKeyframes = keyframes`
|
||||
0% {
|
||||
border-color: transparent;
|
||||
}
|
||||
33% {
|
||||
border-color: hsla(225, 95%, 63%, 1);
|
||||
}
|
||||
66% {
|
||||
border-color: hsla(267, 95%, 63%, 1);
|
||||
}
|
||||
100% {
|
||||
border-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
|
||||
border-color: transparent;
|
||||
border-radius: 12px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
height: 40px;
|
||||
margin-top: 12px;
|
||||
animation-direction: alternate;
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.slow};
|
||||
animation-fill-mode: none;
|
||||
animation-iteration-count: 2;
|
||||
animation-name: ${BuyCryptoButtonBorderKeyframes};
|
||||
animation-play-state: ${({ $animateBorder }) => ($animateBorder ? 'running' : 'paused')};
|
||||
animation-timing-function: ${({ theme }) => theme.transition.timing.inOut};
|
||||
`
|
||||
const WalletButton = styled(ThemeButton)`
|
||||
border-radius: 12px;
|
||||
padding-top: 10px;
|
||||
@@ -75,7 +111,20 @@ const USDText = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
`
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
@@ -108,7 +157,14 @@ const AccountContainer = styled(ThemedText.BodySmall)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-top: 2.5px;
|
||||
`
|
||||
|
||||
const StyledInfoIcon = styled(Info)`
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
flex: 1 1 auto;
|
||||
`
|
||||
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
|
||||
fill: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
const BalanceWrapper = styled.div`
|
||||
padding: 16px 0;
|
||||
`
|
||||
@@ -137,7 +193,6 @@ const AuthenticatedHeader = () => {
|
||||
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
|
||||
const navigate = useNavigate()
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
|
||||
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
|
||||
const resetSellAssets = useSellAsset((state) => state.reset)
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
@@ -147,7 +202,7 @@ const AuthenticatedHeader = () => {
|
||||
const isUnclaimed = useUserHasAvailableClaim(account)
|
||||
const connectionType = getConnection(connector).type
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
|
||||
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
|
||||
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
|
||||
const disconnect = useCallback(() => {
|
||||
@@ -159,18 +214,63 @@ const AuthenticatedHeader = () => {
|
||||
}, [connector, dispatch])
|
||||
|
||||
const amountUSD = useMemo(() => {
|
||||
if (!nativeCurrencyPrice || !balanceString) return undefined
|
||||
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
|
||||
const balance = parseFloat(balanceString || '0')
|
||||
const balance = parseFloat(balanceString)
|
||||
return price * balance
|
||||
}, [balanceString, nativeCurrencyPrice])
|
||||
|
||||
const navigateToProfile = () => {
|
||||
const navigateToProfile = useCallback(() => {
|
||||
resetSellAssets()
|
||||
setSellPageState(ProfilePageStateType.VIEWING)
|
||||
clearCollectionFilters()
|
||||
navigate('/nfts/profile')
|
||||
closeModal()
|
||||
}
|
||||
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
// animate the border of the buy crypto button when a user navigates here from the feature announcement
|
||||
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
|
||||
const [acknowledgements, acknowledge] = useFiatOnrampAck()
|
||||
const animateBuyCryptoButtonBorder = acknowledgements?.user && !acknowledgements.system
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
let timeoutId = 0
|
||||
if (animateBuyCryptoButtonBorder) {
|
||||
timeoutId = setTimeout(() => {
|
||||
if (stale) return
|
||||
acknowledge({ system: true })
|
||||
}, ms`2 seconds`) as unknown as number
|
||||
// as unknown as number is necessary so it's not incorrectly typed as a NodeJS.Timeout
|
||||
}
|
||||
return () => {
|
||||
stale = true
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [acknowledge, animateBuyCryptoButtonBorder])
|
||||
|
||||
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const [shouldCheck, setShouldCheck] = useState(false)
|
||||
const {
|
||||
available: fiatOnrampAvailable,
|
||||
availabilityChecked: fiatOnrampAvailabilityChecked,
|
||||
error,
|
||||
loading: fiatOnrampAvailabilityLoading,
|
||||
} = useFiatOnrampAvailability(shouldCheck, openFiatOnrampModal)
|
||||
|
||||
const handleBuyCryptoClick = useCallback(() => {
|
||||
if (!fiatOnrampAvailabilityChecked) {
|
||||
setShouldCheck(true)
|
||||
} else if (fiatOnrampAvailable) {
|
||||
openFiatOnrampModal()
|
||||
}
|
||||
}, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFiatOnrampModal])
|
||||
const disableBuyCryptoButton = Boolean(
|
||||
error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading
|
||||
)
|
||||
const [showFiatOnrampUnavailableTooltip, setShow] = useState<boolean>(false)
|
||||
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
|
||||
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<AuthenticatedHeaderWrapper>
|
||||
@@ -192,7 +292,7 @@ const AuthenticatedHeader = () => {
|
||||
<IconButton onClick={copy} Icon={Copy}>
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
</IconButton>
|
||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
|
||||
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
|
||||
<Trans>Explore</Trans>
|
||||
</IconButton>
|
||||
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
|
||||
@@ -205,7 +305,7 @@ const AuthenticatedHeader = () => {
|
||||
<Text fontSize={36} fontWeight={400}>
|
||||
{balanceString} {nativeCurrencySymbol}
|
||||
</Text>
|
||||
<USDText>${amountUSD.toFixed(2)} USD</USDText>
|
||||
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
|
||||
</BalanceWrapper>
|
||||
<ProfileButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
@@ -215,6 +315,44 @@ const AuthenticatedHeader = () => {
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</ProfileButton>
|
||||
{fiatOnrampFlag === BaseVariant.Enabled && (
|
||||
<>
|
||||
<BuyCryptoButton
|
||||
$animateBorder={animateBuyCryptoButtonBorder}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</BuyCryptoButton>
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
<Tooltip
|
||||
show={showFiatOnrampUnavailableTooltip}
|
||||
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
|
||||
>
|
||||
<FiatOnrampAvailabilityExternalLink
|
||||
onMouseEnter={openFiatOnrampUnavailableTooltip}
|
||||
onMouseLeave={closeFiatOnrampUnavailableTooltip}
|
||||
style={{ color: 'inherit' }}
|
||||
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
|
||||
>
|
||||
<StyledInfoIcon />
|
||||
</FiatOnrampAvailabilityExternalLink>
|
||||
</Tooltip>
|
||||
</FiatOnrampNotAvailableText>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isUnclaimed && (
|
||||
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
||||
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection } from 'connection/utils'
|
||||
@@ -9,7 +10,7 @@ import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
@@ -205,6 +206,10 @@ function Web3StatusInner() {
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const theme = useTheme()
|
||||
const toggleWalletDropdown = useToggleWalletDropdown()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent('FOR Account Dropdown Button Clicks')
|
||||
toggleWalletDropdown()
|
||||
}, [toggleWalletDropdown])
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
@@ -221,13 +226,12 @@ function Web3StatusInner() {
|
||||
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const toggleWallet = toggleWalletDropdown
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWallet}>
|
||||
<Web3StatusError onClick={handleWalletDropdownClick}>
|
||||
<NetworkIcon />
|
||||
<Text>
|
||||
<Trans>Error</Trans>
|
||||
@@ -243,7 +247,7 @@ function Web3StatusInner() {
|
||||
return (
|
||||
<Web3StatusConnected
|
||||
data-testid="web3-status-connected"
|
||||
onClick={toggleWallet}
|
||||
onClick={handleWalletDropdownClick}
|
||||
pending={hasPendingTransactions}
|
||||
isClaimAvailable={isClaimAvailable}
|
||||
>
|
||||
@@ -281,7 +285,7 @@ function Web3StatusInner() {
|
||||
<Trans>Connect</Trans>
|
||||
</StyledConnectButton>
|
||||
<VerticalDivider />
|
||||
<ChevronWrapper onClick={toggleWalletDropdown} data-testid="navbar-toggle-dropdown">
|
||||
<ChevronWrapper onClick={handleWalletDropdownClick} data-testid="navbar-toggle-dropdown">
|
||||
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
|
||||
</ChevronWrapper>
|
||||
</Web3StatusConnectWrapper>
|
||||
@@ -312,6 +316,7 @@ export default function Web3Status() {
|
||||
return (
|
||||
<span ref={ref}>
|
||||
<Web3StatusInner />
|
||||
<FiatOnrampAnnouncement />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<Portal>
|
||||
<span ref={walletRef}>
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
interface StakingModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
stakingInfo: StakingInfo
|
||||
}
|
||||
|
||||
export default function ClaimRewardModal({ isOpen, onDismiss, stakingInfo }: StakingModalProps) {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
// monitor call to help UI loading state
|
||||
const addTransaction = useTransactionAdder()
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState(false)
|
||||
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
|
||||
|
||||
async function onClaimReward() {
|
||||
if (stakingContract && stakingInfo?.stakedAmount && account) {
|
||||
setAttempting(true)
|
||||
await stakingContract
|
||||
.getReward({ gasLimit: 350000 })
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
type: TransactionType.CLAIM,
|
||||
recipient: account,
|
||||
})
|
||||
setHash(response.hash)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
setAttempting(false)
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let error: ReactNode | undefined
|
||||
if (!account) {
|
||||
error = <Trans>Connect Wallet</Trans>
|
||||
}
|
||||
if (!stakingInfo?.stakedAmount) {
|
||||
error = error ?? <Trans>Enter an amount</Trans>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Claim</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<CloseIcon onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
{stakingInfo?.earnedAmount && (
|
||||
<AutoColumn justify="center" gap="md">
|
||||
<ThemedText.HeadlineLarge>{stakingInfo?.earnedAmount?.toSignificant(6)}</ThemedText.HeadlineLarge>
|
||||
<ThemedText.DeprecatedBody>
|
||||
<Trans>Unclaimed UNI</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
)}
|
||||
<ThemedText.DeprecatedSubHeader style={{ textAlign: 'center' }}>
|
||||
<Trans>When you claim without withdrawing your liquidity remains in the mining pool.</Trans>
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
<ButtonError disabled={!!error} error={!!error && !!stakingInfo?.stakedAmount} onClick={onClaimReward}>
|
||||
{error ?? <Trans>Claim</Trans>}
|
||||
</ButtonError>
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<LoadingView onDismiss={wrappedOnDismiss}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Claiming {stakingInfo?.earnedAmount?.toSignificant(6)} UNI</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</LoadingView>
|
||||
)}
|
||||
{hash && (
|
||||
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedLargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Claimed UNI!</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</SubmittedView>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import JSBI from 'jsbi'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { BIG_INT_SECONDS_IN_WEEK } from '../../constants/misc'
|
||||
import { useColor } from '../../hooks/useColor'
|
||||
import useStablecoinPrice from '../../hooks/useStablecoinPrice'
|
||||
import { useTotalSupply } from '../../hooks/useTotalSupply'
|
||||
import { useV2Pair } from '../../hooks/useV2Pairs'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { StyledInternalLink, ThemedText } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
import { unwrappedToken } from '../../utils/unwrappedToken'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { RowBetween } from '../Row'
|
||||
import { Break, CardBGImage, CardNoise } from './styled'
|
||||
|
||||
const StatContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-left: 1rem;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const Wrapper = styled(AutoColumn)<{ showBackground: boolean; bgColor: any }>`
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
opacity: ${({ showBackground }) => (showBackground ? '1' : '1')};
|
||||
background: ${({ theme, bgColor, showBackground }) =>
|
||||
`radial-gradient(91.85% 100% at 1.84% 0%, ${bgColor} 0%, ${
|
||||
showBackground ? theme.black : theme.deprecated_bg5
|
||||
} 100%) `};
|
||||
color: ${({ theme, showBackground }) => (showBackground ? theme.white : theme.textPrimary)} !important;
|
||||
|
||||
${({ showBackground }) =>
|
||||
showBackground &&
|
||||
` box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);`}
|
||||
`
|
||||
|
||||
const TopSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 120px;
|
||||
grid-gap: 0px;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
z-index: 1;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
grid-template-columns: 48px 1fr 96px;
|
||||
`};
|
||||
`
|
||||
|
||||
const BottomSection = styled.div<{ showBackground: boolean }>`
|
||||
padding: 12px 16px;
|
||||
opacity: ${({ showBackground }) => (showBackground ? '1' : '0.4')};
|
||||
border-radius: 0 0 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo }) {
|
||||
const token0 = stakingInfo.tokens[0]
|
||||
const token1 = stakingInfo.tokens[1]
|
||||
|
||||
const currency0 = unwrappedToken(token0)
|
||||
const currency1 = unwrappedToken(token1)
|
||||
|
||||
const isStaking = Boolean(stakingInfo.stakedAmount.greaterThan('0'))
|
||||
|
||||
// get the color of the token
|
||||
const token = currency0.isNative ? token1 : token0
|
||||
const WETH = currency0.isNative ? token0 : token1
|
||||
const backgroundColor = useColor(token)
|
||||
|
||||
const totalSupplyOfStakingToken = useTotalSupply(stakingInfo.stakedAmount.currency)
|
||||
const [, stakingTokenPair] = useV2Pair(...stakingInfo.tokens)
|
||||
|
||||
// let returnOverMonth: Percent = new Percent('0')
|
||||
let valueOfTotalStakedAmountInWETH: CurrencyAmount<Token> | undefined
|
||||
if (totalSupplyOfStakingToken && stakingTokenPair) {
|
||||
// take the total amount of LP tokens staked, multiply by ETH value of all LP tokens, divide by all LP tokens
|
||||
valueOfTotalStakedAmountInWETH = CurrencyAmount.fromRawAmount(
|
||||
WETH,
|
||||
JSBI.divide(
|
||||
JSBI.multiply(
|
||||
JSBI.multiply(stakingInfo.totalStakedAmount.quotient, stakingTokenPair.reserveOf(WETH).quotient),
|
||||
JSBI.BigInt(2) // this is b/c the value of LP shares are ~double the value of the WETH they entitle owner to
|
||||
),
|
||||
totalSupplyOfStakingToken.quotient
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// get the USD value of staked WETH
|
||||
const USDPrice = useStablecoinPrice(WETH)
|
||||
const valueOfTotalStakedAmountInUSDC =
|
||||
valueOfTotalStakedAmountInWETH && USDPrice?.quote(valueOfTotalStakedAmountInWETH)
|
||||
|
||||
return (
|
||||
<Wrapper showBackground={isStaking} bgColor={backgroundColor}>
|
||||
<CardBGImage desaturate />
|
||||
<CardNoise />
|
||||
|
||||
<TopSection>
|
||||
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={24} />
|
||||
<ThemedText.DeprecatedWhite fontWeight={600} fontSize={24} style={{ marginLeft: '8px' }}>
|
||||
{currency0.symbol}-{currency1.symbol}
|
||||
</ThemedText.DeprecatedWhite>
|
||||
|
||||
<StyledInternalLink to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`} style={{ width: '100%' }}>
|
||||
<ButtonPrimary padding="8px" $borderRadius="8px">
|
||||
{isStaking ? <Trans>Manage</Trans> : <Trans>Deposit</Trans>}
|
||||
</ButtonPrimary>
|
||||
</StyledInternalLink>
|
||||
</TopSection>
|
||||
|
||||
<StatContainer>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite>
|
||||
<Trans>Total deposited</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
<ThemedText.DeprecatedWhite>
|
||||
{valueOfTotalStakedAmountInUSDC ? (
|
||||
<Trans>${valueOfTotalStakedAmountInUSDC.toFixed(0, { groupSeparator: ',' })}</Trans>
|
||||
) : (
|
||||
<Trans>{valueOfTotalStakedAmountInWETH?.toSignificant(4, { groupSeparator: ',' }) ?? '-'} ETH</Trans>
|
||||
)}
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite>
|
||||
<Trans>Pool rate</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
<ThemedText.DeprecatedWhite>
|
||||
{stakingInfo ? (
|
||||
stakingInfo.active ? (
|
||||
<Trans>
|
||||
{stakingInfo.totalRewardRate?.multiply(BIG_INT_SECONDS_IN_WEEK)?.toFixed(0, { groupSeparator: ',' })}{' '}
|
||||
UNI / week
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>0 UNI / week</Trans>
|
||||
)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
</StatContainer>
|
||||
|
||||
{isStaking && (
|
||||
<>
|
||||
<Break />
|
||||
<BottomSection showBackground={true}>
|
||||
<ThemedText.DeprecatedBlack color="white" fontWeight={500}>
|
||||
<span>
|
||||
<Trans>Your rate</Trans>
|
||||
</span>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
|
||||
<ThemedText.DeprecatedBlack style={{ textAlign: 'right' }} color="white" fontWeight={500}>
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '0.5rem' }}>
|
||||
⚡
|
||||
</span>
|
||||
{stakingInfo ? (
|
||||
stakingInfo.active ? (
|
||||
<Trans>
|
||||
{stakingInfo.rewardRate
|
||||
?.multiply(BIG_INT_SECONDS_IN_WEEK)
|
||||
?.toSignificant(4, { groupSeparator: ',' })}{' '}
|
||||
UNI / week
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>0 UNI / week</Trans>
|
||||
)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</BottomSection>
|
||||
</>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useV2LiquidityTokenPermit } from 'hooks/useV2LiquidityTokenPermit'
|
||||
import { useCallback, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
|
||||
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
|
||||
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { formatCurrencyAmount } from '../../utils/formatCurrencyAmount'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
import { ButtonConfirmed, ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import CurrencyInputPanel from '../CurrencyInputPanel'
|
||||
import Modal from '../Modal'
|
||||
import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import ProgressCircles from '../ProgressSteps'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const HypotheticalRewardRate = styled.div<{ dim: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
|
||||
opacity: ${({ dim }) => (dim ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
interface StakingModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
stakingInfo: StakingInfo
|
||||
userLiquidityUnstaked: CurrencyAmount<Token> | undefined
|
||||
}
|
||||
|
||||
export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiquidityUnstaked }: StakingModalProps) {
|
||||
const { provider } = useWeb3React()
|
||||
|
||||
// track and parse user input
|
||||
const [typedValue, setTypedValue] = useState('')
|
||||
const { parsedAmount, error } = useDerivedStakeInfo(
|
||||
typedValue,
|
||||
stakingInfo.stakedAmount.currency,
|
||||
userLiquidityUnstaked
|
||||
)
|
||||
const parsedAmountWrapped = parsedAmount?.wrapped
|
||||
|
||||
let hypotheticalRewardRate: CurrencyAmount<Token> = CurrencyAmount.fromRawAmount(stakingInfo.rewardRate.currency, '0')
|
||||
if (parsedAmountWrapped?.greaterThan('0')) {
|
||||
hypotheticalRewardRate = stakingInfo.getHypotheticalRewardRate(
|
||||
stakingInfo.stakedAmount.add(parsedAmountWrapped),
|
||||
stakingInfo.totalStakedAmount.add(parsedAmountWrapped),
|
||||
stakingInfo.totalRewardRate
|
||||
)
|
||||
}
|
||||
|
||||
// state for pending and submitted txn views
|
||||
const addTransaction = useTransactionAdder()
|
||||
const [attempting, setAttempting] = useState<boolean>(false)
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const wrappedOnDismiss = useCallback(() => {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
}, [onDismiss])
|
||||
|
||||
// pair contract for this token to be staked
|
||||
const dummyPair = new Pair(
|
||||
CurrencyAmount.fromRawAmount(stakingInfo.tokens[0], '0'),
|
||||
CurrencyAmount.fromRawAmount(stakingInfo.tokens[1], '0')
|
||||
)
|
||||
const pairContract = usePairContract(dummyPair.liquidityToken.address)
|
||||
|
||||
// approval data for stake
|
||||
const deadline = useTransactionDeadline()
|
||||
const router = useV2RouterContract()
|
||||
const { signatureData, gatherPermitSignature } = useV2LiquidityTokenPermit(parsedAmountWrapped, router?.address)
|
||||
const [approval, approveCallback] = useApproveCallback(parsedAmount, stakingInfo.stakingRewardAddress)
|
||||
|
||||
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
|
||||
async function onStake() {
|
||||
setAttempting(true)
|
||||
if (stakingContract && parsedAmount && deadline) {
|
||||
if (approval === ApprovalState.APPROVED) {
|
||||
await stakingContract.stake(`0x${parsedAmount.quotient.toString(16)}`, { gasLimit: 350000 })
|
||||
} else if (signatureData) {
|
||||
stakingContract
|
||||
.stakeWithPermit(
|
||||
`0x${parsedAmount.quotient.toString(16)}`,
|
||||
signatureData.deadline,
|
||||
signatureData.v,
|
||||
signatureData.r,
|
||||
signatureData.s,
|
||||
{ gasLimit: 350000 }
|
||||
)
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
type: TransactionType.DEPOSIT_LIQUIDITY_STAKING,
|
||||
token0Address: stakingInfo.tokens[0].address,
|
||||
token1Address: stakingInfo.tokens[1].address,
|
||||
})
|
||||
setHash(response.hash)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
setAttempting(false)
|
||||
console.log(error)
|
||||
})
|
||||
} else {
|
||||
setAttempting(false)
|
||||
throw new Error('Attempting to stake without approval or a signature. Please contact support.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wrapped onUserInput to clear signatures
|
||||
const onUserInput = useCallback((typedValue: string) => {
|
||||
setTypedValue(typedValue)
|
||||
}, [])
|
||||
|
||||
// used for max input button
|
||||
const maxAmountInput = maxAmountSpend(userLiquidityUnstaked)
|
||||
const atMaxAmount = Boolean(maxAmountInput && parsedAmount?.equalTo(maxAmountInput))
|
||||
const handleMax = useCallback(() => {
|
||||
maxAmountInput && onUserInput(maxAmountInput.toExact())
|
||||
}, [maxAmountInput, onUserInput])
|
||||
|
||||
async function onAttemptToApprove() {
|
||||
if (!pairContract || !provider || !deadline) throw new Error('missing dependencies')
|
||||
if (!parsedAmount) throw new Error('missing liquidity amount')
|
||||
|
||||
if (gatherPermitSignature) {
|
||||
try {
|
||||
await gatherPermitSignature()
|
||||
} catch (error) {
|
||||
// try to approve if gatherPermitSignature failed for any reason other than the user rejecting it
|
||||
if (error?.code !== 4001) {
|
||||
await approveCallback()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await approveCallback()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Deposit</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<CloseIcon onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<CurrencyInputPanel
|
||||
value={typedValue}
|
||||
onUserInput={onUserInput}
|
||||
onMax={handleMax}
|
||||
showMaxButton={!atMaxAmount}
|
||||
currency={stakingInfo.stakedAmount.currency}
|
||||
pair={dummyPair}
|
||||
label=""
|
||||
renderBalance={(amount) => <Trans>Available to deposit: {formatCurrencyAmount(amount, 4)}</Trans>}
|
||||
id="stake-liquidity-token"
|
||||
/>
|
||||
|
||||
<HypotheticalRewardRate dim={!hypotheticalRewardRate.greaterThan('0')}>
|
||||
<div>
|
||||
<ThemedText.DeprecatedBlack fontWeight={600}>
|
||||
<Trans>Weekly Rewards</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</div>
|
||||
|
||||
<ThemedText.DeprecatedBlack>
|
||||
<Trans>
|
||||
{hypotheticalRewardRate
|
||||
.multiply((60 * 60 * 24 * 7).toString())
|
||||
.toSignificant(4, { groupSeparator: ',' })}{' '}
|
||||
UNI / week
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</HypotheticalRewardRate>
|
||||
|
||||
<RowBetween>
|
||||
<ButtonConfirmed
|
||||
mr="0.5rem"
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
>
|
||||
<Trans>Approve</Trans>
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
disabled={!!error || (signatureData === null && approval !== ApprovalState.APPROVED)}
|
||||
error={!!error && !!parsedAmount}
|
||||
onClick={onStake}
|
||||
>
|
||||
{error ?? <Trans>Deposit</Trans>}
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
<ProgressCircles steps={[approval === ApprovalState.APPROVED || signatureData !== null]} disabled={true} />
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<LoadingView onDismiss={wrappedOnDismiss}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedLargeHeader>
|
||||
<Trans>Depositing Liquidity</Trans>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>{parsedAmount?.toSignificant(4)} UNI-V2</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</LoadingView>
|
||||
)}
|
||||
{attempting && hash && (
|
||||
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedLargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Deposited {parsedAmount?.toSignificant(4)} UNI-V2</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</SubmittedView>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import FormattedCurrencyAmount from '../FormattedCurrencyAmount'
|
||||
import Modal from '../Modal'
|
||||
import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
interface StakingModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
stakingInfo: StakingInfo
|
||||
}
|
||||
|
||||
export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: StakingModalProps) {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
// monitor call to help UI loading state
|
||||
const addTransaction = useTransactionAdder()
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState(false)
|
||||
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
|
||||
|
||||
async function onWithdraw() {
|
||||
if (stakingContract && stakingInfo?.stakedAmount) {
|
||||
setAttempting(true)
|
||||
await stakingContract
|
||||
.exit({ gasLimit: 300000 })
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
type: TransactionType.WITHDRAW_LIQUIDITY_STAKING,
|
||||
token0Address: stakingInfo.tokens[0].address,
|
||||
token1Address: stakingInfo.tokens[1].address,
|
||||
})
|
||||
setHash(response.hash)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
setAttempting(false)
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let error: ReactNode | undefined
|
||||
if (!account) {
|
||||
error = <Trans>Connect a wallet</Trans>
|
||||
}
|
||||
if (!stakingInfo?.stakedAmount) {
|
||||
error = error ?? <Trans>Enter an amount</Trans>
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Withdraw</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<CloseIcon onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
{stakingInfo?.stakedAmount && (
|
||||
<AutoColumn justify="center" gap="md">
|
||||
<ThemedText.HeadlineLarge>
|
||||
<FormattedCurrencyAmount currencyAmount={stakingInfo.stakedAmount} />
|
||||
</ThemedText.HeadlineLarge>
|
||||
<ThemedText.DeprecatedBody>
|
||||
<Trans>Deposited liquidity:</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
)}
|
||||
{stakingInfo?.earnedAmount && (
|
||||
<AutoColumn justify="center" gap="md">
|
||||
<ThemedText.HeadlineLarge>
|
||||
<FormattedCurrencyAmount currencyAmount={stakingInfo?.earnedAmount} />
|
||||
</ThemedText.HeadlineLarge>
|
||||
<ThemedText.DeprecatedBody>
|
||||
<Trans>Unclaimed UNI</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
)}
|
||||
<ThemedText.DeprecatedSubHeader style={{ textAlign: 'center' }}>
|
||||
<Trans>When you withdraw, your UNI is claimed and your liquidity is removed from the mining pool.</Trans>
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
<ButtonError disabled={!!error} error={!!error && !!stakingInfo?.stakedAmount} onClick={onWithdraw}>
|
||||
{error ?? <Trans>Withdraw & Claim</Trans>}
|
||||
</ButtonError>
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<LoadingView onDismiss={wrappedOnDismiss}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Withdrawing {stakingInfo?.stakedAmount?.toSignificant(4)} UNI-V2</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Claiming {stakingInfo?.earnedAmount?.toSignificant(4)} UNI</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</LoadingView>
|
||||
)}
|
||||
{hash && (
|
||||
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<ThemedText.DeprecatedLargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Withdrew UNI-V2!</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedBody fontSize={20}>
|
||||
<Trans>Claimed UNI!</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</SubmittedView>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -110,19 +110,10 @@ interface SwapDetailsInlineProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
syncing: boolean
|
||||
loading: boolean
|
||||
showInverted: boolean
|
||||
setShowInverted: React.Dispatch<React.SetStateAction<boolean>>
|
||||
allowedSlippage: Percent
|
||||
}
|
||||
|
||||
export default function SwapDetailsDropdown({
|
||||
trade,
|
||||
syncing,
|
||||
loading,
|
||||
showInverted,
|
||||
setShowInverted,
|
||||
allowedSlippage,
|
||||
}: SwapDetailsInlineProps) {
|
||||
export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) {
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
@@ -169,11 +160,7 @@ export default function SwapDetailsDropdown({
|
||||
)}
|
||||
{trade ? (
|
||||
<LoadingOpacityContainer $loading={syncing}>
|
||||
<TradePrice
|
||||
price={trade.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</LoadingOpacityContainer>
|
||||
) : loading || syncing ? (
|
||||
<ThemedText.DeprecatedMain fontSize={14}>
|
||||
|
||||
@@ -75,7 +75,6 @@ export default function SwapModalHeader({
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice)
|
||||
const [priceUpdate, setPriceUpdate] = useState<number | undefined>()
|
||||
|
||||
@@ -153,7 +152,7 @@ export default function SwapModalHeader({
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}>
|
||||
<TradePrice price={trade.executionPrice} showInverted={showInverted} setShowInverted={setShowInverted} />
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</RowBetween>
|
||||
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Price } from '@uniswap/sdk-core'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
import { useCallback } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { useCallback, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { formatDollar, formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
|
||||
|
||||
interface TradePriceProps {
|
||||
price: Price<Currency, Currency>
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
const StyledPriceContainer = styled.button`
|
||||
@@ -30,8 +27,8 @@ const StyledPriceContainer = styled.button`
|
||||
user-select: text;
|
||||
`
|
||||
|
||||
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useTheme()
|
||||
export default function TradePrice({ price }: TradePriceProps) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
|
||||
const usdcPrice = useStablecoinPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
|
||||
|
||||
@@ -58,9 +55,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
}}
|
||||
title={text}
|
||||
>
|
||||
<Text fontWeight={500} color={theme.textPrimary}>
|
||||
{text}
|
||||
</Text>{' '}
|
||||
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
|
||||
{usdcPrice && (
|
||||
<ThemedText.DeprecatedDarkGray>
|
||||
<Trans>({formatDollar({ num: priceToPreciseFloat(usdcPrice), isPrice: true })})</Trans>
|
||||
|
||||
@@ -11,9 +11,6 @@ export const L2_DEADLINE_FROM_NOW = 60 * 5
|
||||
export const DEFAULT_TXN_DISMISS_MS = 25000
|
||||
export const L2_TXN_DISMISS_MS = 5000
|
||||
|
||||
// used for rewards deadlines
|
||||
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
|
||||
|
||||
export const BIG_INT_ZERO = JSBI.BigInt(0)
|
||||
|
||||
// one basis JSBI.BigInt
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FeatureFlag {
|
||||
fiatOnramp = 'fiatOnramp',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
}
|
||||
|
||||
6
src/featureFlags/flags/fiatOnramp.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useFiatOnrampFlag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
// return useBaseFlag(FeatureFlag.fiatOnramp)
|
||||
}
|
||||
6
src/hooks/useIsLandingPage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export function useIsLandingPage() {
|
||||
const { pathname } = useLocation()
|
||||
return pathname.endsWith('/')
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { body, bodySmall, buttonTextMedium, subhead } from 'nft/css/common.css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset, Rarity, TokenType, WalletAsset } from 'nft/types'
|
||||
import { GenieAsset, Rarity, TokenType, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
|
||||
import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils'
|
||||
import { floorFormatter } from 'nft/utils/numbers'
|
||||
import {
|
||||
@@ -240,16 +240,62 @@ const ImageContainer = ({ children, isDisabled = false }: { children: ReactNode;
|
||||
<StyledImageContainer isDisabled={isDisabled}>{children}</StyledImageContainer>
|
||||
)
|
||||
|
||||
/* -------- CARD IMAGE -------- */
|
||||
const handleUniformAspectRatio = (
|
||||
uniformAspectRatio: UniformAspectRatio,
|
||||
e: React.SyntheticEvent<HTMLElement, Event>,
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void,
|
||||
renderedHeight?: number,
|
||||
setRenderedHeight?: (renderedHeight: number | undefined) => void
|
||||
) => {
|
||||
if (uniformAspectRatio !== UniformAspectRatios.square && setUniformAspectRatio) {
|
||||
const height = e.currentTarget.clientHeight
|
||||
const width = e.currentTarget.clientWidth
|
||||
const aspectRatio = width / height
|
||||
|
||||
const Image = () => {
|
||||
if (
|
||||
(!renderedHeight || renderedHeight !== height) &&
|
||||
aspectRatio < 1 &&
|
||||
uniformAspectRatio !== UniformAspectRatios.square &&
|
||||
setRenderedHeight
|
||||
) {
|
||||
setRenderedHeight(height)
|
||||
}
|
||||
|
||||
if (uniformAspectRatio === UniformAspectRatios.unset) {
|
||||
setUniformAspectRatio(aspectRatio >= 1 ? UniformAspectRatios.square : aspectRatio)
|
||||
} else if (uniformAspectRatio !== aspectRatio) {
|
||||
setUniformAspectRatio(UniformAspectRatios.square)
|
||||
setRenderedHeight && setRenderedHeight(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getHeightFromAspectRatio(uniformAspectRatio: UniformAspectRatio, renderedHeight?: number): number | undefined {
|
||||
return uniformAspectRatio === UniformAspectRatios.square || uniformAspectRatio === UniformAspectRatios.unset
|
||||
? undefined
|
||||
: renderedHeight
|
||||
}
|
||||
|
||||
interface ImageProps {
|
||||
uniformAspectRatio?: UniformAspectRatio
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
renderedHeight?: number
|
||||
setRenderedHeight?: (renderedHeight: number | undefined) => void
|
||||
}
|
||||
|
||||
const Image = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
renderedHeight,
|
||||
setRenderedHeight,
|
||||
}: ImageProps) => {
|
||||
const { hovered, asset } = useCardContext()
|
||||
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
if (noContent) {
|
||||
return <NoContentContainer />
|
||||
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -258,7 +304,7 @@ const Image = () => {
|
||||
as="img"
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
aspectRatio: `${uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'}`,
|
||||
transition: 'transform 0.25s ease 0s',
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
@@ -266,6 +312,7 @@ const Image = () => {
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && !isMobile && styles.cardImageHover, !loaded && styles.loadingBackground)}
|
||||
@@ -274,12 +321,26 @@ const Image = () => {
|
||||
)
|
||||
}
|
||||
|
||||
function getMediaAspectRatio(
|
||||
uniformAspectRatio: UniformAspectRatio,
|
||||
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
): string {
|
||||
return uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
|
||||
}
|
||||
|
||||
interface MediaProps {
|
||||
shouldPlay: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}
|
||||
|
||||
const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
const Video = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
renderedHeight,
|
||||
setRenderedHeight,
|
||||
shouldPlay,
|
||||
setCurrentTokenPlayingMedia,
|
||||
}: MediaProps & ImageProps) => {
|
||||
const vidRef = useRef<HTMLVideoElement>(null)
|
||||
const { hovered, asset } = useCardContext()
|
||||
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
|
||||
@@ -293,7 +354,7 @@ const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
}
|
||||
|
||||
if (noContent) {
|
||||
return <NoContentContainer />
|
||||
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -304,7 +365,7 @@ const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
|
||||
transition: 'transform 0.25s ease 0s',
|
||||
willChange: 'transform',
|
||||
}}
|
||||
@@ -312,7 +373,8 @@ const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
objectFit="contain"
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={() => {
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
visibility={shouldPlay ? 'hidden' : 'visible'}
|
||||
@@ -339,7 +401,9 @@ const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
ref={vidRef}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
aspectRatio: `${
|
||||
uniformAspectRatio === UniformAspectRatios.square || !setUniformAspectRatio ? '1' : 'auto'
|
||||
}`,
|
||||
}}
|
||||
onEnded={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -372,7 +436,14 @@ const Video = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const Audio = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
const Audio = ({
|
||||
uniformAspectRatio = UniformAspectRatios.square,
|
||||
setUniformAspectRatio,
|
||||
renderedHeight,
|
||||
setRenderedHeight,
|
||||
shouldPlay,
|
||||
setCurrentTokenPlayingMedia,
|
||||
}: MediaProps & ImageProps) => {
|
||||
const audRef = useRef<HTMLAudioElement>(null)
|
||||
const { hovered, asset } = useCardContext()
|
||||
const [noContent, setNoContent] = useState(!asset.smallImageUrl && !asset.imageUrl)
|
||||
@@ -386,7 +457,7 @@ const Audio = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
}
|
||||
|
||||
if (noContent) {
|
||||
return <NoContentContainer />
|
||||
return <NoContentContainer height={getHeightFromAspectRatio(uniformAspectRatio, renderedHeight)} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -397,7 +468,7 @@ const Audio = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
alt={asset.name || asset.tokenId}
|
||||
width="full"
|
||||
style={{
|
||||
aspectRatio: '1',
|
||||
aspectRatio: getMediaAspectRatio(uniformAspectRatio, setUniformAspectRatio),
|
||||
transition: 'transform 0.4s ease 0s',
|
||||
}}
|
||||
src={asset.imageUrl || asset.smallImageUrl}
|
||||
@@ -405,6 +476,7 @@ const Audio = ({ shouldPlay, setCurrentTokenPlayingMedia }: MediaProps) => {
|
||||
draggable={false}
|
||||
onError={() => setNoContent(true)}
|
||||
onLoad={(e) => {
|
||||
handleUniformAspectRatio(uniformAspectRatio, e, setUniformAspectRatio, renderedHeight, setRenderedHeight)
|
||||
setImageLoaded(true)
|
||||
}}
|
||||
className={clsx(hovered && !isMobile && styles.cardImageHover, !imageLoaded && styles.loadingBackground)}
|
||||
@@ -729,12 +801,13 @@ const Pool = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const NoContentContainer = () => (
|
||||
const NoContentContainer = ({ height }: { height?: number }) => (
|
||||
<>
|
||||
<Box
|
||||
position="relative"
|
||||
width="full"
|
||||
style={{
|
||||
height: height ? `${height}px` : 'auto',
|
||||
paddingTop: '100%',
|
||||
background: `linear-gradient(90deg, ${themeVars.colors.backgroundSurface} 0%, ${themeVars.colors.backgroundInteractive} 95.83%)`,
|
||||
}}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Tooltip from 'components/Tooltip'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { bodySmall } from 'nft/css/common.css'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { GenieAsset, isPooledMarket, TokenType } from 'nft/types'
|
||||
import { GenieAsset, isPooledMarket, TokenType, UniformAspectRatio } from 'nft/types'
|
||||
import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -22,6 +22,10 @@ interface CollectionAssetProps {
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
rarityVerified?: boolean
|
||||
uniformAspectRatio: UniformAspectRatio
|
||||
setUniformAspectRatio: (uniformAspectRatio: UniformAspectRatio) => void
|
||||
renderedHeight?: number
|
||||
setRenderedHeight: (renderedHeight: number | undefined) => void
|
||||
}
|
||||
|
||||
const TOOLTIP_TIMEOUT = 2000
|
||||
@@ -43,6 +47,10 @@ export const CollectionAsset = ({
|
||||
mediaShouldBePlaying,
|
||||
setCurrentTokenPlayingMedia,
|
||||
rarityVerified,
|
||||
uniformAspectRatio,
|
||||
setUniformAspectRatio,
|
||||
renderedHeight,
|
||||
setRenderedHeight,
|
||||
}: CollectionAssetProps) => {
|
||||
const bagManuallyClosed = useBag((state) => state.bagManuallyClosed)
|
||||
const addAssetsToBag = useBag((state) => state.addAssetsToBag)
|
||||
@@ -161,11 +169,30 @@ export const CollectionAsset = ({
|
||||
timeout={isMobile ? TOOLTIP_TIMEOUT : undefined}
|
||||
>
|
||||
{assetMediaType === AssetMediaType.Image ? (
|
||||
<Card.Image />
|
||||
<Card.Image
|
||||
uniformAspectRatio={uniformAspectRatio}
|
||||
setUniformAspectRatio={setUniformAspectRatio}
|
||||
renderedHeight={renderedHeight}
|
||||
setRenderedHeight={setRenderedHeight}
|
||||
/>
|
||||
) : assetMediaType === AssetMediaType.Video ? (
|
||||
<Card.Video shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
|
||||
<Card.Video
|
||||
shouldPlay={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
uniformAspectRatio={uniformAspectRatio}
|
||||
setUniformAspectRatio={setUniformAspectRatio}
|
||||
renderedHeight={renderedHeight}
|
||||
setRenderedHeight={setRenderedHeight}
|
||||
/>
|
||||
) : (
|
||||
<Card.Audio shouldPlay={mediaShouldBePlaying} setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} />
|
||||
<Card.Audio
|
||||
shouldPlay={mediaShouldBePlaying}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
uniformAspectRatio={uniformAspectRatio}
|
||||
setUniformAspectRatio={setUniformAspectRatio}
|
||||
renderedHeight={renderedHeight}
|
||||
setRenderedHeight={setRenderedHeight}
|
||||
/>
|
||||
)}
|
||||
</MouseoverTooltip>
|
||||
</Card.ImageContainer>
|
||||
|
||||
@@ -35,7 +35,16 @@ import {
|
||||
} from 'nft/hooks'
|
||||
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
|
||||
import { usePriceRange } from 'nft/hooks/usePriceRange'
|
||||
import { DropDownOption, GenieAsset, GenieCollection, isPooledMarket, Markets, TokenType } from 'nft/types'
|
||||
import {
|
||||
DropDownOption,
|
||||
GenieAsset,
|
||||
GenieCollection,
|
||||
isPooledMarket,
|
||||
Markets,
|
||||
TokenType,
|
||||
UniformAspectRatio,
|
||||
UniformAspectRatios,
|
||||
} from 'nft/types'
|
||||
import {
|
||||
calcPoolPrice,
|
||||
calcSudoSwapPrice,
|
||||
@@ -169,17 +178,17 @@ const MarketNameWrapper = styled(Row)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
export const LoadingAssets = ({ count }: { count?: number }) => (
|
||||
export const LoadingAssets = ({ count, height }: { count?: number; height?: number }) => (
|
||||
<>
|
||||
{Array.from(Array(count ?? ASSET_PAGE_SIZE), (_, index) => (
|
||||
<CollectionAssetLoading key={index} />
|
||||
<CollectionAssetLoading key={index} height={height} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
const CollectionNftsLoading = () => (
|
||||
const CollectionNftsLoading = ({ height }: { height?: number }) => (
|
||||
<Box width="full" className={styles.assetList}>
|
||||
<LoadingAssets />
|
||||
<LoadingAssets height={height} />
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -269,6 +278,9 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
const debouncedMaxPrice = useDebounce(maxPrice, 500)
|
||||
const debouncedSearchByNameText = useDebounce(searchByNameText, 500)
|
||||
|
||||
const [uniformAspectRatio, setUniformAspectRatio] = useState<UniformAspectRatio>(UniformAspectRatios.unset)
|
||||
const [renderedHeight, setRenderedHeight] = useState<number | undefined>()
|
||||
|
||||
const [sweepIsOpen, setSweepOpen] = useState(false)
|
||||
// Load all sweep queries. Loading them on the parent allows lazy-loading, but avoids waterfalling requests.
|
||||
const collectionParams = useSweepFetcherParams(contractAddress, 'others', debouncedMinPrice, debouncedMaxPrice)
|
||||
@@ -413,9 +425,13 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
mediaShouldBePlaying={asset.tokenId === currentTokenPlayingMedia}
|
||||
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
|
||||
rarityVerified={rarityVerified}
|
||||
uniformAspectRatio={uniformAspectRatio}
|
||||
setUniformAspectRatio={setUniformAspectRatio}
|
||||
renderedHeight={renderedHeight}
|
||||
setRenderedHeight={setRenderedHeight}
|
||||
/>
|
||||
))
|
||||
}, [collectionAssets, currentTokenPlayingMedia, isMobile, rarityVerified])
|
||||
}, [collectionAssets, isMobile, currentTokenPlayingMedia, rarityVerified, uniformAspectRatio, renderedHeight])
|
||||
|
||||
const hasNfts = collectionAssets && collectionAssets.length > 0
|
||||
const hasErc1155s = hasNfts && collectionAssets[0] && collectionAssets[0].tokenType === TokenType.ERC1155
|
||||
@@ -461,6 +477,11 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
setUniformAspectRatio(UniformAspectRatios.unset)
|
||||
setRenderedHeight(undefined)
|
||||
}, [contractAddress])
|
||||
|
||||
useEffect(() => {
|
||||
if (collectionStats && collectionStats.stats?.floor_price) {
|
||||
const lowValue = collectionStats.stats?.floor_price
|
||||
@@ -601,7 +622,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
<InfiniteScroll
|
||||
next={handleNextPageLoad}
|
||||
hasMore={hasNext}
|
||||
loader={Boolean(hasNext && hasNfts) && <LoadingAssets />}
|
||||
loader={Boolean(hasNext && hasNfts) && <LoadingAssets height={renderedHeight} />}
|
||||
dataLength={collectionAssets?.length ?? 0}
|
||||
style={{ overflow: 'unset' }}
|
||||
className={hasNfts || isLoadingNext ? styles.assetList : undefined}
|
||||
@@ -624,7 +645,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
</EmptyCollectionWrapper>
|
||||
</Center>
|
||||
) : (
|
||||
<CollectionNftsLoading />
|
||||
<CollectionNftsLoading height={renderedHeight} />
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
</InfiniteScrollWrapper>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
import Modal from 'components/Modal'
|
||||
import { useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme/components'
|
||||
import { ThemedText } from 'theme/components/text'
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 30% 24px 24px;
|
||||
overflow: hidden;
|
||||
height: fit-content;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const CloseButton = styled(X)`
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 24px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const Background = styled.img`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const Link = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.accentActive};
|
||||
stroke: ${({ theme }) => theme.accentActive};
|
||||
`
|
||||
|
||||
const Title = styled(ThemedText.LargeHeader)`
|
||||
@media (max-width: ${({ theme }) => theme.breakpoint.xl}px) {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
`
|
||||
|
||||
const Paragraph = styled(ThemedText.BodySecondary)`
|
||||
line-height: 24px;
|
||||
|
||||
@media (max-width: ${({ theme }) => theme.breakpoint.xl}px) {
|
||||
font-size: 14px !important;
|
||||
line-height: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const BACKGROUND_IMAGE = {
|
||||
dark: {
|
||||
src: require('../../../assets/images/welcomeModal-dark.jpg').default,
|
||||
srcSet: `
|
||||
${require('../../../assets/images/welcomeModal-dark@2x.jpg').default} 2x,
|
||||
${require('../../../assets/images/welcomeModal-dark@3x.jpg').default} 3x,
|
||||
`,
|
||||
},
|
||||
light: {
|
||||
src: require('../../../assets/images/welcomeModal-light.jpg').default,
|
||||
srcSet: `
|
||||
${require('../../../assets/images/welcomeModal-light@2x.jpg').default} 2x,
|
||||
${require('../../../assets/images/welcomeModal-light@3x.jpg').default} 3x,
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
export function WelcomeModal({ onDismissed }: { onDismissed: () => void }) {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
const dismiss = () => {
|
||||
setIsOpen(false)
|
||||
setTimeout(() => onDismissed())
|
||||
}
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onSwipe={dismiss} maxWidth={720} isBottomSheet={false}>
|
||||
<Container data-testid="nft-welcome-modal">
|
||||
<Background
|
||||
{...(theme.darkMode ? BACKGROUND_IMAGE.dark : BACKGROUND_IMAGE.light)}
|
||||
alt="Welcome modal background"
|
||||
draggable={false}
|
||||
/>
|
||||
<Content>
|
||||
<Title>Introducing NFTs on Uniswap</Title>
|
||||
<Paragraph>
|
||||
You can now buy and sell NFTs on Uniswap across marketplaces. Trade here to find more listings and better
|
||||
prices. <br />
|
||||
<br />
|
||||
NFTs on Uniswap replaces Genie, which was{' '}
|
||||
<Link href="https://uniswap.org/blog/genie" title="Uniswap Labs has acquired Genie">
|
||||
acquired{' '}
|
||||
</Link>{' '}
|
||||
by Uniswap Labs earlier this year. If you have used Genie in the past, you may be eligible for a USDC
|
||||
airdrop.{' '}
|
||||
<Link
|
||||
href="https://uniswap.org/blog/uniswap-nft-aggregator-announcement"
|
||||
title="Uniswap NFT aggregator announcement"
|
||||
>
|
||||
Learn more.
|
||||
</Link>
|
||||
</Paragraph>
|
||||
<CloseButton data-testid="nft-intro-modal" size={24} onClick={dismiss} />
|
||||
</Content>
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { LARGE_MEDIA_BREAKPOINT, SMALL_MOBILE_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { bodySmall, subhead } from 'nft/css/common.css'
|
||||
import { X } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useHideNftPromoBanner } from 'state/user/hooks'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import nftPromoImage1 from '../nftExploreBanner/nftArt1.png'
|
||||
import nftPromoImage2 from '../nftExploreBanner/nftArt2.png'
|
||||
import nftPromoImage3 from '../nftExploreBanner/nftArt3.png'
|
||||
|
||||
function getRandom(list: any[]) {
|
||||
return list[Math.floor(Math.random() * list.length)]
|
||||
}
|
||||
const randomizedNftImage = getRandom([nftPromoImage1, nftPromoImage2, nftPromoImage3])
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
display: ${({ show }) => (show ? 'flex' : 'none')};
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: clamp(0px, 1vw, 16px);
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
width: 98vw;
|
||||
bottom: 55px;
|
||||
@media screen and (min-width: ${LARGE_MEDIA_BREAKPOINT}) {
|
||||
bottom: 48px;
|
||||
}
|
||||
@media screen and (min-width: ${SMALL_MOBILE_MEDIA_BREAKPOINT}) {
|
||||
width: 391px;
|
||||
}
|
||||
:hover {
|
||||
border: double 1px transparent;
|
||||
border-radius: 12px;
|
||||
background-image: ${({ theme }) =>
|
||||
`linear-gradient(${theme.backgroundSurface}, ${theme.backgroundSurface}),
|
||||
radial-gradient(circle at top left, hsla(299, 100%, 87%, 1), hsla(299, 100%, 61%, 1))`};
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
}
|
||||
`
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
`
|
||||
|
||||
const StyledXButton = styled(X)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
&:active {
|
||||
opacity: ${({ theme }) => theme.opacity.click};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledImageContainer = styled(Box)`
|
||||
width: 20%;
|
||||
cursor: pointer;
|
||||
aspectratio: 1;
|
||||
transition: transform 0.25s ease 0s;
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const LinkStyle = css`
|
||||
color: ${({ theme }) => theme.accentActive};
|
||||
stroke: ${({ theme }) => theme.accentActive};
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
${ClickableStyle}
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
export default function NftExploreBanner() {
|
||||
const [hideNftPromoBanner, toggleHideNftPromoBanner] = useHideNftPromoBanner()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const navigateToNfts = () => {
|
||||
navigate('/nfts')
|
||||
toggleHideNftPromoBanner()
|
||||
}
|
||||
|
||||
return (
|
||||
<PopupContainer show={!hideNftPromoBanner} onClick={navigateToNfts}>
|
||||
<InnerContainer>
|
||||
<StyledImageContainer as="img" src={randomizedNftImage} draggable={false} />
|
||||
<TextContainer>
|
||||
{/* <HeaderText> */}
|
||||
<div className={subhead}>
|
||||
<Trans>Introducing NFTs on Uniswap</Trans>
|
||||
</div>
|
||||
{/* </HeaderText> */}
|
||||
|
||||
{/* <Description> */}
|
||||
<div className={bodySmall}>
|
||||
<Trans>Buy and sell NFTs across more listings at better prices.</Trans>{' '}
|
||||
<StyledLink to="/nfts">
|
||||
<Trans>Explore NFTs</Trans>
|
||||
</StyledLink>{' '}
|
||||
</div>
|
||||
</TextContainer>
|
||||
{/* </Description> */}
|
||||
<StyledXButton
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleHideNftPromoBanner()
|
||||
}}
|
||||
/>
|
||||
</InnerContainer>
|
||||
</PopupContainer>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 58 KiB |
@@ -34,7 +34,7 @@ const ListingHeader = styled(Row)`
|
||||
margin-top: 18px;
|
||||
|
||||
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
margin-top: 40px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -10,9 +10,21 @@ import { LOOKS_RARE_CREATOR_BASIS_POINTS } from 'nft/utils'
|
||||
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { Dispatch, FormEvent, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import * as styles from './ListPage.css'
|
||||
|
||||
const TableHeader = styled.div`
|
||||
display: flex;
|
||||
position: sticky;
|
||||
align-items: center;
|
||||
top: 72px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
z-index: 1;
|
||||
background-color: ${({ theme }) => theme.backgroundBackdrop};
|
||||
`
|
||||
|
||||
enum SetPriceMethod {
|
||||
SAME_PRICE,
|
||||
FLOOR_PRICE,
|
||||
@@ -44,7 +56,7 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Row marginTop="20">
|
||||
<TableHeader>
|
||||
<Column
|
||||
marginLeft={selectedMarkets.length > 1 ? '36' : '0'}
|
||||
transition="500"
|
||||
@@ -86,7 +98,7 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM
|
||||
You receive
|
||||
</Column>
|
||||
</Row>
|
||||
</Row>
|
||||
</TableHeader>
|
||||
{sellAssets.map((asset) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,10 +2,8 @@ import { Trace } from '@uniswap/analytics'
|
||||
import { PageName } from '@uniswap/analytics-events'
|
||||
import Banner from 'nft/components/explore/Banner'
|
||||
import TrendingCollections from 'nft/components/explore/TrendingCollections'
|
||||
import { WelcomeModal } from 'nft/components/explore/WelcomeModal'
|
||||
import { useBag } from 'nft/hooks'
|
||||
import { useEffect } from 'react'
|
||||
import { useHideNFTWelcomeModal } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const ExploreContainer = styled.div`
|
||||
@@ -25,7 +23,6 @@ const ExploreContainer = styled.div`
|
||||
|
||||
const NftExplore = () => {
|
||||
const setBagExpanded = useBag((state) => state.setBagExpanded)
|
||||
const [isModalHidden, hideModal] = useHideNFTWelcomeModal()
|
||||
|
||||
useEffect(() => {
|
||||
setBagExpanded({ bagExpanded: false, manualClose: false })
|
||||
@@ -38,7 +35,6 @@ const NftExplore = () => {
|
||||
<Banner />
|
||||
<TrendingCollections />
|
||||
</ExploreContainer>
|
||||
{!isModalHidden && <WelcomeModal onDismissed={hideModal} />}
|
||||
</Trace>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -36,12 +36,12 @@ export type CollectionSort = Record<
|
||||
'asc' | 'desc' | 1 | -1 | { $gte?: string | number; $lte?: string | number } | string | number
|
||||
>
|
||||
|
||||
export enum UniformHeights {
|
||||
export enum UniformAspectRatios {
|
||||
unset,
|
||||
notUniform,
|
||||
square,
|
||||
}
|
||||
|
||||
export type UniformHeight = UniformHeights | number
|
||||
export type UniformAspectRatio = UniformAspectRatios | number
|
||||
|
||||
export enum ActivityEventType {
|
||||
Listing = 'LISTING',
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, EventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
const DARK_MODE_GRADIENT = 'linear-gradient(180deg, rgba(19, 22, 27, 0.54) 0%, #13161b 100%)'
|
||||
|
||||
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string }>`
|
||||
display: flex;
|
||||
background: ${({ isDarkMode, backgroundImgSrc }) =>
|
||||
isDarkMode
|
||||
? `${DARK_MODE_GRADIENT} ${backgroundImgSrc ? `, url(${backgroundImgSrc})` : ''}`
|
||||
: `url(${backgroundImgSrc})`};
|
||||
background-size: auto 100%;
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
padding: 24px;
|
||||
height: 200px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? 'transparent' : theme.backgroundOutline)};
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundOutline : theme.textTertiary)};
|
||||
}
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 200)}px;
|
||||
padding: 40px;
|
||||
}
|
||||
`
|
||||
|
||||
const CardTitle = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const CardDescription = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
`
|
||||
|
||||
const Card = ({
|
||||
title,
|
||||
description,
|
||||
to,
|
||||
external,
|
||||
backgroundImgSrc,
|
||||
elementName,
|
||||
}: {
|
||||
title: string
|
||||
description: string
|
||||
to: string
|
||||
external?: boolean
|
||||
backgroundImgSrc?: string
|
||||
elementName: string
|
||||
}) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={elementName}>
|
||||
<StyledCard
|
||||
as={external ? 'a' : Link}
|
||||
to={external ? undefined : to}
|
||||
href={external ? to : undefined}
|
||||
target={external ? '_blank' : undefined}
|
||||
rel={external ? 'noopenener noreferrer' : undefined}
|
||||
isDarkMode={isDarkMode}
|
||||
backgroundImgSrc={backgroundImgSrc}
|
||||
>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</StyledCard>
|
||||
</TraceEvent>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
@@ -1,52 +0,0 @@
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
const StyledStep = styled.div<{ selected: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 24px 0;
|
||||
color: ${({ theme, selected }) => (selected ? theme.textPrimary : theme.textSecondary)};
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} color`};
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const StepIndex = styled.span`
|
||||
margin-right: 24px;
|
||||
margin-left: 8px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
margin-right: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const Step = ({
|
||||
index,
|
||||
title,
|
||||
onSelect,
|
||||
selected,
|
||||
}: {
|
||||
index: number
|
||||
title: string
|
||||
onSelect: () => void
|
||||
selected: boolean
|
||||
}) => {
|
||||
return (
|
||||
<StyledStep onClick={onSelect} onMouseEnter={onSelect} selected={selected}>
|
||||
<StepIndex>{index + 1}</StepIndex>
|
||||
{title}
|
||||
</StyledStep>
|
||||
)
|
||||
}
|
||||
|
||||
export default Step
|
||||
@@ -1,44 +0,0 @@
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
export const Title = styled.h1<{ isDarkMode: boolean }>`
|
||||
color: transparent;
|
||||
font-size: 48px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0px;
|
||||
max-width: 800px;
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'conic-gradient(from 180deg at 50% 50%, #FFF4CF 0deg, #EBFFBF 95.62deg, #E3CDFF 175.81deg, #FFCDF4 269.07deg, #FFFBEF 360deg);'
|
||||
: 'linear-gradient(230.12deg, #8A80FF 37.26%, #FF7DE2 52.98%, #FF3998 68.06%)'};
|
||||
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 64px;
|
||||
line-height: 72px;
|
||||
}
|
||||
`
|
||||
|
||||
export const SubTitle = styled.h2<{ isDarkMode?: boolean }>`
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
max-width: 340px;
|
||||
color: transparent;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'conic-gradient(from 180deg at 50% 50%, #FFF4CF 0deg, #EBFFBF 95.62deg, #E3CDFF 175.81deg, #FFCDF4 269.07deg, #FFFBEF 360deg);'
|
||||
: 'linear-gradient(230.12deg, #8A80FF 37.26%, #FF7DE2 52.98%, #FF3998 68.06%)'};
|
||||
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
}
|
||||
`
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ElementName } from '@uniswap/analytics-events'
|
||||
|
||||
import darkNftCardImgSrc from './images/darkNftCard.png'
|
||||
import darkSwapSrc from './images/darkSwap.png'
|
||||
import darkSwapCardImgSrc from './images/darkSwapCard.png'
|
||||
import darkWalletsSrc from './images/darkWallets.png'
|
||||
import lightNftCardImgSrc from './images/lightNftCard.png'
|
||||
import lightSwapSrc from './images/lightSwap.png'
|
||||
import lightSwapCardImgSrc from './images/lightSwapCard.png'
|
||||
import lightWalletsSrc from './images/lightWallets.png'
|
||||
import tokens from './images/tokens.png'
|
||||
|
||||
export const CARDS = [
|
||||
{
|
||||
to: '/swap',
|
||||
title: 'Swap tokens',
|
||||
description: 'Buy, sell, and explore tokens on Ethereum, Polygon, Optimism, and more.',
|
||||
darkBackgroundImgSrc: darkSwapCardImgSrc,
|
||||
lightBackgroundImgSrc: lightSwapCardImgSrc,
|
||||
elementName: ElementName.ABOUT_PAGE_SWAP_CARD,
|
||||
},
|
||||
{
|
||||
to: '/nfts',
|
||||
title: 'Trade NFTs',
|
||||
description: 'Buy and sell NFTs across marketplaces to find more listings at better prices.',
|
||||
darkBackgroundImgSrc: darkNftCardImgSrc,
|
||||
lightBackgroundImgSrc: lightNftCardImgSrc,
|
||||
elementName: ElementName.ABOUT_PAGE_NFTS_CARD,
|
||||
},
|
||||
]
|
||||
|
||||
export const STEPS = [
|
||||
{
|
||||
title: 'Connect a wallet',
|
||||
description: 'Connect your preferred crypto wallet to the Uniswap Interface.',
|
||||
lightImgSrc: lightWalletsSrc,
|
||||
darkImgSrc: darkWalletsSrc,
|
||||
},
|
||||
{
|
||||
title: 'Transfer crypto',
|
||||
description: 'Trade crypto and NFTs through Uniswap’s platform',
|
||||
lightImgSrc: tokens,
|
||||
darkImgSrc: tokens,
|
||||
},
|
||||
{
|
||||
title: 'Trade tokens and NFTs',
|
||||
description: 'Trade crypto and NFTs through Uniswap’s platform',
|
||||
lightImgSrc: lightSwapSrc,
|
||||
darkImgSrc: darkSwapSrc,
|
||||
},
|
||||
]
|
||||
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 54 KiB |
@@ -1,335 +0,0 @@
|
||||
import { Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events'
|
||||
import { ButtonOutlined } from 'components/Button'
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { BookOpen, Globe, Heart, Twitter } from 'react-feather'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import Card from './Card'
|
||||
import { CARDS, STEPS } from './constants'
|
||||
import backgroundImgSrcDark from './images/About_BG_Dark.jpg'
|
||||
import backgroundImgSrcLight from './images/About_BG_Light.jpg'
|
||||
import Step from './Step'
|
||||
import { SubTitle, Title } from './Title'
|
||||
|
||||
const Page = styled.div<{ isDarkMode: boolean; titleHeight: number }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: calc(100vh - ${({ titleHeight }) => titleHeight + 200}px);
|
||||
`
|
||||
|
||||
const PageBackground = styled.div<{ isDarkMode: boolean }>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
top: -${({ theme }) => theme.navHeight}px;
|
||||
left: 0;
|
||||
opacity: ${({ isDarkMode }) => (isDarkMode ? 0.4 : 0.2)};
|
||||
background: ${({ isDarkMode }) => (isDarkMode ? `url(${backgroundImgSrcDark})` : `url(${backgroundImgSrcLight})`)};
|
||||
-webkit-mask-image: linear-gradient(to bottom, black 40%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, black 40%, transparent 100%);
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
`
|
||||
|
||||
const Panels = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
gap: 24px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
gap: 120px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const Content = styled.div`
|
||||
max-width: 1280px;
|
||||
pointer-events: all;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 0px 16px 16px 16px;
|
||||
gap: 48px;
|
||||
z-index: 1;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
padding: 0px 80px 80px 80px;
|
||||
gap: 96px;
|
||||
}
|
||||
`
|
||||
|
||||
const CardGrid = styled.div`
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoButton = styled(ButtonOutlined)`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
padding: 12px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const ActionsContainer = styled.span`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
`
|
||||
|
||||
const StepList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const Intro = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
`
|
||||
|
||||
const IntroCopy = styled.p`
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const ThumbnailContainer = styled.div`
|
||||
align-self: center;
|
||||
`
|
||||
|
||||
const Thumbnail = styled.img`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const FooterLinks = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
`
|
||||
|
||||
const FooterLink = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
`
|
||||
|
||||
const Footer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 48px;
|
||||
`
|
||||
|
||||
const Copyright = styled.span`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
`
|
||||
|
||||
const WrappedExternalArrow = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
export default function About() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const titleRef = useRef<HTMLDivElement>(null)
|
||||
const [titleHeight, setTitleHeight] = useState(0)
|
||||
useLayoutEffect(() => {
|
||||
if (titleRef.current) {
|
||||
setTitleHeight(titleRef.current.scrollHeight)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [selectedStepIndex, setSelectedStepIndex] = useState(0)
|
||||
const selectedStep = STEPS[selectedStepIndex]
|
||||
const thumbnailImgSrc = isDarkMode ? selectedStep?.darkImgSrc : selectedStep?.lightImgSrc
|
||||
|
||||
return (
|
||||
<Trace page={PageName.ABOUT_PAGE} shouldLogImpression>
|
||||
<Page isDarkMode={isDarkMode} titleHeight={titleHeight}>
|
||||
<Content>
|
||||
<Title ref={titleRef} isDarkMode={isDarkMode}>
|
||||
Uniswap is the leading on-chain marketplace for tokens and NFTs
|
||||
</Title>
|
||||
<Panels>
|
||||
<div>
|
||||
<SubTitle isDarkMode={isDarkMode}>Powered by the Uniswap Protocol</SubTitle>
|
||||
</div>
|
||||
<Intro>
|
||||
<IntroCopy>The leading decentralized crypto trading protocol, governed by a global community</IntroCopy>
|
||||
<ActionsContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.LEGACY_LANDING_PAGE_LINK}
|
||||
>
|
||||
<InfoButton as="a" rel="noopener noreferrer" href="https://uniswap.org" target="_blank">
|
||||
Learn more<WrappedExternalArrow> ↗</WrappedExternalArrow>
|
||||
</InfoButton>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.DOCS_LINK}
|
||||
>
|
||||
<InfoButton as="a" rel="noopener noreferrer" href="https://docs.uniswap.org" target="_blank">
|
||||
Read docs<WrappedExternalArrow> ↗</WrappedExternalArrow>
|
||||
</InfoButton>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
</Intro>
|
||||
</Panels>
|
||||
<CardGrid>
|
||||
{CARDS.map(({ darkBackgroundImgSrc, lightBackgroundImgSrc, ...card }) => (
|
||||
<Card
|
||||
{...card}
|
||||
backgroundImgSrc={isDarkMode ? darkBackgroundImgSrc : lightBackgroundImgSrc}
|
||||
key={card.title}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
<div>
|
||||
<SubTitle isDarkMode={isDarkMode}>Get Started</SubTitle>
|
||||
<Panels>
|
||||
<ThumbnailContainer>
|
||||
<Thumbnail alt="Thumbnail" src={thumbnailImgSrc} />
|
||||
</ThumbnailContainer>
|
||||
<StepList>
|
||||
{STEPS.map((step, index) => (
|
||||
<Step
|
||||
selected={selectedStepIndex === index}
|
||||
onSelect={() => setSelectedStepIndex(index)}
|
||||
index={index}
|
||||
key={step.title}
|
||||
title={step.title}
|
||||
/>
|
||||
))}
|
||||
</StepList>
|
||||
</Panels>
|
||||
</div>
|
||||
<Footer>
|
||||
<FooterLinks>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.SUPPORT_LINK}
|
||||
>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://support.uniswap.org">
|
||||
<Globe /> Support
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.TWITTER_LINK}
|
||||
>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://twitter.com/uniswap">
|
||||
<Twitter /> Twitter
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.BLOG_LINK}
|
||||
>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://uniswap.org/blog">
|
||||
<BookOpen /> Blog
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.CAREERS_LINK}
|
||||
>
|
||||
<FooterLink rel="noopener noreferrer" target="_blank" href="https://boards.greenhouse.io/uniswaplabs">
|
||||
<Heart /> Careers
|
||||
</FooterLink>
|
||||
</TraceEvent>
|
||||
</FooterLinks>
|
||||
<Copyright>© {new Date().getFullYear()} Uniswap Labs</Copyright>
|
||||
</Footer>
|
||||
</Content>
|
||||
<PageBackground isDarkMode={isDarkMode} />
|
||||
</Page>
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
@@ -27,12 +27,9 @@ import Polling from '../components/Polling'
|
||||
import Popups from '../components/Popups'
|
||||
import { useIsExpertMode } from '../state/user/hooks'
|
||||
import DarkModeQueryParamReader from '../theme/components/DarkModeQueryParamReader'
|
||||
import About from './About'
|
||||
import AddLiquidity from './AddLiquidity'
|
||||
import { RedirectDuplicateTokenIds } from './AddLiquidity/redirects'
|
||||
import { RedirectDuplicateTokenIdsV2 } from './AddLiquidityV2/redirects'
|
||||
import Earn from './Earn'
|
||||
import Manage from './Earn/Manage'
|
||||
import Landing from './Landing'
|
||||
import MigrateV2 from './MigrateV2'
|
||||
import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
|
||||
@@ -44,7 +41,7 @@ import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
import RemoveLiquidityV3 from './RemoveLiquidity/V3'
|
||||
import Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly } from './Swap/redirects'
|
||||
import { RedirectPathToSwapOnly } from './Swap/redirects'
|
||||
import Tokens from './Tokens'
|
||||
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
@@ -102,7 +99,7 @@ const HeaderWrapper = styled.div<{ transparent?: boolean }>`
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
`
|
||||
|
||||
function getCurrentPageFromLocation(locationPathname: string): PageName | undefined {
|
||||
@@ -218,9 +215,6 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
<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="send" element={<RedirectPathToSwapOnly />} />
|
||||
<Route path="swap" element={<Swap />} />
|
||||
@@ -254,8 +248,6 @@ export default function App() {
|
||||
<Route path="migrate/v2" element={<MigrateV2 />} />
|
||||
<Route path="migrate/v2/:address" element={<MigrateV2Pair />} />
|
||||
|
||||
<Route path="about" element={<About />} />
|
||||
|
||||
<Route
|
||||
path="/nfts"
|
||||
element={
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { REWARDS_DURATION_DAYS, STAKING_GENESIS } from '../../state/stake/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
|
||||
const MINUTE = 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = HOUR * 24
|
||||
const REWARDS_DURATION = DAY * REWARDS_DURATION_DAYS
|
||||
|
||||
export function Countdown({ exactEnd }: { exactEnd?: Date }) {
|
||||
// get end/beginning times
|
||||
const end = useMemo(
|
||||
() => (exactEnd ? Math.floor(exactEnd.getTime() / 1000) : STAKING_GENESIS + REWARDS_DURATION),
|
||||
[exactEnd]
|
||||
)
|
||||
const begin = useMemo(() => end - REWARDS_DURATION, [end])
|
||||
|
||||
// get current time
|
||||
const [time, setTime] = useState(() => Math.floor(Date.now() / 1000))
|
||||
useEffect((): (() => void) | void => {
|
||||
// we only need to tick if rewards haven't ended yet
|
||||
if (time <= end) {
|
||||
const timeout = setTimeout(() => setTime(Math.floor(Date.now() / 1000)), 1000)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
}, [time, end])
|
||||
|
||||
const timeUntilGenesis = begin - time
|
||||
const timeUntilEnd = end - time
|
||||
|
||||
let timeRemaining: number
|
||||
let message: string
|
||||
if (timeUntilGenesis >= 0) {
|
||||
message = 'Rewards begin in'
|
||||
timeRemaining = timeUntilGenesis
|
||||
} else {
|
||||
const ongoing = timeUntilEnd >= 0
|
||||
if (ongoing) {
|
||||
message = 'Rewards end in'
|
||||
timeRemaining = timeUntilEnd
|
||||
} else {
|
||||
message = 'Rewards have ended!'
|
||||
timeRemaining = Infinity
|
||||
}
|
||||
}
|
||||
|
||||
const days = (timeRemaining - (timeRemaining % DAY)) / DAY
|
||||
timeRemaining -= days * DAY
|
||||
const hours = (timeRemaining - (timeRemaining % HOUR)) / HOUR
|
||||
timeRemaining -= hours * HOUR
|
||||
const minutes = (timeRemaining - (timeRemaining % MINUTE)) / MINUTE
|
||||
timeRemaining -= minutes * MINUTE
|
||||
const seconds = timeRemaining
|
||||
|
||||
return (
|
||||
<ThemedText.DeprecatedBlack fontWeight={400}>
|
||||
{message}{' '}
|
||||
{Number.isFinite(timeRemaining) && (
|
||||
<code>
|
||||
{`${days}:${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds
|
||||
.toString()
|
||||
.padStart(2, '0')}`}
|
||||
</code>
|
||||
)}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
)
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import JSBI from 'jsbi'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CountUp } from 'use-count-up'
|
||||
|
||||
import { ButtonEmpty, ButtonPrimary } from '../../components/Button'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import DoubleCurrencyLogo from '../../components/DoubleLogo'
|
||||
import ClaimRewardModal from '../../components/earn/ClaimRewardModal'
|
||||
import StakingModal from '../../components/earn/StakingModal'
|
||||
import { CardBGImage, CardNoise, CardSection, DataCard } from '../../components/earn/styled'
|
||||
import UnstakingModal from '../../components/earn/UnstakingModal'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import { BIG_INT_SECONDS_IN_WEEK, BIG_INT_ZERO } from '../../constants/misc'
|
||||
import { useCurrency } from '../../hooks/Tokens'
|
||||
import { useColor } from '../../hooks/useColor'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import useStablecoinPrice from '../../hooks/useStablecoinPrice'
|
||||
import { useTotalSupply } from '../../hooks/useTotalSupply'
|
||||
import { useV2Pair } from '../../hooks/useV2Pairs'
|
||||
import { useToggleWalletModal } from '../../state/application/hooks'
|
||||
import { useTokenBalance } from '../../state/connection/hooks'
|
||||
import { useStakingInfo } from '../../state/stake/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
padding: 68px 8px 0px;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
padding: 48px 8px 0px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const PositionInfo = styled(AutoColumn)<{ dim: any }>`
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
opacity: ${({ dim }) => (dim ? 0.6 : 1)};
|
||||
`
|
||||
|
||||
const BottomSection = styled(AutoColumn)`
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const StyledDataCard = styled(DataCard)<{ bgColor?: any; showBackground?: any }>`
|
||||
background: radial-gradient(76.02% 75.41% at 1.84% 0%, #1e1a31 0%, #3d51a5 100%);
|
||||
z-index: 2;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
background: ${({ theme, bgColor, showBackground }) =>
|
||||
`radial-gradient(91.85% 100% at 1.84% 0%, ${bgColor} 0%, ${
|
||||
showBackground ? theme.black : theme.deprecated_bg5
|
||||
} 100%) `};
|
||||
`
|
||||
|
||||
const StyledBottomCard = styled(DataCard)<{ dim: any }>`
|
||||
background: ${({ theme }) => theme.deprecated_bg3};
|
||||
opacity: ${({ dim }) => (dim ? 0.4 : 1)};
|
||||
margin-top: -40px;
|
||||
padding: 0 1.25rem 1rem 1.25rem;
|
||||
padding-top: 32px;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const PoolData = styled(DataCard)`
|
||||
background: none;
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg4};
|
||||
padding: 1rem;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const VoteCard = styled(DataCard)`
|
||||
background: radial-gradient(76.02% 75.41% at 1.84% 0%, #27ae60 0%, #000000 100%);
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Manage() {
|
||||
const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string }>()
|
||||
const { account } = useWeb3React()
|
||||
|
||||
// get currencies and pair
|
||||
const [currencyA, currencyB] = [useCurrency(currencyIdA), useCurrency(currencyIdB)]
|
||||
const tokenA = (currencyA ?? undefined)?.wrapped
|
||||
const tokenB = (currencyB ?? undefined)?.wrapped
|
||||
|
||||
const [, stakingTokenPair] = useV2Pair(tokenA, tokenB)
|
||||
const stakingInfo = useStakingInfo(stakingTokenPair)?.[0]
|
||||
|
||||
// detect existing unstaked LP position to show add button if none found
|
||||
const userLiquidityUnstaked = useTokenBalance(account ?? undefined, stakingInfo?.stakedAmount?.currency)
|
||||
const showAddLiquidityButton = Boolean(stakingInfo?.stakedAmount?.equalTo('0') && userLiquidityUnstaked?.equalTo('0'))
|
||||
|
||||
// toggle for staking modal and unstaking modal
|
||||
const [showStakingModal, setShowStakingModal] = useState(false)
|
||||
const [showUnstakingModal, setShowUnstakingModal] = useState(false)
|
||||
const [showClaimRewardModal, setShowClaimRewardModal] = useState(false)
|
||||
|
||||
// fade cards if nothing staked or nothing earned yet
|
||||
const disableTop = !stakingInfo?.stakedAmount || stakingInfo.stakedAmount.equalTo(JSBI.BigInt(0))
|
||||
|
||||
const token = currencyA?.isNative ? tokenB : tokenA
|
||||
const WETH = currencyA?.isNative ? tokenA : tokenB
|
||||
const backgroundColor = useColor(token)
|
||||
|
||||
// get WETH value of staked LP tokens
|
||||
const totalSupplyOfStakingToken = useTotalSupply(stakingInfo?.stakedAmount?.currency)
|
||||
let valueOfTotalStakedAmountInWETH: CurrencyAmount<Token> | undefined
|
||||
if (totalSupplyOfStakingToken && stakingTokenPair && stakingInfo && WETH) {
|
||||
// take the total amount of LP tokens staked, multiply by ETH value of all LP tokens, divide by all LP tokens
|
||||
valueOfTotalStakedAmountInWETH = CurrencyAmount.fromRawAmount(
|
||||
WETH,
|
||||
JSBI.divide(
|
||||
JSBI.multiply(
|
||||
JSBI.multiply(stakingInfo.totalStakedAmount.quotient, stakingTokenPair.reserveOf(WETH).quotient),
|
||||
JSBI.BigInt(2) // this is b/c the value of LP shares are ~double the value of the WETH they entitle owner to
|
||||
),
|
||||
totalSupplyOfStakingToken.quotient
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const countUpAmount = stakingInfo?.earnedAmount?.toFixed(6) ?? '0'
|
||||
const countUpAmountPrevious = usePrevious(countUpAmount) ?? '0'
|
||||
|
||||
// get the USD value of staked WETH
|
||||
const USDPrice = useStablecoinPrice(WETH)
|
||||
const valueOfTotalStakedAmountInUSDC =
|
||||
valueOfTotalStakedAmountInWETH && USDPrice?.quote(valueOfTotalStakedAmountInWETH)
|
||||
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
|
||||
const handleDepositClick = useCallback(() => {
|
||||
if (account) {
|
||||
setShowStakingModal(true)
|
||||
} else {
|
||||
toggleWalletModal()
|
||||
}
|
||||
}, [account, toggleWalletModal])
|
||||
|
||||
return (
|
||||
<PageWrapper gap="lg" justify="center">
|
||||
<RowBetween style={{ gap: '24px' }}>
|
||||
<ThemedText.DeprecatedMediumHeader style={{ margin: 0 }}>
|
||||
<Trans>
|
||||
{currencyA?.symbol}-{currencyB?.symbol} Liquidity Mining
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<DoubleCurrencyLogo currency0={currencyA ?? undefined} currency1={currencyB ?? undefined} size={24} />
|
||||
</RowBetween>
|
||||
|
||||
<DataRow style={{ gap: '24px' }}>
|
||||
<PoolData>
|
||||
<AutoColumn gap="sm">
|
||||
<ThemedText.DeprecatedBody style={{ margin: 0 }}>
|
||||
<Trans>Total deposits</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedBody fontSize={24} fontWeight={500}>
|
||||
{valueOfTotalStakedAmountInUSDC
|
||||
? `$${valueOfTotalStakedAmountInUSDC.toFixed(0, { groupSeparator: ',' })}`
|
||||
: `${valueOfTotalStakedAmountInWETH?.toSignificant(4, { groupSeparator: ',' }) ?? '-'} ETH`}
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</PoolData>
|
||||
<PoolData>
|
||||
<AutoColumn gap="sm">
|
||||
<ThemedText.DeprecatedBody style={{ margin: 0 }}>
|
||||
<Trans>Pool Rate</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedBody fontSize={24} fontWeight={500}>
|
||||
{stakingInfo?.active ? (
|
||||
<Trans>
|
||||
{stakingInfo.totalRewardRate?.multiply(BIG_INT_SECONDS_IN_WEEK)?.toFixed(0, { groupSeparator: ',' })}{' '}
|
||||
UNI / week
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>0 UNI / week</Trans>
|
||||
)}
|
||||
</ThemedText.DeprecatedBody>
|
||||
</AutoColumn>
|
||||
</PoolData>
|
||||
</DataRow>
|
||||
|
||||
{showAddLiquidityButton && (
|
||||
<VoteCard>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
<CardSection>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite fontWeight={600}>
|
||||
<Trans>Step 1. Get UNI-V2 Liquidity tokens</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
<RowBetween style={{ marginBottom: '1rem' }}>
|
||||
<ThemedText.DeprecatedWhite fontSize={14}>
|
||||
<Trans>
|
||||
UNI-V2 LP tokens are required. Once you've added liquidity to the {currencyA?.symbol}-
|
||||
{currencyB?.symbol} pool you can stake your liquidity tokens on this page.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
$borderRadius="8px"
|
||||
width="fit-content"
|
||||
as={Link}
|
||||
to={`/add/${currencyA && currencyId(currencyA)}/${currencyB && currencyId(currencyB)}`}
|
||||
>
|
||||
<Trans>
|
||||
Add {currencyA?.symbol}-{currencyB?.symbol} liquidity
|
||||
</Trans>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</CardSection>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
</VoteCard>
|
||||
)}
|
||||
|
||||
{stakingInfo && (
|
||||
<>
|
||||
<StakingModal
|
||||
isOpen={showStakingModal}
|
||||
onDismiss={() => setShowStakingModal(false)}
|
||||
stakingInfo={stakingInfo}
|
||||
userLiquidityUnstaked={userLiquidityUnstaked}
|
||||
/>
|
||||
<UnstakingModal
|
||||
isOpen={showUnstakingModal}
|
||||
onDismiss={() => setShowUnstakingModal(false)}
|
||||
stakingInfo={stakingInfo}
|
||||
/>
|
||||
<ClaimRewardModal
|
||||
isOpen={showClaimRewardModal}
|
||||
onDismiss={() => setShowClaimRewardModal(false)}
|
||||
stakingInfo={stakingInfo}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<PositionInfo gap="lg" justify="center" dim={showAddLiquidityButton}>
|
||||
<BottomSection gap="lg" justify="center">
|
||||
<StyledDataCard disabled={disableTop} bgColor={backgroundColor} showBackground={!showAddLiquidityButton}>
|
||||
<CardSection>
|
||||
<CardBGImage desaturate />
|
||||
<CardNoise />
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite fontWeight={600}>
|
||||
<Trans>Your liquidity deposits</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
<RowBetween style={{ alignItems: 'baseline' }}>
|
||||
<ThemedText.DeprecatedWhite fontSize={36} fontWeight={600}>
|
||||
{stakingInfo?.stakedAmount?.toSignificant(6) ?? '-'}
|
||||
</ThemedText.DeprecatedWhite>
|
||||
<ThemedText.DeprecatedWhite>
|
||||
<Trans>
|
||||
UNI-V2 {currencyA?.symbol}-{currencyB?.symbol}
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</CardSection>
|
||||
</StyledDataCard>
|
||||
<StyledBottomCard dim={stakingInfo?.stakedAmount?.equalTo(JSBI.BigInt(0))}>
|
||||
<CardBGImage desaturate />
|
||||
<CardNoise />
|
||||
<AutoColumn gap="sm">
|
||||
<RowBetween>
|
||||
<div>
|
||||
<ThemedText.DeprecatedBlack>
|
||||
<Trans>Your unclaimed UNI</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</div>
|
||||
{stakingInfo?.earnedAmount && JSBI.notEqual(BIG_INT_ZERO, stakingInfo?.earnedAmount?.quotient) && (
|
||||
<ButtonEmpty
|
||||
padding="8px"
|
||||
$borderRadius="8px"
|
||||
width="fit-content"
|
||||
onClick={() => setShowClaimRewardModal(true)}
|
||||
>
|
||||
<Trans>Claim</Trans>
|
||||
</ButtonEmpty>
|
||||
)}
|
||||
</RowBetween>
|
||||
<RowBetween style={{ alignItems: 'baseline' }}>
|
||||
<ThemedText.DeprecatedLargeHeader fontSize={36} fontWeight={600}>
|
||||
<CountUp
|
||||
key={countUpAmount}
|
||||
isCounting
|
||||
decimalPlaces={4}
|
||||
start={parseFloat(countUpAmountPrevious)}
|
||||
end={parseFloat(countUpAmount)}
|
||||
thousandsSeparator=","
|
||||
duration={1}
|
||||
/>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedBlack fontSize={16} fontWeight={500}>
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '8px ' }}>
|
||||
⚡
|
||||
</span>
|
||||
|
||||
{stakingInfo?.active ? (
|
||||
<Trans>
|
||||
{stakingInfo.rewardRate?.multiply(BIG_INT_SECONDS_IN_WEEK)?.toFixed(0, { groupSeparator: ',' })}{' '}
|
||||
UNI / week
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>0 UNI / week</Trans>
|
||||
)}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</StyledBottomCard>
|
||||
</BottomSection>
|
||||
<ThemedText.DeprecatedMain style={{ textAlign: 'center' }} fontSize={14}>
|
||||
<span role="img" aria-label="wizard-icon" style={{ marginRight: '8px' }}>
|
||||
⭐️
|
||||
</span>
|
||||
<Trans>When you withdraw, the contract will automagically claim UNI on your behalf!</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
|
||||
{!showAddLiquidityButton && (
|
||||
<DataRow style={{ marginBottom: '1rem' }}>
|
||||
{stakingInfo && stakingInfo.active && (
|
||||
<ButtonPrimary padding="8px" $borderRadius="8px" width="160px" onClick={handleDepositClick}>
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) ? (
|
||||
<Trans>Deposit</Trans>
|
||||
) : (
|
||||
<Trans>Deposit UNI-V2 LP Tokens</Trans>
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
|
||||
{stakingInfo?.stakedAmount?.greaterThan(JSBI.BigInt(0)) && (
|
||||
<>
|
||||
<ButtonPrimary
|
||||
padding="8px"
|
||||
$borderRadius="8px"
|
||||
width="160px"
|
||||
onClick={() => setShowUnstakingModal(true)}
|
||||
>
|
||||
<Trans>Withdraw</Trans>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
</DataRow>
|
||||
)}
|
||||
{!userLiquidityUnstaked ? null : userLiquidityUnstaked.equalTo('0') ? null : !stakingInfo?.active ? null : (
|
||||
<ThemedText.DeprecatedMain>
|
||||
<Trans>{userLiquidityUnstaked.toSignificant(6)} UNI-V2 LP tokens available</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
)}
|
||||
</PositionInfo>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import JSBI from 'jsbi'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { OutlineCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import PoolCard from '../../components/earn/PoolCard'
|
||||
import { CardBGImage, CardNoise, CardSection, DataCard } from '../../components/earn/styled'
|
||||
import Loader from '../../components/Loader'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import { BIG_INT_ZERO } from '../../constants/misc'
|
||||
import { STAKING_REWARDS_INFO, useStakingInfo } from '../../state/stake/hooks'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
import { Countdown } from './Countdown'
|
||||
|
||||
const PageWrapper = styled(AutoColumn)`
|
||||
padding: 68px 8px 0px;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
padding: 48px 8px 0px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
padding-top: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const TopSection = styled(AutoColumn)`
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const PoolSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: 10px;
|
||||
row-gap: 15px;
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
`
|
||||
|
||||
const DataRow = styled(RowBetween)`
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Earn() {
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
// staking info for connected account
|
||||
const stakingInfos = useStakingInfo()
|
||||
|
||||
/**
|
||||
* only show staking cards with balance
|
||||
* @todo only account for this if rewards are inactive
|
||||
*/
|
||||
const stakingInfosWithBalance = stakingInfos?.filter((s) => JSBI.greaterThan(s.stakedAmount.quotient, BIG_INT_ZERO))
|
||||
|
||||
// toggle copy if rewards are inactive
|
||||
const stakingRewardsExist = Boolean(typeof chainId === 'number' && (STAKING_REWARDS_INFO[chainId]?.length ?? 0) > 0)
|
||||
|
||||
return (
|
||||
<PageWrapper gap="lg" justify="center">
|
||||
<TopSection gap="md">
|
||||
<DataCard>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
<CardSection>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite fontWeight={600}>
|
||||
<Trans>Uniswap liquidity mining</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedWhite fontSize={14}>
|
||||
<Trans>
|
||||
Deposit your Liquidity Provider tokens to receive UNI, the Uniswap protocol governance token.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</RowBetween>{' '}
|
||||
<ExternalLink
|
||||
style={{ color: theme.white, textDecoration: 'underline' }}
|
||||
href="https://uniswap.org/blog/uni/"
|
||||
target="_blank"
|
||||
>
|
||||
<ThemedText.DeprecatedWhite fontSize={14}>
|
||||
<Trans>Read more about UNI</Trans>
|
||||
</ThemedText.DeprecatedWhite>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</CardSection>
|
||||
<CardBGImage />
|
||||
<CardNoise />
|
||||
</DataCard>
|
||||
</TopSection>
|
||||
|
||||
<AutoColumn gap="lg" style={{ width: '100%', maxWidth: '720px' }}>
|
||||
<DataRow style={{ alignItems: 'baseline' }}>
|
||||
<ThemedText.DeprecatedMediumHeader style={{ marginTop: '0.5rem' }}>
|
||||
<Trans>Participating pools</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<Countdown exactEnd={stakingInfos?.[0]?.periodFinish} />
|
||||
</DataRow>
|
||||
|
||||
<PoolSection>
|
||||
{stakingRewardsExist && stakingInfos?.length === 0 ? (
|
||||
<Loader style={{ margin: 'auto' }} />
|
||||
) : !stakingRewardsExist ? (
|
||||
<OutlineCard>
|
||||
<Trans>No active pools</Trans>
|
||||
</OutlineCard>
|
||||
) : stakingInfos?.length !== 0 && stakingInfosWithBalance.length === 0 ? (
|
||||
<OutlineCard>
|
||||
<Trans>No active pools</Trans>
|
||||
</OutlineCard>
|
||||
) : (
|
||||
stakingInfosWithBalance?.map((stakingInfo) => {
|
||||
// need to sort by added liquidity here
|
||||
return <PoolCard key={stakingInfo.stakingRewardAddress} stakingInfo={stakingInfo} />
|
||||
})
|
||||
)}
|
||||
</PoolSection>
|
||||
</AutoColumn>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +1,74 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, ElementName, EventName, PageName } from '@uniswap/analytics-events'
|
||||
import { AboutFooter } from 'components/About/AboutFooter'
|
||||
import Card, { CardType } from 'components/About/Card'
|
||||
import { MAIN_CARDS, MORE_CARDS } from 'components/About/constants'
|
||||
import ProtocolBanner from 'components/About/ProtocolBanner'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import Swap from 'pages/Swap'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { parse } from 'qs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ArrowDownCircle } from 'react-feather'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Link as NativeLink } from 'react-router-dom'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const PageContainer = styled.div`
|
||||
const PageContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: ${({ theme }) => theme.navHeight}px 0px 0px 0px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.navHeight + theme.mobileBottomBarHeight}px)`};
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.navHeight}px)`};
|
||||
}
|
||||
`
|
||||
|
||||
const Gradient = styled.div<{ isDarkMode: boolean }>`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)'
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'};
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
`
|
||||
|
||||
const Gradient = styled.div<{ isDarkMode: boolean }>`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
min-height: 550px;
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)'
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'};
|
||||
z-index: ${Z_INDEX.under_dropdown};
|
||||
pointer-events: none;
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.mobileBottomBarHeight}px)`};
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
height: 100vh;
|
||||
}
|
||||
`
|
||||
|
||||
const GlowContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.mobileBottomBarHeight}px)`};
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||
height: 100vh;
|
||||
}
|
||||
`
|
||||
|
||||
const Glow = styled.div`
|
||||
@@ -45,41 +80,39 @@ const Glow = styled.div`
|
||||
border-radius: 24px;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
padding: 0 0 40px;
|
||||
max-width: min(720px, 90%);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
padding: 32px 0 80px;
|
||||
min-height: 500px;
|
||||
z-index: ${Z_INDEX.under_dropdown};
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.navHeight + theme.mobileBottomBarHeight}px)`};
|
||||
pointer-events: none;
|
||||
* {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
padding: 64px 0;
|
||||
}
|
||||
`
|
||||
|
||||
const TitleText = styled.h1<{ isDarkMode: boolean }>`
|
||||
color: transparent;
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 24px;
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(20deg, rgba(255, 244, 207, 1) 10%, rgba(255, 87, 218, 1) 100%)'
|
||||
: 'linear-gradient(10deg, rgba(255,79,184,1) 0%, rgba(255,159,251,1) 100%)'};
|
||||
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
|
||||
@@ -120,7 +153,7 @@ const LandingButton = styled(BaseButton)`
|
||||
`
|
||||
|
||||
const ButtonCTA = styled(LandingButton)`
|
||||
background: linear-gradient(10deg, rgba(255, 0, 199, 1) 0%, rgba(255, 159, 251, 1) 100%);
|
||||
background: linear-gradient(93.06deg, #ff00c7 2.66%, #ff9ffb 98.99%);
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.white};
|
||||
transition: ${({ theme }) => `all ${theme.transition.duration.medium} ${theme.transition.timing.ease}`};
|
||||
@@ -144,6 +177,84 @@ const ButtonCTAText = styled.p`
|
||||
const ActionsContainer = styled.span`
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const LearnMoreContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 36px 0 0;
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
pointer-events: auto;
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const LearnMoreArrow = styled(ArrowDownCircle)`
|
||||
margin-left: 14px;
|
||||
size: 20px;
|
||||
`
|
||||
|
||||
const AboutContentContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 24px 5rem;
|
||||
width: 100%;
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(179.82deg, rgba(0, 0, 0, 0) 0.16%, #050026 99.85%)'
|
||||
: 'linear-gradient(179.82deg, rgba(255, 255, 255, 0) 0.16%, #eaeaea 99.85%)'};
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
padding: 0 96px 5rem;
|
||||
}
|
||||
`
|
||||
|
||||
const CardGrid = styled.div<{ cols: number }>`
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 24px 0 0;
|
||||
max-width: 1440px;
|
||||
scroll-margin: ${({ theme }) => `${theme.navHeight}px 0 0`};
|
||||
|
||||
grid-template-columns: 1fr;
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
// At this screen size, we show up to 2 columns.
|
||||
grid-template-columns: ${({ cols }) =>
|
||||
Array.from(Array(cols === 2 ? 2 : 1))
|
||||
.map(() => '1fr')
|
||||
.join(' ')};
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
|
||||
// at this screen size, always show the max number of columns
|
||||
grid-template-columns: ${({ cols }) =>
|
||||
Array.from(Array(cols))
|
||||
.map(() => '1fr')
|
||||
.join(' ')};
|
||||
gap: 32px;
|
||||
}
|
||||
`
|
||||
|
||||
const LandingSwapContainer = styled.div`
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.mobileBottomBarHeight}px)`};
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LandingSwap = styled(Swap)`
|
||||
@@ -166,50 +277,89 @@ const Link = styled(NativeLink)`
|
||||
export default function Landing() {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const cardsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [showContent, setShowContent] = useState(false)
|
||||
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const isOpen = location.pathname === '/'
|
||||
const queryParams = parse(location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
})
|
||||
|
||||
// This can be simplified significantly once the flag is removed! For now being explicit is clearer.
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto'
|
||||
if (queryParams.intro || !selectedWallet) {
|
||||
setShowContent(true)
|
||||
} else {
|
||||
navigate('/swap')
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!isOpen) return null
|
||||
}, [navigate, selectedWallet, queryParams.intro])
|
||||
|
||||
return (
|
||||
<Trace page={PageName.LANDING_PAGE} shouldLogImpression>
|
||||
<PageContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.LANDING_PAGE_SWAP_ELEMENT}
|
||||
>
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
</TraceEvent>
|
||||
<Glow />
|
||||
<Gradient isDarkMode={isDarkMode} />
|
||||
<ContentContainer isDarkMode={isDarkMode}>
|
||||
<TitleText isDarkMode={isDarkMode}>Trade crypto & NFTs with confidence</TitleText>
|
||||
<SubTextContainer>
|
||||
<SubText>Buy, sell, and explore tokens and NFTs</SubText>
|
||||
</SubTextContainer>
|
||||
<ActionsContainer>
|
||||
{showContent && (
|
||||
<PageContainer isDarkMode={isDarkMode} data-testid="landing-page">
|
||||
<LandingSwapContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.CONTINUE_BUTTON}
|
||||
element={ElementName.LANDING_PAGE_SWAP_ELEMENT}
|
||||
>
|
||||
<ButtonCTA as={Link} to="/swap">
|
||||
<ButtonCTAText>Get started</ButtonCTAText>
|
||||
</ButtonCTA>
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
</ContentContainer>
|
||||
</PageContainer>
|
||||
</LandingSwapContainer>
|
||||
<Gradient isDarkMode={isDarkMode} />
|
||||
<GlowContainer>
|
||||
<Glow />
|
||||
</GlowContainer>
|
||||
<ContentContainer isDarkMode={isDarkMode}>
|
||||
<TitleText isDarkMode={isDarkMode}>Trade crypto & NFTs with confidence</TitleText>
|
||||
<SubTextContainer>
|
||||
<SubText>Buy, sell, and explore tokens and NFTs</SubText>
|
||||
</SubTextContainer>
|
||||
<ActionsContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={EventName.ELEMENT_CLICKED}
|
||||
element={ElementName.CONTINUE_BUTTON}
|
||||
>
|
||||
<ButtonCTA as={Link} to="/swap">
|
||||
<ButtonCTAText>Get started</ButtonCTAText>
|
||||
</ButtonCTA>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
<LearnMoreContainer
|
||||
onClick={() => {
|
||||
cardsRef?.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
<Trans>Learn more</Trans>
|
||||
<LearnMoreArrow />
|
||||
</LearnMoreContainer>
|
||||
</ContentContainer>
|
||||
<AboutContentContainer isDarkMode={isDarkMode}>
|
||||
<CardGrid cols={2} ref={cardsRef}>
|
||||
{MAIN_CARDS.map(({ darkBackgroundImgSrc, lightBackgroundImgSrc, ...card }) => (
|
||||
<Card
|
||||
{...card}
|
||||
backgroundImgSrc={isDarkMode ? darkBackgroundImgSrc : lightBackgroundImgSrc}
|
||||
key={card.title}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
<CardGrid cols={3}>
|
||||
{MORE_CARDS.map(({ darkIcon, lightIcon, ...card }) => (
|
||||
<Card {...card} icon={isDarkMode ? darkIcon : lightIcon} key={card.title} type={CardType.Secondary} />
|
||||
))}
|
||||
</CardGrid>
|
||||
<ProtocolBanner />
|
||||
<AboutFooter />
|
||||
</AboutContentContainer>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -441,7 +441,6 @@ export default function Swap({ className }: { className?: string }) {
|
||||
])
|
||||
|
||||
// errors
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>()
|
||||
|
||||
// warnings on the greater of fiat value price impact and execution price impact
|
||||
@@ -663,8 +662,6 @@ export default function Swap({ className }: { className?: string }) {
|
||||
trade={trade}
|
||||
syncing={routeIsSyncing}
|
||||
loading={routeIsLoading}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
allowedSlippage={allowedSlippage}
|
||||
/>
|
||||
</DetailsSwapSection>
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
import { ApplicationModal, setOpenModal } from '../../state/application/reducer'
|
||||
|
||||
// Redirects to swap but only replace the pathname
|
||||
export function RedirectPathToSwapOnly() {
|
||||
const location = useLocation()
|
||||
return <Navigate to={{ ...location, pathname: '/swap' }} replace />
|
||||
}
|
||||
|
||||
export function OpenClaimAddressModalAndRedirectToSwap() {
|
||||
const dispatch = useAppDispatch()
|
||||
useEffect(() => {
|
||||
dispatch(setOpenModal(ApplicationModal.ADDRESS_CLAIM))
|
||||
}, [dispatch])
|
||||
return <RedirectPathToSwapOnly />
|
||||
}
|
||||
|
||||
@@ -1,15 +1,87 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
|
||||
import { AppState } from '../index'
|
||||
import { addPopup, ApplicationModal, PopupContent, removePopup, setOpenModal } from './reducer'
|
||||
import {
|
||||
addPopup,
|
||||
ApplicationModal,
|
||||
PopupContent,
|
||||
removePopup,
|
||||
setFiatOnrampAvailability,
|
||||
setOpenModal,
|
||||
} from './reducer'
|
||||
|
||||
export function useModalIsOpen(modal: ApplicationModal): boolean {
|
||||
const openModal = useAppSelector((state: AppState) => state.application.openModal)
|
||||
return openModal === modal
|
||||
}
|
||||
|
||||
/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#ip_addresses */
|
||||
interface MoonpayIPAddressesResponse {
|
||||
alpha3?: string
|
||||
isAllowed?: boolean
|
||||
isBuyAllowed?: boolean
|
||||
isSellAllowed?: boolean
|
||||
}
|
||||
|
||||
async function getMoonpayAvailability(): Promise<boolean> {
|
||||
const moonpayPublishableKey = process.env.REACT_APP_MOONPAY_PUBLISHABLE_KEY
|
||||
if (!moonpayPublishableKey) {
|
||||
throw new Error('Must provide a publishable key for moonpay.')
|
||||
}
|
||||
const moonpayApiURI = process.env.REACT_APP_MOONPAY_API
|
||||
if (!moonpayApiURI) {
|
||||
throw new Error('Must provide an api endpoint for moonpay.')
|
||||
}
|
||||
const res = await fetch(`${moonpayApiURI}/v4/ip_address?apiKey=${moonpayPublishableKey}`)
|
||||
const data = await (res.json() as Promise<MoonpayIPAddressesResponse>)
|
||||
return data.isBuyAllowed ?? false
|
||||
}
|
||||
|
||||
export function useFiatOnrampAvailability(shouldCheck: boolean, callback?: () => void) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { available, availabilityChecked } = useAppSelector((state: AppState) => state.application.fiatOnramp)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAvailability() {
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await getMoonpayAvailability()
|
||||
sendAnalyticsEvent('MoonPay Geochecker', { success: result })
|
||||
if (stale) return
|
||||
dispatch(setFiatOnrampAvailability(result))
|
||||
if (result && callback) {
|
||||
callback()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error checking onramp availability', e.toString())
|
||||
if (stale) return
|
||||
setError('Error, try again later.')
|
||||
dispatch(setFiatOnrampAvailability(false))
|
||||
} finally {
|
||||
if (stale) return
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!availabilityChecked && shouldCheck) {
|
||||
checkAvailability()
|
||||
}
|
||||
|
||||
let stale = false
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [availabilityChecked, callback, dispatch, shouldCheck])
|
||||
|
||||
return { available, availabilityChecked, loading, error }
|
||||
}
|
||||
|
||||
export function useToggleModal(modal: ApplicationModal): () => void {
|
||||
const isOpen = useModalIsOpen(modal)
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -21,6 +93,11 @@ export function useCloseModal(_modal: ApplicationModal): () => void {
|
||||
return useCallback(() => dispatch(setOpenModal(null)), [dispatch])
|
||||
}
|
||||
|
||||
export function useOpenModal(modal: ApplicationModal): () => void {
|
||||
const dispatch = useAppDispatch()
|
||||
return useCallback(() => dispatch(setOpenModal(modal)), [dispatch, modal])
|
||||
}
|
||||
|
||||
export function useToggleWalletModal(): () => void {
|
||||
return useToggleModal(ApplicationModal.WALLET)
|
||||
}
|
||||
|
||||
@@ -15,36 +15,39 @@ export type PopupContent =
|
||||
|
||||
export enum ApplicationModal {
|
||||
ADDRESS_CLAIM,
|
||||
UNISWAP_NFT_AIRDROP_CLAIM,
|
||||
BLOCKED_ACCOUNT,
|
||||
DELEGATE,
|
||||
CLAIM_POPUP,
|
||||
DELEGATE,
|
||||
EXECUTE,
|
||||
FEATURE_FLAGS,
|
||||
FIAT_ONRAMP,
|
||||
MENU,
|
||||
NETWORK_FILTER,
|
||||
NETWORK_SELECTOR,
|
||||
POOL_OVERVIEW_OPTIONS,
|
||||
PRIVACY_POLICY,
|
||||
QUEUE,
|
||||
SELF_CLAIM,
|
||||
SETTINGS,
|
||||
SHARE,
|
||||
TIME_SELECTOR,
|
||||
VOTE,
|
||||
WALLET,
|
||||
WALLET_DROPDOWN,
|
||||
QUEUE,
|
||||
EXECUTE,
|
||||
TIME_SELECTOR,
|
||||
SHARE,
|
||||
NETWORK_FILTER,
|
||||
FEATURE_FLAGS,
|
||||
UNISWAP_NFT_AIRDROP_CLAIM,
|
||||
}
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
|
||||
|
||||
export interface ApplicationState {
|
||||
readonly chainId: number | null
|
||||
readonly fiatOnramp: { available: boolean; availabilityChecked: boolean }
|
||||
readonly openModal: ApplicationModal | null
|
||||
readonly popupList: PopupList
|
||||
}
|
||||
|
||||
const initialState: ApplicationState = {
|
||||
fiatOnramp: { available: false, availabilityChecked: false },
|
||||
chainId: null,
|
||||
openModal: null,
|
||||
popupList: [],
|
||||
@@ -54,6 +57,9 @@ const applicationSlice = createSlice({
|
||||
name: 'application',
|
||||
initialState,
|
||||
reducers: {
|
||||
setFiatOnrampAvailability(state, { payload: available }) {
|
||||
state.fiatOnramp = { available, availabilityChecked: true }
|
||||
},
|
||||
updateChainId(state, action) {
|
||||
const { chainId } = action.payload
|
||||
state.chainId = chainId
|
||||
@@ -81,5 +87,6 @@ const applicationSlice = createSlice({
|
||||
},
|
||||
})
|
||||
|
||||
export const { updateChainId, setOpenModal, addPopup, removePopup } = applicationSlice.actions
|
||||
export const { updateChainId, setFiatOnrampAvailability, setOpenModal, addPopup, removePopup } =
|
||||
applicationSlice.actions
|
||||
export default applicationSlice.reducer
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import useIsWindowVisible from 'hooks/useIsWindowVisible'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { supportedChainId } from 'utils/supportedChainId'
|
||||
|
||||
import { updateChainId } from './reducer'
|
||||
import { useCloseModal } from './hooks'
|
||||
import { ApplicationModal, updateChainId } from './reducer'
|
||||
|
||||
export default function Updater(): null {
|
||||
const { chainId, provider } = useWeb3React()
|
||||
const { account, chainId, provider } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
const windowVisible = useIsWindowVisible()
|
||||
|
||||
const [activeChainId, setActiveChainId] = useState(chainId)
|
||||
|
||||
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
|
||||
const previousAccountValue = useRef(account)
|
||||
useEffect(() => {
|
||||
if (account && account !== previousAccountValue.current) {
|
||||
previousAccountValue.current = account
|
||||
closeModal()
|
||||
}
|
||||
}, [account, closeModal])
|
||||
|
||||
useEffect(() => {
|
||||
if (provider && chainId && windowVisible) {
|
||||
setActiveChainId(chainId)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
@@ -8,8 +7,7 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import JSBI from 'jsbi'
|
||||
import { NEVER_RELOAD, useMultipleContractSingleData } from 'lib/hooks/multicall'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { DAI, UNI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
@@ -17,9 +15,7 @@ const STAKING_REWARDS_INTERFACE = new Interface(STAKING_REWARDS_ABI)
|
||||
|
||||
export const STAKING_GENESIS = 1600387200
|
||||
|
||||
export const REWARDS_DURATION_DAYS = 60
|
||||
|
||||
export const STAKING_REWARDS_INFO: {
|
||||
const STAKING_REWARDS_INFO: {
|
||||
[chainId: number]: {
|
||||
tokens: [Token, Token]
|
||||
stakingRewardAddress: string
|
||||
@@ -45,7 +41,7 @@ export const STAKING_REWARDS_INFO: {
|
||||
],
|
||||
}
|
||||
|
||||
export interface StakingInfo {
|
||||
interface StakingInfo {
|
||||
// the address of the reward contract
|
||||
stakingRewardAddress: string
|
||||
// the tokens involved in this pair
|
||||
@@ -227,35 +223,3 @@ export function useStakingInfo(pairToFilterBy?: Pair | null): StakingInfo[] {
|
||||
uni,
|
||||
])
|
||||
}
|
||||
|
||||
// based on typed value
|
||||
export function useDerivedStakeInfo(
|
||||
typedValue: string,
|
||||
stakingToken: Token | undefined,
|
||||
userLiquidityUnstaked: CurrencyAmount<Token> | undefined
|
||||
): {
|
||||
parsedAmount?: CurrencyAmount<Token>
|
||||
error?: ReactNode
|
||||
} {
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const parsedInput: CurrencyAmount<Token> | undefined = tryParseCurrencyAmount(typedValue, stakingToken)
|
||||
|
||||
const parsedAmount =
|
||||
parsedInput && userLiquidityUnstaked && JSBI.lessThanOrEqual(parsedInput.quotient, userLiquidityUnstaked.quotient)
|
||||
? parsedInput
|
||||
: undefined
|
||||
|
||||
let error: ReactNode | undefined
|
||||
if (!account) {
|
||||
error = <Trans>Connect Wallet</Trans>
|
||||
}
|
||||
if (!parsedAmount) {
|
||||
error = error ?? <Trans>Enter an amount</Trans>
|
||||
}
|
||||
|
||||
return {
|
||||
parsedAmount,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@ import { AppState } from '../index'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
updateFiatOnrampAcknowledgments,
|
||||
updateHideClosedPositions,
|
||||
updateHideNFTWelcomeModal,
|
||||
updateShowNftPromoBanner,
|
||||
updateUserClientSideRouter,
|
||||
updateUserDarkMode,
|
||||
updateUserDeadline,
|
||||
@@ -105,13 +104,24 @@ export function useExpertModeManager(): [boolean, () => void] {
|
||||
return [expertMode, toggleSetExpertMode]
|
||||
}
|
||||
|
||||
export function useHideNFTWelcomeModal(): [boolean | undefined, () => void] {
|
||||
interface FiatOnrampAcknowledgements {
|
||||
renderCount: number
|
||||
system: boolean
|
||||
user: boolean
|
||||
}
|
||||
export function useFiatOnrampAck(): [
|
||||
FiatOnrampAcknowledgements,
|
||||
(acknowledgements: Partial<FiatOnrampAcknowledgements>) => void
|
||||
] {
|
||||
const dispatch = useAppDispatch()
|
||||
const hideNFTWelcomeModal = useAppSelector((state) => state.user.hideNFTWelcomeModal)
|
||||
const hideModal = useCallback(() => {
|
||||
dispatch(updateHideNFTWelcomeModal({ hideNFTWelcomeModal: true }))
|
||||
}, [dispatch])
|
||||
return [hideNFTWelcomeModal, hideModal]
|
||||
const fiatOnrampAcknowledgments = useAppSelector((state) => state.user.fiatOnrampAcknowledgments)
|
||||
const setAcknowledgements = useCallback(
|
||||
(acks: Partial<FiatOnrampAcknowledgements>) => {
|
||||
dispatch(updateFiatOnrampAcknowledgments(acks))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
return [fiatOnrampAcknowledgments, setAcknowledgements]
|
||||
}
|
||||
|
||||
export function useClientSideRouter(): [boolean, (userClientSideRouter: boolean) => void] {
|
||||
@@ -258,17 +268,6 @@ export function useURLWarningVisible(): boolean {
|
||||
return useAppSelector((state: AppState) => state.user.URLWarningVisible)
|
||||
}
|
||||
|
||||
export function useHideNftPromoBanner(): [boolean, () => void] {
|
||||
const dispatch = useAppDispatch()
|
||||
const hideNftPromoBanner = useAppSelector((state) => state.user.hideNFTPromoBanner)
|
||||
|
||||
const toggleHideNftPromoBanner = useCallback(() => {
|
||||
dispatch(updateShowNftPromoBanner({ hideNFTPromoBanner: true }))
|
||||
}, [dispatch])
|
||||
|
||||
return [hideNftPromoBanner, toggleHideNftPromoBanner]
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two tokens return the liquidity token that represents its liquidity shares
|
||||
* @param tokenA one of the two tokens
|
||||
|
||||
@@ -9,6 +9,9 @@ import { SerializedPair, SerializedToken } from './types'
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
export interface UserState {
|
||||
fiatOnrampAcknowledgments: { renderCount: number; system: boolean; user: boolean }
|
||||
fiatOnrampDismissed: boolean
|
||||
|
||||
selectedWallet?: ConnectionType
|
||||
|
||||
// the timestamp of the last updateVersion action
|
||||
@@ -48,12 +51,9 @@ export interface UserState {
|
||||
|
||||
timestamp: number
|
||||
URLWarningVisible: boolean
|
||||
hideNFTPromoBanner: boolean // whether or not we should hide the nft explore promo banner
|
||||
|
||||
// undefined means has not gone through A/B split yet
|
||||
showSurveyPopup: boolean | undefined
|
||||
|
||||
hideNFTWelcomeModal: boolean
|
||||
}
|
||||
|
||||
function pairKey(token0Address: string, token1Address: string) {
|
||||
@@ -61,6 +61,9 @@ function pairKey(token0Address: string, token1Address: string) {
|
||||
}
|
||||
|
||||
export const initialState: UserState = {
|
||||
fiatOnrampAcknowledgments: { renderCount: 0, system: false, user: false },
|
||||
fiatOnrampDismissed: false,
|
||||
|
||||
selectedWallet: undefined,
|
||||
matchesDarkMode: false,
|
||||
userDarkMode: null,
|
||||
@@ -75,15 +78,22 @@ export const initialState: UserState = {
|
||||
pairs: {},
|
||||
timestamp: currentTimestamp(),
|
||||
URLWarningVisible: true,
|
||||
hideNFTPromoBanner: false,
|
||||
showSurveyPopup: undefined,
|
||||
hideNFTWelcomeModal: false,
|
||||
}
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateFiatOnrampAcknowledgments(
|
||||
state,
|
||||
{ payload }: { payload: Partial<{ renderCount: number; user: boolean; system: boolean }> }
|
||||
) {
|
||||
state.fiatOnrampAcknowledgments = { ...state.fiatOnrampAcknowledgments, ...payload }
|
||||
},
|
||||
dismissFiatOnramp(state) {
|
||||
state.fiatOnrampDismissed = true
|
||||
},
|
||||
updateSelectedWallet(state, { payload: { wallet } }) {
|
||||
state.selectedWallet = wallet
|
||||
},
|
||||
@@ -117,12 +127,6 @@ const userSlice = createSlice({
|
||||
updateHideClosedPositions(state, action) {
|
||||
state.userHideClosedPositions = action.payload.userHideClosedPositions
|
||||
},
|
||||
updateHideNFTWelcomeModal(state, action) {
|
||||
state.hideNFTWelcomeModal = action.payload.hideNFTWelcomeModal
|
||||
},
|
||||
updateShowNftPromoBanner(state, action) {
|
||||
state.hideNFTPromoBanner = action.payload.hideNFTPromoBanner
|
||||
},
|
||||
addSerializedToken(state, { payload: { serializedToken } }) {
|
||||
if (!state.tokens) {
|
||||
state.tokens = {}
|
||||
@@ -181,18 +185,18 @@ const userSlice = createSlice({
|
||||
})
|
||||
|
||||
export const {
|
||||
updateSelectedWallet,
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
updateFiatOnrampAcknowledgments,
|
||||
dismissFiatOnramp,
|
||||
updateSelectedWallet,
|
||||
updateHideClosedPositions,
|
||||
updateMatchesDarkMode,
|
||||
updateUserClientSideRouter,
|
||||
updateHideNFTWelcomeModal,
|
||||
updateUserDarkMode,
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
updateUserLocale,
|
||||
updateUserSlippageTolerance,
|
||||
updateShowNftPromoBanner,
|
||||
} = userSlice.actions
|
||||
export default userSlice.reducer
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export enum Z_INDEX {
|
||||
deprecated_zero = 0,
|
||||
deprecated_content = 1,
|
||||
under_dropdown = 990,
|
||||
dropdown = 1000,
|
||||
sticky = 1020,
|
||||
fixed = 1030,
|
||||
|
||||
16
yarn.lock
@@ -4195,10 +4195,10 @@
|
||||
"@typescript-eslint/types" "4.33.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@uniswap/analytics-events@1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-1.3.1.tgz#e1ad912001646268ea16f64622c683ca2a63002d"
|
||||
integrity sha512-EzYLBU123TpTCNPfa8cN7cI3Ap8D5Rn0771/bAtjSElZROR6YnGFNj0cjnaHwo8SIUr5Ema7JLoXbMVUyreFXQ==
|
||||
"@uniswap/analytics-events@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-1.5.0.tgz#1f5a6e0fedb46e551745c2b4d2b791fe44964cfa"
|
||||
integrity sha512-grrPk71hoBeXiBivlqgeh97lY7TmKEYhzzOLXdMXjXX/mcGOM9HLV0XR1olr/WPwuhBFx9GefaiB3GJU9+/sOQ==
|
||||
|
||||
"@uniswap/analytics@1.2.0":
|
||||
version "1.2.0"
|
||||
@@ -4209,10 +4209,10 @@
|
||||
react "^18.2.0"
|
||||
react-dom "^18.2.0"
|
||||
|
||||
"@uniswap/conedison@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.1.0.tgz#462a1e7dcfc70a653e765c145655faa207fd53f7"
|
||||
integrity sha512-mHnYkvy+xKfWDzWsqzgWKFl/V8C/KmSrj/2PCgT1R2ASxPzMCU/wzTW29HtIxf1cJ+A6sPwH2HqDZsbnNhKqNQ==
|
||||
"@uniswap/conedison@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.1.1.tgz#affec246613d1f52da3cdd0571ef8195b7b54d17"
|
||||
integrity sha512-xFHAcWRrU+/+/BInXy6SRiiNwUG0vxLWsoYgod66wWifUvnjfpItzlvJHUer1OOpLDsz0CL5Fb70vFJOGAGi8w==
|
||||
|
||||
"@uniswap/default-token-list@^2.0.0":
|
||||
version "2.2.0"
|
||||
|
||||