refactor: wallet specific Option components (#4065)

* refactor: wallet specific Option components

* fix

* fix

* fix coinbase wallet logic

* injected logic

* remove wallet.ts

* install metamask

* move all into InjectedOption

* fix mobile metamask

* wip

* more mocking

* more test fixes

* refactor

* more special casing

* isMetaMask

* simplify components

* fix imports

* fix coinbase wallet

* test fix

* fix connectors changing

* Revert "fix connectors changing"

This reverts commit 2acfe645ca506048e599d515674a54b27d12144f.

* more to typescript logic instead of jsx
This commit is contained in:
Vignesh Mohankumar 2022-07-12 12:33:24 -10:00 committed by GitHub
parent 817d808ec5
commit 869691d43f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 246 additions and 201 deletions

@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import CopyHelper from 'components/AccountDetails/Copy'
import { coinbaseWalletConnection, injectedConnection } from 'connection'
import { getConnection } from 'connection/utils'
import { coinbaseWalletConnection } from 'connection'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMask } from 'connection/utils'
import { useCallback, useContext } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
@ -11,7 +11,6 @@ import styled, { ThemeContext } from 'styled-components/macro'
import { isMobile } from 'utils/userAgent'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { shortenAddress } from '../../utils'
@ -210,23 +209,14 @@ export default function AccountDetails({
const theme = useContext(ThemeContext)
const dispatch = useAppDispatch()
const isMetaMask = !!window.ethereum?.isMetaMask
const isCoinbaseWallet = !!window.ethereum?.isCoinbaseWallet
const isMetaMask = getIsMetaMask()
const isCoinbaseWallet = getIsCoinbaseWallet()
const isInjectedMobileBrowser = (isMetaMask || isCoinbaseWallet) && isMobile
function formatConnectorName() {
const { ethereum } = window
const isMetaMask = !!(ethereum && ethereum.isMetaMask)
const name = Object.keys(SUPPORTED_WALLETS)
.filter(
(k) =>
SUPPORTED_WALLETS[k].connector === connector &&
(connector !== injectedConnection.connector || isMetaMask === (k === 'METAMASK'))
)
.map((k) => SUPPORTED_WALLETS[k].name)[0]
return (
<WalletName>
<Trans>Connected with {name}</Trans>
<Trans>Connected with</Trans> {getConnectionName(connectionType, isMetaMask)}
</WalletName>
)
}

@ -0,0 +1,36 @@
import { Connector } from '@web3-react/types'
import COINBASE_ICON_URL from 'assets/images/coinbaseWalletIcon.svg'
import { coinbaseWalletConnection, ConnectionType } from 'connection'
import { getConnectionName } from 'connection/utils'
import Option from './Option'
const BASE_PROPS = {
color: '#315CF5',
icon: COINBASE_ICON_URL,
id: 'coinbase-wallet',
}
export function OpenCoinbaseWalletOption() {
const isActive = coinbaseWalletConnection.hooks.useIsActive()
return (
<Option
{...BASE_PROPS}
isActive={isActive}
link="https://go.cb-w.com/mtUDhEZPy1"
header="Open in Coinbase Wallet"
/>
)
}
export function CoinbaseWalletOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = coinbaseWalletConnection.hooks.useIsActive()
return (
<Option
{...BASE_PROPS}
isActive={isActive}
onClick={() => tryActivation(coinbaseWalletConnection.connector)}
header={getConnectionName(ConnectionType.COINBASE_WALLET)}
/>
)
}

@ -0,0 +1,24 @@
import { Connector } from '@web3-react/types'
import FORTMATIC_ICON_URL from 'assets/images/fortmaticIcon.png'
import { ConnectionType, fortmaticConnection } from 'connection'
import { getConnectionName } from 'connection/utils'
import Option from './Option'
const BASE_PROPS = {
color: '#6748FF',
icon: FORTMATIC_ICON_URL,
id: 'fortmatic',
}
export function FortmaticOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = fortmaticConnection.hooks.useIsActive()
return (
<Option
{...BASE_PROPS}
isActive={isActive}
onClick={() => tryActivation(fortmaticConnection.connector)}
header={getConnectionName(ConnectionType.FORTMATIC)}
/>
)
}

@ -0,0 +1,48 @@
import { Trans } from '@lingui/macro'
import { Connector } from '@web3-react/types'
import INJECTED_ICON_URL from 'assets/images/arrow-right.svg'
import METAMASK_ICON_URL from 'assets/images/metamask.png'
import { ConnectionType, injectedConnection } from 'connection'
import { getConnectionName } from 'connection/utils'
import Option from './Option'
const INJECTED_PROPS = {
color: '#010101',
icon: INJECTED_ICON_URL,
id: 'injected',
}
const METAMASK_PROPS = {
color: '#E8831D',
icon: METAMASK_ICON_URL,
id: 'metamask',
}
export function InstallMetaMaskOption() {
return <Option {...METAMASK_PROPS} header={<Trans>Install MetaMask</Trans>} link={'https://metamask.io/'} />
}
export function MetaMaskOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = injectedConnection.hooks.useIsActive()
return (
<Option
{...METAMASK_PROPS}
isActive={isActive}
header={getConnectionName(ConnectionType.INJECTED, true)}
onClick={() => tryActivation(injectedConnection.connector)}
/>
)
}
export function InjectedOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = injectedConnection.hooks.useIsActive()
return (
<Option
{...INJECTED_PROPS}
isActive={isActive}
header={getConnectionName(ConnectionType.INJECTED, false)}
onClick={() => tryActivation(injectedConnection.connector)}
/>
)
}

