feat(chain-connectivity): warn when the user is not receiving blocks (#2123)
* logic for tracking time since last block * add polling times to chain info * pr feedback * add gui for chain connectivity warning * add arb support * update title of warning to indicate internet connectivity issues may be the problem * pr review * clean up useBlockWarningTimer * softer language on mainnet * only show warning if user has the window visible
This commit is contained in:
parent
c2093ce040
commit
bcf64bcb11
76
src/components/Header/ChainConnectivityWarning.tsx
Normal file
76
src/components/Header/ChainConnectivityWarning.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CHAIN_INFO, L2ChainInfo, SupportedChainId } from 'constants/chains'
|
||||
import { useActiveWeb3React } from 'hooks/web3'
|
||||
import { AlertOctagon } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
|
||||
const BodyRow = styled.div`
|
||||
color: ${({ theme }) => theme.black};
|
||||
font-size: 12px;
|
||||
`
|
||||
const CautionIcon = styled(AlertOctagon)`
|
||||
color: ${({ theme }) => theme.black};
|
||||
`
|
||||
const Link = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.black};
|
||||
text-decoration: underline;
|
||||
`
|
||||
const TitleRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
const TitleText = styled.div`
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin: 0px 12px;
|
||||
`
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.yellow3};
|
||||
border-radius: 12px;
|
||||
bottom: 60px;
|
||||
display: none;
|
||||
max-width: 348px;
|
||||
padding: 16px 20px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
export function ChainConnectivityWarning() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const info = CHAIN_INFO[chainId ?? SupportedChainId.MAINNET]
|
||||
const label = info?.label
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<TitleRow>
|
||||
<CautionIcon />
|
||||
<TitleText>
|
||||
<Trans>Network Warning</Trans>
|
||||
</TitleText>
|
||||
</TitleRow>
|
||||
<BodyRow>
|
||||
{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>
|
||||
)}{' '}
|
||||
{(info as L2ChainInfo).statusPage !== undefined && (
|
||||
<span>
|
||||
<Trans>Check network status</Trans>{' '}
|
||||
<Link href={(info as L2ChainInfo).statusPage || ''}>
|
||||
<Trans>here.</Trans>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
</BodyRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
@ -1,19 +1,21 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
import { useActiveWeb3React } from '../../hooks/web3'
|
||||
|
||||
import { useBlockNumber } from '../../state/application/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { ChainConnectivityWarning } from './ChainConnectivityWarning'
|
||||
|
||||
const StyledPolling = styled.div`
|
||||
const StyledPolling = styled.div<{ warning: boolean }>`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
color: ${({ theme }) => theme.green1};
|
||||
color: ${({ theme, warning }) => (warning ? theme.yellow3 : theme.green1)};
|
||||
transition: 250ms ease color;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
display: none;
|
||||
@ -26,14 +28,15 @@ const StyledPollingNumber = styled(TYPE.small)<{ breathe: boolean; hovering: boo
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
const StyledPollingDot = styled.div`
|
||||
const StyledPollingDot = styled.div<{ warning: boolean }>`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
min-height: 8px;
|
||||
min-width: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
background-color: ${({ theme }) => theme.green1};
|
||||
background-color: ${({ theme, warning }) => (warning ? theme.yellow3 : theme.green1)};
|
||||
transition: 250ms ease background-color;
|
||||
`
|
||||
|
||||
const rotate360 = keyframes`
|
||||
@ -45,19 +48,20 @@ const rotate360 = keyframes`
|
||||
}
|
||||
`
|
||||
|
||||
const Spinner = styled.div`
|
||||
const Spinner = styled.div<{ warning: boolean }>`
|
||||
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
|
||||
transform: translateZ(0);
|
||||
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-left: 2px solid ${({ theme }) => theme.green1};
|
||||
border-left: 2px solid ${({ theme, warning }) => (warning ? theme.yellow3 : theme.green1)};
|
||||
background: transparent;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: 250ms ease border-color;
|
||||
|
||||
left: -3px;
|
||||
top: -3px;
|
||||
@ -65,11 +69,10 @@ const Spinner = styled.div`
|
||||
|
||||
export default function Polling() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const blockNumber = useBlockNumber()
|
||||
|
||||
const [isMounting, setIsMounting] = useState(false)
|
||||
const [isHover, setIsHover] = useState(false)
|
||||
const chainConnectivityWarning = useAppSelector((state) => state.application.chainConnectivityWarning)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
@ -90,15 +93,24 @@ export default function Polling() {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExternalLink
|
||||
href={chainId && blockNumber ? getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) : ''}
|
||||
>
|
||||
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<StyledPolling
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
warning={chainConnectivityWarning}
|
||||
>
|
||||
<StyledPollingNumber breathe={isMounting} hovering={isHover}>
|
||||
{blockNumber} 
|
||||
</StyledPollingNumber>
|
||||
<StyledPollingDot>{isMounting && <Spinner />}</StyledPollingDot>
|
||||
<StyledPollingDot warning={chainConnectivityWarning}>
|
||||
{isMounting && <Spinner warning={chainConnectivityWarning} />}
|
||||
</StyledPollingDot>{' '}
|
||||
</StyledPolling>
|
||||
</ExternalLink>
|
||||
{chainConnectivityWarning && <ChainConnectivityWarning />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import arbitrumLogoUrl from 'assets/svg/arbitrum_logo.svg'
|
||||
import optimismLogoUrl from 'assets/svg/optimism_logo.svg'
|
||||
import ms from 'ms.macro'
|
||||
|
||||
export enum SupportedChainId {
|
||||
MAINNET = 1,
|
||||
@ -47,6 +48,7 @@ export const L2_CHAIN_IDS = [
|
||||
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
|
||||
|
||||
interface L1ChainInfo {
|
||||
readonly blockWaitMsBeforeWarning?: number
|
||||
readonly docs: string
|
||||
readonly explorer: string
|
||||
readonly infoLink: string
|
||||
@ -55,6 +57,7 @@ interface L1ChainInfo {
|
||||
export interface L2ChainInfo extends L1ChainInfo {
|
||||
readonly bridge: string
|
||||
readonly logoUrl: string
|
||||
readonly statusPage?: string
|
||||
}
|
||||
|
||||
type ChainInfo = { readonly [chainId: number]: L1ChainInfo | L2ChainInfo } & {
|
||||
@ -64,6 +67,7 @@ type ChainInfo = { readonly [chainId: number]: L1ChainInfo | L2ChainInfo } & {
|
||||
|
||||
export const CHAIN_INFO: ChainInfo = {
|
||||
[SupportedChainId.ARBITRUM_ONE]: {
|
||||
blockWaitMsBeforeWarning: ms`10m`,
|
||||
bridge: 'https://bridge.arbitrum.io/',
|
||||
docs: 'https://offchainlabs.com/',
|
||||
explorer: 'https://arbiscan.io/',
|
||||
@ -72,6 +76,7 @@ export const CHAIN_INFO: ChainInfo = {
|
||||
logoUrl: arbitrumLogoUrl,
|
||||
},
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: {
|
||||
blockWaitMsBeforeWarning: ms`10m`,
|
||||
bridge: 'https://bridge.arbitrum.io/',
|
||||
docs: 'https://offchainlabs.com/',
|
||||
explorer: 'https://rinkeby-explorer.arbitrum.io/',
|
||||
@ -110,19 +115,23 @@ export const CHAIN_INFO: ChainInfo = {
|
||||
label: 'Görli',
|
||||
},
|
||||
[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',
|
||||
logoUrl: optimismLogoUrl,
|
||||
statusPage: 'https://optimism.io/status',
|
||||
},
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: {
|
||||
blockWaitMsBeforeWarning: ms`10m`,
|
||||
bridge: 'https://gateway.optimism.io/',
|
||||
docs: 'https://optimism.io/',
|
||||
explorer: 'https://optimistic.etherscan.io/',
|
||||
infoLink: 'https://info.uniswap.org/#/optimism',
|
||||
label: 'Optimistic Kovan',
|
||||
logoUrl: optimismLogoUrl,
|
||||
statusPage: 'https://optimism.io/status',
|
||||
},
|
||||
}
|
||||
|
@ -27,3 +27,4 @@ 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 setChainConnectivityWarning = createAction<{ warn: boolean }>('application/setChainConnectivityWarning')
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
ApplicationModal,
|
||||
setOpenModal,
|
||||
updateChainId,
|
||||
setChainConnectivityWarning,
|
||||
} from './actions'
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>
|
||||
@ -15,6 +16,7 @@ type PopupList = Array<{ key: string; show: boolean; content: PopupContent; remo
|
||||
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 openModal: ApplicationModal | null
|
||||
@ -22,6 +24,7 @@ export interface ApplicationState {
|
||||
|
||||
const initialState: ApplicationState = {
|
||||
chainId: null,
|
||||
chainConnectivityWarning: false,
|
||||
blockNumber: {},
|
||||
popupList: [],
|
||||
openModal: null,
|
||||
@ -61,4 +64,7 @@ export default createReducer(initialState, (builder) =>
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(setChainConnectivityWarning, (state, { payload: { warn } }) => {
|
||||
state.chainConnectivityWarning = warn
|
||||
})
|
||||
)
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { CHAIN_INFO } from 'constants/chains'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import useIsWindowVisible from 'hooks/useIsWindowVisible'
|
||||
import { useActiveWeb3React } from 'hooks/web3'
|
||||
import ms from 'ms.macro'
|
||||
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 useDebounce from '../../hooks/useDebounce'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { useActiveWeb3React } from '../../hooks/web3'
|
||||
import { updateBlockNumber, updateChainId } from './actions'
|
||||
import { setChainConnectivityWarning, updateBlockNumber, updateChainId } from './actions'
|
||||
import { useBlockNumber } from './hooks'
|
||||
|
||||
function useQueryCacheInvalidator() {
|
||||
const dispatch = useAppDispatch()
|
||||
@ -20,10 +23,46 @@ function useQueryCacheInvalidator() {
|
||||
}, [chainId, dispatch])
|
||||
}
|
||||
|
||||
const NETWORK_HEALTH_CHECK_MS = ms`15s`
|
||||
const DEFAULT_MS_BEFORE_WARNING = ms`10m`
|
||||
|
||||
function useBlockWarningTimer() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
const chainConnectivityWarningActive = useAppSelector((state) => state.application.chainConnectivityWarning)
|
||||
const timeout = useRef<NodeJS.Timeout>()
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
const [msSinceLastBlock, setMsSinceLastBlock] = useState(0)
|
||||
const currentBlock = useBlockNumber()
|
||||
|
||||
useEffect(() => {
|
||||
setMsSinceLastBlock(0)
|
||||
}, [currentBlock])
|
||||
|
||||
useEffect(() => {
|
||||
const waitMsBeforeWarning =
|
||||
(chainId ? CHAIN_INFO[chainId]?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
setMsSinceLastBlock(NETWORK_HEALTH_CHECK_MS + msSinceLastBlock)
|
||||
if (msSinceLastBlock > waitMsBeforeWarning && isWindowVisible) {
|
||||
dispatch(setChainConnectivityWarning({ warn: true }))
|
||||
} else if (chainConnectivityWarningActive) {
|
||||
dispatch(setChainConnectivityWarning({ warn: false }))
|
||||
}
|
||||
}, NETWORK_HEALTH_CHECK_MS)
|
||||
|
||||
return function cleanup() {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current)
|
||||
}
|
||||
}
|
||||
}, [chainId, chainConnectivityWarningActive, dispatch, isWindowVisible, msSinceLastBlock, setMsSinceLastBlock])
|
||||
}
|
||||
|
||||
export default function Updater(): null {
|
||||
const { library, chainId } = useActiveWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const windowVisible = useIsWindowVisible()
|
||||
|
||||
const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({
|
||||
@ -31,6 +70,7 @@ export default function Updater(): null {
|
||||
blockNumber: null,
|
||||
})
|
||||
|
||||
useBlockWarningTimer()
|
||||
useQueryCacheInvalidator()
|
||||
|
||||
const blockNumberCallback = useCallback(
|
||||
|
Loading…
Reference in New Issue
Block a user