Commit 4eaf16b6 authored by cartcrom's avatar cartcrom Committed by GitHub

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
parent 857e2915
...@@ -60,7 +60,7 @@ const Socks = () => { ...@@ -60,7 +60,7 @@ const Socks = () => {
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => { const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
return ( return (
<MiniIconContainer side={side}> <MiniIconContainer side={side}>
<MiniImg src={connection.icon} alt={`${connection.name} icon`} /> <MiniImg src={connection.getIcon?.()} alt={`${connection.getName()} icon`} />
</MiniIconContainer> </MiniIconContainer>
) )
} }
...@@ -71,7 +71,7 @@ const MainWalletIcon = ({ connection, size }: { connection: Connection; size: nu ...@@ -71,7 +71,7 @@ const MainWalletIcon = ({ connection, size }: { connection: Connection; size: nu
if (!account) { if (!account) {
return null 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} /> return <Identicon size={size} />
} else { } else {
return <Unicon address={account} size={size} /> return <Unicon address={account} size={size} />
......
...@@ -72,7 +72,7 @@ export default function Option({ connection, pendingConnectionType, activate }: ...@@ -72,7 +72,7 @@ export default function Option({ connection, pendingConnectionType, activate }:
<TraceEvent <TraceEvent
events={[BrowserEvent.onClick]} events={[BrowserEvent.onClick]}
name={InterfaceEventName.WALLET_SELECTED} name={InterfaceEventName.WALLET_SELECTED}
properties={{ wallet_type: connection.name }} properties={{ wallet_type: connection.getName() }}
element={InterfaceElementName.WALLET_TYPE_OPTION} element={InterfaceElementName.WALLET_TYPE_OPTION}
> >
<OptionCardClickable <OptionCardClickable
...@@ -83,9 +83,9 @@ export default function Option({ connection, pendingConnectionType, activate }: ...@@ -83,9 +83,9 @@ export default function Option({ connection, pendingConnectionType, activate }:
> >
<OptionCardLeft> <OptionCardLeft>
<IconWrapper> <IconWrapper>
<img src={connection.icon} alt="Icon" /> <img src={connection.getIcon?.()} alt="Icon" />
</IconWrapper> </IconWrapper>
<HeaderText>{connection.name}</HeaderText> <HeaderText>{connection.getName()}</HeaderText>
{connection.isNew && <NewBadge />} {connection.isNew && <NewBadge />}
</OptionCardLeft> </OptionCardLeft>
{isPending && <Loader />} {isPending && <Loader />}
......
...@@ -7,7 +7,7 @@ import { AutoColumn } from 'components/Column' ...@@ -7,7 +7,7 @@ import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import { useWalletDrawer } from 'components/WalletDropdown' import { useWalletDrawer } from 'components/WalletDropdown'
import IconButton from 'components/WalletDropdown/IconButton' 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 { useGetConnection } from 'connection'
import { ErrorCode } from 'connection/utils' import { ErrorCode } from 'connection/utils'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
...@@ -91,7 +91,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -91,7 +91,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>() const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
const [pendingError, setPendingError] = useState<any>() const [pendingError, setPendingError] = useState<any>()
const connections = useConnections() const connections = getConnections()
const getConnection = useGetConnection() const getConnection = useGetConnection()
useEffect(() => { useEffect(() => {
...@@ -116,7 +116,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -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. // When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => { useEffect(() => {
if (account && account !== lastActiveWalletAddress) { if (account && account !== lastActiveWalletAddress) {
const walletName = getConnection(connector).name const walletName = getConnection(connector).getName()
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
const isReconnect = const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0 connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
...@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -141,6 +141,9 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
const tryActivation = useCallback( const tryActivation = useCallback(
async (connection: Connection) => { 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 // log selected wallet
sendEvent({ sendEvent({
category: 'Wallet', category: 'Wallet',
...@@ -165,7 +168,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -165,7 +168,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, { sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED, result: WalletConnectionResult.FAILED,
wallet_type: connection.name, wallet_type: connection.getName(),
}) })
} }
} }
...@@ -190,11 +193,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -190,11 +193,11 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
<OptionGrid data-testid="option-grid"> <OptionGrid data-testid="option-grid">
{connections.map((connection) => {connections.map((connection) =>
// Hides Uniswap Wallet if mgtm is disabled // Hides Uniswap Wallet if mgtm is disabled
connection.shouldDisplay && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? ( connection.shouldDisplay() && !(connection.type === ConnectionType.UNIWALLET && !mgtmEnabled) ? (
<Option <Option
key={connection.name} key={connection.getName()}
connection={connection} connection={connection}
activate={connection.overrideActivate ?? (() => tryActivation(connection))} activate={() => tryActivation(connection)}
pendingConnectionType={pendingConnection?.type} pendingConnectionType={pendingConnection?.type}
/> />
) : null ) : null
......
...@@ -12,7 +12,7 @@ export default function Web3Provider({ children }: { children: ReactNode }) { ...@@ -12,7 +12,7 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
const connections = useOrderedConnections() const connections = useOrderedConnections()
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks]) 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 ( return (
<Web3ReactProvider connectors={connectors} key={key}> <Web3ReactProvider connectors={connectors} key={key}>
......
This diff is collapsed.
...@@ -9,17 +9,15 @@ import GNOSIS_ICON_URL from 'assets/images/gnosis.png' ...@@ -9,17 +9,15 @@ import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
import METAMASK_ICON_URL from 'assets/images/metamask.svg' import METAMASK_ICON_URL from 'assets/images/metamask.svg'
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg' import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.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 INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg' import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { isMobile, isNonIOSPhone } from 'utils/userAgent' import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks' import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS } from '../constants/providers' import { RPC_PROVIDERS } from '../constants/providers'
import { isCoinbaseWallet, isInjected, isMetaMaskWallet } from './utils' import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect' import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
export enum ConnectionType { export enum ConnectionType {
...@@ -32,13 +30,14 @@ export enum ConnectionType { ...@@ -32,13 +30,14 @@ export enum ConnectionType {
} }
export interface Connection { export interface Connection {
name: string getName(): string
connector: Connector connector: Connector
hooks: Web3ReactHooks hooks: Web3ReactHooks
type: ConnectionType type: ConnectionType
icon?: string // TODO(WEB-3130): add darkmode check for icons
shouldDisplay?: boolean getIcon?(): string
overrideActivate?: () => void shouldDisplay(): boolean
overrideActivate?: () => boolean
isNew?: boolean isNew?: boolean
} }
...@@ -50,73 +49,72 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>( ...@@ -50,73 +49,72 @@ const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 }) (actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
) )
export const networkConnection: Connection = { export const networkConnection: Connection = {
name: 'Network', getName: () => 'Network',
connector: web3Network, connector: web3Network,
hooks: web3NetworkHooks, hooks: web3NetworkHooks,
type: ConnectionType.NETWORK, type: ConnectionType.NETWORK,
shouldDisplay: false, shouldDisplay: () => false,
} }
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet const getIsCoinbaseWalletBrowser = () => isMobile && getIsCoinbaseWallet()
const isMetaMaskBrowser = isMobile && isMetaMaskWallet const getIsMetaMaskBrowser = () => isMobile && getIsMetaMaskWallet()
const getIsInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser const getIsInjectedMobileBrowser = () => getIsCoinbaseWalletBrowser() || getIsMetaMaskBrowser()
const getShouldAdvertiseMetaMask = !isMetaMaskWallet && !isMobile && (!isInjected || isCoinbaseWallet) const getShouldAdvertiseMetaMask = () =>
const isGenericInjector = isInjected && !isMetaMaskWallet && !isCoinbaseWallet !getIsMetaMaskWallet() && !isMobile && (!getIsInjected() || getIsCoinbaseWallet())
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError })) const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
const baseInjectedConnection: Omit<Connection, 'icon'> = { const injectedConnection: Connection = {
name: isGenericInjector ? 'Browser Wallet' : 'MetaMask', // TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
connector: web3Injected, connector: web3Injected,
hooks: web3InjectedHooks, hooks: web3InjectedHooks,
type: ConnectionType.INJECTED, 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 // If on non-injected, non-mobile browser, prompt user to install Metamask
overrideActivate: getShouldAdvertiseMetaMask ? () => window.open('https://metamask.io/', 'inst_metamask') : undefined, overrideActivate: () => {
} if (getShouldAdvertiseMetaMask()) {
window.open('https://metamask.io/', 'inst_metamask')
export const darkInjectedConnection: Connection = { return true
...baseInjectedConnection, }
icon: isGenericInjector ? INJECTED_DARK_ICON_URL : METAMASK_ICON_URL, return false
} },
export const lightInjectedConnection: Connection = {
...baseInjectedConnection,
icon: isGenericInjector ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL,
} }
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions })) const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
export const gnosisSafeConnection: Connection = { export const gnosisSafeConnection: Connection = {
name: 'Gnosis Safe', getName: () => 'Gnosis Safe',
connector: web3GnosisSafe, connector: web3GnosisSafe,
hooks: web3GnosisSafeHooks, hooks: web3GnosisSafeHooks,
type: ConnectionType.GNOSIS_SAFE, type: ConnectionType.GNOSIS_SAFE,
icon: GNOSIS_ICON_URL, getIcon: () => GNOSIS_ICON_URL,
shouldDisplay: false, shouldDisplay: () => false,
} }
const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>( const [web3WalletConnect, web3WalletConnectHooks] = initializeConnector<WalletConnectPopup>(
(actions) => new WalletConnectPopup({ actions, onError }) (actions) => new WalletConnectPopup({ actions, onError })
) )
export const walletConnectConnection: Connection = { export const walletConnectConnection: Connection = {
name: 'WalletConnect', getName: () => 'WalletConnect',
connector: web3WalletConnect, connector: web3WalletConnect,
hooks: web3WalletConnectHooks, hooks: web3WalletConnectHooks,
type: ConnectionType.WALLET_CONNECT, type: ConnectionType.WALLET_CONNECT,
icon: WALLET_CONNECT_ICON_URL, getIcon: () => WALLET_CONNECT_ICON_URL,
shouldDisplay: !getIsInjectedMobileBrowser, shouldDisplay: () => !getIsInjectedMobileBrowser(),
} }
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>( const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
(actions) => new UniwalletConnect({ actions, onError }) (actions) => new UniwalletConnect({ actions, onError })
) )
export const uniwalletConnectConnection: Connection = { export const uniwalletConnectConnection: Connection = {
name: 'Uniswap Wallet', getName: () => 'Uniswap Wallet',
connector: web3UniwalletConnect, connector: web3UniwalletConnect,
hooks: web3UniwalletConnectHooks, hooks: web3UniwalletConnectHooks,
type: ConnectionType.UNIWALLET, type: ConnectionType.UNIWALLET,
icon: UNIWALLET_ICON_URL, getIcon: () => UNIWALLET_ICON_URL,
shouldDisplay: Boolean(!getIsInjectedMobileBrowser && !isNonIOSPhone), shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
isNew: true, isNew: true,
} }
...@@ -134,24 +132,28 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba ...@@ -134,24 +132,28 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
}) })
) )
export const coinbaseWalletConnection: Connection = { const coinbaseWalletConnection: Connection = {
name: 'Coinbase Wallet', getName: () => 'Coinbase Wallet',
connector: web3CoinbaseWallet, connector: web3CoinbaseWallet,
hooks: web3CoinbaseWalletHooks, hooks: web3CoinbaseWalletHooks,
type: ConnectionType.COINBASE_WALLET, type: ConnectionType.COINBASE_WALLET,
icon: COINBASE_ICON_URL, getIcon: () => COINBASE_ICON_URL,
shouldDisplay: Boolean((isMobile && !getIsInjectedMobileBrowser) || !isMobile || isCoinbaseWalletBrowser), shouldDisplay: () =>
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app // If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
overrideActivate: overrideActivate: () => {
isMobile && !getIsInjectedMobileBrowser if (isMobile && !getIsInjectedMobileBrowser()) {
? () => window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet') window.open('https://go.cb-w.com/mtUDhEZPy1', 'cbwallet')
: undefined, return true
}
return false
},
} }
export function getConnections(isDarkMode: boolean) { export function getConnections() {
return [ return [
uniwalletConnectConnection, uniwalletConnectConnection,
isDarkMode ? darkInjectedConnection : lightInjectedConnection, injectedConnection,
walletConnectConnection, walletConnectConnection,
coinbaseWalletConnection, coinbaseWalletConnection,
gnosisSafeConnection, gnosisSafeConnection,
...@@ -159,17 +161,10 @@ export function getConnections(isDarkMode: boolean) { ...@@ -159,17 +161,10 @@ export function getConnections(isDarkMode: boolean) {
] ]
} }
export function useConnections() {
const isDarkMode = useIsDarkMode()
return getConnections(isDarkMode)
}
export function useGetConnection() { export function useGetConnection() {
const isDarkMode = useIsDarkMode() return useCallback((c: Connector | ConnectionType) => {
return useCallback(
(c: Connector | ConnectionType) => {
if (c instanceof Connector) { if (c instanceof Connector) {
const connection = getConnections(isDarkMode).find((connection) => connection.connector === c) const connection = getConnections().find((connection) => connection.connector === c)
if (!connection) { if (!connection) {
throw Error('unsupported connector') throw Error('unsupported connector')
} }
...@@ -177,7 +172,7 @@ export function useGetConnection() { ...@@ -177,7 +172,7 @@ export function useGetConnection() {
} else { } else {
switch (c) { switch (c) {
case ConnectionType.INJECTED: case ConnectionType.INJECTED:
return isDarkMode ? darkInjectedConnection : lightInjectedConnection return injectedConnection
case ConnectionType.COINBASE_WALLET: case ConnectionType.COINBASE_WALLET:
return coinbaseWalletConnection return coinbaseWalletConnection
case ConnectionType.WALLET_CONNECT: case ConnectionType.WALLET_CONNECT:
...@@ -190,7 +185,5 @@ export function useGetConnection() { ...@@ -190,7 +185,5 @@ export function useGetConnection() {
return gnosisSafeConnection return gnosisSafeConnection
} }
} }
}, }, [])
[isDarkMode]
)
} }
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 // 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 // This variable should be true only when using the MetaMask extension
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask // https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect' type NonMetaMaskFlag = 'isRabby' | 'isBraveWallet' | 'isTrustWallet' | 'isLedgerConnect'
const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect'] const allNonMetamaskFlags: NonMetaMaskFlag[] = ['isRabby', 'isBraveWallet', 'isTrustWallet', 'isLedgerConnect']
export const isMetaMaskWallet = Boolean( export const getIsMetaMaskWallet = () =>
window.ethereum?.isMetaMask && !allNonMetamaskFlags.some((flag) => window.ethereum?.[flag]) 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 // https://eips.ethereum.org/EIPS/eip-1193#provider-errors
export enum ErrorCode { export enum ErrorCode {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment