fix: eliminate flake from account connect race condition (#7212)

* fix: reduce flake from race condition

* use interface instead of type for props

* closeMenu instead of closeModal with type

* simplify useCloseModal

* fix bug

* pr feedback

* remove unnecessary code

* fix button wrapper style

* remove ApplicationModal.WALLET

* update color for spore
This commit is contained in:
Jordan Frankfurt 2023-09-06 17:52:47 -05:00 committed by GitHub
parent 147a9bcbb2
commit 7306423192
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 57 deletions

@ -15,14 +15,22 @@ describe('Swap settings', () => {
}) })
it('should open the mobile settings menu', () => { it('should open the mobile settings menu', () => {
// Set viewport to iPhone 6
cy.viewport('iphone-6') cy.viewport('iphone-6')
cy.visit('/swap') cy.visit('/swap')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('mobile-settings-menu')).should('exist') // Click the button to open the settings dialog
cy.contains('Max slippage').should('exist') cy.get(getTestSelector('open-settings-dialog-button')).click({ waitForAnimations: true })
cy.contains('Transaction deadline').should('exist')
cy.contains('UniswapX').should('exist') // Verify the mobile settings menu and its contents
cy.contains('Local routing').should('exist') cy.get(getTestSelector('mobile-settings-menu'))
cy.get(getTestSelector('mobile-settings-scrim')).click({ force: true }) .should('exist')
.within(() => {
cy.contains('Max slippage').should('exist')
cy.contains('UniswapX').should('exist')
cy.contains('Local routing').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.get(getTestSelector('mobile-settings-close')).click()
})
}) })
}) })

@ -40,7 +40,7 @@ export function useAccountDrawer(): [boolean, () => void] {
return [accountDrawerOpen, useToggleAccountDrawer()] return [accountDrawerOpen, useToggleAccountDrawer()]
} }
const ScrimBackground = styled.div<{ open: boolean }>` const ScrimBackground = styled.div<{ $open: boolean }>`
z-index: ${Z_INDEX.modalBackdrop}; z-index: ${Z_INDEX.modalBackdrop};
overflow: hidden; overflow: hidden;
top: 0; top: 0;
@ -53,22 +53,27 @@ const ScrimBackground = styled.div<{ open: boolean }>`
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
opacity: ${({ open }) => (open ? 1 : 0)}; opacity: ${({ $open }) => ($open ? 1 : 0)};
pointer-events: ${({ open }) => (open ? 'auto' : 'none')}; pointer-events: ${({ $open }) => ($open ? 'auto' : 'none')};
transition: opacity ${({ theme }) => theme.transition.duration.medium} ease-in-out; transition: opacity ${({ theme }) => theme.transition.duration.medium} ease-in-out;
} }
` `
export const Scrim = ({ onClick, open, testId }: { onClick: () => void; open: boolean; testId?: string }) => {
interface ScrimBackgroundProps extends React.ComponentPropsWithRef<'div'> {
$open: boolean
}
export const Scrim = (props: ScrimBackgroundProps) => {
const { width } = useWindowSize() const { width } = useWindowSize()
useEffect(() => { useEffect(() => {
if (width && width < BREAKPOINTS.sm && open) document.body.style.overflow = 'hidden' if (width && width < BREAKPOINTS.sm && props.$open) document.body.style.overflow = 'hidden'
return () => { return () => {
document.body.style.overflow = 'visible' document.body.style.overflow = 'visible'
} }
}, [open, width]) }, [props.$open, width])
return <ScrimBackground data-testid={testId} onClick={onClick} open={open} /> return <ScrimBackground {...props} />
} }
const AccountDrawerScrollWrapper = styled.div` const AccountDrawerScrollWrapper = styled.div`
@ -245,7 +250,7 @@ function AccountDrawer() {
</CloseDrawer> </CloseDrawer>
</TraceEvent> </TraceEvent>
)} )}
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} /> <Scrim onClick={toggleWalletDrawer} $open={walletDrawerOpen} />
<AccountDrawerWrapper <AccountDrawerWrapper
open={walletDrawerOpen} open={walletDrawerOpen}
{...(isMobile {...(isMobile

@ -125,7 +125,7 @@ export default function FiatOnrampModal() {
}, [fetchSignedIframeUrl]) }, [fetchSignedIframeUrl])
return ( return (
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} height={80 /* vh */}> <Modal isOpen={fiatOnrampModalOpen} onDismiss={() => closeModal()} height={80 /* vh */}>
<Wrapper data-testid="fiat-onramp-modal" isDarkMode={isDarkMode}> <Wrapper data-testid="fiat-onramp-modal" isDarkMode={isDarkMode}>
{error ? ( {error ? (
<> <>

@ -10,13 +10,14 @@ import useDisableScrolling from 'hooks/useDisableScrolling'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Portal } from 'nft/components/common/Portal' import { Portal } from 'nft/components/common/Portal'
import { useIsMobile } from 'nft/hooks' import { useIsMobile } from 'nft/hooks'
import { useMemo, useRef } from 'react' import { useCallback, useMemo, useRef } from 'react'
import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks' import { X } from 'react-feather'
import { useCloseModal, useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils' import { isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components' import styled from 'styled-components'
import { CloseIcon, Divider, ThemedText } from 'theme' import { Divider, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import MaxSlippageSettings from './MaxSlippageSettings' import MaxSlippageSettings from './MaxSlippageSettings'
@ -24,6 +25,16 @@ import MenuButton from './MenuButton'
import RouterPreferenceSettings from './RouterPreferenceSettings' import RouterPreferenceSettings from './RouterPreferenceSettings'
import TransactionDeadlineSettings from './TransactionDeadlineSettings' import TransactionDeadlineSettings from './TransactionDeadlineSettings'
const CloseButton = styled.button`
background: transparent;
border: none;
color: ${({ theme }) => theme.neutral1};
cursor: pointer;
height: 24px;
padding: 0;
width: 24px;
`
const Menu = styled.div` const Menu = styled.div`
position: relative; position: relative;
` `
@ -64,14 +75,14 @@ const MobileMenuContainer = styled(Row)`
z-index: ${Z_INDEX.fixed}; z-index: ${Z_INDEX.fixed};
` `
const MobileMenuWrapper = styled(Column)<{ open: boolean }>` const MobileMenuWrapper = styled(Column)<{ $open: boolean }>`
height: min-content; height: min-content;
width: 100%; width: 100%;
padding: 8px 16px 24px; padding: 8px 16px 24px;
background-color: ${({ theme }) => theme.surface1}; background-color: ${({ theme }) => theme.surface1};
overflow: hidden; overflow: hidden;
position: absolute; position: absolute;
bottom: ${({ open }) => (open ? `100vh` : 0)}; bottom: ${({ $open }) => ($open ? `100vh` : 0)};
transition: bottom ${({ theme }) => theme.transition.duration.medium}; transition: bottom ${({ theme }) => theme.transition.duration.medium};
border: ${({ theme }) => `1px solid ${theme.surface3}`}; border: ${({ theme }) => `1px solid ${theme.surface3}`};
border-radius: 12px; border-radius: 12px;
@ -97,17 +108,18 @@ export default function SettingsTab({
}) { }) {
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId)) const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
const node = useRef<HTMLDivElement | null>(null) const node = useRef<HTMLDivElement | null>(null)
const isOpen = useModalIsOpen(ApplicationModal.SETTINGS) const isOpen = useModalIsOpen(ApplicationModal.SETTINGS)
const closeModal = useCloseModal()
const closeMenu = useCallback(() => closeModal(ApplicationModal.SETTINGS), [closeModal])
const toggleMenu = useToggleSettingsMenu() const toggleMenu = useToggleSettingsMenu()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const isOpenMobile = isOpen && isMobile const isOpenMobile = isOpen && isMobile
const isOpenDesktop = isOpen && !isMobile const isOpenDesktop = isOpen && !isMobile
useOnClickOutside(node, isOpenDesktop ? toggleMenu : undefined) useOnClickOutside(node, isOpenDesktop ? closeMenu : undefined)
useDisableScrolling(isOpen) useDisableScrolling(isOpen)
const isChainSupported = isSupportedChain(chainId) const isChainSupported = isSupportedChain(chainId)
@ -136,22 +148,18 @@ export default function SettingsTab({
) )
return ( return (
<> <Menu ref={node}>
<Menu ref={node}> <MenuButton disabled={!isChainSupported || chainId !== connectedChainId} isActive={isOpen} onClick={toggleMenu} />
<MenuButton {isOpenDesktop && <MenuFlyout>{Settings}</MenuFlyout>}
disabled={!isChainSupported || chainId !== connectedChainId} {isOpenMobile && (
isActive={isOpen}
onClick={toggleMenu}
/>
{isOpenDesktop && !isMobile && <MenuFlyout>{Settings}</MenuFlyout>}
</Menu>
{isMobile && (
<Portal> <Portal>
<MobileMenuContainer data-testid="mobile-settings-menu"> <MobileMenuContainer data-testid="mobile-settings-menu">
<Scrim testId="mobile-settings-scrim" onClick={toggleMenu} open={isOpenMobile} /> <Scrim onClick={closeMenu} $open />
<MobileMenuWrapper open={isOpenMobile}> <MobileMenuWrapper $open>
<MobileMenuHeader padding="8px 0px 4px"> <MobileMenuHeader padding="8px 0px 4px">
<CloseIcon size={24} onClick={toggleMenu} /> <CloseButton data-testid="mobile-settings-close" onClick={closeMenu}>
<X size={24} />
</CloseButton>
<Row padding="0px 24px 0px 0px" justify="center"> <Row padding="0px 24px 0px 0px" justify="center">
<ThemedText.SubHeader> <ThemedText.SubHeader>
<Trans>Settings</Trans> <Trans>Settings</Trans>
@ -163,6 +171,6 @@ export default function SettingsTab({
</MobileMenuContainer> </MobileMenuContainer>
</Portal> </Portal>
)} )}
</> </Menu>
) )
} }

