Commit 3153db9f authored by Tina's avatar Tina Committed by GitHub

feat: add chainId (network) as url parameter (#3057)

* read from query param and change networks if necessary

* dont open network selector menu on url param change

* prompt network change when url changes

* keep url, network in sync

* use chain name instead of id in url param

* only prompt network switch if url chain doesnt match
parent bbdb5f3f
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { CHAIN_INFO } from 'constants/chainInfo' import { CHAIN_INFO } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains' import { CHAIN_IDS_TO_NAMES, SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React' import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useCallback, useRef } from 'react' import useParsedQueryString from 'hooks/useParsedQueryString'
import usePrevious from 'hooks/usePrevious'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useRef } from 'react'
import { ArrowDownCircle, ChevronDown } from 'react-feather' import { ArrowDownCircle, ChevronDown } from 'react-feather'
import { useHistory } from 'react-router-dom'
import { useModalOpen, useToggleModal } from 'state/application/hooks' import { useModalOpen, useToggleModal } from 'state/application/hooks'
import { addPopup, ApplicationModal } from 'state/application/reducer' import { addPopup, ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme' import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { replaceURLParam } from 'utils/routes'
import { useAppDispatch } from '../../state/hooks' import { useAppDispatch } from '../../state/hooks'
import { switchToNetwork } from '../../utils/switchToNetwork' import { switchToNetwork } from '../../utils/switchToNetwork'
...@@ -211,31 +216,88 @@ function Row({ ...@@ -211,31 +216,88 @@ function Row({
return rowContent return rowContent
} }
const getParsedChainId = (parsedQs?: ParsedQs) => {
const chain = parsedQs?.chain
if (!chain || typeof chain !== 'string') return { urlChain: undefined, urlChainId: undefined }
return { urlChain: chain.toLowerCase(), urlChainId: getChainIdFromName(chain) }
}
const getChainIdFromName = (name: string) => {
const entry = Object.entries(CHAIN_IDS_TO_NAMES).find(([_, n]) => n === name)
const chainId = entry?.[0]
return chainId ? parseInt(chainId) : undefined
}
const getChainNameFromId = (id: string | number) => {
// casting here may not be right but fine to return undefined if it's not a supported chain ID
return CHAIN_IDS_TO_NAMES[id as SupportedChainId] || ''
}
export default function NetworkSelector() { export default function NetworkSelector() {
const { chainId, library } = useActiveWeb3React() const { chainId, library } = useActiveWeb3React()
const parsedQs = useParsedQueryString()
const { urlChain, urlChainId } = getParsedChainId(parsedQs)
const prevChainId = usePrevious(chainId)
const node = useRef<HTMLDivElement>() const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.NETWORK_SELECTOR) const open = useModalOpen(ApplicationModal.NETWORK_SELECTOR)
const toggle = useToggleModal(ApplicationModal.NETWORK_SELECTOR) const toggle = useToggleModal(ApplicationModal.NETWORK_SELECTOR)
useOnClickOutside(node, open ? toggle : undefined) useOnClickOutside(node, open ? toggle : undefined)
const history = useHistory()
const info = chainId ? CHAIN_INFO[chainId] : undefined const info = chainId ? CHAIN_INFO[chainId] : undefined
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const handleRowClick = useCallback( const handleChainSwitch = useCallback(
(targetChain: number) => { (targetChain: number, skipToggle?: boolean) => {
if (!library) return if (!library) return
switchToNetwork({ library, chainId: targetChain }) switchToNetwork({ library, chainId: targetChain })
.then(() => toggle()) .then(() => {
if (!skipToggle) {
toggle()
}
history.replace({
search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(targetChain)),
})
})
.catch((error) => { .catch((error) => {
console.error('Failed to switch networks', error) console.error('Failed to switch networks', error)
toggle()
// we want app network <-> chainId param to be in sync, so if user changes the network by changing the URL
// but the request fails, revert the URL back to current chainId
if (chainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
}
if (!skipToggle) {
toggle()
}
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` })) dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
}) })
}, },
[dispatch, library, toggle] [dispatch, library, toggle, history, chainId]
) )
useEffect(() => {
// when network change originates from wallet or dropdown selector, just update URL
if (chainId && chainId !== prevChainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
// otherwise assume network change originates from URL
} else if (urlChainId && urlChainId !== chainId) {
handleChainSwitch(urlChainId, true)
}
}, [chainId, urlChainId, prevChainId, handleChainSwitch, history])
// set chain parameter on initial load if not there
useEffect(() => {
if (chainId && !urlChainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
}
}, [chainId, history, urlChainId, urlChain])
if (!chainId || !info || !library) { if (!chainId || !info || !library) {
return null return null
} }
...@@ -252,10 +314,10 @@ export default function NetworkSelector() { ...@@ -252,10 +314,10 @@ export default function NetworkSelector() {
<FlyoutHeader> <FlyoutHeader>
<Trans>Select a network</Trans> <Trans>Select a network</Trans>
</FlyoutHeader> </FlyoutHeader>
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.MAINNET} /> <Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.POLYGON} /> <Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.OPTIMISM} /> <Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.ARBITRUM_ONE} /> <Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
</FlyoutMenu> </FlyoutMenu>
)} )}
</SelectorWrapper> </SelectorWrapper>
......
...@@ -18,6 +18,20 @@ export enum SupportedChainId { ...@@ -18,6 +18,20 @@ export enum SupportedChainId {
POLYGON_MUMBAI = 80001, POLYGON_MUMBAI = 80001,
} }
export const CHAIN_IDS_TO_NAMES = {
[SupportedChainId.MAINNET]: 'mainnet',
[SupportedChainId.ROPSTEN]: 'ropsten',
[SupportedChainId.RINKEBY]: 'rinkeby',
[SupportedChainId.GOERLI]: 'goerli',
[SupportedChainId.KOVAN]: 'kovan',
[SupportedChainId.POLYGON]: 'polygon',
[SupportedChainId.POLYGON_MUMBAI]: 'polygon_mumbai',
[SupportedChainId.ARBITRUM_ONE]: 'arbitrum',
[SupportedChainId.ARBITRUM_RINKEBY]: 'arbitrum_rinkeby',
[SupportedChainId.OPTIMISM]: 'optimism',
[SupportedChainId.OPTIMISTIC_KOVAN]: 'optimistic_kovan',
}
/** /**
* Array of all the supported chain IDs * Array of all the supported chain IDs
*/ */
......
export const replaceURLParam = (search: string, param: string, newValue: string) => {
const searchParams = new URLSearchParams(search)
searchParams.set(param, newValue)
return searchParams.toString()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment