Compare commits

...

12 Commits

Author SHA1 Message Date
Jordan Frankfurt
7b9a23d920 feat: reduce severity of phishing filter to allow url token names (#6282)
* feat: reduce severity of phishing filter to allow url token names

* tests

* remove unused var from test

* test rendering mini portfolio pools list

* update owner

* update variable names to match cmcewen's suggestions

* checkStringForURL -> hasURL
2023-03-31 12:59:02 -05:00
Connor McEwen
120ad935fa revert: "fix: mini portfolio layout fixes" (#6279)
Revert "fix: mini portfolio layout fixes (#6260)"

This reverts commit fb05439d32.
2023-03-30 18:47:42 -04:00
cartcrom
4eaf16b624 fix: injection detection bug (#6276)
* fix: use functions to check injection status rather than static vars

* fix: unnused field

* fix: don't prompt mm install for generics

* fix: generic injector function

* fix: display name for MM on cb browser

* fix: re-add ios mobile check for uniswap wallet

* fix: reword comment

* fix: refactor delayed-injection test

* feat: added comments

* fix: revert to minimal changes

* fix: update tests
2023-03-30 17:50:20 -04:00
Connor McEwen
857e2915ab fix: put environment in the wrong place (#6277)
* fix: put environment in the wrong place

* move to proper step
2023-03-30 16:54:45 -04:00
Connor McEwen
7410c81b42 chore: update workflow release env (#6275) 2023-03-30 15:43:18 -04:00
eddie
fb05439d32 fix: mini portfolio layout fixes (#6260)
* fix: mini portfolio layout fixes

* feat: refactor MP drawer CSS
2023-03-30 12:07:02 -07:00
eddie
fb7eade70b fix: l2 icon borders in MP (#6254)
* fix: l2 icon borders in MP

* fix: bool logic

* fix: comments and add test

* fix: change variable name

* fix: split l2 icon into two components
2023-03-30 11:59:46 -07:00
eddie
bd2b2c487a fix: close MP drawer on nft nav (#6251)
* fix: close MP drawer on nft nav

* fix: make callbacks optional, rename props

* fix: improve card API

* fix: add e2e test
2023-03-29 15:08:30 -07:00
eddie
2f004ed1d9 fix: remove deprecated default imports from zustand (#6270)
* fix: replace default imports from zustand

* fix: add eslint rule

* fix: typo

o
2023-03-29 13:25:30 -07:00
Zach Pomerantz
db257c73f2 fix: improve chain id error (#6266)
* build: upgrade sentry

* fix: improve chain id error
2023-03-29 12:38:33 -07:00
Zach Pomerantz
7c37b9d00e build: upgrade sentry (#6264)
* build: upgrade sentry

* chore: comment nits
2023-03-29 11:05:13 -07:00
Connor McEwen
7688c527f0 chore: update codecov yaml (#6262)
chore: set patch coverage to 80%, lower tolerance threshold, remove unused keys
2023-03-29 11:31:27 -04:00
46 changed files with 751 additions and 365 deletions

View File

@@ -4,4 +4,23 @@ require('@uniswap/eslint-config/load')
module.exports = {
extends: '@uniswap/eslint-config/react',
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'zustand',
importNames: ['default'],
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
},
],
},
],
},
},
],
}

View File

@@ -42,6 +42,8 @@ jobs:
needs: tag
if: ${{ needs.tag.outputs.new_tag != null }}
runs-on: ubuntu-latest
environment:
name: release
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup

View File

@@ -14,9 +14,8 @@ coverage:
project:
default:
target: auto
threshold: 5%
threshold: 1%
if_ci_failed: error
patch:
default:
enabled: no
if_not_found: success
target: 80%

View File

@@ -53,4 +53,11 @@ describe('Testing nfts', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('nft-view-self-nfts')).click()
})
it('should close the sidebar when navigating to NFT details', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
cy.contains('Buy crypto').should('not.be.visible')
})
})

View File

@@ -132,8 +132,8 @@
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "^7.40.0",
"@sentry/tracing": "^7.40.0",
"@sentry/react": "^7.45.0",
"@sentry/tracing": "^7.45.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.8.0",

View File