@ -95,7 +95,7 @@ export default function Option({
onClick = null,
color,
header,
subheader = null,
subheader,
icon,
isActive = false,
id,
@ -106,7 +106,7 @@ export default function Option({
onClick?: null | (() => void)
color: string
header: React.ReactNode
subheader: React.ReactNode | null
subheader?: React.ReactNode
icon: string
isActive?: boolean
id: string

@ -0,0 +1,24 @@
import { Connector } from '@web3-react/types'
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
import { ConnectionType, walletConnectConnection } from 'connection'
import { getConnectionName } from 'connection/utils'
import Option from './Option'
const BASE_PROPS = {
color: '#4196FC',
icon: WALLET_CONNECT_ICON_URL,
id: 'wallet-connect',
}
export function WalletConnectOption({ tryActivation }: { tryActivation: (connector: Connector) => void }) {
const isActive = walletConnectConnection.hooks.useIsActive()
return (
<Option
{...BASE_PROPS}
isActive={isActive}
onClick={() => tryActivation(walletConnectConnection.connector)}
header={getConnectionName(ConnectionType.WALLET_CONNECT)}
/>
)
}

@ -1,14 +1,12 @@
import * as connectionUtils from 'connection/utils'
import { ApplicationModal } from 'state/application/reducer'
import { render, screen } from '../../test-utils'
import WalletModal from './index'
beforeEach(() => {
delete global.window.ethereum
})
afterAll(() => {
delete global.window.ethereum
afterEach(() => {
jest.clearAllMocks()
jest.resetModules()
})
const UserAgentMock = jest.requireMock('utils/userAgent')
@ -47,8 +45,23 @@ it('loads Wallet Modal on desktop', async () => {
expect(screen.getAllByTestId('wallet-modal-option')).toHaveLength(4)
})
it('loads Wallet Modal on desktop with generic Injected', async () => {
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Injected')).toBeInTheDocument()
expect(screen.getByText('Coinbase Wallet')).toBeInTheDocument()
expect(screen.getByText('WalletConnect')).toBeInTheDocument()
expect(screen.getByText('Fortmatic')).toBeInTheDocument()
expect(screen.getAllByTestId('wallet-modal-option')).toHaveLength(4)
})
it('loads Wallet Modal on desktop with MetaMask installed', async () => {
global.window.ethereum = { isMetaMask: true }
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('MetaMask')).toBeInTheDocument()
@ -61,6 +74,10 @@ it('loads Wallet Modal on desktop with MetaMask installed', async () => {
it('loads Wallet Modal on mobile', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Open in Coinbase Wallet')).toBeInTheDocument()
expect(screen.getByText('WalletConnect')).toBeInTheDocument()
@ -70,7 +87,10 @@ it('loads Wallet Modal on mobile', async () => {
it('loads Wallet Modal on MetaMask browser', async () => {
UserAgentMock.isMobile = true
global.window.ethereum = { isMetaMask: true }
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('MetaMask')).toBeInTheDocument()
@ -79,7 +99,10 @@ it('loads Wallet Modal on MetaMask browser', async () => {
it('loads Wallet Modal on Coinbase Wallet browser', async () => {
UserAgentMock.isMobile = true
global.window.ethereum = { isCoinbaseWallet: true }
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(true)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Coinbase Wallet')).toBeInTheDocument()

@ -4,27 +4,28 @@ import { Connector } from '@web3-react/types'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { ConnectionType, injectedConnection } from 'connection'
import { getConnection } from 'connection/utils'
import { ConnectionType } from 'connection'
import { getConnection, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { updateConnectionError } from 'state/connection/reducer'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { isMobile } from 'utils/userAgent'
import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { useModalIsOpen, useToggleWalletModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { ExternalLink, ThemedText } from '../../theme'
import { isMobile } from '../../utils/userAgent'
import AccountDetails from '../AccountDetails'
import { LightCard } from '../Card'
import Modal from '../Modal'
import Option from './Option'
import { CoinbaseWalletOption, OpenCoinbaseWalletOption } from './CoinbaseWalletOption'
import { FortmaticOption } from './FortmaticOption'
import { InjectedOption, InstallMetaMaskOption, MetaMaskOption } from './InjectedOption'
import PendingView from './PendingView'
import { WalletConnectOption } from './WalletConnectOption'
const CloseIcon = styled.div`
position: absolute;
@ -120,7 +121,7 @@ export default function WalletModal({
ENSName?: string
}) {
const dispatch = useAppDispatch()
const { connector, account } = useWeb3React()
const { account } = useWeb3React()
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
@ -182,91 +183,48 @@ export default function WalletModal({
[dispatch, toggleWalletModal]
)
// get wallets user can switch too, depending on device/browser
function getOptions() {
const isMetaMask = !!window.ethereum?.isMetaMask
const isCoinbaseWallet = !!window.ethereum?.isCoinbaseWallet
return Object.keys(SUPPORTED_WALLETS).map((key) => {
const option = SUPPORTED_WALLETS[key]
const isInjected = getIsInjected()
const isMetaMask = getIsMetaMask()
const isCoinbaseWallet = getIsCoinbaseWallet()
const optionProps = {
isActive: option.connector === connector,
id: `connect-${key}`,
link: option.href,
header: option.name,
color: option.color,
key,
icon: option.iconURL,
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
const isMetaMaskBrowser = isMobile && isMetaMask
const isInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
let injectedOption
if (!isInjected) {
if (!isMobile) {
injectedOption = <InstallMetaMaskOption />
}
// check for mobile options
if (isMobile) {
if (
(!window.web3 && !window.ethereum && option.mobile) ||
(isMetaMask && option.name === 'MetaMask') ||
(isCoinbaseWallet && option.name === 'Coinbase Wallet')
) {
return (
<Option
{...optionProps}
onClick={() => {
if (!option.href && !!option.connector) {
tryActivation(option.connector)
}
}}
subheader={null}
/>
)
}
return null
} else if (!isCoinbaseWallet) {
if (isMetaMask) {
injectedOption = <MetaMaskOption tryActivation={tryActivation} />
} else {
injectedOption = <InjectedOption tryActivation={tryActivation} />
}
}
// overwrite injected when needed
if (option.connector === injectedConnection.connector) {
// don't show injected if there's no injected provider
if (!(window.web3 || window.ethereum)) {
if (option.name === 'MetaMask') {
return (
<Option
id={`connect-${key}`}
key={key}
color={'#E8831D'}
header={<Trans>Install MetaMask</Trans>}
subheader={null}
link={'https://metamask.io/'}
icon={MetamaskIcon}
/>
)
} else {
return null //dont want to return install twice
}
}
// don't return metamask if injected provider isn't metamask
else if (option.name === 'MetaMask' && !isMetaMask) {
return null
}
// likewise for generic
else if (option.name === 'Injected' && isMetaMask) {
return null
}
}
let coinbaseWalletOption
if (isMobile && !isInjectedMobileBrowser) {
coinbaseWalletOption = <OpenCoinbaseWalletOption />
} else if (!isMobile || isCoinbaseWalletBrowser) {
coinbaseWalletOption = <CoinbaseWalletOption tryActivation={tryActivation} />
}
// return rest of options
return (
!isMobile &&
!option.mobileOnly && (
<Option
{...optionProps}
onClick={() => {
option.connector === connector
? setWalletView(WALLET_VIEWS.ACCOUNT)
: !option.href && option.connector && tryActivation(option.connector)
}}
subheader={null} //use option.descriptio to bring back multi-line
/>
)
)
})
const walletConnectionOption =
(!isInjectedMobileBrowser && <WalletConnectOption tryActivation={tryActivation} />) ?? null
const fortmaticOption = (!isInjectedMobileBrowser && <FortmaticOption tryActivation={tryActivation} />) ?? null
return (
<>
{injectedOption}
{coinbaseWalletOption}
{walletConnectionOption}
{fortmaticOption}
</>
)
}
function getModalContent() {

@ -18,6 +18,18 @@ const CONNECTIONS = [
gnosisSafeConnection,
]
export function getIsInjected(): boolean {
return Boolean(window.ethereum)
}
export function getIsMetaMask(): boolean {
return window.ethereum?.isMetaMask ?? false
}
export function getIsCoinbaseWallet(): boolean {
return window.ethereum?.isCoinbaseWallet ?? false
}
export function getConnection(c: Connector | ConnectionType) {
if (c instanceof Connector) {
const connection = CONNECTIONS.find((connection) => connection.connector === c)
@ -42,3 +54,20 @@ export function getConnection(c: Connector | ConnectionType) {
}
}
}
export function getConnectionName(connectionType: ConnectionType, isMetaMask?: boolean) {
switch (connectionType) {
case ConnectionType.INJECTED:
return isMetaMask ? 'MetaMask' : 'Injected'
case ConnectionType.COINBASE_WALLET:
return 'Coinbase Wallet'
case ConnectionType.WALLET_CONNECT:
return 'WalletConnect'
case ConnectionType.FORTMATIC:
return 'Fortmatic'
case ConnectionType.NETWORK:
return 'Network'
case ConnectionType.GNOSIS_SAFE:
return 'Gnosis Safe'
}
}

@ -1,87 +0,0 @@
import { Connector } from '@web3-react/types'
import {
coinbaseWalletConnection,
ConnectionType,
fortmaticConnection,
injectedConnection,
walletConnectConnection,
} from 'connection'
import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'
import FORTMATIC_ICON_URL from '../assets/images/fortmaticIcon.png'
import METAMASK_ICON_URL from '../assets/images/metamask.png'
import WALLETCONNECT_ICON_URL from '../assets/images/walletConnectIcon.svg'
interface WalletInfo {
connector?: Connector
connectionType?: ConnectionType
name: string
iconURL: string
description: string
href: string | null
color: string
primary?: true
mobile?: true
mobileOnly?: true
}
export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
INJECTED: {
connector: injectedConnection.connector,
connectionType: ConnectionType.INJECTED,
name: 'Injected',
iconURL: INJECTED_ICON_URL,
description: 'Injected web3 provider.',
href: null,
color: '#010101',
primary: true,
},
METAMASK: {
connector: injectedConnection.connector,
connectionType: ConnectionType.INJECTED,
name: 'MetaMask',
iconURL: METAMASK_ICON_URL,
description: 'Easy-to-use browser extension.',
href: null,
color: '#E8831D',
},
WALLET_CONNECT: {
connector: walletConnectConnection.connector,
connectionType: ConnectionType.WALLET_CONNECT,
name: 'WalletConnect',
iconURL: WALLETCONNECT_ICON_URL,
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC',
mobile: true,
},
COINBASE_WALLET: {
connector: coinbaseWalletConnection.connector,
connectionType: ConnectionType.COINBASE_WALLET,
name: 'Coinbase Wallet',
iconURL: COINBASE_ICON_URL,
description: 'Use Coinbase Wallet app on mobile device',
href: null,
color: '#315CF5',
},
COINBASE_LINK: {
name: 'Open in Coinbase Wallet',
iconURL: COINBASE_ICON_URL,
description: 'Open in Coinbase Wallet app.',
href: 'https://go.cb-w.com/mtUDhEZPy1',
color: '#315CF5',
mobile: true,
mobileOnly: true,
},
FORTMATIC: {
connector: fortmaticConnection.connector,
connectionType: ConnectionType.FORTMATIC,
name: 'Fortmatic',
iconURL: FORTMATIC_ICON_URL,
description: 'Login using Fortmatic hosted wallet',
href: null,
color: '#6748FF',
mobile: true,
},
}

@ -1,6 +1,6 @@
import { Connector } from '@web3-react/types'
import { gnosisSafeConnection, injectedConnection, networkConnection } from 'connection'
import { getConnection } from 'connection/utils'
import { getConnection, getIsMetaMask } from 'connection/utils'
import { useEffect } from 'react'
import { BACKFILLABLE_WALLETS } from 'state/connection/constants'
import { useAppSelector } from 'state/hooks'
@ -22,7 +22,7 @@ export default function useEagerlyConnect() {
const selectedWalletBackfilled = useAppSelector((state) => state.user.selectedWalletBackfilled)
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
const isMetaMask = !!window.ethereum?.isMetaMask
const isMetaMask = getIsMetaMask()
useEffect(() => {
connect(gnosisSafeConnection.connector)