@ -88,9 +88,21 @@ export function useToggleModal(modal: ApplicationModal): () => void {
return useCallback(() => dispatch(setOpenModal(isOpen ? null : modal)), [dispatch, modal, isOpen]) return useCallback(() => dispatch(setOpenModal(isOpen ? null : modal)), [dispatch, modal, isOpen])
} }
export function useCloseModal(): () => void { export function useCloseModal() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return useCallback(() => dispatch(setOpenModal(null)), [dispatch]) const currentlyOpenModal = useAppSelector((state: AppState) => state.application.openModal)
return useCallback(
(modalToClose?: ApplicationModal) => {
if (!modalToClose) {
// Close any open modal if no modal is specified.
dispatch(setOpenModal(null))
} else if (currentlyOpenModal === modalToClose) {
// Close the currently open modal if it is the one specified.
dispatch(setOpenModal(null))
}
},
[currentlyOpenModal, dispatch]
)
} }
export function useOpenModal(modal: ApplicationModal): () => void { export function useOpenModal(modal: ApplicationModal): () => void {

@ -72,11 +72,7 @@ describe('application reducer', () => {
}) })
describe('setOpenModal', () => { describe('setOpenModal', () => {
it('set wallet modal', () => { it('should correctly set the open modal', () => {
store.dispatch(setOpenModal(ApplicationModal.WALLET))
expect(store.getState().openModal).toEqual(ApplicationModal.WALLET)
store.dispatch(setOpenModal(ApplicationModal.WALLET))
expect(store.getState().openModal).toEqual(ApplicationModal.WALLET)
store.dispatch(setOpenModal(ApplicationModal.CLAIM_POPUP)) store.dispatch(setOpenModal(ApplicationModal.CLAIM_POPUP))
expect(store.getState().openModal).toEqual(ApplicationModal.CLAIM_POPUP) expect(store.getState().openModal).toEqual(ApplicationModal.CLAIM_POPUP)
store.dispatch(setOpenModal(null)) store.dispatch(setOpenModal(null))

@ -43,7 +43,6 @@ export enum ApplicationModal {
TAX_SERVICE, TAX_SERVICE,
TIME_SELECTOR, TIME_SELECTOR,
VOTE, VOTE,
WALLET,
UNISWAP_NFT_AIRDROP_CLAIM, UNISWAP_NFT_AIRDROP_CLAIM,
} }

@ -2,28 +2,18 @@ import { useWeb3React } from '@web3-react/core'
import { asSupportedChain } from 'constants/chains' import { asSupportedChain } from 'constants/chains'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import useIsWindowVisible from 'hooks/useIsWindowVisible' import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useEffect, useRef, useState } from 'react' import { useEffect, useState } from 'react'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { useCloseModal } from './hooks'
import { updateChainId } from './reducer' import { updateChainId } from './reducer'
export default function Updater(): null { export default function Updater(): null {
const { account, chainId, provider } = useWeb3React() const { chainId, provider } = useWeb3React()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const windowVisible = useIsWindowVisible() const windowVisible = useIsWindowVisible()
const [activeChainId, setActiveChainId] = useState(chainId) const [activeChainId, setActiveChainId] = useState(chainId)
const closeModal = useCloseModal()
const previousAccountValue = useRef(account)
useEffect(() => {
if (account && account !== previousAccountValue.current) {
previousAccountValue.current = account
closeModal()
}
}, [account, closeModal])
useEffect(() => { useEffect(() => {
if (provider && chainId && windowVisible) { if (provider && chainId && windowVisible) {
setActiveChainId(chainId) setActiveChainId(chainId)