@@ -60,7 +60,7 @@ const Socks = () => {
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
return (
<MiniIconContainer side={side}>
<MiniImg src={connection.icon} alt={`${connection.name} icon`} />
<MiniImg src={connection.getIcon?.()} alt={`${connection.getName()} icon`} />
</MiniIconContainer>
)
}
@@ -71,7 +71,7 @@ const MainWalletIcon = ({ connection, size }: { connection: Connection; size: nu
if (!account) {
return null
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.name === 'MetaMask')) {
} else if (avatar || (connection.type === ConnectionType.INJECTED && connection.getName() === 'MetaMask')) {
return <Identicon size={size} />
} else {
return <Unicon address={account} size={size} />

View File

@@ -1,38 +1,91 @@
import { BigNumber } from '@ethersproject/bignumber'
import { render, screen } from 'test-utils'
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { useToken } from 'hooks/Tokens'
import { usePool } from 'hooks/usePools'
import { PoolState } from 'hooks/usePools'
import { render } from 'test-utils'
import { unwrappedToken } from 'utils/unwrappedToken'
import PositionListItem from '.'
jest.mock('hooks/Tokens', () => {
const originalModule = jest.requireActual('hooks/Tokens')
const uniSDK = jest.requireActual('@uniswap/sdk-core')
jest.mock('utils/unwrappedToken')
const mockUnwrappedToken = unwrappedToken as jest.MockedFunction<typeof unwrappedToken>
jest.mock('hooks/usePools')
const mockUsePool = usePool as jest.MockedFunction<typeof usePool>
jest.mock('hooks/Tokens')
const mockUseToken = useToken as jest.MockedFunction<typeof useToken>
// eslint-disable-next-line react/display-name
jest.mock('components/DoubleLogo', () => () => <div />)
jest.mock('@web3-react/core', () => {
const web3React = jest.requireActual('@web3-react/core')
return {
__esModule: true,
...originalModule,
useToken: jest.fn(
() =>
new uniSDK.Token(
1,
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
8,
'https://www.example.com',
'example.com coin'
)
),
...web3React,
useWeb3React: () => ({
chainId: 1,
}),
}
})
test('PositionListItem should not render when the name contains a url', () => {
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
beforeEach(() => {
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
mockUseToken.mockImplementation((tokenAddress?: string | null | undefined) => {
if (!tokenAddress) return null
if (tokenAddress === susToken0.address) return susToken0
return new Token(1, tokenAddress, 8, 'symbol', 'name')
})
mockUsePool.mockReturnValue([
PoolState.EXISTS,
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
])
mockUnwrappedToken.mockReturnValue(susToken0)
})
test('PositionListItem should not render when token0 symbol contains a url', () => {
const positionDetails = {
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
token0: susToken0Address,
token1: USDC_MAINNET.address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
render(<PositionListItem {...positionDetails} />)
screen.debug()
expect(screen.queryByText('.com', { exact: false })).toBe(null)
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).toBeEmptyDOMElement()
})
test('PositionListItem should not render when token1 symbol contains a url', () => {
const positionDetails = {
token0: USDC_MAINNET.address,
token1: susToken0Address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).toBeEmptyDOMElement()
})
test('PositionListItem should render a position', () => {
const positionDetails = {
token0: USDC_MAINNET.address,
token1: WETH9[SupportedChainId.MAINNET].address,
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
const { container } = render(<PositionListItem {...positionDetails} />)
expect(container).not.toBeEmptyDOMElement()
})

View File

@@ -203,12 +203,9 @@ export default function PositionListItem({
const removed = liquidity?.eq(0)
const containsURL = useMemo(
() => [token0?.name, token0?.symbol, token1?.name, token1?.symbol].some((testString) => hasURL(testString)),
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
)
const shouldHidePosition = hasURL(token0?.symbol) || hasURL(token1?.symbol)
if (containsURL) {
if (shouldHidePosition) {
return null
}

View File

@@ -5,6 +5,7 @@ import Row from 'components/Row'
import { useToggleWalletDrawer } from 'components/WalletDropdown'
import { Box } from 'nft/components/Box'
import { NftCard } from 'nft/components/card'
import { detailsHref } from 'nft/components/card/utils'
import { VerifiedIcon } from 'nft/components/icons'
import { WalletAsset } from 'nft/types'
import { floorFormatter } from 'nft/utils'
@@ -50,8 +51,8 @@ export function NFT({
const trace = useTrace()
const navigateToNFTDetails = () => {
navigate(`/nfts/asset/${asset.asset_contract.address}/${asset.tokenId}`)
toggleWalletDrawer()
navigate(detailsHref(asset))
}
return (
@@ -62,10 +63,7 @@ export function NFT({
display={{ disabledInfo: true }}
isSelected={false}
isDisabled={false}
selectAsset={navigateToNFTDetails}
unselectAsset={() => {
/* */
}}
onCardClick={navigateToNFTDetails}
sendAnalyticsEvent={() =>
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM,
@@ -77,6 +75,7 @@ export function NFT({
}
mediaShouldBePlaying={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
testId="mini-portfolio-nft"
/>
<NFTDetails asset={asset} />
</NFTContainer>

View File

@@ -0,0 +1,67 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils'
import Pools from '.'
import useMultiChainPositions from './useMultiChainPositions'
jest.mock('./useMultiChainPositions')
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
const pool = new Pool(
USDC_MAINNET,
WETH9[SupportedChainId.MAINNET],
FeeAmount.MEDIUM,
'1851127709498178402383049949138810',
'7076437181775065414',
201189
)
const position = new Position({
pool,
liquidity: 1341008833950736,
tickLower: 200040,
tickUpper: 202560,
})
const details = {
nonce: BigNumber.from('0'),
tokenId: BigNumber.from('0'),
operator: '0x0',
token0: USDC_MAINNET.address,
token1: WETH9[SupportedChainId.MAINNET].address,
fee: FeeAmount.MEDIUM,
tickLower: -100,
tickUpper: 100,
liquidity: BigNumber.from('9000'),
feeGrowthInside0LastX128: BigNumber.from('0'),
feeGrowthInside1LastX128: BigNumber.from('0'),
tokensOwed0: BigNumber.from('0'),
tokensOwed1: BigNumber.from('0'),
}
const useMultiChainPositionsReturnValue = {
positions: [
{
owner,
chainId: SupportedChainId.MAINNET,
position,
pool,
details,
inRange: true,
closed: false,
},
],
loading: false,
}
beforeEach(() => {
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
})
test('Pools should render LP positions', () => {
const props = { account: owner }
const { container } = render(<Pools {...props} />)
expect(container).not.toBeEmptyDOMElement()
})

View File

@@ -111,15 +111,9 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
)
const containsURL = useMemo(
() =>
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
hasURL(testString)
),
[pool]
)
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
if (containsURL) {
if (shouldHidePosition) {
return null
}

View File

@@ -0,0 +1,20 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils'
import { PortfolioLogo } from './PortfolioLogo'
describe('PortfolioLogo', () => {
it('renders without L2 icon', () => {
const { container } = render(<PortfolioLogo chainId={SupportedChainId.MAINNET} currencies={[DAI, USDC_MAINNET]} />)
expect(container).toMatchSnapshot()
})
it('renders with L2 icon', () => {
const { container } = render(
<PortfolioLogo chainId={SupportedChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM, USDC_ARBITRUM]} />
)
expect(container).toMatchSnapshot()
})
})

View File

@@ -9,7 +9,7 @@ import useTokenLogoSource from 'hooks/useAssetLogoSource'
import useENSAvatar from 'hooks/useENSAvatar'
import React from 'react'
import { Loader } from 'react-feather'
import styled from 'styled-components/macro'
import styled, { useTheme } from 'styled-components/macro'
const UnknownContract = styled(UnknownStatus)`
color: ${({ theme }) => theme.textSecondary};
`
@@ -57,34 +57,28 @@ const ENSAvatarImg = styled.img`
width: 40px;
`
const StyledChainLogo = styled.img<{ isSquare: boolean }>`
height: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
width: ${({ isSquare }) => (isSquare ? '16px' : '14px')};
margin-top: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
margin-left: ${({ isSquare }) => (isSquare ? '0px' : '1px')};
position: absolute;
top: 68%;
left: 68%;
const StyledChainLogo = styled.img`
height: 14px;
width: 14px;
`
const ChainLogoSquareBackground = styled.div`
height: 18px;
width: 18px;
border-radius: 4px;
background-color: ${({ theme }) => theme.backgroundSurface};
const SquareChainLogo = styled.img`
height: 100%;
width: 100%;
`
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
background-color: ${({ $backgroundColor }) => $backgroundColor};
border-radius: 2px;
height: 16px;
left: 60%;
position: absolute;
top: 60%;
left: 60%;
`
const SquareBackgroundForNonSquareLogo = styled.div`
height: 16px;
outline: 2px solid ${({ theme }) => theme.backgroundSurface};
width: 16px;
border-radius: 2px;
background-color: ${({ theme }) => theme.textPrimary};
position: absolute;
top: 68%;
left: 68%;
display: flex;
align-items: center;
justify-content: center;
`
/**
@@ -101,6 +95,7 @@ export function PortfolioLogo({
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
const chainLogo = squareLogoUrl ?? logoUrl
const { avatar, loading } = useENSAvatar(accountAddress, false)
const theme = useTheme()
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
@@ -147,13 +142,15 @@ export function PortfolioLogo({
}
const L2Logo =
chainId === SupportedChainId.MAINNET ? null : (
<div>
{chainLogo && <ChainLogoSquareBackground />}
{!squareLogoUrl && logoUrl && <SquareBackgroundForNonSquareLogo />}
{chainLogo && <StyledChainLogo isSquare={!!squareLogoUrl} src={chainLogo} alt="chainLogo" />}
</div>
)
chainId !== SupportedChainId.MAINNET && chainLogo ? (
<L2LogoContainer $backgroundColor={squareLogoUrl ? theme.backgroundSurface : theme.textPrimary}>
{squareLogoUrl ? (
<SquareChainLogo src={chainLogo} alt="chainLogo" />
) : (
<StyledChainLogo src={chainLogo} alt="chainLogo" />
)}
</L2LogoContainer>
) : null
return (
<StyledLogoParentContainer>

View File

@@ -0,0 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PortfolioLogo renders with L2 icon 1`] = `
.c3 {
width: 40px;
height: 40px;
border-radius: 50%;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c1 .c2:nth-child(n) {
width: 19px;
height: 40px;
object-fit: cover;
}
.c1 .c2:nth-child(1) {
border-radius: 20px 0 0 20px;
object-position: 0 0;
}
.c1 .c2:nth-child(2) {
border-radius: 0 20px 20px 0;
object-position: 100% 0;
}
.c0 {
position: relative;
top: 0;
left: 0;
}
.c5 {
height: 14px;
width: 14px;
}
.c4 {
background-color: #0D111C;
border-radius: 2px;
height: 16px;
left: 60%;
position: absolute;
top: 60%;
outline: 2px solid #FFFFFF;
width: 16px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
<div>
<div
class="c0"
>
<div
class="c1"
>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
/>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
/>
</div>
<div
class="c4"
>
<img
alt="chainLogo"
class="c5"
src="arbitrum_logo.svg"
/>
</div>
</div>
</div>
`;
exports[`PortfolioLogo renders without L2 icon 1`] = `
.c3 {
width: 40px;
height: 40px;
border-radius: 50%;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c1 .c2:nth-child(n) {
width: 19px;
height: 40px;
object-fit: cover;
}
.c1 .c2:nth-child(1) {
border-radius: 20px 0 0 20px;
object-position: 0 0;
}
.c1 .c2:nth-child(2) {
border-radius: 0 20px 20px 0;
object-position: 100% 0;
}
.c0 {
position: relative;
top: 0;
left: 0;
}
<div>
<div
class="c0"
>
<div
class="c1"
>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
/>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
/>
</div>
</div>
</div>
`;

View File

@@ -55,6 +55,7 @@ const PageWrapper = styled.div`
interface Page {
title: React.ReactNode
key: string
component: ({ account }: { account: string }) => JSX.Element
loggingElementName: string
}
@@ -62,13 +63,25 @@ interface Page {
const Pages: Array<Page> = [
{
title: <Trans>Tokens</Trans>,
key: 'tokens',
component: Tokens,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB,
},
{ title: <Trans>NFTs</Trans>, component: NFTs, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB },
{ title: <Trans>Pools</Trans>, component: Pools, loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB },
{
title: <Trans>NFTs</Trans>,
key: 'nfts',
component: NFTs,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB,
},
{
title: <Trans>Pools</Trans>,
key: 'pools',
component: Pools,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB,
},
{
title: <Trans>Activity</Trans>,
key: 'activity',
component: ActivityTab,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
},
@@ -83,7 +96,7 @@ function MiniPortfolio({ account }: { account: string }) {
return (
<Wrapper>
<Nav>
{Pages.map(({ title, loggingElementName }, index) => {
{Pages.map(({ title, loggingElementName, key }, index) => {
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
return (
<TraceEvent
@@ -93,6 +106,7 @@ function MiniPortfolio({ account }: { account: string }) {
key={index}
>
<NavItem
data-testid={`mini-portfolio-nav-${key}`}
onClick={() => setCurrentPage(index)}
active={currentPage === index}
key={`Mini Portfolio page ${index}`}

View File

@@ -72,7 +72,7 @@ export default function Option({ connection, pendingConnectionType, activate }:
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.WALLET_SELECTED}
properties={{ wallet_type: connection.name }}
properties={{ wallet_type: connection.getName() }}
element={InterfaceElementName.WALLET_TYPE_OPTION}
>
<OptionCardClickable
@@ -83,9 +83,9 @@ export default function Option({ connection, pendingConnectionType, activate }:
>
<OptionCardLeft>
<IconWrapper>
<img src={connection.icon} alt="Icon" />
<img src={connection.getIcon?.()} alt="Icon" />
</IconWrapper>
<HeaderText>{connection.name}</HeaderText>
<HeaderText>{connection.getName()}</HeaderText>
{connection.isNew && <NewBadge />}
</OptionCardLeft>
{isPending && <Loader />}

View File

@@ -7,7 +7,7 @@ import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { useWalletDrawer } from 'components/WalletDropdown'
import IconButton from 'components/WalletDropdown/IconButton'
import { Connection, ConnectionType, networkConnection, useConnections } from 'connection'
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
import { useGetConnection } from 'connection'
import { ErrorCode } from 'connection/utils'
import { isSupportedChain } from 'constants/chains'
@@ -91,7 +91,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
const [pendingError, setPendingError] = useState<any>()
const connections = useConnections()
const connections = getConnections()
const getConnection = useGetConnection()
useEffect(() => {
@@ -116,7 +116,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {
const walletName = getConnection(connector).name
const walletName = getConnection(connector).getName()
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const tryActivation = useCallback(
async (connection: Connection) => {
// Skips wallet connection if the connection should override the default behavior, i.e. install metamask or launch coinbase app
if (connection.overrideActivate?.()) return
// log selected wallet
sendEvent({
category: 'Wallet',
@@ -165,7 +168,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED,
wallet_type: connection.name,
wallet_type: connection.getName(),
})
}
}
@@ -190,11 +193,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
<OptionGrid data-testid="option-grid">
{connections.map((connection) =>
// Hides Uniswap Wallet if mgtm is disabled
connection.shouldDisplay && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
connection.shouldDisplay() && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
<Option
key={connection.name}
key={connection.getName()}
connection={connection}
activate={connection.overrideActivate ?? (() => tryActivation(connection))}
activate={() => tryActivation(connection)}
pendingConnectionType={pendingConnection?.type}
/>
) : null

View File

@@ -12,7 +12,7 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
const connections = useOrderedConnections()
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
const key = useMemo(() => connections.map((connection) => connection.name).join('-'), [connections])
const key = useMemo(() => connections.map((connection) => connection.getName()).join('-'), [connections])
return (
<Web3ReactProvider connectors={connectors} key={key}>

View File

@@ -1,111 +1,145 @@
// eslint-disable-next-line jest/no-export
export {}
import { ConnectionType, getConnections, useGetConnection } from 'connection'
import { renderHook } from 'test-utils'
beforeEach(() => {
jest.resetModules()
jest.resetAllMocks()
})
it('Non-injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
const UserAgentMock = jest.requireMock('utils/userAgent')
jest.mock('utils/userAgent', () => ({
isMobile: false,
}))
it('MetaMask Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
describe('connection utility/metadata tests', () => {
const createWalletEnvironment = (ethereum: Window['window']['ethereum'], isMobile = false) => {
UserAgentMock.isMobile = isMobile
global.window.ethereum = ethereum
it('Coinbase Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeDefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
const displayed = getConnections().filter((c) => c.shouldDisplay())
const getConnection = renderHook(() => useGetConnection()).result.current
const injected = getConnection(ConnectionType.INJECTED)
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
const uniswap = getConnection(ConnectionType.UNIWALLET)
const walletconnect = getConnection(ConnectionType.WALLET_CONNECT)
it('Coinbase and MetaMask Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
return { displayed, injected, coinbase, uniswap, walletconnect }
}
it('Generic Injected Desktop', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: false }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
expect(connection.darkInjectedConnection.overrideActivate).toBeUndefined()
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(4)
})
it('Non-injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment(undefined)
it('Generic Injected Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('Browser Wallet')
})
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
it('MetaMask Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: true, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(true)
expect(connection.darkInjectedConnection.name).toBe('MetaMask')
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
})
expect(displayed.length).toEqual(4)
})
it('Coinbase Mobile Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: true, isMetaMaskWallet: false, isCoinbaseWallet: true }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
it('MetaMask-Injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true })
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.coinbaseWalletConnection.overrideActivate).toBeUndefined()
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(1)
})
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
it('mWeb Browser', async () => {
jest.mock('connection/utils', () => ({ isInjected: false, isMetaMaskWallet: false, isCoinbaseWallet: false }))
jest.mock('utils/userAgent', () => ({ isMobile: true }))
const connection = await import('connection')
expect(connection.darkInjectedConnection.shouldDisplay).toBe(false)
expect(connection.coinbaseWalletConnection.shouldDisplay).toBe(true)
expect(connection.coinbaseWalletConnection.overrideActivate).toBeDefined()
expect(connection.uniwalletConnectConnection.shouldDisplay).toBe(true)
expect(connection.walletConnectConnection.shouldDisplay).toBe(true)
expect(connection.getConnections(true).filter((c) => c.shouldDisplay).length).toEqual(3)
expect(displayed.length).toEqual(4)
})
it('Coinbase-Injected Desktop', async () => {
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true })
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
expect(displayed.length).toEqual(4)
})
it('Coinbase and MetaMask Injected Desktop', async () => {
const { displayed, injected, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true, isMetaMask: true })
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Injected Desktop', async () => {
const { displayed, injected } = createWalletEnvironment({ isTrustWallet: true })
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Browser Wallet that injects as MetaMask', async () => {
const { displayed, injected } = createWalletEnvironment({ isRabby: true, isMetaMask: true })
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(4)
})
it('Generic Wallet Browser with delayed injection', async () => {
const { injected } = createWalletEnvironment(undefined)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeTruthy()
createWalletEnvironment({ isTrustWallet: true })
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
})
const UNKNOWN_INJECTOR = { isRandomWallet: true } as Window['window']['ethereum']
it('Generic Unknown Injected Wallet Browser', async () => {
const { displayed, injected } = createWalletEnvironment(UNKNOWN_INJECTOR, true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('Browser Wallet')
expect(injected.overrideActivate?.()).toBeFalsy()
// Ensures we provide multiple connection options if in an unknown injected browser
expect(displayed.length).toEqual(4)
})
it('MetaMask Mobile Browser', async () => {
const { displayed, injected } = createWalletEnvironment({ isMetaMask: true }, true)
expect(displayed.includes(injected)).toBe(true)
expect(injected.getName()).toBe('MetaMask')
expect(injected.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(1)
})
it('Coinbase Mobile Browser', async () => {
const { displayed, coinbase } = createWalletEnvironment({ isCoinbaseWallet: true }, true)
expect(displayed.includes(coinbase)).toBe(true)
// Expect coinbase option to not override activation in a the cb mobile browser
expect(coinbase.overrideActivate?.()).toBeFalsy()
expect(displayed.length).toEqual(1)
})
it('Uninjected mWeb Browser', async () => {
const { displayed, injected, coinbase, walletconnect } = createWalletEnvironment(undefined, true)
expect(displayed.includes(coinbase)).toBe(true)
expect(displayed.includes(walletconnect)).toBe(true)
// Don't show injected connection on plain mWeb browser
expect(displayed.includes(injected)).toBe(false)
// Expect coinbase option to launch coinbase app in a regular mobile browser
expect(coinbase.overrideActivate?.()).toBeTruthy()
expect(displayed.length).toEqual(3)
})
})

View File

@@ -9,17 +9,15 @@ import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
import METAMASK_ICON_URL from 'assets/images/metamask.svg'
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
import INJECTED_DARK_ICON_URL from 'assets/svg/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
import { SupportedChainId } from 'constants/chains'
import { useCallback } from 'react'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS } from '../constants/providers'
import { isCoinbaseWallet, isInjected, isMetaMaskWallet } from './utils'
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
export enum ConnectionType {
@@ -32,13 +30,14 @@ export enum ConnectionType {
}
export interface Connection {
name: string
getName(): string
connector: Connector
hooks: Web3ReactHooks
type: ConnectionType
icon?: string
shouldDisplay?: boolean
overrideActivate?: () => void
// TODO(WEB-3130): add darkmode check for icons
getIcon?(): string
shouldDisplay(): boolean
overrideActivate?: () => boolean
isNew?: boolean
}
@@ -50,73 +49,72 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
)
export const networkConnection: Connection = {
name: 'Network',
getName: () => 'Network',
connector: web3Network,
hooks: web3NetworkHooks,
type: ConnectionType.NETWORK,
shouldDisplay: false,
shouldDisplay: () => false,
}
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
const isMetaMaskBrowser = isMobile && isMetaMaskWallet
const getIsInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
const getShouldAdvertiseMetaMask = !isMetaMaskWallet && !isMobile && (!isInjected || isCoinbaseWallet)
const isGenericInjector = isInjected && !isMetaMaskWallet && !isCoinbaseWallet
const getShouldAdvertiseMetaMask = () =>
!getIsMetaMaskWallet() && !isMobile && (!getIsInjected() || getIsCoinbaseWallet())
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
const baseInjectedConnection: Omit<Connection, 'icon'> = {
name: isGenericInjector ? 'Browser Wallet' : 'MetaMask',
const injectedConnection: Connection = {
// TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
connector: web3Injected,
hooks: web3InjectedHooks,
type: ConnectionType.INJECTED,
shouldDisplay: isMetaMaskWallet || getShouldAdvertiseMetaMask || isGenericInjector,
getIcon: () => (getIsGenericInjector() ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL),
shouldDisplay: () => getIsMetaMaskWallet() || getShouldAdvertiseMetaMask() || getIsGenericInjector(),
// If on non-injected, non-mobile browser, prompt user to install Metamask
overrideActivate: getShouldAdvertiseMetaMask ? () => window.open('https://metamask.io/', 'inst_metamask') : undefined,
}
export const darkInjectedConnection: Connection = {
...baseInjectedConnection,
icon: isGenericInjector ? INJECTED_DARK_ICON_URL : METAMASK_ICON_URL,
}
export const lightInjectedConnection: Connection = {
...baseInjectedConnection,
icon: isGenericInjector ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL,
overrideActivate: () => {
if (getShouldAdvertiseMetaMask()) {
window.open('https://metamask.io/', 'inst_metamask')
return true
}
return false
},
}
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
export const gnosisSafeConnection: Connection = {
name: 'Gnosis Safe',
getName: () => 'Gnosis Safe',
connector: web3GnosisSafe,
hooks: web3GnosisSafeHooks,
type: ConnectionType.GNOSIS_SAFE,
icon: GNOSIS_ICON_URL,
shouldDisplay: false,
getIcon: () => GNOSIS_ICON_URL,
shouldDisplay: () => false,
}
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>(
(actions) => new WalletConnectPopup({ actions, onError })
)
export const walletConnectConnection: Connection = {
name: 'WalletConnect',
getName: () => 'WalletConnect',
connector: web3WalletConnect,
hooks: web3WalletConnectHooks,
type: ConnectionType.WALLET_CONNECT,
icon: WALLET_CONNECT_ICON_URL,
shouldDisplay: !getIsInjectedMobileBrowser,
getIcon: () => WALLET_CONNECT_ICON_URL,
shouldDisplay: () => !getIsInjectedMobileBrowser(),
}
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
(actions) => new UniwalletConnect({ actions, onError })
)
export const uniwalletConnectConnection: Connection = {
name: 'Uniswap Wallet',
getName: () => 'Uniswap Wallet',
connector: web3UniwalletConnect,
hooks: web3UniwalletConnectHooks,
type: ConnectionType.UNIWALLET,
icon: UNIWALLET_ICON_URL,
shouldDisplay: Boolean(!getIsInjectedMobileBrowser && !isNonIOSPhone),
getIcon: () => UNIWALLET_ICON_URL,
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
isNew: true,
}
@@ -134,24 +132,28 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
})
)
export const coinbaseWalletConnection: Connection = {
name: 'Coinbase Wallet',
const coinbaseWalletConnection: Connection = {
getName: () => 'Coinbase Wallet',
connector: web3CoinbaseWallet,
hooks: web3CoinbaseWalletHooks,
type: ConnectionType.COINBASE_WALLET,
icon: COINBASE_ICON_URL,
shouldDisplay: Boolean((isMobile && !getIsInjectedMobileBrowser) || !isMobile || isCoinbaseWalletBrowser),
getIcon: () => COINBASE_ICON_URL,
shouldDisplay: () =>
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
overrideActivate:
isMobile && !getIsInjectedMobileBrowser
? () => window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
: undefined,
overrideActivate: () => {
if (isMobile && !getIsInjectedMobileBrowser()) {
window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
return true
}
return false
},
}
export function getConnections(isDarkMode: boolean) {
export function getConnections() {
return [
uniwalletConnectConnection,
isDarkMode ? darkInjectedConnection : lightInjectedConnection,
injectedConnection,
walletConnectConnection,
coinbaseWalletConnection,
gnosisSafeConnection,
@@ -159,38 +161,29 @@ export function getConnections(isDarkMode: boolean) {
]
}
export function useConnections() {
const isDarkMode = useIsDarkMode()
return getConnections(isDarkMode)
}
export function useGetConnection() {
const isDarkMode = useIsDarkMode()
return useCallback(
(c: Connector | ConnectionType) => {
if (c instanceof Connector) {
const connection = getConnections(isDarkMode).find((connection) => connection.connector === c)
if (!connection) {
throw Error('unsupported connector')
}
return connection
} else {
switch (c) {
case ConnectionType.INJECTED:
return isDarkMode ? darkInjectedConnection : lightInjectedConnection
case ConnectionType.COINBASE_WALLET:
return coinbaseWalletConnection
case ConnectionType.WALLET_CONNECT:
return walletConnectConnection
case ConnectionType.UNIWALLET:
return uniwalletConnectConnection
case ConnectionType.NETWORK:
return networkConnection
case ConnectionType.GNOSIS_SAFE:
return gnosisSafeConnection
}
return useCallback((c: Connector | ConnectionType) => {
if (c instanceof Connector) {
const connection = getConnections().find((connection) => connection.connector === c)
if (!connection) {
throw Error('unsupported connector')
}
},
[isDarkMode]
)
return connection
} else {
switch (c) {
case ConnectionType.INJECTED:
return injectedConnection
case ConnectionType.COINBASE_WALLET:
return coinbaseWalletConnection
case ConnectionType.WALLET_CONNECT:
return walletConnectConnection
case ConnectionType.UNIWALLET:
return uniwalletConnectConnection
case ConnectionType.NETWORK:
return networkConnection
case ConnectionType.GNOSIS_SAFE:
return gnosisSafeConnection
}
}
}, [])
}

View File

@@ -1,15 +1,14 @@
export const isInjected = Boolean(window.ethereum)
export const getIsInjected = () => Boolean(window.ethereum)
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
// This variable should be true only when using the MetaMask extension
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect'
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect']
export const isMetaMaskWallet = Boolean(
window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag])
)
export const getIsMetaMaskWallet = () =>
Boolean(window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag]))
export const isCoinbaseWallet = Boolean(window.ethereum?.isCoinbaseWallet)
export const getIsCoinbaseWallet = () => Boolean(window.ethereum?.isCoinbaseWallet)
// https://eips.ethereum.org/EIPS/eip-1193#provider-errors
export enum ErrorCode {

View File

@@ -489,7 +489,7 @@ class ExtendedEther extends Ether {
public get wrapped(): Token {
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
if (wrapped) return wrapped
throw new Error('Unsupported chain ID')
throw new Error(`Unsupported chain ID: ${this.chainId}`)
}
private static _cachedExtendedEther: { [chainId: number]: NativeCurrency } = {}

View File

@@ -186,22 +186,20 @@ const Container = ({
isSelected,
isDisabled,
detailsHref,
doNotLinkToDetails = false,
testId,
onClick,
children,
}: {
isSelected: boolean
isDisabled: boolean
detailsHref: string
doNotLinkToDetails: boolean
detailsHref?: string
testId?: string
children: ReactNode
onClick?: (e: React.MouseEvent) => void
}) => {
return (
<CardContainer isSelected={isSelected} isDisabled={isDisabled} testId={testId} onClick={onClick}>
<StyledLink to={doNotLinkToDetails ? '' : detailsHref}>{children}</StyledLink>
{detailsHref ? <StyledLink to={detailsHref}>{children}</StyledLink> : children}
</CardContainer>
)
}

View File

@@ -13,11 +13,11 @@ interface NftCardProps {
display: NftCardDisplayProps
isSelected: boolean
isDisabled: boolean
selectAsset: () => void
unselectAsset: () => void
onClick?: () => void
selectAsset?: () => void
unselectAsset?: () => void
onButtonClick?: () => void
onCardClick?: () => void
sendAnalyticsEvent?: () => void
doNotLinkToDetails?: boolean
mediaShouldBePlaying: boolean
uniformAspectRatio?: UniformAspectRatio
setUniformAspectRatio?: (uniformAspectRatio: UniformAspectRatio) => void
@@ -38,6 +38,12 @@ export interface NftCardDisplayProps {
disabledInfo?: ReactNode
}
/**
* NftCard is a component that displays an NFT asset.
*
* By default, clicking on the card will navigate to the details page.
* If you wish to override this behavior, pass a value for the onCardClick prop.
*/
export const NftCard = ({
asset,
display,
@@ -45,9 +51,9 @@ export const NftCard = ({
selectAsset,
unselectAsset,
isDisabled,
onClick,
onButtonClick,
onCardClick,
sendAnalyticsEvent,
doNotLinkToDetails = false,
mediaShouldBePlaying,
uniformAspectRatio = UniformAspectRatios.square,
setUniformAspectRatio,
@@ -57,7 +63,13 @@ export const NftCard = ({
testId,
hideDetails = false,
}: NftCardProps) => {
const clickActionButton = useSelectAsset(selectAsset, unselectAsset, isSelected, isDisabled, onClick)
const clickActionButton = useSelectAsset({
selectAsset,
unselectAsset,
isSelected,
isDisabled,
onClick: onButtonClick,
})
const { bagExpanded, setBagExpanded } = useBag(
(state) => ({
bagExpanded: state.bagExpanded,
@@ -77,11 +89,11 @@ export const NftCard = ({
<Card.Container
isSelected={isSelected}
isDisabled={isDisabled}
detailsHref={detailsHref(asset)}
doNotLinkToDetails={doNotLinkToDetails}
detailsHref={onCardClick ? undefined : detailsHref(asset)}
testId={testId}
onClick={() => {
if (bagExpanded) setBagExpanded({ bagExpanded: false })
onCardClick?.()
sendAnalyticsEvent?.()
}}
>

View File

@@ -96,13 +96,19 @@ export function getNftDisplayComponent(
}
}
export function useSelectAsset(
selectAsset: () => void,
unselectAsset: () => void,
isSelected: boolean,
isDisabled: boolean,
export function useSelectAsset({
selectAsset,
unselectAsset,
isSelected,
isDisabled,
onClick,
}: {
selectAsset?: () => void
unselectAsset?: () => void
isSelected: boolean
isDisabled: boolean
onClick?: () => void
) {
}) {
return useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
@@ -117,7 +123,7 @@ export function useSelectAsset(
return
}
return isSelected ? unselectAsset() : selectAsset()
return isSelected ? unselectAsset?.() : selectAsset?.()
},
[selectAsset, isDisabled, onClick, unselectAsset, isSelected]
)

View File

@@ -3,10 +3,12 @@ import { useTrace } from '@uniswap/analytics'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { NftCard, NftCardDisplayProps } from 'nft/components/card'
import { detailsHref } from 'nft/components/card/utils'
import { VerifiedIcon } from 'nft/components/icons'
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
import { WalletAsset } from 'nft/types'
import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
interface ViewMyNftsAssetProps {
asset: WalletAsset
@@ -27,6 +29,7 @@ export const ViewMyNftsAsset = ({
const cartExpanded = useBag((state) => state.bagExpanded)
const toggleCart = useBag((state) => state.toggleBag)
const isMobile = useIsMobile()
const navigate = useNavigate()
const isSelected = useMemo(() => {
return sellAssets.some(
@@ -35,7 +38,7 @@ export const ViewMyNftsAsset = ({
}, [asset, sellAssets])
const trace = useTrace()
const onCardClick = () => handleSelect(isSelected)
const toggleSelect = () => handleSelect(isSelected)
const handleSelect = (removeAsset: boolean) => {
if (removeAsset) {
@@ -79,11 +82,13 @@ export const ViewMyNftsAsset = ({
isDisabled={Boolean(isDisabled)}
selectAsset={() => handleSelect(false)}
unselectAsset={() => handleSelect(true)}
onClick={onCardClick}
onButtonClick={toggleSelect}
onCardClick={() => {
if (!hideDetails) navigate(detailsHref(asset))
}}
mediaShouldBePlaying={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
testId="nft-profile-asset"
doNotLinkToDetails={hideDetails}
/>
)
}

View File

@@ -1,7 +1,7 @@
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface BagState {

View File

@@ -1,5 +1,5 @@
import { NftAssetSortableField } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
export enum SortBy {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface State {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface State {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface NFTClaim {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
export type MarketplaceOption = { name: string; icon: string }

View File

@@ -1,5 +1,5 @@
import { CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface NFTListState {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { OpenSeaAsset } from '../types'

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface PriceRangeProps {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { ProfilePageStateType } from '../types'

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { GenieAsset } from '../types'

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { ListingMarket, WalletAsset } from '../types'

View File

@@ -5,7 +5,7 @@ import { ContractReceipt } from '@ethersproject/contracts'
import type { JsonRpcSigner } from '@ethersproject/providers'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import ERC721 from '../../abis/erc721.json'

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { GenieAsset } from '../types'

View File

@@ -1,6 +1,6 @@
import { Currency } from '@uniswap/sdk-core'
import { TokenTradeInput } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface TokenInputState {

View File

@@ -1,4 +1,4 @@
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface traitOpen {

View File

@@ -1,5 +1,5 @@
import { TxResponse } from 'nft/types'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
type TransactionResponseValue = TxResponse | undefined

View File

@@ -1,5 +1,5 @@
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { WalletAsset, WalletCollection } from '../types'

View File

@@ -17,10 +17,13 @@ const AMPLITUDE_DUMMY_KEY = '00000000000000000000000000000000'
export const STATSIG_DUMMY_KEY = 'client-0000000000000000000000000000000000000000000'
Sentry.init({
// General configuration:
dsn: process.env.REACT_APP_SENTRY_DSN,
release: process.env.REACT_APP_GIT_COMMIT_HASH,
environment: getEnvName(),
// Exception reporting configuration:
enabled: isSentryEnabled(),
// Performance tracing configuration:
tracesSampleRate: Number(process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE ?? 0),
integrations: [
new BrowserTracing({
@@ -28,13 +31,6 @@ Sentry.init({
startTransactionOnPageLoad: true,
}),
],
/**
* TODO(INFRA-143)
* According to Sentry, this shouldn't be necessary, as they default to `3` when not set.
* Unfortunately, that doesn't work right now, so we workaround it by explicitly setting
* the `normalizeDepth` to `10`. This should be removed once the issue is fixed.
*/
normalizeDepth: 10,
})
initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INTERFACE, {

View File

@@ -3190,67 +3190,75 @@
dependencies:
cross-fetch "^3.1.5"
"@sentry/browser@7.42.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.42.0.tgz#357731e5ab65a226c98370f9e487fe48cadab765"
integrity sha512-xTwfvrQPmYTkAvGivoJFadPLKLDS2N57D/18NA1gcrnF8NwR+I28x3I9ziVUiMCYX+6nJuqBNlMALAEPbb2G5A==
"@sentry-internal/tracing@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.45.0.tgz#01f347d0d1b42451b340b32b12923dc22e042d27"
integrity sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==
dependencies:
"@sentry/core" "7.42.0"
"@sentry/replay" "7.42.0"
"@sentry/types" "7.42.0"
"@sentry/utils" "7.42.0"
"@sentry/core" "7.45.0"
"@sentry/types" "7.45.0"
"@sentry/utils" "7.45.0"
tslib "^1.9.3"
"@sentry/core@7.42.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.42.0.tgz#3333a1b868e8e69b6fbc8e5a9e9281be49321ac7"
integrity sha512-vNcTyoQz5kUXo5vMGDyc5BJMO0UugPvMfYMQVxqt/BuDNR30LVhY+DL2tW1DFZDvRvyn5At+H7kSTj6GFrANXQ==
"@sentry/browser@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.45.0.tgz#c9f8031ad184558d08c374d4e4ee30996cc5b543"
integrity sha512-/dUrUwnI34voMj+jSJT7b5Jun+xy1utVyzzwTq3Oc22N+SB17ZOX9svZ4jl1Lu6tVJPVjPyvL6zlcbrbMwqFjg==
dependencies:
"@sentry/types" "7.42.0"
"@sentry/utils" "7.42.0"
"@sentry-internal/tracing" "7.45.0"
"@sentry/core" "7.45.0"
"@sentry/replay" "7.45.0"
"@sentry/types" "7.45.0"
"@sentry/utils" "7.45.0"
tslib "^1.9.3"
"@sentry/react@^7.40.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.42.0.tgz#7662faef398032f253fe5b174860e1ec5f76ddd5"
integrity sha512-DOGK+vuSZq5lTiqVU6wVay0AUMjtSPZu25gzLIXntfoqw36CLUswP7ew61+Tas6tpXDdf4lR3uxJRwySiQLopw==
"@sentry/core@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.45.0.tgz#87fdb283c211f145e508cc8ff89dabdf2fbcfc39"
integrity sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==
dependencies:
"@sentry/browser" "7.42.0"
"@sentry/types" "7.42.0"
"@sentry/utils" "7.42.0"
"@sentry/types" "7.45.0"
"@sentry/utils" "7.45.0"
tslib "^1.9.3"
"@sentry/react@^7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.45.0.tgz#9d3634b7b93bc1fa0872cdec49c1fbacd4ef2506"
integrity sha512-Dbz85nfvMUikbLHUuIt6fBNPmTvThFn+rWB5KS1NIOJifyWAdpIU3X7yCUJE5xhsUObNLiHlNJlqhaQI4nR1bQ==
dependencies:
"@sentry/browser" "7.45.0"
"@sentry/types" "7.45.0"
"@sentry/utils" "7.45.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
"@sentry/replay@7.42.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.42.0.tgz#03be2bdab35e0f2d4e415a770c23d50dba8d73b5"
integrity sha512-81HQm20hrW0+0eZ5sZf8KsSekkAlI0/u/M+9ZmOn2bHpGihqAM/O/lrXhTzaRXdpX/9NSwSCGY9k7LIRNMKaEQ==
"@sentry/replay@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.45.0.tgz#1da15e8c419bb77ec7475c7b1879d11f17edad20"
integrity sha512-smM7FIcFIyKu30BqCl8BzLo1gH/z9WwXdGX6V0fNvHab9fJZ09+xjFn+LmIyo6N8H8jjwsup0+yQ12kiF/ZsEw==
dependencies:
"@sentry/core" "7.42.0"
"@sentry/types" "7.42.0"
"@sentry/utils" "7.42.0"
"@sentry/core" "7.45.0"
"@sentry/types" "7.45.0"
"@sentry/utils" "7.45.0"
"@sentry/tracing@^7.40.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.42.0.tgz#bcdac21e1cb5f786465e6252625bd4bf0736e631"
integrity sha512-0veGu3Ntweuj1pwWrJIFHmVdow4yufCreGIhsNDyrclwOjaTY3uI8iA6N62+hhtxOvqv+xueB98K1DvT5liPCQ==
"@sentry/tracing@^7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.45.0.tgz#77fe1075b3fdfd5026bf8d816a855bbe992b64a3"
integrity sha512-FsoFmZPzTBGvWeJH73NxSF1ot61Zw3aIZo5XolengiKnRmcrQOFxebtMKBiZ61QBRYGqsm5uT7QB7zITU6Ikgg==
dependencies:
"@sentry/core" "7.42.0"
"@sentry/types" "7.42.0"
"@sentry/utils" "7.42.0"
tslib "^1.9.3"
"@sentry-internal/tracing" "7.45.0"
"@sentry/types@7.42.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.42.0.tgz#e5019cd41a8c4a98c296e2ff28d6adab4b2eb14e"
integrity sha512-Ga0xaBIR/peuXQ88hI9a5TNY3GLNoH8jpsgPaAjAtRHkLsTx0y3AR+PrD7pUysza9QjvG+Qux01DRvLgaNKOHA==
"@sentry/types@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.45.0.tgz#b5e2db7a421f6090398565b0a72fb3bbdc94233a"
integrity sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw==
"@sentry/utils@7.42.0":
version "7.42.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.42.0.tgz#fcffd0404836cb56975fbef9e889a42cc55de596"
integrity sha512-cBiDZVipC+is+IVgsTQLJyZWUZQxlLZ9GarNT+XZOZ5BFh0acFtz88hO6+S7vGmhcx2aCvsdC9yb2Yf+BphK6Q==
"@sentry/utils@7.45.0":
version "7.45.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.45.0.tgz#e13e075098578557ec3a0decf735cbad6a26ce63"
integrity sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==
dependencies:
"@sentry/types" "7.42.0"
"@sentry/types" "7.45.0"
tslib "^1.9.3"
"@sinonjs/commons@^1.7.0":