feat: account suspense (#7337)
* feat: eagerly connect outside of react lifecycle * test: reflect selected wallet in localStorage * test: spy only on portfolio balances * feat: connectionReady * feat: connecting state * feat: leave space for address * fix tests * better meta * fix * fix wallet change * add interactivity earlier * add validation * update localstorage key in cypress setup * even less thrash * load per account * simplify, hopefully * explanatory * inf render * whoopsie * ordering
This commit is contained in:
parent
622c72d4a8
commit
ed87df6269
@ -1,3 +1,4 @@
|
||||
import { connectionMetaKey } from '../../src/connection/meta'
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
@ -13,7 +14,12 @@ export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWall
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
|
||||
// Selected wallet should also be reflected in localStorage, so that eager connections work.
|
||||
if (state.selectedWallet) {
|
||||
win.localStorage.setItem('selected_wallet', state.selectedWallet)
|
||||
win.localStorage.setItem(
|
||||
connectionMetaKey,
|
||||
JSON.stringify({
|
||||
type: state.selectedWallet,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
win.indexedDB.deleteDatabase('redux')
|
||||
|
@ -4,17 +4,19 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { sendAnalyticsEvent, TraceEvent } from 'analytics'
|
||||
import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { usePendingActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import Loader, { LoaderV3 } from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import { getConnection } from 'connection'
|
||||
import { useConnectionReady } from 'connection/eagerlyConnect'
|
||||
import { ConnectionMeta, getPersistedConnectionMeta, setPersistedConnectionMeta } from 'connection/meta'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import useLast from 'hooks/useLast'
|
||||
import { navSearchInputVisibleSize } from 'hooks/useScreenSize'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import styled from 'styled-components'
|
||||
import { colors } from 'theme/colors'
|
||||
@ -94,8 +96,15 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{
|
||||
}
|
||||
`
|
||||
|
||||
const AddressAndChevronContainer = styled.div`
|
||||
const Web3StatusConnecting = styled(Web3StatusConnected)`
|
||||
&:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const AddressAndChevronContainer = styled.div<{ loading?: boolean }>`
|
||||
display: flex;
|
||||
opacity: ${({ loading, theme }) => loading && theme.opacity.disabled};
|
||||
|
||||
@media only screen and (max-width: ${navSearchInputVisibleSize}px) {
|
||||
display: none;
|
||||
@ -128,8 +137,11 @@ const StyledConnectButton = styled.button`
|
||||
function Web3StatusInner() {
|
||||
const switchingChain = useAppSelector((state) => state.wallets.switchingChain)
|
||||
const ignoreWhileSwitchingChain = useCallback(() => !switchingChain, [switchingChain])
|
||||
const { account, connector } = useLast(useWeb3React(), ignoreWhileSwitchingChain)
|
||||
const { ENSName } = useENSName(account)
|
||||
const connectionReady = useConnectionReady()
|
||||
const activeWeb3 = useWeb3React()
|
||||
const lastWeb3 = useLast(useWeb3React(), ignoreWhileSwitchingChain)
|
||||
const { account, connector } = useMemo(() => (activeWeb3.account ? activeWeb3 : lastWeb3), [activeWeb3, lastWeb3])
|
||||
const { ENSName, loading: ENSLoading } = useENSName(account)
|
||||
const connection = getConnection(connector)
|
||||
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
@ -141,6 +153,48 @@ function Web3StatusInner() {
|
||||
|
||||
const { hasPendingActivity, pendingActivityCount } = usePendingActivity()
|
||||
|
||||
// Display a loading state while initializing the connection, based on the last session's persisted connection.
|
||||
// The connection will go through three states:
|
||||
// - startup: connection is not ready
|
||||
// - initializing: account is available, but ENS (if preset on the persisted initialMeta) is still loading
|
||||
// - initialized: account and ENS are available
|
||||
// Subsequent connections are always considered initialized, and will not display startup/initializing states.
|
||||
const initialConnection = useRef(getPersistedConnectionMeta())
|
||||
const isConnectionInitializing = Boolean(
|
||||
initialConnection.current?.address === account && initialConnection.current?.ENSName && ENSLoading
|
||||
)
|
||||
const isConnectionInitialized = connectionReady && !isConnectionInitializing
|
||||
// Clear the initial connection once initialized so it does not interfere with subsequent connections.
|
||||
useEffect(() => {
|
||||
if (isConnectionInitialized) {
|
||||
initialConnection.current = undefined
|
||||
}
|
||||
}, [isConnectionInitialized])
|
||||
// Persist the connection if it changes, so it can be used to initialize the next session's connection.
|
||||
useEffect(() => {
|
||||
if (account || ENSName) {
|
||||
const meta: ConnectionMeta = {
|
||||
type: connection.type,
|
||||
address: account,
|
||||
ENSName: ENSName ?? undefined,
|
||||
}
|
||||
setPersistedConnectionMeta(meta)
|
||||
}
|
||||
}, [ENSName, account, connection.type])
|
||||
|
||||
if (!isConnectionInitialized) {
|
||||
return (
|
||||
<Web3StatusConnecting disabled={!isConnectionInitializing} onClick={handleWalletDropdownClick}>
|
||||
<IconWrapper size={24}>
|
||||
<LoaderV3 size="24px" />
|
||||
</IconWrapper>
|
||||
<AddressAndChevronContainer loading={true}>
|
||||
<Text>{initialConnection.current?.ENSName ?? shortenAddress(initialConnection.current?.address)}</Text>
|
||||
</AddressAndChevronContainer>
|
||||
</Web3StatusConnecting>
|
||||
)
|
||||
}
|
||||
|
||||
if (account) {
|
||||
return (
|
||||
<TraceEvent
|
||||
@ -167,7 +221,7 @@ function Web3StatusInner() {
|
||||
</RowBetween>
|
||||
) : (
|
||||
<AddressAndChevronContainer>
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
<Text>{ENSName ?? shortenAddress(account)}</Text>
|
||||
</AddressAndChevronContainer>
|
||||
)}
|
||||
</Web3StatusConnected>
|
||||
|
@ -1,7 +1,25 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
|
||||
import { getConnection, gnosisSafeConnection, networkConnection } from './index'
|
||||
import { ConnectionType, selectedWalletKey, toConnectionType } from './types'
|
||||
import { deletePersistedConnectionMeta, getPersistedConnectionMeta } from './meta'
|
||||
import { ConnectionType } from './types'
|
||||
|
||||
class FailedToConnect extends Error {}
|
||||
|
||||
let connectionReady: Promise<void> | true = true
|
||||
|
||||
export function useConnectionReady() {
|
||||
return useSyncExternalStore(
|
||||
(onStoreChange) => {
|
||||
if (connectionReady instanceof Promise) {
|
||||
connectionReady.finally(onStoreChange)
|
||||
}
|
||||
return () => undefined
|
||||
},
|
||||
() => connectionReady === true
|
||||
)
|
||||
}
|
||||
|
||||
async function connect(connector: Connector, type: ConnectionType) {
|
||||
performance.mark(`web3:connect:${type}:start`)
|
||||
@ -28,15 +46,25 @@ if (window !== window.parent) {
|
||||
|
||||
connect(networkConnection.connector, ConnectionType.NETWORK)
|
||||
|
||||
const selectedWallet = toConnectionType(localStorage.getItem(selectedWalletKey) ?? undefined)
|
||||
if (selectedWallet) {
|
||||
const selectedConnection = getConnection(selectedWallet)
|
||||
// Get the persisted wallet type from the last session.
|
||||
const meta = getPersistedConnectionMeta()
|
||||
if (meta?.type) {
|
||||
const selectedConnection = getConnection(meta.type)
|
||||
if (selectedConnection) {
|
||||
connect(selectedConnection.connector, selectedWallet).then((connected) => {
|
||||
if (!connected) {
|
||||
// only clear the persisted wallet type if it failed to connect.
|
||||
localStorage.removeItem(selectedWalletKey)
|
||||
}
|
||||
})
|
||||
connectionReady = connect(selectedConnection.connector, meta.type)
|
||||
.then((connected) => {
|
||||
if (!connected) throw new FailedToConnect()
|
||||
})
|
||||
.catch((error) => {
|
||||
// Clear the persisted wallet type if it failed to connect.
|
||||
deletePersistedConnectionMeta()
|
||||
// Log it if it threw an unknown error.
|
||||
if (!(error instanceof FailedToConnect)) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
connectionReady = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
37
src/connection/meta.ts
Normal file
37
src/connection/meta.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { ConnectionType, toConnectionType } from './types'
|
||||
|
||||
export interface ConnectionMeta {
|
||||
type: ConnectionType
|
||||
address?: string
|
||||
ENSName?: string
|
||||
}
|
||||
|
||||
export const connectionMetaKey = 'connection_meta'
|
||||
|
||||
export function getPersistedConnectionMeta(): ConnectionMeta | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem(connectionMetaKey)
|
||||
if (value) {
|
||||
const raw = JSON.parse(value) as ConnectionMeta
|
||||
const connectionType = toConnectionType(raw.type)
|
||||
if (connectionType) {
|
||||
return {
|
||||
type: connectionType,
|
||||
address: raw.address,
|
||||
ENSName: raw.ENSName,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
export function setPersistedConnectionMeta(meta: ConnectionMeta) {
|
||||
localStorage.setItem(connectionMetaKey, JSON.stringify(meta))
|
||||
}
|
||||
|
||||
export function deletePersistedConnectionMeta() {
|
||||
localStorage.removeItem(connectionMetaKey)
|
||||
}
|
@ -2,8 +2,6 @@ import { ChainId } from '@uniswap/sdk-core'
|
||||
import { Web3ReactHooks } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
|
||||
export const selectedWalletKey = 'selected_wallet'
|
||||
|
||||
export enum ConnectionType {
|
||||
UNISWAP_WALLET_V2 = 'UNISWAP_WALLET_V2',
|
||||
INJECTED = 'INJECTED',
|
||||
|
@ -34,11 +34,12 @@ export default function useENSName(address?: string): { ENSName: string | null;
|
||||
const checkedName = address === fwdAddr?.address ? name : null
|
||||
|
||||
const changed = debouncedAddress !== address
|
||||
const loading = changed || resolverAddress.loading || nameCallRes.loading || fwdAddr.loading
|
||||
return useMemo(
|
||||
() => ({
|
||||
ENSName: changed ? null : checkedName,
|
||||
loading: changed || resolverAddress.loading || nameCallRes.loading,
|
||||
loading,
|
||||
}),
|
||||
[changed, nameCallRes.loading, checkedName, resolverAddress.loading]
|
||||
[changed, checkedName, loading]
|
||||
)
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
|
||||
import SwapHeader from 'components/swap/SwapHeader'
|
||||
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { useConnectionReady } from 'connection/eagerlyConnect'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { asSupportedChain, isSupportedChain } from 'constants/chains'
|
||||
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens'
|
||||
@ -183,6 +184,7 @@ export function Swap({
|
||||
onCurrencyChange?: (selected: Pick<SwapState, Field.INPUT | Field.OUTPUT>) => void
|
||||
disableTokenInputs?: boolean
|
||||
}) {
|
||||
const connectionReady = useConnectionReady()
|
||||
const { account, chainId: connectedChainId, connector } = useWeb3React()
|
||||
const trace = useTrace()
|
||||
|
||||
@ -723,7 +725,7 @@ export function Swap({
|
||||
<ButtonPrimary $borderRadius="16px" disabled={true}>
|
||||
<Trans>Connecting to {getChainInfo(switchingChain)?.label}</Trans>
|
||||
</ButtonPrimary>
|
||||
) : !account ? (
|
||||
) : connectionReady && !account ? (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
|
@ -74,6 +74,12 @@ jest.mock('@web3-react/core', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('connection/eagerlyConnect', () => {
|
||||
return {
|
||||
useConnectionReady: () => true,
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('state/routing/slice', () => {
|
||||
const routingSlice = jest.requireActual('state/routing/slice')
|
||||
return {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ChainId, Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useConnectionReady } from 'connection/eagerlyConnect'
|
||||
import { useFotAdjustmentsEnabled } from 'featureFlags/flags/fotAdjustments'
|
||||
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
|
||||
import { useDebouncedTrade } from 'hooks/useDebouncedTrade'
|
||||
@ -168,11 +169,12 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
|
||||
// slippage amount used to submit the trade
|
||||
const allowedSlippage = uniswapXAutoSlippage ?? classicAllowedSlippage
|
||||
|
||||
const connectionReady = useConnectionReady()
|
||||
const inputError = useMemo(() => {
|
||||
let inputError: ReactNode | undefined
|
||||
|
||||
if (!account) {
|
||||
inputError = <Trans>Connect Wallet</Trans>
|
||||
inputError = connectionReady ? <Trans>Connect Wallet</Trans> : <Trans>Connecting Wallet...</Trans>
|
||||
}
|
||||
|
||||
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
|
||||
@ -200,7 +202,7 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
|
||||
}
|
||||
|
||||
return inputError
|
||||
}, [account, currencies, parsedAmount, to, currencyBalances, trade.trade, allowedSlippage])
|
||||
}, [account, currencies, parsedAmount, to, currencyBalances, trade?.trade, allowedSlippage, connectionReady])
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { deletePersistedConnectionMeta, getPersistedConnectionMeta } from 'connection/meta'
|
||||
|
||||
import { ConnectionType, selectedWalletKey, toConnectionType } from '../../connection/types'
|
||||
import { ConnectionType } from '../../connection/types'
|
||||
import { SupportedLocale } from '../../constants/locales'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
|
||||
import { RouterPreference } from '../../state/routing/types'
|
||||
import { SerializedPair, SerializedToken, SlippageTolerance } from './types'
|
||||
|
||||
const selectedWallet = toConnectionType(localStorage.getItem(selectedWalletKey) ?? undefined)
|
||||
const selectedWallet = getPersistedConnectionMeta()?.type
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
export interface UserState {
|
||||
@ -79,7 +80,9 @@ const userSlice = createSlice({
|
||||
initialState,
|
||||
reducers: {
|
||||
updateSelectedWallet(state, { payload: { wallet } }) {
|
||||
localStorage.setItem(selectedWalletKey, wallet)
|
||||
if (!wallet) {
|
||||
deletePersistedConnectionMeta()
|
||||
}
|
||||
state.selectedWallet = wallet
|
||||
},
|
||||
updateUserLocale(state, action) {
|
||||
|
Loading…
Reference in New Issue
Block a user