diff --git a/src/assets/svg/optimistic_ethereum.svg b/src/assets/svg/optimistic_ethereum.svg new file mode 100644 index 0000000000..c63b1e5ee4 --- /dev/null +++ b/src/assets/svg/optimistic_ethereum.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 82d25f8ed8..fa6d7d8e14 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -8,7 +8,7 @@ import useTheme from 'hooks/useTheme' type ButtonProps = Omit -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}; diff --git a/src/components/DowntimeWarning/index.tsx b/src/components/DowntimeWarning/index.tsx new file mode 100644 index 0000000000..35401bcfb0 --- /dev/null +++ b/src/components/DowntimeWarning/index.tsx @@ -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 ( +
+ + 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.{' '} + + Read more. + + +
+ ) + case SupportedChainId.ARBITRUM_ONE: + case SupportedChainId.ARBITRUM_RINKEBY: + return ( +
+ + Arbitrum is in Beta and may experience downtime. During downtime, your position will not earn fees and you + will be unable to remove liquidity.{' '} + + Read more. + + +
+ ) + default: + return null + } + } + + return ( + + + + + ) +} diff --git a/src/components/Header/ChainConnectivityWarning.tsx b/src/components/Header/ChainConnectivityWarning.tsx index e33b6862f7..87c992c88b 100644 --- a/src/components/Header/ChainConnectivityWarning.tsx +++ b/src/components/Header/ChainConnectivityWarning.tsx @@ -60,7 +60,7 @@ export function ChainConnectivityWarning() { {chainId === SupportedChainId.MAINNET ? ( You may have lost your network connection. ) : ( - {label} may be down right now, or you may have lost your network connection. + You may have lost your network connection, or {label} might be down right now. )}{' '} {(info as L2ChainInfo).statusPage !== undefined && ( diff --git a/src/components/Header/NetworkCard.tsx b/src/components/Header/NetworkCard.tsx deleted file mode 100644 index a34986ad95..0000000000 --- a/src/components/Header/NetworkCard.tsx +++ /dev/null @@ -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(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 ( - - - - {info.label} - - - {open && ( - - -
{isArbitrum ? {info.label} Bridge : Optimistic L2 Gateway}
- -
- - {isArbitrum ? {info.label} Explorer : Optimistic Etherscan} - - - -
- Learn more -
- -
- {implements3085 ? ( - switchToNetwork({ library, chainId: SupportedChainId.MAINNET })}> -
- Switch to L1 (Mainnet) -
- -
- ) : ( - - Change your network to go back to L1 - - )} -
- )} -
- ) - } - - return {info.label} -} diff --git a/src/components/Header/NetworkSelector.tsx b/src/components/Header/NetworkSelector.tsx new file mode 100644 index 0000000000..5b12509b90 --- /dev/null +++ b/src/components/Header/NetworkSelector.tsx @@ -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 Arbitrum Bridge + case SupportedChainId.OPTIMISM: + case SupportedChainId.OPTIMISTIC_KOVAN: + return Optimism Gateway + default: + return Bridge + } +} +const ExplorerText = ({ chainId }: { chainId: SupportedL2ChainId }) => { + switch (chainId) { + case SupportedChainId.ARBITRUM_ONE: + case SupportedChainId.ARBITRUM_RINKEBY: + return Arbiscan + case SupportedChainId.OPTIMISM: + case SupportedChainId.OPTIMISTIC_KOVAN: + return Optimistic Etherscan + default: + return Explorer + } +} + +export default function NetworkSelector() { + const { chainId, library } = useActiveWeb3React() + const node = useRef() + 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 = () => ( + + + {rowText} + {chainId === targetChain && } + + ) + const helpCenterLink = isOptimism ? OPTIMISM_HELP_CENTER_LINK : ARBITRUM_HELP_CENTER_LINK + if (active && hasExtendedInfo) { + return ( + + + + + + + + + + + Help Center + + + + ) + } + return + } + + return ( + + + + {info.label} + {showSelector && } + + {open && ( + + + Select a network + + + + + + )} + + ) +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index dbece88e22..a04efa3b1c 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -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() { - + + + {availableClaim && !showClaimPopup && ( @@ -326,6 +332,8 @@ export default function Header() { ) : null} + + diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 500f1fbe5a..cb63623635 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -62,7 +62,6 @@ const UNIbutton = styled(ButtonPrimary)` ` const StyledMenu = styled.div` - margin-left: 0.5rem; display: flex; justify-content: center; align-items: center; diff --git a/src/components/NetworkAlert/AddLiquidityNetworkAlert.tsx b/src/components/NetworkAlert/AddLiquidityNetworkAlert.tsx deleted file mode 100644 index 67d49e28cb..0000000000 --- a/src/components/NetworkAlert/AddLiquidityNetworkAlert.tsx +++ /dev/null @@ -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 ( - - - - This is an alpha release of Uniswap on the {info.label} network. - You must bridge L1 assets to the network to use them.{' '} - - Read more - - - - Deposit to {info.label} - - - - ) -} diff --git a/src/components/NetworkAlert/MinimalNetworkAlert.tsx b/src/components/NetworkAlert/MinimalNetworkAlert.tsx deleted file mode 100644 index 4a0e7eeb1b..0000000000 --- a/src/components/NetworkAlert/MinimalNetworkAlert.tsx +++ /dev/null @@ -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 ( - - - - This is an alpha release of Uniswap on the {info.label} network. - You must bridge L1 assets to the network to use them.{' '} - - Read more - - - - Deposit to {info.label} - - - - ) -} diff --git a/src/components/NetworkAlert/NetworkAlert.tsx b/src/components/NetworkAlert/NetworkAlert.tsx index f88e1ec729..6b4621d986 100644 --- a/src/components/NetworkAlert/NetworkAlert.tsx +++ b/src/components/NetworkAlert/NetworkAlert.tsx @@ -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 ( - - - - -
- Uniswap on {info.label} -
- - - This is an alpha release of Uniswap on the {info.label} network. You must bridge L1 assets to the network to - swap them. - {' '} - - Read more - - + + Beta + + {showCloseIcon && } + + +
+ Uniswap on {info.label} +
+ + + 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}. + + +
+ + + Deposit Assets + + + + Learn More + +
- - Deposit to {info.label} - -
) } diff --git a/src/components/NetworkAlert/styles.ts b/src/components/NetworkAlert/styles.ts deleted file mode 100644 index 602183226f..0000000000 --- a/src/components/NetworkAlert/styles.ts +++ /dev/null @@ -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; -` diff --git a/src/components/OptimismDowntimeWarning/index.tsx b/src/components/OptimismDowntimeWarning/index.tsx deleted file mode 100644 index fdcdf4a944..0000000000 --- a/src/components/OptimismDowntimeWarning/index.tsx +++ /dev/null @@ -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 ( - - - - Optimism Planned Downtime - - - - 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.{' '} - - Read more. - - - - - ) -} diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 0c87a51ec2..0e37fff34d 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -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ξ' diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index 8ee502441e..fec3afec4b 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -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 ( <> - - + - - - - - - Learn about providing liquidity ↗ - - - Check out our v3 LP walkthrough and migration guides. - - - - - - - Top pools ↗ - - - Explore popular pools on Uniswap Analytics. - - - - - + + + + + Learn about providing liquidity ↗ + + + Check out our v3 LP walkthrough and migration guides. + + + + + + + Top pools ↗ + + + Explore popular pools on Uniswap Analytics. + + + + ) } diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index b0e8e29dfd..97bedac0e2 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -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() { + + diff --git a/src/state/application/actions.ts b/src/state/application/actions.ts index 6c401226ba..fea08e33f4 100644 --- a/src/state/application/actions.ts +++ b/src/state/application/actions.ts @@ -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('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') diff --git a/src/state/application/reducer.ts b/src/state/application/reducer.ts index d0b840c952..a56c86983d 100644 --- a/src/state/application/reducer.ts +++ b/src/state/application/reducer.ts @@ -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 }) diff --git a/src/state/application/updater.ts b/src/state/application/updater.ts index 1bd467cc44..b41f818d0d 100644 --- a/src/state/application/updater.ts +++ b/src/state/application/updater.ts @@ -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 } diff --git a/src/state/user/actions.ts b/src/state/user/actions.ts index 4f17d70b26..bc6d10f162 100644 --- a/src/state/user/actions.ts +++ b/src/state/user/actions.ts @@ -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') diff --git a/src/state/user/hooks.tsx b/src/state/user/hooks.tsx index 67093c182a..c5b6b705dc 100644 --- a/src/state/user/hooks.tsx +++ b/src/state/user/hooks.tsx @@ -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] +} diff --git a/src/state/user/reducer.ts b/src/state/user/reducer.ts index 77f3160f36..cbd95824ba 100644 --- a/src/state/user/reducer.ts +++ b/src/state/user/reducer.ts @@ -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() diff --git a/src/theme/index.tsx b/src/theme/index.tsx index 282268a22b..e7792bb7fb 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -37,6 +37,7 @@ const black = '#000000' function colors(darkMode: boolean): Colors { return { + darkMode, // base white, black, diff --git a/src/theme/styled.d.ts b/src/theme/styled.d.ts index 90194e7ace..96dc94f451 100644 --- a/src/theme/styled.d.ts +++ b/src/theme/styled.d.ts @@ -2,6 +2,8 @@ import { FlattenSimpleInterpolation, ThemedCssFunction } from 'styled-components export type Color = string export interface Colors { + darkMode: boolean + // base white: Color black: Color diff --git a/src/utils/addNetwork.ts b/src/utils/addNetwork.ts new file mode 100644 index 0000000000..52615b3de4 --- /dev/null +++ b/src/utils/addNetwork.ts @@ -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, 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 { + 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) + } +} diff --git a/src/utils/switchToNetwork.ts b/src/utils/switchToNetwork.ts index a6119370e4..5ec95e3bb9 100644 --- a/src/utils/switchToNetwork.ts +++ b/src/utils/switchToNetwork.ts @@ -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, 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 + } + } }