feat(L2-beta-launch): network selector (#2129)

* fix an issue with optimism/arbitrum alert distinction, add a prompt to switch to Optimism on mainnet

* only prompt to switch networks if the user has a wallet connected

* add readmore link styles

* add network to the user's wallet if it hasn't been added already

* network selector

* hide arbitrum until it launches

* add arbitrum for testing

* update copy and some margins

* fix alert opacity issue

* remove the launch alert :(

* adjust icon position and add curvier corners

* lighten some colors

* keep the selector around even if the user's wallet doesn't support the eip when they're on L2, just hide all other networks

* copy updates and some other small tweaks

* better mobile experience

* shrink on mobile

* fix some links and css

* differentiate between selector and row logos

* fix some copy

* remove network alert from add liquidity pages, update copy and buttons on swap page, remove close option if no eth, persist close state otherwise

* design polish

* update read more links

* update downtime warning to be less intense

* oe logo
'

* design polish sesh

* fix a couple bugs
This commit is contained in:
Jordan Frankfurt 2021-09-21 14:57:29 -04:00 committed by GitHub
parent 119c79d623
commit 97ba8fb5b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 700 additions and 697 deletions

@ -0,0 +1,16 @@
<svg width="170" height="168" viewBox="0 0 170 168" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF505F"/>
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF0320"/>
<path d="M85.05 0C132.022 0 170.1 37.8949 170.1 84.6407C170.1 131.386 0 131.386 0 84.6407C0 37.8949 38.0782 0 85.05 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.665 64.0394L112.444 12.3742L89.0263 78.9477L144.665 64.0394Z" fill="#FF4E65"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.777 64.215L112.444 12.3742L165.349 58.4347L143.777 64.215Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.551 63.613L142.479 124.467L88.912 78.5213L144.551 63.613Z" fill="#D0001A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.663 63.7886L142.479 124.467L165.235 58.0083L143.663 63.7886Z" fill="#FF697B"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="170" height="168" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -8,7 +8,7 @@ import useTheme from 'hooks/useTheme'
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
const Base = styled(RebassButton)<
export const BaseButton = styled(RebassButton)<
{
padding?: string
width?: string
@ -50,7 +50,7 @@ const Base = styled(RebassButton)<
}
`
export const ButtonPrimary = styled(Base)`
export const ButtonPrimary = styled(BaseButton)`
background-color: ${({ theme }) => theme.primary1};
color: white;
&:focus {
@ -67,7 +67,8 @@ export const ButtonPrimary = styled(Base)`
&:disabled {
background-color: ${({ theme, altDisabledStyle, disabled }) =>
altDisabledStyle ? (disabled ? theme.primary1 : theme.bg2) : theme.bg2};
color: ${({ theme }) => theme.text2};
color: ${({ altDisabledStyle, disabled, theme }) =>
altDisabledStyle ? (disabled ? theme.white : theme.text2) : theme.text2};
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
@ -75,7 +76,7 @@ export const ButtonPrimary = styled(Base)`
}
`
export const ButtonLight = styled(Base)`
export const ButtonLight = styled(BaseButton)`
background-color: ${({ theme }) => theme.primary5};
color: ${({ theme }) => theme.primaryText1};
font-size: 16px;
@ -103,7 +104,7 @@ export const ButtonLight = styled(Base)`
}
`
export const ButtonGray = styled(Base)`
export const ButtonGray = styled(BaseButton)`
background-color: ${({ theme }) => theme.bg1};
color: ${({ theme }) => theme.text2};
font-size: 16px;
@ -117,7 +118,7 @@ export const ButtonGray = styled(Base)`
}
`
export const ButtonSecondary = styled(Base)`
export const ButtonSecondary = styled(BaseButton)`
border: 1px solid ${({ theme }) => theme.primary4};
color: ${({ theme }) => theme.primary1};
background-color: transparent;
@ -145,7 +146,7 @@ export const ButtonSecondary = styled(Base)`
}
`
export const ButtonOutlined = styled(Base)`
export const ButtonOutlined = styled(BaseButton)`
border: 1px solid ${({ theme }) => theme.bg2};
background-color: transparent;
color: ${({ theme }) => theme.text1};
@ -164,7 +165,7 @@ export const ButtonOutlined = styled(Base)`
}
`
export const ButtonYellow = styled(Base)`
export const ButtonYellow = styled(BaseButton)`
background-color: ${({ theme }) => theme.yellow3};
color: white;
&:focus {
@ -185,7 +186,7 @@ export const ButtonYellow = styled(Base)`
}
`
export const ButtonEmpty = styled(Base)`
export const ButtonEmpty = styled(BaseButton)`
background-color: transparent;
color: ${({ theme }) => theme.primary1};
display: flex;
@ -207,7 +208,7 @@ export const ButtonEmpty = styled(Base)`
}
`
export const ButtonText = styled(Base)`
export const ButtonText = styled(BaseButton)`
padding: 0;
width: fit-content;
background: none;
@ -229,7 +230,7 @@ export const ButtonText = styled(Base)`
}
`
const ButtonConfirmedStyle = styled(Base)`
const ButtonConfirmedStyle = styled(BaseButton)`
background-color: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.text1};
/* border: 1px solid ${({ theme }) => theme.green1}; */
@ -242,7 +243,7 @@ const ButtonConfirmedStyle = styled(Base)`
}
`
const ButtonErrorStyle = styled(Base)`
const ButtonErrorStyle = styled(BaseButton)`
background-color: ${({ theme }) => theme.red1};
border: 1px solid ${({ theme }) => theme.red1};

@ -0,0 +1,77 @@
import { Trans } from '@lingui/macro'
import { L2_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
const Root = styled.div`
background-color: ${({ theme }) => (theme.darkMode ? '#888D9B' : '#CED0D9')};
border-radius: 18px;
color: black;
display: flex;
flex-direction: row;
font-size: 14px;
margin: 12px auto;
padding: 16px;
width: 100%;
max-width: 880px;
`
const WarningIcon = styled(AlertOctagon)`
display: block;
margin: auto 16px auto 0;
min-height: 22px;
min-width: 22px;
`
const ReadMoreLink = styled(ExternalLink)`
color: black;
text-decoration: underline;
`
export default function DowntimeWarning() {
const { chainId } = useActiveWeb3React()
if (!chainId || !L2_CHAIN_IDS.includes(chainId)) {
return null
}
const Content = () => {
switch (chainId) {
case SupportedChainId.OPTIMISM:
case SupportedChainId.OPTIMISTIC_KOVAN:
return (
<div>
<Trans>
Optimistic Ethereum is in Beta and may experience downtime. Optimism expects planned downtime to upgrade
the network in the near future. During downtime, your position will not earn fees and you will be unable
to remove liquidity.{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5406082-what-happens-if-the-optimistic-ethereum-network-experiences-downtime">
Read more.
</ReadMoreLink>
</Trans>
</div>
)
case SupportedChainId.ARBITRUM_ONE:
case SupportedChainId.ARBITRUM_RINKEBY:
return (
<div>
<Trans>
Arbitrum is in Beta and may experience downtime. During downtime, your position will not earn fees and you
will be unable to remove liquidity.{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5576122-arbitrum-network-downtime">
Read more.
</ReadMoreLink>
</Trans>
</div>
)
default:
return null
}
}
return (
<Root>
<WarningIcon />
<Content />
</Root>
)
}

@ -60,7 +60,7 @@ export function ChainConnectivityWarning() {
{chainId === SupportedChainId.MAINNET ? (
<Trans>You may have lost your network connection.</Trans>
) : (
<Trans>{label} may be down right now, or you may have lost your network connection.</Trans>
<Trans>You may have lost your network connection, or {label} might be down right now.</Trans>
)}{' '}
{(info as L2ChainInfo).statusPage !== undefined && (
<span>

@ -1,235 +0,0 @@
import { Trans } from '@lingui/macro'
import { YellowCard } from 'components/Card'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useActiveWeb3React } from 'hooks/web3'
import { useEffect, useRef, useState } from 'react'
import { ArrowDownCircle, ChevronDown, ToggleLeft } from 'react-feather'
import { ApplicationModal } from 'state/application/actions'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import styled, { css } from 'styled-components/macro'
import { ExternalLink } from 'theme'
import { switchToNetwork } from 'utils/switchToNetwork'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from '../../constants/chains'
const BaseWrapper = css`
position: relative;
margin-right: 8px;
${({ theme }) => theme.mediaWidth.upToMedium`
justify-self: end;
`};
${({ theme }) => theme.mediaWidth.upToSmall`
margin: 0 0.5rem 0 0;
width: initial;
text-overflow: ellipsis;
flex-shrink: 1;
`};
`
const L2Wrapper = styled.div`
${BaseWrapper}
`
const BaseMenuItem = css`
align-items: center;
background-color: transparent;
border-radius: 12px;
color: ${({ theme }) => theme.text2};
cursor: pointer;
display: flex;
flex: 1;
flex-direction: row;
font-size: 16px;
font-weight: 400;
justify-content: space-between;
:hover {
color: ${({ theme }) => theme.text1};
text-decoration: none;
}
`
const DisabledMenuItem = styled.div`
${BaseMenuItem}
align-items: center;
background-color: ${({ theme }) => theme.bg2};
cursor: auto;
display: flex;
font-size: 10px;
font-style: italic;
justify-content: center;
:hover,
:active,
:focus {
color: ${({ theme }) => theme.text2};
}
`
const FallbackWrapper = styled(YellowCard)`
${BaseWrapper}
width: auto;
border-radius: 12px;
padding: 8px 12px;
width: 100%;
user-select: none;
`
const Icon = styled.img`
width: 16px;
margin-right: 2px;
${({ theme }) => theme.mediaWidth.upToSmall`
margin-right: 4px;
`};
`
const MenuFlyout = styled.span`
background-color: ${({ theme }) => theme.bg1};
border: 1px solid ${({ theme }) => theme.bg0};
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);
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
font-size: 1rem;
position: absolute;
left: 0rem;
top: 3rem;
z-index: 100;
width: 237px;
${({ theme }) => theme.mediaWidth.upToMedium`
bottom: unset;
top: 4.5em
right: 0;
`};
> {
padding: 12px;
}
> :not(:first-child) {
margin-top: 8px;
}
> :not(:last-child) {
margin-bottom: 8px;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 16px;
height: 16px;
opacity: 0.6;
`
const MenuItem = styled(ExternalLink)`
${BaseMenuItem}
`
const ButtonMenuItem = styled.button`
${BaseMenuItem}
border: none;
box-shadow: none;
color: ${({ theme }) => theme.text2};
outline: none;
padding: 0;
`
const NetworkInfo = styled.button<{ chainId: SupportedChainId }>`
align-items: center;
background-color: ${({ theme }) => theme.bg0};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.bg0};
color: ${({ theme }) => theme.text1};
display: flex;
flex-direction: row;
font-weight: 500;
font-size: 12px;
height: 100%;
margin: 0;
height: 38px;
padding: 0.7rem;
:hover,
:focus {
cursor: pointer;
outline: none;
border: 1px solid ${({ theme }) => theme.bg3};
}
`
const NetworkName = styled.div<{ chainId: SupportedChainId }>`
border-radius: 6px;
font-size: 16px;
font-weight: 500;
padding: 0 2px 0.5px 4px;
margin: 0 2px;
white-space: pre;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
export default function NetworkCard() {
const { chainId, library } = useActiveWeb3React()
const node = useRef<HTMLDivElement>(null)
const open = useModalOpen(ApplicationModal.ARBITRUM_OPTIONS)
const toggle = useToggleModal(ApplicationModal.ARBITRUM_OPTIONS)
useOnClickOutside(node, open ? toggle : undefined)
const [implements3085, setImplements3085] = useState(false)
useEffect(() => {
// metamask is currently the only known implementer of this EIP
// here we proceed w/ a noop feature check to ensure the user's version of metamask supports network switching
// if not, we hide the UI
if (!library?.provider?.request || !chainId || !library?.provider?.isMetaMask) {
return
}
switchToNetwork({ library, chainId })
.then((x) => x ?? setImplements3085(true))
.catch(() => setImplements3085(false))
}, [library, chainId])
const info = chainId ? CHAIN_INFO[chainId] : undefined
if (!chainId || chainId === SupportedChainId.MAINNET || !info || !library) {
return null
}
if (L2_CHAIN_IDS.includes(chainId)) {
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const isArbitrum = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY].includes(chainId)
return (
<L2Wrapper ref={node}>
<NetworkInfo onClick={toggle} chainId={chainId}>
<Icon src={info.logoUrl} />
<NetworkName chainId={chainId}>{info.label}</NetworkName>
<ChevronDown size={16} style={{ marginTop: '2px' }} strokeWidth={2.5} />
</NetworkInfo>
{open && (
<MenuFlyout>
<MenuItem href={info.bridge}>
<div>{isArbitrum ? <Trans>{info.label} Bridge</Trans> : <Trans>Optimistic L2 Gateway</Trans>}</div>
<LinkOutCircle />
</MenuItem>
<MenuItem href={info.explorer}>
{isArbitrum ? <Trans>{info.label} Explorer</Trans> : <Trans>Optimistic Etherscan</Trans>}
<LinkOutCircle />
</MenuItem>
<MenuItem href={info.docs}>
<div>
<Trans>Learn more</Trans>
</div>
<LinkOutCircle />
</MenuItem>
{implements3085 ? (
<ButtonMenuItem onClick={() => switchToNetwork({ library, chainId: SupportedChainId.MAINNET })}>
<div>
<Trans>Switch to L1 (Mainnet)</Trans>
</div>
<ToggleLeft opacity={0.6} size={16} />
</ButtonMenuItem>
) : (
<DisabledMenuItem>
<Trans>Change your network to go back to L1</Trans>
</DisabledMenuItem>
)}
</MenuFlyout>
)}
</L2Wrapper>
)
}
return <FallbackWrapper title={info.label}>{info.label}</FallbackWrapper>
}

@ -0,0 +1,249 @@
import { Trans } from '@lingui/macro'
import {
ARBITRUM_HELP_CENTER_LINK,
CHAIN_INFO,
L2_CHAIN_IDS,
OPTIMISM_HELP_CENTER_LINK,
SupportedChainId,
SupportedL2ChainId,
} from 'constants/chains'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useRef } from 'react'
import { ArrowDownCircle, ChevronDown } from 'react-feather'
import { ApplicationModal } from 'state/application/actions'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import { useAppSelector } from 'state/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { switchToNetwork } from 'utils/switchToNetwork'
const ActiveRowLinkList = styled.div`
display: flex;
flex-direction: column;
padding: 0 8px;
& > a {
align-items: center;
color: ${({ theme }) => theme.text2};
display: flex;
flex-direction: row;
font-size: 14px;
font-weight: 500;
justify-content: space-between;
padding: 8px 0 4px;
text-decoration: none;
}
& > a:first-child {
border-top: 1px solid ${({ theme }) => theme.text2};
margin: 0;
margin-top: 6px;
padding-top: 10px;
}
`
const ActiveRowWrapper = styled.div`
background-color: ${({ theme }) => theme.bg2};
border-radius: 8px;
cursor: pointer;
padding: 8px 0 8px 0;
width: 100%;
`
const FlyoutHeader = styled.div`
color: ${({ theme }) => theme.text2};
font-weight: 400;
`
const FlyoutMenu = styled.div`
align-items: flex-start;
background-color: ${({ theme }) => theme.bg1};
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);
border-radius: 20px;
display: flex;
flex-direction: column;
font-size: 16px;
overflow: auto;
padding: 16px;
position: absolute;
top: 64px;
width: 272px;
z-index: 99;
& > *:not(:last-child) {
margin-bottom: 12px;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
top: 50px;
}
`
const FlyoutRow = styled.div<{ active: boolean }>`
align-items: center;
background-color: ${({ active, theme }) => (active ? theme.bg2 : 'transparent')};
border-radius: 8px;
cursor: pointer;
display: flex;
font-weight: 500;
justify-content: space-between;
padding: 6px 8px;
text-align: left;
width: 100%;
`
const FlyoutRowActiveIndicator = styled.div`
background-color: ${({ theme }) => theme.green1};
border-radius: 50%;
height: 9px;
width: 9px;
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 16px;
height: 16px;
`
const Logo = styled.img`
height: 20px;
width: 20px;
margin-right: 8px;
`
const NetworkLabel = styled.div`
flex: 1 1 auto;
`
const SelectorLabel = styled(NetworkLabel)`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
margin-right: 8px;
}
`
const SelectorControls = styled.div<{ interactive: boolean }>`
align-items: center;
background-color: ${({ theme }) => theme.bg1};
border: 2px solid ${({ theme }) => theme.bg1};
border-radius: 12px;
color: ${({ theme }) => theme.text1};
cursor: ${({ interactive }) => (interactive ? 'pointer' : 'auto')};
display: flex;
font-weight: 500;
justify-content: space-between;
padding: 6px 8px;
`
const SelectorLogo = styled(Logo)<{ interactive?: boolean }>`
margin-right: ${({ interactive }) => (interactive ? 8 : 0)}px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
margin-right: 8px;
}
`
const SelectorWrapper = styled.div`
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
position: relative;
}
`
const StyledChevronDown = styled(ChevronDown)`
width: 12px;
`
const BridgeText = ({ chainId }: { chainId: SupportedL2ChainId }) => {
switch (chainId) {
case SupportedChainId.ARBITRUM_ONE:
case SupportedChainId.ARBITRUM_RINKEBY:
return <Trans>Arbitrum Bridge</Trans>
case SupportedChainId.OPTIMISM:
case SupportedChainId.OPTIMISTIC_KOVAN:
return <Trans>Optimism Gateway</Trans>
default:
return <Trans>Bridge</Trans>
}
}
const ExplorerText = ({ chainId }: { chainId: SupportedL2ChainId }) => {
switch (chainId) {
case SupportedChainId.ARBITRUM_ONE:
case SupportedChainId.ARBITRUM_RINKEBY:
return <Trans>Arbiscan</Trans>
case SupportedChainId.OPTIMISM:
case SupportedChainId.OPTIMISTIC_KOVAN:
return <Trans>Optimistic Etherscan</Trans>
default:
return <Trans>Explorer</Trans>
}
}
export default function NetworkSelector() {
const { chainId, library } = useActiveWeb3React()
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.NETWORK_SELECTOR)
const toggle = useToggleModal(ApplicationModal.NETWORK_SELECTOR)
useOnClickOutside(node, open ? toggle : undefined)
const implements3085 = useAppSelector((state) => state.application.implements3085)
const info = chainId ? CHAIN_INFO[chainId] : undefined
const isOnL2 = chainId ? L2_CHAIN_IDS.includes(chainId) : false
const showSelector = Boolean(implements3085 || isOnL2)
const mainnetInfo = CHAIN_INFO[SupportedChainId.MAINNET]
const conditionalToggle = useCallback(() => {
if (showSelector) {
toggle()
}
}, [showSelector, toggle])
if (!chainId || !info || !library) {
return null
}
function Row({ targetChain }: { targetChain: number }) {
if (!library || !chainId || (!implements3085 && targetChain !== chainId)) {
return null
}
const handleRowClick = () => {
switchToNetwork({ library, chainId: targetChain })
toggle()
}
const active = chainId === targetChain
const hasExtendedInfo = L2_CHAIN_IDS.includes(targetChain)
const isOptimism = targetChain === SupportedChainId.OPTIMISM
const rowText = `${CHAIN_INFO[targetChain].label}${isOptimism ? ' (Optimism)' : ''}`
const RowContent = () => (
<FlyoutRow onClick={handleRowClick} active={active}>
<Logo src={CHAIN_INFO[targetChain].logoUrl} />
<NetworkLabel>{rowText}</NetworkLabel>
{chainId === targetChain && <FlyoutRowActiveIndicator />}
</FlyoutRow>
)
const helpCenterLink = isOptimism ? OPTIMISM_HELP_CENTER_LINK : ARBITRUM_HELP_CENTER_LINK
if (active && hasExtendedInfo) {
return (
<ActiveRowWrapper>
<RowContent />
<ActiveRowLinkList>
<ExternalLink href={CHAIN_INFO[targetChain as SupportedL2ChainId].bridge}>
<BridgeText chainId={chainId} /> <LinkOutCircle />
</ExternalLink>
<ExternalLink href={CHAIN_INFO[targetChain].explorer}>
<ExplorerText chainId={chainId} /> <LinkOutCircle />
</ExternalLink>
<ExternalLink href={helpCenterLink}>
<Trans>Help Center</Trans> <LinkOutCircle />
</ExternalLink>
</ActiveRowLinkList>
</ActiveRowWrapper>
)
}
return <RowContent />
}
return (
<SelectorWrapper ref={node as any}>
<SelectorControls onClick={conditionalToggle} interactive={showSelector}>
<SelectorLogo interactive={showSelector} src={info.logoUrl || mainnetInfo.logoUrl} />
<SelectorLabel>{info.label}</SelectorLabel>
{showSelector && <StyledChevronDown />}
</SelectorControls>
{open && (
<FlyoutMenu>
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row targetChain={SupportedChainId.MAINNET} />
<Row targetChain={SupportedChainId.OPTIMISM} />
<Row targetChain={SupportedChainId.ARBITRUM_ONE} />
</FlyoutMenu>
)}
</SelectorWrapper>
)
}

@ -22,7 +22,7 @@ import Modal from '../Modal'
import Row from '../Row'
import { Dots } from '../swap/styleds'
import Web3Status from '../Web3Status'
import NetworkCard from './NetworkCard'
import NetworkSelector from './NetworkSelector'
import UniBalanceContent from './UniBalanceContent'
const HeaderFrame = styled.div<{ showBackground: boolean }>`
@ -72,6 +72,10 @@ const HeaderElement = styled.div`
display: flex;
align-items: center;
&:not(:first-child) {
margin-left: 0.5em;
}
/* addresses safari's lack of support for "gap" */
& > *:not(:first-child) {
margin-left: 8px;
@ -300,7 +304,9 @@ export default function Header() {
</HeaderLinks>
<HeaderControls>
<NetworkCard />
<HeaderElement>
<NetworkSelector />
</HeaderElement>
<HeaderElement>
{availableClaim && !showClaimPopup && (
<UNIWrapper onClick={toggleClaimModal}>
@ -326,6 +332,8 @@ export default function Header() {
) : null}
<Web3Status />
</AccountElement>
</HeaderElement>
<HeaderElement>
<Menu />
</HeaderElement>
</HeaderControls>

@ -62,7 +62,6 @@ const UNIbutton = styled(ButtonPrimary)`
`
const StyledMenu = styled.div`
margin-left: 0.5rem;
display: flex;
justify-content: center;
align-items: center;

@ -1,134 +0,0 @@
import { Trans } from '@lingui/macro'
import {
ArbitrumWrapperBackgroundDarkMode,
ArbitrumWrapperBackgroundLightMode,
OptimismWrapperBackgroundDarkMode,
OptimismWrapperBackgroundLightMode,
} from 'components/NetworkAlert/NetworkAlert'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { ArrowDownCircle } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { ReadMoreLink } from './styles'
const L2Icon = styled.img`
display: none;
height: 40px;
margin: auto 20px auto 4px;
width: 40px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const DesktopTextBreak = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const Wrapper = styled.div<{ chainId: SupportedL2ChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) =>
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
? OptimismWrapperBackgroundDarkMode
: OptimismWrapperBackgroundLightMode
: darkMode
? ArbitrumWrapperBackgroundDarkMode
: ArbitrumWrapperBackgroundLightMode};
border-radius: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 12px;
position: relative;
width: 100%;
:before {
background-image: url(${({ logoUrl }) => logoUrl});
background-repeat: no-repeat;
background-size: 300px;
content: '';
height: 300px;
opacity: 0.1;
position: absolute;
transform: rotate(25deg) translate(-90px, -40px);
width: 300px;
z-index: -1;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
flex-direction: row;
padding: 16px 20px;
}
`
const Body = styled.div`
font-size: 12px;
line-height: 143%;
margin: 12px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
flex: 1 1 auto;
margin: auto 0;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 20px;
height: 20px;
margin-left: 12px;
`
const LinkOutToBridge = styled(ExternalLink)`
align-items: center;
background-color: black;
border-radius: 16px;
color: white;
display: flex;
font-size: 14px;
justify-content: space-between;
margin: 0;
max-height: 47px;
padding: 16px 12px;
text-decoration: none;
width: auto;
:hover,
:focus,
:active {
background-color: black;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
margin: auto 0 auto auto;
padding: 14px 16px;
min-width: 226px;
}
`
export function AddLiquidityNetworkAlert() {
const { chainId } = useActiveWeb3React()
const [darkMode] = useDarkModeManager()
const [arbitrumAlphaAcknowledged] = useArbitrumAlphaAlert()
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged) {
return null
}
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const isOptimism = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
const depositUrl = isOptimism ? `${info.bridge}?chainId=1` : info.bridge
const readMoreLink = isOptimism
? 'https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism'
: 'https://help.uniswap.org/en/articles/5538618-how-to-deposit-tokens-to-arbitrum'
return (
<Wrapper darkMode={darkMode} chainId={chainId} logoUrl={info.logoUrl}>
<L2Icon src={info.logoUrl} />
<Body>
<Trans>This is an alpha release of Uniswap on the {info.label} network.</Trans>
<DesktopTextBreak /> <Trans>You must bridge L1 assets to the network to use them.</Trans>{' '}
<ReadMoreLink href={readMoreLink}>
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</Wrapper>
)
}

@ -1,134 +0,0 @@
import { Trans } from '@lingui/macro'
import {
ArbitrumWrapperBackgroundDarkMode,
ArbitrumWrapperBackgroundLightMode,
OptimismWrapperBackgroundDarkMode,
OptimismWrapperBackgroundLightMode,
} from 'components/NetworkAlert/NetworkAlert'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { ArrowDownCircle } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { ReadMoreLink } from './styles'
const L2Icon = styled.img`
display: none;
height: 40px;
margin: auto 20px auto 4px;
width: 40px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: block;
}
`
const DesktopTextBreak = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const Wrapper = styled.div<{ chainId: SupportedL2ChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) =>
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
? OptimismWrapperBackgroundDarkMode
: OptimismWrapperBackgroundLightMode
: darkMode
? ArbitrumWrapperBackgroundDarkMode
: ArbitrumWrapperBackgroundLightMode};
border-radius: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 12px;
position: relative;
width: 100%;
:before {
background-image: url(${({ logoUrl }) => logoUrl});
background-repeat: no-repeat;
background-size: 300px;
content: '';
height: 300px;
opacity: 0.1;
position: absolute;
transform: rotate(25deg) translate(-90px, -40px);
width: 300px;
z-index: -1;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex-direction: row;
padding: 16px 20px;
}
`
const Body = styled.div`
font-size: 12px;
line-height: 143%;
margin: 12px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex: 1 1 auto;
margin: auto 0;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 20px;
height: 20px;
margin-left: 12px;
`
const LinkOutToBridge = styled(ExternalLink)`
align-items: center;
background-color: black;
border-radius: 16px;
color: white;
display: flex;
font-size: 14px;
justify-content: space-between;
margin: 0;
max-height: 47px;
padding: 16px 8px;
text-decoration: none;
width: auto;
:hover,
:focus,
:active {
background-color: black;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
margin: auto 0 auto auto;
padding: 14px 17px;
min-width: 226px;
}
`
export function MinimalNetworkAlert() {
const { chainId } = useActiveWeb3React()
const [darkMode] = useDarkModeManager()
const [arbitrumAlphaAcknowledged] = useArbitrumAlphaAlert()
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged) {
return null
}
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const isOptimism = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
const depositUrl = isOptimism ? `${info.bridge}?chainId=1` : info.bridge
const readMoreLink = isOptimism
? 'https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism'
: 'https://help.uniswap.org/en/articles/5538618-how-to-deposit-tokens-to-arbitrum'
return (
<Wrapper darkMode={darkMode} chainId={chainId} logoUrl={info.logoUrl}>
<L2Icon src={info.logoUrl} />
<Body>
<Trans>This is an alpha release of Uniswap on the {info.label} network.</Trans>
<DesktopTextBreak /> <Trans>You must bridge L1 assets to the network to use them.</Trans>{' '}
<ReadMoreLink href={readMoreLink}>
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</Wrapper>
)
}

@ -1,27 +1,74 @@
import { Trans } from '@lingui/macro'
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import {
ARBITRUM_HELP_CENTER_LINK,
L2_CHAIN_IDS,
OPTIMISM_HELP_CENTER_LINK,
SupportedChainId,
SupportedL2ChainId,
} from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useState } from 'react'
import { ArrowDownCircle, X } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import { useArbitrumAlphaAlert, useDarkModeManager, useOptimismAlphaAlert } from 'state/user/hooks'
import { useETHBalances } from 'state/wallet/hooks'
import styled, { css } from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { CHAIN_INFO } from '../../constants/chains'
import { ReadMoreLink } from './styles'
export const DesktopTextBreak = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const L2Icon = styled.img`
width: 40px;
height: 40px;
width: 36px;
height: 36px;
justify-self: center;
`
const BetaTag = styled.span<{ color: string }>`
align-items: center;
background-color: ${({ color }) => color};
border-radius: 6px;
color: ${({ theme }) => theme.white};
display: flex;
font-size: 14px;
height: 28px;
justify-content: center;
left: -16px;
position: absolute;
transform: rotate(-15deg);
top: -16px;
width: 60px;
z-index: 1;
`
const Body = styled.p`
font-size: 12px;
grid-column: 1 / 3;
line-height: 143%;
margin: 0;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
grid-column: 2 / 3;
}
`
export const Controls = styled.div<{ thin?: boolean }>`
align-items: center;
display: flex;
justify-content: flex-start;
${({ thin }) =>
thin &&
css`
margin: auto 32px auto 0;
`}
`
const CloseIcon = styled(X)`
cursor: pointer;
position: absolute;
top: 16px;
right: 16px;
`
const ContentWrapper = styled.div`
const BodyText = styled.div`
align-items: center;
display: grid;
grid-gap: 4px;
@ -33,6 +80,37 @@ const ContentWrapper = styled.div`
grid-gap: 8px;
}
`
const LearnMoreLink = styled(ExternalLink)<{ thin?: boolean }>`
align-items: center;
background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 8px;
color: ${({ theme }) => theme.text1};
display: flex;
font-size: 16px;
height: 44px;
justify-content: space-between;
margin: 0 0 20px 0;
padding: 12px 16px;
text-decoration: none;
width: auto;
:hover,
:focus,
:active {
background-color: rgba(255, 255, 255, 0.05);
}
transition: background-color 150ms ease-in-out;
${({ thin }) =>
thin &&
css`
font-size: 14px;
margin: auto;
width: 112px;
`}
`
const RootWrapper = styled.div`
position: relative;
`
export const ArbitrumWrapperBackgroundDarkMode = css`
background: radial-gradient(285% 8200% at 30% 50%, rgba(40, 160, 240, 0.1) 0%, rgba(219, 255, 0, 0) 100%),
radial-gradient(75% 75% at 0% 0%, rgba(150, 190, 220, 0.3) 0%, rgba(33, 114, 229, 0.3) 100%), hsla(0, 0%, 100%, 0.1);
@ -49,7 +127,7 @@ export const OptimismWrapperBackgroundLightMode = css`
background: radial-gradient(92% 105% at 50% 7%, rgba(255, 58, 212, 0.04) 0%, rgba(255, 255, 255, 0.03) 100%),
radial-gradient(100% 97% at 0% 12%, rgba(235, 0, 255, 0.1) 0%, rgba(243, 19, 19, 0.1) 100%), hsla(0, 0%, 100%, 0.5);
`
const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string }>`
const ContentWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string; thin?: boolean }>`
${({ chainId, darkMode }) =>
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
@ -66,7 +144,13 @@ const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; l
overflow: hidden;
position: relative;
width: 100%;
${({ thin }) =>
thin &&
css`
flex-direction: row;
max-width: max-content;
min-height: min-content;
`}
:before {
background-image: url(${({ logoUrl }) => logoUrl});
background-repeat: no-repeat;
@ -80,36 +164,29 @@ const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; l
z-index: -1;
}
`
const Header = styled.h2`
const Header = styled.h2<{ thin?: boolean }>`
font-weight: 600;
font-size: 20px;
margin: 0;
padding-right: 30px;
`
const Body = styled.p`
font-size: 12px;
grid-column: 1 / 3;
line-height: 143%;
margin: 0;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
grid-column: 2 / 3;
}
display: ${({ thin }) => (thin ? 'none' : 'block')};
`
const LinkOutCircle = styled(ArrowDownCircle)`
margin-left: 12px;
transform: rotate(230deg);
width: 20px;
height: 20px;
`
const LinkOutToBridge = styled(ExternalLink)`
const LinkOutToBridge = styled(ExternalLink)<{ thin?: boolean }>`
align-items: center;
background-color: black;
border-radius: 16px;
border-radius: 8px;
color: white;
display: flex;
font-size: 16px;
height: 44px;
justify-content: space-between;
margin: 0 20px 20px 20px;
margin: 0 12px 20px 18px;
padding: 12px 16px;
text-decoration: none;
width: auto;
@ -118,52 +195,85 @@ const LinkOutToBridge = styled(ExternalLink)`
:active {
background-color: black;
}
${({ thin }) =>
thin &&
css`
font-size: 14px;
margin: auto 10px;
width: 168px;
`}
`
export function NetworkAlert() {
interface NetworkAlertProps {
thin?: boolean
}
export function NetworkAlert(props: NetworkAlertProps) {
const { account, chainId } = useActiveWeb3React()
const [darkMode] = useDarkModeManager()
const [arbitrumAlphaAcknowledged, setArbitrumAlphaAcknowledged] = useArbitrumAlphaAlert()
const [optimismAlphaAcknowledged, setOptimismAlphaAcknowledged] = useOptimismAlphaAlert()
const [locallyDismissed, setLocallyDimissed] = useState(false)
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
const dismiss = useCallback(() => {
if (userEthBalance?.greaterThan(0)) {
setArbitrumAlphaAcknowledged(true)
switch (chainId) {
case SupportedChainId.OPTIMISM:
setOptimismAlphaAcknowledged(true)
break
case SupportedChainId.ARBITRUM_ONE:
setArbitrumAlphaAcknowledged(true)
break
}
} else {
setLocallyDimissed(true)
}
}, [setArbitrumAlphaAcknowledged, userEthBalance])
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged || locallyDismissed) {
}, [chainId, setArbitrumAlphaAcknowledged, setOptimismAlphaAcknowledged, userEthBalance])
const onOptimismAndOptimismAcknowledged = SupportedChainId.OPTIMISM === chainId && optimismAlphaAcknowledged
const onArbitrumAndArbitrumAcknowledged = SupportedChainId.ARBITRUM_ONE === chainId && arbitrumAlphaAcknowledged
if (
!chainId ||
!L2_CHAIN_IDS.includes(chainId) ||
onArbitrumAndArbitrumAcknowledged ||
onOptimismAndOptimismAcknowledged ||
locallyDismissed
) {
return null
}
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const isOptimism = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
const depositUrl = isOptimism ? `${info.bridge}?chainId=1` : info.bridge
const readMoreLink = isOptimism
? 'https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism'
: 'https://help.uniswap.org/en/articles/5538618-how-to-deposit-tokens-to-arbitrum'
const helpCenterLink = isOptimism ? OPTIMISM_HELP_CENTER_LINK : ARBITRUM_HELP_CENTER_LINK
const showCloseIcon = Boolean(userEthBalance?.greaterThan(0) && !props.thin)
return (
<RootWrapper chainId={chainId} darkMode={darkMode} logoUrl={info.logoUrl}>
<CloseIcon onClick={dismiss} />
<ContentWrapper>
<L2Icon src={info.logoUrl} />
<Header>
<Trans>Uniswap on {info.label}</Trans>
</Header>
<Body>
<Trans>
This is an alpha release of Uniswap on the {info.label} network. You must bridge L1 assets to the network to
swap them.
</Trans>{' '}
<ReadMoreLink href={readMoreLink}>
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
<RootWrapper>
<BetaTag color={isOptimism ? '#ff0420' : '#0490ed'}>Beta</BetaTag>
<ContentWrapper chainId={chainId} darkMode={darkMode} logoUrl={info.logoUrl} thin={props.thin}>
{showCloseIcon && <CloseIcon onClick={dismiss} />}
<BodyText>
<L2Icon src={info.logoUrl} />
<Header thin={props.thin}>
<Trans>Uniswap on {info.label}</Trans>
</Header>
<Body>
<Trans>
To starting trading on {info.label}, first bridge your assets from L1 to L2. Please treat this as a beta
release and learn about the risks before using {info.label}.
</Trans>
</Body>
</BodyText>
<Controls thin={props.thin}>
<LinkOutToBridge href={depositUrl} thin={props.thin}>
<Trans>Deposit Assets</Trans>
<LinkOutCircle />
</LinkOutToBridge>
<LearnMoreLink href={helpCenterLink} thin={props.thin}>
<Trans>Learn More</Trans>
</LearnMoreLink>
</Controls>
</ContentWrapper>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</RootWrapper>
)
}

