Skip to content

Commit

Permalink
feat: add an onSwitchChain handler (#274)
Browse files Browse the repository at this point in the history
* feat: add initial analytics handlers (#266)

* feat: handlers

* feat: onPermitSign

* feat: onSwapPriceUpdateAck

* feat: onExpandSwapDetails

* fix: trades in ack

* feat: more events

* test: update onAmountChange spy

* fix: one destruct

* fix: update fixture handlers

* chore: clean up types

* feat: onSwitchChain

* chore: clarify todo
  • Loading branch information
zzmp authored Oct 12, 2022
1 parent a78654d commit 14c17cc
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 79 deletions.
4 changes: 2 additions & 2 deletions src/components/Error/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { Component, ErrorInfo, PropsWithChildren } from 'react'
import Dialog from '../Dialog'
import ErrorDialog from './ErrorDialog'

export type ErrorHandler = (error: Error, info: ErrorInfo) => void
export type OnError = (error: Error, info: ErrorInfo) => void

interface ErrorBoundaryProps {
onError?: ErrorHandler
onError?: OnError
}

type ErrorBoundaryState = {
Expand Down
15 changes: 3 additions & 12 deletions src/components/Swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import useSyncSwapEventHandlers, { SwapEventHandlers } from 'hooks/swap/useSyncS
import useSyncTokenDefaults, { TokenDefaults } from 'hooks/swap/useSyncTokenDefaults'
import { usePendingTransactions } from 'hooks/transactions'
import useSyncBrandingSetting, { BrandingSettings, useBrandingSetting } from 'hooks/useSyncBrandingSetting'
import useSyncWidgetEventHandlers, { WidgetEventHandlers } from 'hooks/useSyncWidgetEventHandlers'
import { useAtom } from 'jotai'
import { useMemo, useState } from 'react'
import { displayTxHashAtom } from 'state/swap'
Expand All @@ -25,17 +24,10 @@ import SwapActionButton from './SwapActionButton'
import Toolbar from './Toolbar'
import useValidate from './useValidate'

// SwapProps also currently includes props needed for wallet connection,
// SwapProps also currently includes props needed for wallet connection (eg hideConnectionUI),
// since the wallet connection component exists within the Swap component.
// This includes useSyncWidgetEventHandlers.
// TODO(zzmp): refactor WalletConnection outside of Swap component
export interface SwapProps
extends BrandingSettings,
FeeOptions,
SwapController,
SwapEventHandlers,
TokenDefaults,
WidgetEventHandlers {
// TODO(zzmp): refactor WalletConnection into Widget component
export interface SwapProps extends BrandingSettings, FeeOptions, SwapController, SwapEventHandlers, TokenDefaults {
hideConnectionUI?: boolean
routerUrl?: string
}
Expand All @@ -46,7 +38,6 @@ export default function Swap(props: SwapProps) {
useSyncConvenienceFee(props as FeeOptions)
useSyncSwapEventHandlers(props as SwapEventHandlers)
useSyncTokenDefaults(props as TokenDefaults)
useSyncWidgetEventHandlers(props as WidgetEventHandlers)
useSyncBrandingSetting(props as BrandingSettings)

const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
Expand Down
40 changes: 9 additions & 31 deletions src/components/Widget.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { JsonRpcProvider } from '@ethersproject/providers'
import { TokenInfo } from '@uniswap/token-lists'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { Animation, Modal, Provider as DialogProvider } from 'components/Dialog'
import ErrorBoundary, { OnError } from 'components/Error/ErrorBoundary'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { TransactionEventHandlers, TransactionsUpdater } from 'hooks/transactions'
import { BlockNumberProvider } from 'hooks/useBlockNumber'
import useSyncWidgetEventHandlers, { WidgetEventHandlers } from 'hooks/useSyncWidgetEventHandlers'
import { TokenListProvider } from 'hooks/useTokenList'
import { Provider as Web3ReactProvider } from 'hooks/web3'
import { JsonRpcConnectionMap } from 'hooks/web3/useJsonRpcUrlsMap'
import { Provider as Web3Provider, ProviderProps as Web3Props } from 'hooks/web3'
import { Provider as I18nProvider } from 'i18n'
import { Atom, Provider as AtomProvider } from 'jotai'
import { PropsWithChildren, StrictMode, useMemo, useState } from 'react'
Expand All @@ -17,11 +16,6 @@ import { MulticallUpdater } from 'state/multicall'
import styled, { keyframes } from 'styled-components/macro'
import { Theme, ThemeProvider } from 'theme'

import { Animation, Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'

const DEFAULT_CHAIN_ID = SupportedChainId.MAINNET

export const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
Expand Down Expand Up @@ -94,17 +88,14 @@ export const DialogWrapper = styled.div`
}
`

export interface WidgetProps extends TransactionEventHandlers {
export interface WidgetProps extends TransactionEventHandlers, Web3Props, WidgetEventHandlers {
theme?: Theme
locale?: SupportedLocale
provider?: Eip1193Provider | JsonRpcProvider | null
jsonRpcUrlMap?: JsonRpcConnectionMap
defaultChainId?: SupportedChainId
tokenList?: string | TokenInfo[]
width?: string | number
dialog?: HTMLDivElement | null
className?: string
onError?: ErrorHandler
onError?: OnError
}

export default function Widget(props: PropsWithChildren<WidgetProps>) {
Expand Down Expand Up @@ -134,16 +125,6 @@ export function TestableWidget(props: PropsWithChildren<TestableWidgetProps>) {
}
return props.locale ?? DEFAULT_LOCALE
}, [props.locale])
const defaultChainId = useMemo(() => {
if (!props.defaultChainId) return DEFAULT_CHAIN_ID
if (!ALL_SUPPORTED_CHAIN_IDS.includes(props.defaultChainId)) {
console.warn(
`Unsupported chainId: ${props.defaultChainId}. Falling back to ${DEFAULT_CHAIN_ID} (${SupportedChainId[DEFAULT_CHAIN_ID]}).`
)
return DEFAULT_CHAIN_ID
}
return props.defaultChainId
}, [props.defaultChainId])
const [dialog, setDialog] = useState<HTMLDivElement | null>(props.dialog || null)
return (
<StrictMode>
Expand All @@ -156,17 +137,13 @@ export function TestableWidget(props: PropsWithChildren<TestableWidgetProps>) {
<ReduxProvider store={store}>
<AtomProvider initialValues={props.initialAtomValues}>
<WidgetUpdater {...props} />
<Web3ReactProvider
provider={props.provider}
jsonRpcMap={props.jsonRpcUrlMap}
defaultChainId={defaultChainId}
>
<Web3Provider {...(props as Web3Props)}>
<BlockNumberProvider>
<MulticallUpdater />
<TransactionsUpdater {...(props as TransactionEventHandlers)} />
<TokenListProvider list={props.tokenList}>{props.children}</TokenListProvider>
</BlockNumberProvider>
</Web3ReactProvider>
</Web3Provider>
</AtomProvider>
</ReduxProvider>
</ErrorBoundary>
Expand All @@ -180,5 +157,6 @@ export function TestableWidget(props: PropsWithChildren<TestableWidgetProps>) {

/** A component in the scope of AtomProvider to set Widget-scoped state. */
function WidgetUpdater(props: WidgetProps) {
useSyncWidgetEventHandlers(props as WidgetEventHandlers)
return null
}
7 changes: 5 additions & 2 deletions src/cosmos/EventFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defaultTheme, SwapEventHandlers, TransactionEventHandlers } from '@uniswap/widgets'
import { defaultTheme, SwapEventHandlers, TransactionEventHandlers, WidgetEventHandlers } from '@uniswap/widgets'
import Row from 'components/Row'
import styled from 'styled-components/macro'
import * as Type from 'theme/type'
Expand All @@ -25,8 +25,10 @@ const EventData = styled.pre`
margin: 0.5em 0 0;
`

export const HANDLERS: (keyof SwapEventHandlers | keyof TransactionEventHandlers)[] = [
export const HANDLERS: (keyof SwapEventHandlers | keyof TransactionEventHandlers | keyof WidgetEventHandlers)[] = [
'onAmountChange',
'onConnectWalletClick',
'onError',
'onExpandSwapDetails',
'onInitialSwapQuote',
'onSwapApprove',
Expand All @@ -35,6 +37,7 @@ export const HANDLERS: (keyof SwapEventHandlers | keyof TransactionEventHandlers
'onSlippageChange',
'onSubmitSwapClick',
'onSwapPriceUpdateAck',
'onSwitchChain',
'onSwitchTokens',
'onTokenChange',
'onTokenSelectorClick',
Expand Down
53 changes: 38 additions & 15 deletions src/hooks/useSwitchChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { ErrorCode } from 'constants/eip1193'
import useJsonRpcUrlsMap from 'hooks/web3/useJsonRpcUrlsMap'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { useCallback } from 'react'

/** Defined by EIP-3085. */
export interface AddEthereumChainParameter {
chainId: string
chainName: string
nativeCurrency: { name: string; symbol: string; decimals: 18 }
blockExplorerUrls: [string]
rpcUrls: string[]
}

export type OnSwitchChain = (addChainParameter: AddEthereumChainParameter) => void | Promise<void>
export const onSwitchChainAtom = atom<OnSwitchChain | undefined>(undefined)

function toHex(chainId: SupportedChainId): string {
return `0x${chainId.toString(16)}`
}

async function addChain(provider: Web3Provider, chainId: SupportedChainId, rpcUrls: string[]): Promise<void> {
const { label: chainName, nativeCurrency, explorer } = getChainInfo(chainId)
const addChainParameter = {
chainId: toHex(chainId),
chainName,
nativeCurrency,
blockExplorerUrls: [explorer],
}

for (const rpcUrl of rpcUrls) {
async function addChain(provider: Web3Provider, addChainParameter: AddEthereumChainParameter): Promise<void> {
for (const rpcUrl of addChainParameter.rpcUrls) {
try {
await provider.send('wallet_addEthereumChain', [{ ...addChainParameter, rpcUrls: [rpcUrl] }]) // EIP-3085
} catch (error) {
Expand All @@ -32,12 +38,16 @@ async function addChain(provider: Web3Provider, chainId: SupportedChainId, rpcUr
}
}

async function switchChain(provider: Web3Provider, chainId: SupportedChainId, rpcUrls: string[] = []): Promise<void> {
async function switchChain(
provider: Web3Provider,
chainId: SupportedChainId,
addChainParameter?: AddEthereumChainParameter
): Promise<void> {
try {
await provider.send('wallet_switchEthereumChain', [{ chainId: toHex(chainId) }]) // EIP-3326 (used by MetaMask)
} catch (error) {
if (error?.code === ErrorCode.CHAIN_NOT_ADDED && rpcUrls.length) {
await addChain(provider, chainId, rpcUrls)
if (error?.code === ErrorCode.CHAIN_NOT_ADDED && addChainParameter?.rpcUrls.length) {
await addChain(provider, addChainParameter)
return switchChain(provider, chainId)
}
throw error
Expand All @@ -47,9 +57,22 @@ async function switchChain(provider: Web3Provider, chainId: SupportedChainId, rp
export default function useSwitchChain(): (chainId: SupportedChainId) => Promise<void> {
const { connector, provider } = useWeb3React()
const urlMap = useJsonRpcUrlsMap()
const onSwitchChain = useAtomValue(onSwitchChainAtom)
return useCallback(
async (chainId: SupportedChainId) => {
const { label: chainName, nativeCurrency, explorer } = getChainInfo(chainId)
const addChainParameter: AddEthereumChainParameter = {
chainId: toHex(chainId),
chainName,
nativeCurrency,
blockExplorerUrls: [explorer],
rpcUrls: urlMap[chainId],
}
try {
// If the integrator implements onSwitchChain, use that instead.
const switching = onSwitchChain?.(addChainParameter)
if (switching) return switching

try {
// A custom Connector may use a customProvider, in which case it should handle its own chain switching.
if (!provider) throw new Error()
Expand All @@ -58,7 +81,7 @@ export default function useSwitchChain(): (chainId: SupportedChainId) => Promise
// Await both the user action (switchChain) and its result (chainChanged)
// so that the callback does not resolve before the chain switch has visibly occured.
new Promise((resolve) => provider.once('chainChanged', resolve)),
switchChain(provider, chainId, urlMap[chainId]),
switchChain(provider, chainId, addChainParameter),
])
} catch (error) {
if (error?.code === ErrorCode.USER_REJECTED_REQUEST) return
Expand All @@ -68,6 +91,6 @@ export default function useSwitchChain(): (chainId: SupportedChainId) => Promise
throw new Error(`Failed to switch network: ${error}`)
}
},
[connector, provider, urlMap]
[connector, onSwitchChain, provider, urlMap]
)
}
13 changes: 12 additions & 1 deletion src/hooks/useSyncWidgetEventHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { OnError } from 'components/Error/ErrorBoundary'
import { OnSwitchChain, onSwitchChainAtom } from 'hooks/useSwitchChain'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { OnConnectWalletClick, onConnectWalletClickAtom } from 'state/wallet'
export type { OnError } from 'components/Error/ErrorBoundary'
export type { OnSwitchChain } from 'hooks/useSwitchChain'
export type { OnConnectWalletClick } from 'state/wallet'

export interface WidgetEventHandlers {
onConnectWalletClick?: OnConnectWalletClick
onError?: OnError
onSwitchChain?: OnSwitchChain
}

export default function useSyncWidgetEventHandlers({ onConnectWalletClick }: WidgetEventHandlers): void {
export default function useSyncWidgetEventHandlers({ onConnectWalletClick, onSwitchChain }: WidgetEventHandlers): void {
const setOnConnectWalletClick = useUpdateAtom(onConnectWalletClickAtom)
useEffect(() => {
setOnConnectWalletClick(() => onConnectWalletClick)
}, [onConnectWalletClick, setOnConnectWalletClick])

const setOnSwitchChain = useUpdateAtom(onSwitchChainAtom)
useEffect(() => {
setOnSwitchChain(() => onSwitchChain)
}, [onSwitchChain, setOnSwitchChain])
}
33 changes: 23 additions & 10 deletions src/hooks/web3/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Connector, Provider as Eip1193Provider } from '@web3-react/types'
import { SupportedChainId } from 'constants/chains'
import { PropsWithChildren, useEffect, useMemo, useRef } from 'react'
import JsonRpcConnector from 'utils/JsonRpcConnector'
import { supportedChainId } from 'utils/supportedChainId'
import { WalletConnectPopup, WalletConnectQR } from 'utils/WalletConnect'

import { Provider as ConnectorsProvider } from './useConnectors'
Expand All @@ -17,6 +18,8 @@ import {
toJsonRpcUrlMap,
} from './useJsonRpcUrlsMap'

const DEFAULT_CHAIN_ID = SupportedChainId.MAINNET

type Web3ReactConnector<T extends Connector = Connector> = [T, Web3ReactHooks]

interface Web3ReactConnectors {
Expand All @@ -27,23 +30,33 @@ interface Web3ReactConnectors {
network: Web3ReactConnector<Network>
}

interface ProviderProps {
export interface ProviderProps {
defaultChainId?: SupportedChainId
jsonRpcUrlMap?: JsonRpcConnectionMap
/**
* If null, no auto-connection (MetaMask or WalletConnect) will be attempted.
* This is appropriate for integrations which wish to control the connected provider.
*/
provider?: Eip1193Provider | JsonRpcProvider | null
jsonRpcMap?: JsonRpcConnectionMap
defaultChainId?: SupportedChainId
}

export function Provider({
defaultChainId = SupportedChainId.MAINNET,
jsonRpcMap,
defaultChainId: chainId = SupportedChainId.MAINNET,
jsonRpcUrlMap,
provider,
children,
}: PropsWithChildren<ProviderProps>) {
const web3ReactConnectors = useWeb3ReactConnectors({ provider, jsonRpcMap, defaultChainId })
const defaultChainId = useMemo(() => {
if (!supportedChainId(chainId)) {
console.warn(
`Unsupported chainId: ${chainId}. Falling back to ${DEFAULT_CHAIN_ID} (${SupportedChainId[DEFAULT_CHAIN_ID]}).`
)
return DEFAULT_CHAIN_ID
}
return chainId
}, [chainId])

const web3ReactConnectors = useWeb3ReactConnectors({ provider, jsonRpcUrlMap, defaultChainId })

const key = useRef(0)
const prioritizedConnectors = useMemo(() => {
Expand Down Expand Up @@ -87,7 +100,7 @@ export function Provider({

return (
<Web3ReactProvider connectors={prioritizedConnectors} key={key.current}>
<JsonRpcUrlMapProvider jsonRpcMap={jsonRpcMap}>
<JsonRpcUrlMapProvider jsonRpcMap={jsonRpcUrlMap}>
<ConnectorsProvider connectors={connectors}>{children}</ConnectorsProvider>
</JsonRpcUrlMapProvider>
</Web3ReactProvider>
Expand All @@ -104,10 +117,10 @@ function initializeWeb3ReactConnector<T extends Connector, P extends object>(
return [connector, hooks]
}

function useWeb3ReactConnectors({ defaultChainId, provider, jsonRpcMap }: ProviderProps) {
function useWeb3ReactConnectors({ defaultChainId, provider, jsonRpcUrlMap }: ProviderProps) {
const [urlMap, connectionMap] = useMemo(
() => [toJsonRpcUrlMap(jsonRpcMap), toJsonRpcConnectionMap(jsonRpcMap)],
[jsonRpcMap]
() => [toJsonRpcUrlMap(jsonRpcUrlMap), toJsonRpcConnectionMap(jsonRpcUrlMap)],
[jsonRpcUrlMap]
)

const user = useMemo(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export type { Currency } from '@uniswap/sdk-core'
export { TradeType } from '@uniswap/sdk-core'
export type { TokenInfo } from '@uniswap/token-lists'
export type { Provider as Eip1193Provider } from '@web3-react/types'
export type { ErrorHandler } from 'components/Error/ErrorBoundary'
export type { SwapWidgetSkeletonProps } from 'components/Swap/Skeleton'
export { SwapWidgetSkeleton } from 'components/Swap/Skeleton'
export { SupportedChainId } from 'constants/chains'
Expand All @@ -19,7 +18,12 @@ export type { SwapController } from 'hooks/swap/useSyncController'
export type { FeeOptions } from 'hooks/swap/useSyncConvenienceFee'
export type { DefaultAddress, TokenDefaults } from 'hooks/swap/useSyncTokenDefaults'
export type { OnTxFail, OnTxSubmit, OnTxSuccess, TransactionEventHandlers } from 'hooks/transactions'
export type { OnConnectWalletClick, WidgetEventHandlers } from 'hooks/useSyncWidgetEventHandlers'
export type {
OnConnectWalletClick,
OnError,
OnSwitchChain,
WidgetEventHandlers,
} from 'hooks/useSyncWidgetEventHandlers'
export { EMPTY_TOKEN_LIST, UNISWAP_TOKEN_LIST } from 'hooks/useTokenList'
export type { JsonRpcConnectionMap } from 'hooks/web3/useJsonRpcUrlsMap'
export type {
Expand Down
Loading

1 comment on commit 14c17cc

@vercel
Copy link

@vercel vercel bot commented on 14c17cc Oct 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

widgets – ./

widgets-uniswap.vercel.app
widgets-seven-tau.vercel.app
widgets-git-main-uniswap.vercel.app

Please sign in to comment.