@ -1,7 +0,0 @@
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
export const ReadMoreLink = styled(ExternalLink)`
color: ${({ theme }) => theme.text1};
text-decoration: underline;
`

@ -1,63 +0,0 @@
import { Trans } from '@lingui/macro'
import { SupportedChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
const Root = styled.div`
background-color: ${({ theme }) => theme.yellow3};
border-radius: 18px;
color: black;
margin-top: 16px;
padding: 16px;
width: 100%;
max-width: 880px;
`
const WarningIcon = styled(AlertOctagon)`
margin: 0 8px 0 0;
`
const TitleRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 25px;
`
const Body = styled.div`
font-size: 12px;
line-height: 15px;
margin: 8px 0 0 0;
`
const ReadMoreLink = styled(ExternalLink)`
color: black;
text-decoration: underline;
`
export default function OptimismDowntimeWarning() {
const { chainId } = useActiveWeb3React()
if (!chainId || ![SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)) {
return null
}
return (
<Root>
<TitleRow>
<WarningIcon />
<Trans>Optimism Planned Downtime</Trans>
</TitleRow>
<Body>
<Trans>
Optimism expects planned downtime in the near future. Unplanned downtime may also occur. While the network is
down, fees will not be generated and you will be unable to remove liquidity.{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5406082-what-happens-if-the-optimistic-ethereum-network-experiences-downtime">
Read more.
</ReadMoreLink>
</Trans>
</Body>
</Root>
)
}

@ -1,5 +1,6 @@
import arbitrumLogoUrl from 'assets/svg/arbitrum_logo.svg'
import optimismLogoUrl from 'assets/svg/optimism_logo.svg'
import optimismLogoUrl from 'assets/svg/optimistic_ethereum.svg'
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
import ms from 'ms.macro'
export enum SupportedChainId {
@ -47,12 +48,19 @@ export const L2_CHAIN_IDS = [
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
interface L1ChainInfo {
export interface L1ChainInfo {
readonly blockWaitMsBeforeWarning?: number
readonly docs: string
readonly explorer: string
readonly infoLink: string
readonly label: string
readonly logoUrl?: string
readonly rpcUrls?: string[]
readonly nativeCurrency: {
name: string // 'Goerli ETH',
symbol: string // 'gorETH',
decimals: number //18,
}
}
export interface L2ChainInfo extends L1ChainInfo {
readonly bridge: string
@ -60,7 +68,7 @@ export interface L2ChainInfo extends L1ChainInfo {
readonly statusPage?: string
}
type ChainInfo = { readonly [chainId: number]: L1ChainInfo | L2ChainInfo } & {
export type ChainInfo = { readonly [chainId: number]: L1ChainInfo | L2ChainInfo } & {
readonly [chainId in SupportedL2ChainId]: L2ChainInfo
} &
{ readonly [chainId in SupportedL1ChainId]: L1ChainInfo }
@ -74,6 +82,8 @@ export const CHAIN_INFO: ChainInfo = {
infoLink: 'https://info.uniswap.org/#/arbitrum',
label: 'Arbitrum',
logoUrl: arbitrumLogoUrl,
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://arb1.arbitrum.io/rpc'],
},
[SupportedChainId.ARBITRUM_RINKEBY]: {
blockWaitMsBeforeWarning: ms`10m`,
@ -83,45 +93,55 @@ export const CHAIN_INFO: ChainInfo = {
infoLink: 'https://info.uniswap.org/#/arbitrum/',
label: 'Arbitrum Rinkeby',
logoUrl: arbitrumLogoUrl,
nativeCurrency: { name: 'Rinkeby ArbETH', symbol: 'rinkArbETH', decimals: 18 },
rpcUrls: ['https://rinkeby.arbitrum.io/rpc'],
},
[SupportedChainId.MAINNET]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Mainnet',
label: 'Ethereum',
logoUrl: ethereumLogoUrl,
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
},
[SupportedChainId.RINKEBY]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://rinkeby.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Rinkeby',
nativeCurrency: { name: 'Rinkeby ETH', symbol: 'rinkETH', decimals: 18 },
},
[SupportedChainId.ROPSTEN]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://ropsten.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Ropsten',
nativeCurrency: { name: 'Ropsten ETH', symbol: 'ropETH', decimals: 18 },
},
[SupportedChainId.KOVAN]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://kovan.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Kovan',
nativeCurrency: { name: 'Kovan ETH', symbol: 'kovETH', decimals: 18 },
},
[SupportedChainId.GOERLI]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://goerli.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Görli',
nativeCurrency: { name: 'Görli ETH', symbol: 'görETH', decimals: 18 },
},
[SupportedChainId.OPTIMISM]: {
blockWaitMsBeforeWarning: ms`10m`,
bridge: 'https://gateway.optimism.io/',
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/optimism/',
label: 'Optimism',
infoLink: 'https://info.uniswap.org/#/optimism',
label: 'OΞ',
logoUrl: optimismLogoUrl,
nativeCurrency: { name: 'Optimistic ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: ['https://mainnet.optimism.io'],
statusPage: 'https://optimism.io/status',
},
[SupportedChainId.OPTIMISTIC_KOVAN]: {
@ -131,7 +151,13 @@ export const CHAIN_INFO: ChainInfo = {
explorer: 'https://optimistic.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/optimism',
label: 'Optimistic Kovan',
rpcUrls: ['https://kovan.optimism.io'],
logoUrl: optimismLogoUrl,
nativeCurrency: { name: 'Optimistic kovETH', symbol: 'kovOpETH', decimals: 18 },
statusPage: 'https://optimism.io/status',
},
}
export const ARBITRUM_HELP_CENTER_LINK = 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum'
export const OPTIMISM_HELP_CENTER_LINK =
'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ'

@ -28,7 +28,6 @@ import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallbac
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field, Bound } from '../../state/mint/v3/actions'
import { AddLiquidityNetworkAlert } from 'components/NetworkAlert/AddLiquidityNetworkAlert'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { TYPE, ExternalLink } from '../../theme'
@ -71,7 +70,7 @@ import HoverInlineText from 'components/HoverInlineText'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import LiquidityChartRangeInput from 'components/LiquidityChartRangeInput'
import { SupportedChainId } from 'constants/chains'
import OptimismDowntimeWarning from 'components/OptimismDowntimeWarning'
import DowntimeWarning from 'components/DowntimeWarning'
import { CHAIN_INFO } from '../../constants/chains'
const DEFAULT_ADD_IN_RANGE_SLIPPAGE_TOLERANCE = new Percent(50, 10_000)
@ -550,8 +549,7 @@ export default function AddLiquidity({
return (
<>
<ScrollablePage>
<AddLiquidityNetworkAlert />
<OptimismDowntimeWarning />
<DowntimeWarning />
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={handleDismissConfirmation}

@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { AutoColumn } from 'components/Column'
import { MinimalNetworkAlert } from 'components/NetworkAlert/MinimalNetworkAlert'
import { CHAIN_INFO, SupportedChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import styled from 'styled-components/macro'
@ -127,30 +126,27 @@ export default function CTACards() {
const { chainId } = useActiveWeb3React()
const { infoLink } = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
return (
<div>
<MinimalNetworkAlert />
<CTASection>
<CTA1 href={'https://help.uniswap.org/en/articles/5391541-providing-liquidity-on-uniswap-v3'}>
<ResponsiveColumn>
<HeaderText>
<Trans>Learn about providing liquidity</Trans>
</HeaderText>
<TYPE.body fontWeight={300} style={{ alignItems: 'center', display: 'flex', maxWidth: '80%' }}>
<Trans>Check out our v3 LP walkthrough and migration guides.</Trans>
</TYPE.body>
</ResponsiveColumn>
</CTA1>
<CTA2 href={infoLink + 'pools'}>
<ResponsiveColumn>
<HeaderText style={{ alignSelf: 'flex-start' }}>
<Trans>Top pools</Trans>
</HeaderText>
<TYPE.body fontWeight={300} style={{ alignSelf: 'flex-start' }}>
<Trans>Explore popular pools on Uniswap Analytics.</Trans>
</TYPE.body>
</ResponsiveColumn>
</CTA2>
</CTASection>
</div>
<CTASection>
<CTA1 href={'https://help.uniswap.org/en/articles/5391541-providing-liquidity-on-uniswap-v3'}>
<ResponsiveColumn>
<HeaderText>
<Trans>Learn about providing liquidity</Trans>
</HeaderText>
<TYPE.body fontWeight={300} style={{ alignItems: 'center', display: 'flex', maxWidth: '80%' }}>
<Trans>Check out our v3 LP walkthrough and migration guides.</Trans>
</TYPE.body>
</ResponsiveColumn>
</CTA1>
<CTA2 href={infoLink + 'pools'}>
<ResponsiveColumn>
<HeaderText style={{ alignSelf: 'flex-start' }}>
<Trans>Top pools</Trans>
</HeaderText>
<TYPE.body fontWeight={300} style={{ alignSelf: 'flex-start' }}>
<Trans>Explore popular pools on Uniswap Analytics.</Trans>
</TYPE.body>
</ResponsiveColumn>
</CTA2>
</CTASection>
)
}

@ -1,8 +1,10 @@
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonOutlined, ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import DowntimeWarning from 'components/DowntimeWarning'
import { FlyoutAlignment, NewMenu } from 'components/Menu'
import { SwapPoolTabs } from 'components/NavigationTabs'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import PositionList from 'components/PositionList'
import { RowBetween, RowFixed } from 'components/Row'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
@ -221,6 +223,8 @@ export default function Pool() {
</TitleRow>
<HideSmall>
<NetworkAlert thin />
<DowntimeWarning />
<CTACards />
</HideSmall>

@ -18,7 +18,7 @@ export enum ApplicationModal {
DELEGATE,
VOTE,
POOL_OVERVIEW_OPTIONS,
ARBITRUM_OPTIONS,
NETWORK_SELECTOR,
}
export const updateChainId = createAction<{ chainId: number | null }>('application/updateChainId')
@ -27,4 +27,5 @@ export const setOpenModal = createAction<ApplicationModal | null>('application/s
export const addPopup =
createAction<{ key?: string; removeAfterMs?: number | null; content: PopupContent }>('application/addPopup')
export const removePopup = createAction<{ key: string }>('application/removePopup')
export const setImplements3085 = createAction<{ implements3085: boolean }>('application/setImplements3085')
export const setChainConnectivityWarning = createAction<{ warn: boolean }>('application/setChainConnectivityWarning')

@ -2,32 +2,34 @@ import { createReducer, nanoid } from '@reduxjs/toolkit'
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
import {
addPopup,
ApplicationModal,
PopupContent,
removePopup,
updateBlockNumber,
ApplicationModal,
setOpenModal,
updateChainId,
setChainConnectivityWarning,
setImplements3085,
setOpenModal,
updateBlockNumber,
updateChainId,
} from './actions'
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
export interface ApplicationState {
// used by RTK-Query to build dynamic subgraph urls
readonly chainId: number | null
readonly chainConnectivityWarning: boolean
readonly blockNumber: { readonly [chainId: number]: number }
readonly popupList: PopupList
readonly chainConnectivityWarning: boolean
readonly chainId: number | null
readonly implements3085: boolean
readonly openModal: ApplicationModal | null
readonly popupList: PopupList
}
const initialState: ApplicationState = {
chainId: null,
chainConnectivityWarning: false,
blockNumber: {},
popupList: [],
chainConnectivityWarning: false,
chainId: null,
implements3085: false,
openModal: null,
popupList: [],
}
export default createReducer(initialState, (builder) =>
@ -64,6 +66,9 @@ export default createReducer(initialState, (builder) =>
}
})
})
.addCase(setImplements3085, (state, { payload: { implements3085 } }) => {
state.implements3085 = implements3085
})
.addCase(setChainConnectivityWarning, (state, { payload: { warn } }) => {
state.chainConnectivityWarning = warn
})

@ -7,7 +7,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { api, CHAIN_TAG } from 'state/data/enhanced'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { supportedChainId } from 'utils/supportedChainId'
import { setChainConnectivityWarning, updateBlockNumber, updateChainId } from './actions'
import { switchToNetwork } from 'utils/switchToNetwork'
import { setChainConnectivityWarning, setImplements3085, updateBlockNumber, updateChainId } from './actions'
import { useBlockNumber } from './hooks'
function useQueryCacheInvalidator() {
@ -61,7 +62,7 @@ function useBlockWarningTimer() {
}
export default function Updater(): null {
const { library, chainId } = useActiveWeb3React()
const { account, chainId, library } = useActiveWeb3React()
const dispatch = useAppDispatch()
const windowVisible = useIsWindowVisible()
@ -116,5 +117,14 @@ export default function Updater(): null {
)
}, [dispatch, debouncedState.chainId])
useEffect(() => {
if (!account || !library?.provider?.request || !library?.provider?.isMetaMask) {
return
}
switchToNetwork({ library })
.then((x) => x ?? dispatch(setImplements3085({ implements3085: true })))
.catch(() => dispatch(setImplements3085({ implements3085: false })))
}, [account, chainId, dispatch, library])
return null
}

@ -18,6 +18,9 @@ export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>(
export const updateArbitrumAlphaAcknowledged = createAction<{ arbitrumAlphaAcknowledged: boolean }>(
'user/updateArbitrumAlphaAcknowledged'
)
export const updateOptimismAlphaAcknowledged = createAction<{ optimismAlphaAcknowledged: boolean }>(
'user/updateOptimismAlphaAcknowledged'
)
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserLocale = createAction<{ userLocale: SupportedLocale }>('user/updateUserLocale')

@ -20,6 +20,7 @@ import {
SerializedToken,
updateArbitrumAlphaAcknowledged,
updateHideClosedPositions,
updateOptimismAlphaAcknowledged,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
@ -353,3 +354,13 @@ export function useArbitrumAlphaAlert(): [boolean, (arbitrumAlphaAcknowledged: b
return [arbitrumAlphaAcknowledged, setArbitrumAlphaAcknowledged]
}
export function useOptimismAlphaAlert(): [boolean, (optimismAlphaAcknowledged: boolean) => void] {
const dispatch = useAppDispatch()
const optimismAlphaAcknowledged = useAppSelector(({ user }) => user.optimismAlphaAcknowledged)
const setOptimismAlphaAcknowledged = (optimismAlphaAcknowledged: boolean) => {
dispatch(updateOptimismAlphaAcknowledged({ optimismAlphaAcknowledged }))
}
return [optimismAlphaAcknowledged, setOptimismAlphaAcknowledged]
}

@ -12,11 +12,12 @@ import {
updateArbitrumAlphaAcknowledged,
updateHideClosedPositions,
updateMatchesDarkMode,
updateOptimismAlphaAcknowledged,
updateUserClientSideRouter,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserLocale,
updateUserClientSideRouter,
updateUserSlippageTolerance,
} from './actions'
@ -28,9 +29,10 @@ export interface UserState {
// the timestamp of the last updateVersion action
lastUpdateVersionTimestamp?: number
userDarkMode: boolean | null // the user's choice for dark mode or light mode
matchesDarkMode: boolean // whether the dark mode media query matches
optimismAlphaAcknowledged: boolean
userDarkMode: boolean | null // the user's choice for dark mode or light mode
userLocale: SupportedLocale | null
userExpertMode: boolean
@ -70,8 +72,9 @@ function pairKey(token0Address: string, token1Address: string) {
export const initialState: UserState = {
arbitrumAlphaAcknowledged: false,
userDarkMode: null,
matchesDarkMode: false,
optimismAlphaAcknowledged: false,
userDarkMode: null,
userExpertMode: false,
userLocale: null,
userClientSideRouter: false,
@ -131,6 +134,9 @@ export default createReducer(initialState, (builder) =>
.addCase(updateArbitrumAlphaAcknowledged, (state, action) => {
state.arbitrumAlphaAcknowledged = action.payload.arbitrumAlphaAcknowledged
})
.addCase(updateOptimismAlphaAcknowledged, (state, action) => {
state.optimismAlphaAcknowledged = action.payload.optimismAlphaAcknowledged
})
.addCase(updateUserExpertMode, (state, action) => {
state.userExpertMode = action.payload.userExpertMode
state.timestamp = currentTimestamp()

@ -37,6 +37,7 @@ const black = '#000000'
function colors(darkMode: boolean): Colors {
return {
darkMode,
// base
white,
black,

@ -2,6 +2,8 @@ import { FlattenSimpleInterpolation, ThemedCssFunction } from 'styled-components
export type Color = string
export interface Colors {
darkMode: boolean
// base
white: Color
black: Color

34
src/utils/addNetwork.ts Normal file

@ -0,0 +1,34 @@
import { Web3Provider } from '@ethersproject/providers'
import { L1ChainInfo, L2ChainInfo, SupportedChainId } from 'constants/chains'
import { BigNumber, utils } from 'ethers'
interface AddNetworkArguments {
library: Web3Provider
chainId: SupportedChainId
info: L1ChainInfo | L2ChainInfo
}
// provider.request returns Promise<any>, but wallet_switchEthereumChain must return null or throw
// see https://github.com/rekmarks/EIPs/blob/3326-create/EIPS/eip-3326.md for more info on wallet_switchEthereumChain
export async function addNetwork({ library, chainId, info }: AddNetworkArguments): Promise<null | void> {
if (!library?.provider?.request) {
return
}
const formattedChainId = utils.hexStripZeros(BigNumber.from(chainId).toHexString())
try {
await library?.provider.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: formattedChainId,
chainName: info.label,
rpcUrls: info.rpcUrls,
nativeCurrency: info.nativeCurrency,
blockExplorerUrls: [info.explorer],
},
],
})
} catch (error) {
console.error('error adding eth network: ', chainId, info, error)
}
}

@ -1,10 +1,11 @@
import { SupportedChainId } from 'constants/chains'
import { BigNumber, utils } from 'ethers'
import { Web3Provider } from '@ethersproject/providers'
import { CHAIN_INFO, SupportedChainId } from 'constants/chains'
import { BigNumber, utils } from 'ethers'
import { addNetwork } from './addNetwork'
interface SwitchNetworkArguments {
library: Web3Provider
chainId: SupportedChainId
chainId?: SupportedChainId
}
// provider.request returns Promise<any>, but wallet_switchEthereumChain must return null or throw
@ -13,9 +14,27 @@ export async function switchToNetwork({ library, chainId }: SwitchNetworkArgumen
if (!library?.provider?.request) {
return
}
if (!chainId && library?.getNetwork) {
;({ chainId } = await library.getNetwork())
}
const formattedChainId = utils.hexStripZeros(BigNumber.from(chainId).toHexString())
return library?.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: formattedChainId }],
})
try {
await library?.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: formattedChainId }],
})
} catch (error) {
// 4902 is the error code for attempting to switch to an unrecognized chainId
if (error.code === 4902 && chainId !== undefined) {
const info = CHAIN_INFO[chainId]
// metamask (only known implementer) automatically switches after a network is added
// the second call is done here because that behavior is not a part of the spec and cannot be relied upon in the future
// metamask's behavior when switching to the current network is just to return null (a no-op)
await addNetwork({ library, chainId, info })
await switchToNetwork({ library, chainId })
} else {
throw error
}
}
}