Wallet Support with web3-react v6 (#514)
* basic support for desktop and mobile, with or without web3 * stable before mobile view update * mobile modal views * remove unused modules * create global context for wallet modal, update to click button to connect * first pass * drag with react pose * try without pose * replace context with new syntax, basic setup * stable version on all browser types * remove dev flags * fix swap broken * update to clickable connect button if logged out * stable, good entry * fix bugs, exit animations * prep for merge * stable version with updated application context * update animations with correct gesture package * refactor wallet logic to multi-root * small fixes * Style Updates * remove unused imports * refactor wallet page
14
package.json
@ -8,15 +8,21 @@
|
||||
"@reach/dialog": "^0.2.8",
|
||||
"@reach/tooltip": "^0.2.0",
|
||||
"@uniswap/sdk": "^1.0.0-beta.4",
|
||||
"@web3-react/core": "^6.0.0-beta.15",
|
||||
"@web3-react/injected-connector": "^6.0.0-beta.17",
|
||||
"@web3-react/network-connector": "^6.0.0-beta.15",
|
||||
"@web3-react/walletconnect-connector": "^6.0.0-beta.18",
|
||||
"@web3-react/walletlink-connector": "^6.0.0-beta.15",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"history": "^4.9.0",
|
||||
"ethers": "^4.0.36",
|
||||
"history": "^4.9.0",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"polished": "^3.3.2",
|
||||
"qrcode.react": "^0.9.3",
|
||||
"react": "^16.8.6",
|
||||
"react-device-detect": "^1.6.2",
|
||||
"react-dom": "^16.8.6",
|
||||
@ -25,10 +31,10 @@
|
||||
"react-i18next": "^10.7.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "^3.0.1",
|
||||
"react-spring": "^8.0.20",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-switch": "^5.0.1",
|
||||
"styled-components": "^4.2.0",
|
||||
"web3-react": "5.0.5"
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"styled-components": "^4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -1,100 +0,0 @@
|
||||
import { Connectors } from 'web3-react'
|
||||
const { Connector, ErrorCodeMixin } = Connectors
|
||||
|
||||
const InjectedConnectorErrorCodes = ['ETHEREUM_ACCESS_DENIED', 'NO_WEB3', 'UNLOCK_REQUIRED']
|
||||
export default class InjectedConnector extends ErrorCodeMixin(Connector, InjectedConnectorErrorCodes) {
|
||||
constructor(args = {}) {
|
||||
super(args)
|
||||
this.runOnDeactivation = []
|
||||
|
||||
this.networkChangedHandler = this.networkChangedHandler.bind(this)
|
||||
this.accountsChangedHandler = this.accountsChangedHandler.bind(this)
|
||||
|
||||
const { ethereum } = window
|
||||
if (ethereum && ethereum.isMetaMask) {
|
||||
ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
}
|
||||
|
||||
async onActivation() {
|
||||
const { ethereum, web3 } = window
|
||||
|
||||
if (ethereum) {
|
||||
await ethereum.enable().catch(error => {
|
||||
const deniedAccessError = Error(error)
|
||||
deniedAccessError.code = InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED
|
||||
throw deniedAccessError
|
||||
})
|
||||
|
||||
// initialize event listeners
|
||||
if (ethereum.on) {
|
||||
ethereum.on('networkChanged', this.networkChangedHandler)
|
||||
ethereum.on('accountsChanged', this.accountsChangedHandler)
|
||||
|
||||
this.runOnDeactivation.push(() => {
|
||||
if (ethereum.removeListener) {
|
||||
ethereum.removeListener('networkChanged', this.networkChangedHandler)
|
||||
ethereum.removeListener('accountsChanged', this.accountsChangedHandler)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (web3) {
|
||||
console.warn('Your web3 provider is outdated, please upgrade to a modern provider.')
|
||||
} else {
|
||||
const noWeb3Error = Error('Your browser is not equipped with web3 capabilities.')
|
||||
noWeb3Error.code = InjectedConnector.errorCodes.NO_WEB3
|
||||
throw noWeb3Error
|
||||
}
|
||||
}
|
||||
|
||||
async getProvider() {
|
||||
const { ethereum, web3 } = window
|
||||
return ethereum || web3.currentProvider
|
||||
}
|
||||
|
||||
async getAccount(provider) {
|
||||
const account = await super.getAccount(provider)
|
||||
|
||||
if (account === null) {
|
||||
const unlockRequiredError = Error('Ethereum account locked.')
|
||||
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
|
||||
throw unlockRequiredError
|
||||
}
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
onDeactivation() {
|
||||
this.runOnDeactivation.forEach(runner => runner())
|
||||
this.runOnDeactivation = []
|
||||
}
|
||||
|
||||
// event handlers
|
||||
networkChangedHandler(networkId) {
|
||||
const networkIdNumber = Number(networkId)
|
||||
|
||||
try {
|
||||
super._validateNetworkId(networkIdNumber)
|
||||
|
||||
super._web3ReactUpdateHandler({
|
||||
updateNetworkId: true,
|
||||
networkId: networkIdNumber
|
||||
})
|
||||
} catch (error) {
|
||||
super._web3ReactErrorHandler(error)
|
||||
}
|
||||
}
|
||||
|
||||
accountsChangedHandler(accounts) {
|
||||
if (!accounts[0]) {
|
||||
const unlockRequiredError = Error('Ethereum account locked.')
|
||||
unlockRequiredError.code = InjectedConnector.errorCodes.UNLOCK_REQUIRED
|
||||
super._web3ReactErrorHandler(unlockRequiredError)
|
||||
} else {
|
||||
super._web3ReactUpdateHandler({
|
||||
updateAccount: true,
|
||||
account: accounts[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { ethers } from 'ethers'
|
||||
import { Connectors } from 'web3-react'
|
||||
|
||||
const { Connector } = Connectors
|
||||
|
||||
function getFallbackProvider(providerURL) {
|
||||
if (Number(process.env.REACT_APP_NETWORK_ID) === 1) {
|
||||
const etherscan = new ethers.providers.EtherscanProvider()
|
||||
const infura = new ethers.providers.JsonRpcProvider(providerURL)
|
||||
|
||||
const providers = [infura, etherscan]
|
||||
|
||||
return new ethers.providers.FallbackProvider(providers)
|
||||
} else {
|
||||
const infura = new ethers.providers.JsonRpcProvider(providerURL)
|
||||
|
||||
const providers = [infura]
|
||||
|
||||
return new ethers.providers.FallbackProvider(providers)
|
||||
}
|
||||
}
|
||||
|
||||
export default class NetworkOnlyConnector extends Connector {
|
||||
constructor(kwargs) {
|
||||
const { providerURL, ...rest } = kwargs || {}
|
||||
super(rest)
|
||||
this.providerURL = providerURL
|
||||
}
|
||||
|
||||
async onActivation() {
|
||||
if (!this.engine) {
|
||||
const provider = getFallbackProvider(this.providerURL)
|
||||
provider.polling = false
|
||||
provider.pollingInterval = 300000 // 5 minutes
|
||||
this.engine = provider
|
||||
}
|
||||
}
|
||||
|
||||
async getNetworkId(provider) {
|
||||
const networkId = await provider.getNetwork().then(network => network.chainId)
|
||||
return super._validateNetworkId(networkId)
|
||||
}
|
||||
|
||||
async getProvider() {
|
||||
return this.engine
|
||||
}
|
||||
|
||||
async getAccount() {
|
||||
return null
|
||||
}
|
||||
}
|
3
src/assets/images/arrow-right.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.79261 16.1108L17.5398 8.36364L9.79261 0.616477L8.25852 2.15057L13.3807 7.25568H0V9.47159H13.3807L8.25852 14.5852L9.79261 16.1108Z" fill="#333639"/>
|
||||
</svg>
|
After Width: | Height: | Size: 263 B |
9
src/assets/images/coinbaseWalletIcon.svg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
src/assets/images/metamask.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
src/assets/images/trustWallet.png
Normal file
After Width: | Height: | Size: 19 KiB |
9
src/assets/images/walletConnectIcon.svg
Normal file
After Width: | Height: | Size: 13 KiB |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import Copy from './Copy'
|
||||
import { Check } from 'react-feather'
|
||||
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { Link, Spinner } from '../../theme'
|
||||
import Copy from './Copy'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { Check } from 'react-feather'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
@ -75,17 +75,17 @@ const ButtonWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function Transaction({ hash, pending }) {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
return (
|
||||
<TransactionWrapper key={hash}>
|
||||
<TransactionStatusWrapper>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||
<Copy toCopy={hash} />
|
||||
</TransactionStatusWrapper>
|
||||
{pending ? (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Spinner src={Circle} id="pending" />
|
||||
<TransactionStatusText>Pending</TransactionStatusText>
|
||||
@ -94,7 +94,7 @@ export default function Transaction({ hash, pending }) {
|
||||
</ButtonWrapper>
|
||||
) : (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Check size="16" />
|
||||
<TransactionStatusText>Confirmed</TransactionStatusText>
|
327
src/components/AccountDetails/index.js
Normal file
@ -0,0 +1,327 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { injected, walletconnect, walletlink } from '../../connectors'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
|
||||
const OptionButton = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
padding: 8px 24px;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.malibuBlue};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
padding: 1.5rem 1.5rem;
|
||||
font-weight: 500;
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : 'inherit')};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const UpperSection = styled.div`
|
||||
position: relative;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h5:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoCard = styled.div`
|
||||
padding: 1rem;
|
||||
border: 1px solid ${({ theme }) => theme.placeholderGray};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const AccountGroupingRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
|
||||
div {
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
padding: 0rem 1.5rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const GreenCircle = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-child {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-left: 12px;
|
||||
margin-right: 2px;
|
||||
margin-top: -6px;
|
||||
background-color: ${({ theme }) => theme.connectedGreen};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const GreenText = styled.div`
|
||||
color: ${({ theme }) => theme.connectedGreen};
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 2rem;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
|
||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const ConnectButtonRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.royalBlue : theme.doveGray) : theme.royalBlue)};
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.chaliceGray};
|
||||
}
|
||||
`
|
||||
|
||||
const WalletName = styled.div`
|
||||
padding-left: 0.5rem;
|
||||
width: initial;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions, pending) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
return <Transaction key={i} hash={hash} pending={pending} />
|
||||
})}
|
||||
</TransactionListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AccountDetails({
|
||||
toggleWalletModal,
|
||||
pendingTransactions,
|
||||
confirmedTransactions,
|
||||
ENSName,
|
||||
openOptions
|
||||
}) {
|
||||
const { chainId, account, connector } = useWeb3React()
|
||||
|
||||
function formatConnectorName() {
|
||||
const isMetaMask = window.ethereum && window.ethereum.isMetaMask
|
||||
const name = Object.keys(SUPPORTED_WALLETS)
|
||||
.filter(
|
||||
k =>
|
||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||
)
|
||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||
|
||||
return <WalletName>{name}</WalletName>
|
||||
}
|
||||
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<Identicon /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor alt={'close icon'} />
|
||||
</CloseIcon>
|
||||
<HeaderRow>Account</HeaderRow>
|
||||
<AccountSection>
|
||||
<YourAccount>
|
||||
<InfoCard>
|
||||
<AccountGroupingRow>
|
||||
{getStatusIcon()}
|
||||
<GreenText>
|
||||
<GreenCircle>
|
||||
<div />
|
||||
</GreenCircle>
|
||||
</GreenText>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow>
|
||||
{ENSName ? (
|
||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
{ENSName} ↗{' '}
|
||||
</StyledLink>
|
||||
<Copy toCopy={ENSName} />
|
||||
</AccountControl>
|
||||
) : (
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
|
||||
{account} ↗{' '}
|
||||
</StyledLink>
|
||||
<Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
)}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
{!isMobile && (
|
||||
<ConnectButtonRow>
|
||||
<OptionButton
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Connect to a different wallet
|
||||
</OptionButton>
|
||||
</ConnectButtonRow>
|
||||
)}
|
||||
</AccountSection>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<h5>Recent Transactions</h5>
|
||||
{renderTransactions(pendingTransactions, true)}
|
||||
{renderTransactions(confirmedTransactions, false)}
|
||||
</LowerSection>
|
||||
) : (
|
||||
<LowerSection>
|
||||
<h5>Your transactions will appear here...</h5>
|
||||
</LowerSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useDebounce } from '../../hooks'
|
||||
import { useWeb3React, useDebounce } from '../../hooks'
|
||||
|
||||
const InputPanel = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
@ -73,7 +72,7 @@ const Input = styled.input`
|
||||
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { library } = useWeb3Context()
|
||||
const { library } = useWeb3React()
|
||||
|
||||
const [input, setInput] = useState(initialInput.address ? initialInput.address : '')
|
||||
|
||||
|
@ -3,7 +3,6 @@ import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ethers } from 'ethers'
|
||||
import { BigNumber } from '@uniswap/sdk'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import styled from 'styled-components'
|
||||
import escapeStringRegex from 'escape-string-regexp'
|
||||
import { darken } from 'polished'
|
||||
@ -12,7 +11,7 @@ import '@reach/tooltip/styles.css'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
|
||||
import { BorderlessInput } from '../../theme'
|
||||
import { useTokenContract } from '../../hooks'
|
||||
import { useWeb3React, useTokenContract } from '../../hooks'
|
||||
import { isAddress, calculateGasMargin, formatToUsd, formatTokenBalance, formatEthBalance } from '../../utils'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import Modal from '../Modal'
|
||||
@ -225,7 +224,10 @@ const TokenModalRow = styled.div`
|
||||
background-color: ${({ theme }) => theme.tokenRowHover};
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0.8rem 1rem;`}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 0.8rem 1rem;
|
||||
padding-right: 2rem;
|
||||
`}
|
||||
`
|
||||
|
||||
const TokenRowLeft = styled.div`
|
||||
@ -297,7 +299,7 @@ export default function CurrencyInputPanel({
|
||||
|
||||
const allTokens = useAllTokenDetails()
|
||||
|
||||
const { account } = useWeb3Context()
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const userTokenBalance = useAddressBalance(account, selectedTokenAddress)
|
||||
|
||||
@ -426,7 +428,6 @@ export default function CurrencyInputPanel({
|
||||
{!disableTokenSelect && (
|
||||
<CurrencySelectModal
|
||||
isOpen={modalIsOpen}
|
||||
// isOpen={true}
|
||||
onDismiss={() => {
|
||||
setModalIsOpen(false)
|
||||
}}
|
||||
@ -446,7 +447,7 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances })
|
||||
|
||||
const allTokens = useAllTokenDetails()
|
||||
|
||||
const { account } = useWeb3Context()
|
||||
const { account } = useWeb3React()
|
||||
|
||||
// BigNumber.js instance
|
||||
const ethPrice = useUSDPrice()
|
||||
@ -606,6 +607,7 @@ function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, allBalances })
|
||||
isOpen={isOpen}
|
||||
onDismiss={clearInputAndDismiss}
|
||||
minHeight={60}
|
||||
maxHeight={50}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
>
|
||||
<TokenModal>
|
||||
|
@ -1,14 +1,12 @@
|
||||
import React, { useState, useReducer, useEffect } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { createBrowserHistory } from 'history'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { ethers } from 'ethers'
|
||||
import styled from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Button } from '../../theme'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import CurrencyInputPanel from '../CurrencyInputPanel'
|
||||
import AddressInputPanel from '../AddressInputPanel'
|
||||
import OversizedPanel from '../OversizedPanel'
|
||||
@ -21,6 +19,7 @@ import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||
import { useFetchAllBalances } from '../../contexts/AllBalances'
|
||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||
import { useWalletModalToggle } from '../../contexts/Application'
|
||||
|
||||
const INPUT = 0
|
||||
const OUTPUT = 1
|
||||
@ -246,7 +245,7 @@ function getMarketRate(
|
||||
|
||||
export default function ExchangePage({ initialCurrency, sending = false, params }) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useWeb3Context()
|
||||
const { account, error } = useWeb3React()
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
@ -294,7 +293,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
|
||||
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
|
||||
|
||||
const [recipient, setRecipient] = useState({ address: initialRecipient(), name: '' })
|
||||
const [recipient, setRecipient] = useState({
|
||||
address: initialRecipient(),
|
||||
name: ''
|
||||
})
|
||||
const [recipientError, setRecipientError] = useState()
|
||||
|
||||
// get swap type from the currency types
|
||||
@ -416,7 +418,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
@ -439,7 +444,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
@ -469,7 +477,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} else {
|
||||
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
|
||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
||||
@ -483,7 +494,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: calculatedDependentValue })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
@ -621,7 +635,10 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
}
|
||||
|
||||
const estimatedGasLimit = await estimate(...args, { value })
|
||||
method(...args, { value, gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN) }).then(response => {
|
||||
method(...args, {
|
||||
value,
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
|
||||
}).then(response => {
|
||||
addTransaction(response)
|
||||
})
|
||||
}
|
||||
@ -630,6 +647,8 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
|
||||
const allBalances = useFetchAllBalances()
|
||||
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
return (
|
||||
<>
|
||||
<CurrencyInputPanel
|
||||
@ -643,16 +662,25 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
if (valueToSet.gt(ethers.constants.Zero)) {
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false), field: INPUT }
|
||||
payload: {
|
||||
value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false),
|
||||
field: INPUT
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}}
|
||||
onCurrencySelected={inputCurrency => {
|
||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: inputCurrency, field: INPUT } })
|
||||
dispatchSwapState({
|
||||
type: 'SELECT_CURRENCY',
|
||||
payload: { currency: inputCurrency, field: INPUT }
|
||||
})
|
||||
}}
|
||||
onValueChange={inputValue => {
|
||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: inputValue, field: INPUT } })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: inputValue, field: INPUT }
|
||||
})
|
||||
}}
|
||||
showUnlock={showUnlock}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
@ -678,10 +706,16 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
|
||||
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
|
||||
onCurrencySelected={outputCurrency => {
|
||||
dispatchSwapState({ type: 'SELECT_CURRENCY', payload: { currency: outputCurrency, field: OUTPUT } })
|
||||
dispatchSwapState({
|
||||
type: 'SELECT_CURRENCY',
|
||||
payload: { currency: outputCurrency, field: OUTPUT }
|
||||
})
|
||||
}}
|
||||
onValueChange={outputValue => {
|
||||
dispatchSwapState({ type: 'UPDATE_INDEPENDENT', payload: { value: outputValue, field: OUTPUT } })
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: outputValue, field: OUTPUT }
|
||||
})
|
||||
}}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
selectedTokenAddress={outputCurrency}
|
||||
@ -753,11 +787,14 @@ export default function ExchangePage({ initialCurrency, sending = false, params
|
||||
/>
|
||||
<Flex>
|
||||
<Button
|
||||
disabled={!isValid || customSlippageError === 'invalid'}
|
||||
onClick={onSwap}
|
||||
disabled={!account && !error ? false : !isValid || customSlippageError === 'invalid'}
|
||||
onClick={account && !error ? onSwap : toggleWalletModal}
|
||||
warning={highSlippageWarning || customSlippageError === 'warning'}
|
||||
loggedOut={!account}
|
||||
>
|
||||
{sending
|
||||
{!account
|
||||
? 'Connect to a Wallet'
|
||||
: sending
|
||||
? highSlippageWarning || customSlippageError === 'warning'
|
||||
? t('sendAnyway')
|
||||
: t('send')
|
||||
|
28
src/components/Identicon/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import Jazzicon from 'jazzicon'
|
||||
|
||||
const StyledIdenticon = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 1.125rem;
|
||||
background-color: ${({ theme }) => theme.silverGray};
|
||||
`
|
||||
|
||||
export default function Identicon() {
|
||||
const ref = useRef()
|
||||
|
||||
const { account } = useWeb3React()
|
||||
|
||||
useEffect(() => {
|
||||
if (account && ref.current) {
|
||||
ref.current.innerHTML = ''
|
||||
ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
|
||||
}
|
||||
})
|
||||
|
||||
return <StyledIdenticon ref={ref} />
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { animated, useTransition } from 'react-spring'
|
||||
import { animated, useTransition, useSpring } from 'react-spring'
|
||||
import { Spring } from 'react-spring/renderprops'
|
||||
|
||||
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import '@reach/dialog/styles.css'
|
||||
import { transparentize } from 'polished'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
const WrappedDialogOverlay = ({ suppressClassNameWarning, ...rest }) => <AnimatedDialogOverlay {...rest} />
|
||||
const WrappedDialogOverlay = ({ suppressClassNameWarning, mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />
|
||||
const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
|
||||
suppressClassNameWarning: true
|
||||
})`
|
||||
@ -15,34 +19,71 @@ const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => 'transparent'};
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: ${({ theme }) => theme.modalBackground};
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
/* position: absolute; */
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const FilteredDialogContent = ({ minHeight, ...rest }) => <DialogContent {...rest} />
|
||||
const FilteredDialogContent = ({ minHeight, maxHeight, isOpen, slideInAnimation, mobile, ...rest }) => (
|
||||
<DialogContent {...rest} />
|
||||
)
|
||||
const StyledDialogContent = styled(FilteredDialogContent)`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.concreteGray};
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`margin: 0;`};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
|
||||
max-width: 650px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`width: 65vw;`}
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`width: 85vw;`}
|
||||
max-height: 50vh;
|
||||
${({ maxHeight }) =>
|
||||
maxHeight &&
|
||||
css`
|
||||
max-height: ${maxHeight}vh;
|
||||
`}
|
||||
${({ minHeight }) =>
|
||||
minHeight &&
|
||||
css`
|
||||
min-height: ${minHeight}vh;
|
||||
`}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`max-height: 65vh;`}
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`max-height: 80vh;`}
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile, isOpen }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
border-radius: 20px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
`}
|
||||
`}
|
||||
}
|
||||
`
|
||||
|
||||
@ -54,23 +95,97 @@ const HiddenCloseButton = styled.button`
|
||||
border: none;
|
||||
`
|
||||
|
||||
export default function Modal({ isOpen, onDismiss, minHeight = false, initialFocusRef, children }) {
|
||||
export default function Modal({ isOpen, onDismiss, minHeight = false, maxHeight = 50, initialFocusRef, children }) {
|
||||
const transitions = useTransition(isOpen, null, {
|
||||
config: { duration: 150 },
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
})
|
||||
if (velocity > 3 && state.direction[1] > 0) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isMobile) {
|
||||
return transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight}>
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
}}
|
||||
to={{
|
||||
transform: isOpen ? 'translateY(0px)' : 'translateY(200px)'
|
||||
}}
|
||||
>
|
||||
{props => (
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{ transform: xy.interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`) }}
|
||||
>
|
||||
<StyledDialogContent
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<StyledDialogContent
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { transparentize, darken } from 'polished'
|
||||
|
||||
import { Link } from '../../theme/components'
|
||||
|
||||
import { useBodyKeyDown } from '../../hooks'
|
||||
import { useWeb3React, useBodyKeyDown } from '../../hooks'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { isAddress } from '../../utils'
|
||||
import {
|
||||
@ -14,7 +12,7 @@ import {
|
||||
useSaiHolderMessageManager,
|
||||
useGeneralDaiMessageManager
|
||||
} from '../../contexts/LocalStorage'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { Link } from '../../theme/components'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
@ -102,7 +100,7 @@ const WarningHeader = styled.div`
|
||||
const WarningFooter = styled.div`
|
||||
margin-top: 10px;
|
||||
font-size: 10px;
|
||||
textdecoration: italic;
|
||||
text-decoration: italic;
|
||||
color: ${({ theme }) => theme.greyText};
|
||||
`
|
||||
|
||||
@ -164,7 +162,7 @@ function NavigationTabs({ location: { pathname }, history }) {
|
||||
|
||||
const [showSaiHolderMessage, dismissSaiHolderMessage] = useSaiHolderMessageManager()
|
||||
|
||||
const { account } = useWeb3Context()
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const daiBalance = useAddressBalance(account, isAddress('0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'))
|
||||
|
||||
|
@ -1,96 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { Link, Spinner } from '../../theme'
|
||||
import Copy from './Copy'
|
||||
|
||||
import { Check } from 'react-feather'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
const TransactionStatusWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const TransactionWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 0.75rem;
|
||||
a {
|
||||
/* flex: 1 1 auto; */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionStatusText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
`
|
||||
|
||||
const TransactionState = styled.div`
|
||||
background-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid;
|
||||
border-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
|
||||
|
||||
:hover {
|
||||
border-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
a {
|
||||
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
|
||||
}
|
||||
`
|
||||
|
||||
export default function Info({ hash, pending }) {
|
||||
const { networkId } = useWeb3Context()
|
||||
|
||||
return (
|
||||
<TransactionWrapper key={hash}>
|
||||
<TransactionStatusWrapper>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||
<Copy />
|
||||
</TransactionStatusWrapper>
|
||||
{pending ? (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Spinner src={Circle} alt="loader" />
|
||||
<TransactionStatusText>Pending</TransactionStatusText>
|
||||
</TransactionState>
|
||||
</Link>
|
||||
</ButtonWrapper>
|
||||
) : (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(networkId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Check size="16" />
|
||||
<TransactionStatusText>Confirmed</TransactionStatusText>
|
||||
</TransactionState>
|
||||
</Link>
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
</TransactionWrapper>
|
||||
)
|
||||
}
|
90
src/components/WalletModal/Option.js
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { transparentize } from 'polished'
|
||||
import { Link } from '../../theme'
|
||||
|
||||
const InfoCard = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundColor};
|
||||
padding: 1rem;
|
||||
border: 1px solid ${({ theme }) => theme.placeholderGray};
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 48px;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
height: 40px;
|
||||
grid-template-columns: 1fr 40px;
|
||||
padding: 0.6rem 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const OptionCardLeft = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)`
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid ${({ theme }) => theme.malibuBlue};
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderText = styled.div`
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : props.color)};
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
const SubHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
font-size: 10px;
|
||||
`};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Option({ link = null, size = null, onClick = null, color, header, subheader = null, icon }) {
|
||||
const content = (
|
||||
<OptionCardClickable onClick={onClick}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>{header}</HeaderText>
|
||||
{subheader && <SubHeader>{subheader}</SubHeader>}
|
||||
</OptionCardLeft>
|
||||
<IconWrapper size={size}>
|
||||
<img src={icon} alt={'Icon'} />
|
||||
</IconWrapper>
|
||||
</OptionCardClickable>
|
||||
)
|
||||
if (link) {
|
||||
return <Link href={link}>{content}</Link>
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
41
src/components/WalletModal/QrCode.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
import Option from './Option'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
|
||||
const QRSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
h5 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
`
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.placeholderGray};
|
||||
`
|
||||
|
||||
export default function QrCode({ uri, size, header, subheader, icon }) {
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<QRSection>
|
||||
<h5>Scan QR code with a compatible wallet</h5>
|
||||
<QRCodeWrapper>
|
||||
{uri && (
|
||||
<QRCode size={size} value={uri} bgColor={isDark ? '#333639' : 'white'} fgColor={isDark ? 'white' : 'black'} />
|
||||
)}
|
||||
</QRCodeWrapper>
|
||||
<Option color={'#4196FC'} header={header} subheader={subheader} icon={icon} />
|
||||
</QRSection>
|
||||
)
|
||||
}
|
@ -1,23 +1,64 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
|
||||
import Transaction from './Transaction'
|
||||
import Copy from './Copy'
|
||||
import Modal from '../Modal'
|
||||
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import AccountDetails from '../AccountDetails'
|
||||
import QrCode from './QrCode'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS, MOBILE_DEEP_LINKS } from '../../constants'
|
||||
import { usePrevious } from '../../hooks'
|
||||
import { Link } from '../../theme'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect } from '../../connectors'
|
||||
import { useWalletModalToggle, useWalletModalOpen } from '../../contexts/Application'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.chaliceGray};
|
||||
}
|
||||
`
|
||||
|
||||
const Wrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
background-color: ${({ theme }) => theme.backgroundColor};
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
padding: 1.5rem 1.5rem;
|
||||
font-weight: 500;
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : 'inherit')};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
background-color: ${({ theme }) => theme.backgroundColor};
|
||||
padding: 2rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 1rem`};
|
||||
`
|
||||
|
||||
const UpperSection = styled.div`
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
|
||||
h5 {
|
||||
@ -37,172 +78,229 @@ const UpperSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 2rem;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
/* margin-bottom: 0; */
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div`
|
||||
const Blurb = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
margin: 1rem;
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
${({ hasENS, isENS }) =>
|
||||
hasENS &&
|
||||
isENS &&
|
||||
css`
|
||||
margin-bottom: 0.75rem;
|
||||
`}
|
||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`500` : css`400`) : css`500`)};
|
||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? css`1rem` : css`0.8rem`) : css`1rem`)};
|
||||
const OptionGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: 10px;
|
||||
`};
|
||||
`
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
const HoverText = styled.div`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap} /* margin: 0 0 1rem 0; */
|
||||
`
|
||||
const WALLET_VIEWS = {
|
||||
OPTIONS: 'options',
|
||||
ACCOUNT: 'account',
|
||||
WALLET_CONNECT: 'walletConnect'
|
||||
}
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.royalBlue : theme.doveGray) : theme.royalBlue)};
|
||||
`
|
||||
export default function WalletModal({ pendingTransactions, confirmedTransactions, ENSName }) {
|
||||
const { active, account, connector, activate, error } = useWeb3React()
|
||||
|
||||
// function getErrorMessage(event) {
|
||||
// switch (event.code) {
|
||||
// case InjectedConnector.errorCodes.ETHEREUM_ACCESS_DENIED: {
|
||||
// return 'Permission Required'
|
||||
// }
|
||||
// case InjectedConnector.errorCodes.UNLOCK_REQUIRED: {
|
||||
// return 'Account Unlock Required'
|
||||
// }
|
||||
// case InjectedConnector.errorCodes.NO_WEB3: {
|
||||
// return 'Not a Web3 Browser'
|
||||
// }
|
||||
// default: {
|
||||
// return 'Connection Error'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
|
||||
export default function WalletModal({ isOpen, error, onDismiss, pendingTransactions, confirmedTransactions, ENSName }) {
|
||||
const { account, networkId } = useWeb3Context()
|
||||
const walletModalOpen = useWalletModalOpen()
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
function renderTransactions(transactions, pending) {
|
||||
// always reset to account view
|
||||
useEffect(() => {
|
||||
if (walletModalOpen) {
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// set up uri listener for walletconnect
|
||||
const [uri, setUri] = useState()
|
||||
useEffect(() => {
|
||||
const activateWC = uri => {
|
||||
setUri(uri)
|
||||
setWalletView(WALLET_VIEWS.WALLET_CONNECT)
|
||||
}
|
||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
||||
return () => {
|
||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
useEffect(() => {
|
||||
if (walletModalOpen && ((active && !activePrevious) || (connector && connector !== connectorPrevious && !error))) {
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}
|
||||
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
|
||||
|
||||
// get wallets user can switch too, depending on device/browser
|
||||
function getOptions() {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
|
||||
if (isMobile && !window.web3 && !window.ethereum) {
|
||||
return Object.keys(MOBILE_DEEP_LINKS).map(key => {
|
||||
const option = MOBILE_DEEP_LINKS[key]
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
return <Transaction key={i} hash={hash} pending={pending} />
|
||||
})}
|
||||
</TransactionListWrapper>
|
||||
<Option
|
||||
key={key}
|
||||
color={option.color}
|
||||
header={option.name}
|
||||
link={option.href}
|
||||
subheader={option.description}
|
||||
icon={require('../../assets/images/' + option.iconName)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return Object.keys(SUPPORTED_WALLETS).map(key => {
|
||||
const option = SUPPORTED_WALLETS[key]
|
||||
|
||||
// don't show the option we're currently connected to
|
||||
if (option.connector === connector) {
|
||||
return null
|
||||
}
|
||||
|
||||
function wrappedOnDismiss() {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
function getWalletDisplay() {
|
||||
if (error) {
|
||||
if (option.connector === injected) {
|
||||
// don't show injected if there's no injected provider
|
||||
if (!(window.web3 || window.ethereum)) {
|
||||
if (option.name === 'MetaMask') {
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<h4>Wrong Network</h4>
|
||||
<h5>Please connect to the main Ethereum network.</h5>
|
||||
</UpperSection>
|
||||
</>
|
||||
)
|
||||
} else if (account) {
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<YourAccount>
|
||||
<h5>Your Account</h5>
|
||||
{ENSName && (
|
||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(networkId, ENSName, 'address')}>
|
||||
{ENSName} ↗{' '}
|
||||
</StyledLink>
|
||||
|
||||
<Copy toCopy={ENSName} />
|
||||
</AccountControl>
|
||||
)}
|
||||
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(networkId, account, 'address')}>
|
||||
{account} ↗{' '}
|
||||
</StyledLink>
|
||||
|
||||
<Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
</YourAccount>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<h5>Recent Transactions</h5>
|
||||
{renderTransactions(pendingTransactions, true)}
|
||||
{renderTransactions(confirmedTransactions, false)}
|
||||
</LowerSection>
|
||||
) : (
|
||||
<LowerSection>
|
||||
<h5>Your transactions will appear here...</h5>
|
||||
</LowerSection>
|
||||
)}
|
||||
</>
|
||||
<Option
|
||||
key={key}
|
||||
color={'#E8831D'}
|
||||
header={'Install Metamask'}
|
||||
subheader={'Easy to use browser extension.'}
|
||||
link={'https://metamask.io/'}
|
||||
icon={MetamaskIcon}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<h4>No Ethereum account found</h4>
|
||||
<h5>Please visit this page in a Web3 enabled browser.</h5>
|
||||
<h5>
|
||||
<Link href={'https://ethereum.org/use/#_3-what-is-a-wallet-and-which-one-should-i-use'}>
|
||||
Learn more ↗
|
||||
</Link>
|
||||
</h5>
|
||||
</UpperSection>
|
||||
</>
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} minHeight={null}>
|
||||
<Wrapper>{getWalletDisplay()}</Wrapper>
|
||||
<Option
|
||||
onClick={() => {
|
||||
activate(option.connector)
|
||||
}}
|
||||
key={key}
|
||||
color={option.color}
|
||||
header={option.name}
|
||||
subheader={walletView === WALLET_VIEWS.OPTIONS ? null : option.description}
|
||||
icon={require('../../assets/images/' + option.iconName)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getModalContent() {
|
||||
if (error) {
|
||||
return (
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor alt={'close icon'} />
|
||||
</CloseIcon>
|
||||
<HeaderRow>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error connecting'}</HeaderRow>
|
||||
<ContentWrapper>
|
||||
{error instanceof UnsupportedChainIdError ? (
|
||||
<h5>Please connect to the main Ethereum network.</h5>
|
||||
) : (
|
||||
'Error connecting. Try refreshing the page.'
|
||||
)}
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
)
|
||||
}
|
||||
if (account && walletView === WALLET_VIEWS.ACCOUNT) {
|
||||
return (
|
||||
<AccountDetails
|
||||
toggleWalletModal={toggleWalletModal}
|
||||
pendingTransactions={pendingTransactions}
|
||||
confirmedTransactions={confirmedTransactions}
|
||||
ENSName={ENSName}
|
||||
openOptions={() => setWalletView(WALLET_VIEWS.OPTIONS)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor alt={'close icon'} />
|
||||
</CloseIcon>
|
||||
{walletView !== WALLET_VIEWS.ACCOUNT ? (
|
||||
<HeaderRow
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}}
|
||||
>
|
||||
<HoverText>Back</HoverText>
|
||||
</HeaderRow>
|
||||
) : (
|
||||
<HeaderRow>
|
||||
<HoverText>Connect To A Wallet</HoverText>
|
||||
</HeaderRow>
|
||||
)}
|
||||
<ContentWrapper>
|
||||
{walletView === WALLET_VIEWS.WALLET_CONNECT ? (
|
||||
<QrCode
|
||||
uri={uri}
|
||||
header={'Connect with Wallet Connect'}
|
||||
subheader={'Open protocol supported by major mobile wallets'}
|
||||
size={220}
|
||||
icon={WalletConnectIcon}
|
||||
/>
|
||||
) : !account ? (
|
||||
getOptions()
|
||||
) : (
|
||||
<OptionGrid>{getOptions()}</OptionGrid>
|
||||
)}
|
||||
<Blurb>
|
||||
<span>New to Ethereum? </span>{' '}
|
||||
<Link href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
|
||||
Learn more about wallets
|
||||
</Link>
|
||||
</Blurb>
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ userSelect: 'none' }}
|
||||
isOpen={walletModalOpen}
|
||||
onDismiss={toggleWalletModal}
|
||||
minHeight={null}
|
||||
maxHeight={90}
|
||||
>
|
||||
<Wrapper>{getModalContent()}</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useWeb3Context, Connectors } from 'web3-react'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import styled from 'styled-components'
|
||||
import { ethers } from 'ethers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
|
||||
import { network } from '../../connectors'
|
||||
import { useEagerConnect, useInactiveListener } from '../../hooks'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
const { Connector } = Connectors
|
||||
import { NetworkContextName } from '../../constants'
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -31,82 +30,73 @@ const SpinnerWrapper = styled(Spinner)`
|
||||
}
|
||||
`
|
||||
|
||||
function tryToSetConnector(setConnector, setError) {
|
||||
setConnector('Injected', { suppressAndThrowErrors: true }).catch(() => {
|
||||
setConnector('Network', { suppressAndThrowErrors: true }).catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default function Web3ReactManager({ children }) {
|
||||
const { t } = useTranslation()
|
||||
const { active, error, setConnector, setError } = useWeb3Context()
|
||||
// control whether or not we render the error, after parsing
|
||||
const blockRender = error && error.code && error.code === Connector.errorCodes.UNSUPPORTED_NETWORK
|
||||
const { active } = useWeb3React()
|
||||
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
|
||||
|
||||
// try to eagerly connect to an injected provider, if it exists and has granted access already
|
||||
const triedEager = useEagerConnect()
|
||||
|
||||
// after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd
|
||||
// TODO think about not doing this at all
|
||||
useEffect(() => {
|
||||
if (!active && !error) {
|
||||
if (window.ethereum || window.web3) {
|
||||
if (isMobile) {
|
||||
tryToSetConnector(setConnector, setError)
|
||||
} else {
|
||||
const library = new ethers.providers.Web3Provider(window.ethereum || window.web3)
|
||||
library.listAccounts().then(accounts => {
|
||||
if (accounts.length >= 1) {
|
||||
tryToSetConnector(setConnector, setError)
|
||||
} else {
|
||||
setConnector('Network', { suppressAndThrowErrors: true }).catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
if (triedEager && !networkActive && !networkError && !active) {
|
||||
activateNetwork(network)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
setConnector('Network', { suppressAndThrowErrors: true }).catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [triedEager, networkActive, networkError, activateNetwork, active])
|
||||
|
||||
// parse the error
|
||||
// 'pause' the network connector if we're ever connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
// if the user changes to the wrong network, unset the connector
|
||||
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||
setConnector('Network', { suppressAndThrowErrors: true }).catch(error => {
|
||||
setError(error)
|
||||
})
|
||||
if (active && networkActive) {
|
||||
network.pause()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [active, networkActive])
|
||||
|
||||
// 'resume' the network connector if we're ever not connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (!active && networkActive) {
|
||||
network.resume()
|
||||
}
|
||||
}, [active, networkActive])
|
||||
|
||||
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
|
||||
useInactiveListener(!triedEager)
|
||||
|
||||
// handle delayed loader state
|
||||
const [showLoader, setShowLoader] = useState(false)
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setShowLoader(true)
|
||||
}, 600)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (blockRender) {
|
||||
// on page load, do nothing until we've tried to connect to the injected connector
|
||||
if (!triedEager) {
|
||||
return null
|
||||
} else if (error) {
|
||||
}
|
||||
|
||||
// if the account context isn't active, and there's an error on the network context, it's an irrecoverable error
|
||||
if (!active && networkError) {
|
||||
return (
|
||||
<MessageWrapper>
|
||||
<Message>{t('unknownError')}</Message>
|
||||
</MessageWrapper>
|
||||
)
|
||||
} else if (!active) {
|
||||
}
|
||||
|
||||
// if neither context is active, spin
|
||||
if (!active && !networkActive) {
|
||||
return showLoader ? (
|
||||
<MessageWrapper>
|
||||
<SpinnerWrapper src={Circle} />
|
||||
</MessageWrapper>
|
||||
) : null
|
||||
} else {
|
||||
return children
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
import React, { useReducer, useEffect, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3Context, Connectors } from 'web3-react'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { darken, transparentize } from 'polished'
|
||||
import Jazzicon from 'jazzicon'
|
||||
import { ethers } from 'ethers'
|
||||
import { Activity } from 'react-feather'
|
||||
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useENSName } from '../../hooks'
|
||||
import WalletModal from '../WalletModal'
|
||||
import { useAllTransactions } from '../../contexts/Transactions'
|
||||
import { useWalletModalToggle } from '../../contexts/Application'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
const { Connector } = Connectors
|
||||
import { injected, walletconnect, walletlink } from '../../connectors'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
const Web3StatusGeneric = styled.button`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
@ -42,15 +44,30 @@ const Web3StatusError = styled(Web3StatusGeneric)`
|
||||
`
|
||||
|
||||
const Web3StatusConnect = styled(Web3StatusGeneric)`
|
||||
background-color: ${({ theme }) => theme.royalBlue};
|
||||
background-color: transparent;
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.white};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
font-weight: 500;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
background-color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
|
||||
${({ faded }) =>
|
||||
faded &&
|
||||
css`
|
||||
background-color: transparent;
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const Web3StatusConnected = styled(Web3StatusGeneric)`
|
||||
@ -59,10 +76,6 @@ const Web3StatusConnected = styled(Web3StatusGeneric)`
|
||||
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.doveGray)};
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
|
||||
/* > P {
|
||||
color: ${({ theme }) => theme.uniswapPink};
|
||||
} */
|
||||
background-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0.9, theme.royalBlue) : darken(0.05, theme.inputBackground)};
|
||||
|
||||
@ -70,6 +83,7 @@ const Web3StatusConnected = styled(Web3StatusGeneric)`
|
||||
border: 1px solid
|
||||
${({ pending, theme }) => (pending ? darken(0.1, theme.royalBlue) : darken(0.1, theme.mercuryGray))};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Text = styled.p`
|
||||
@ -81,13 +95,6 @@ const Text = styled.p`
|
||||
font-size: 0.83rem;
|
||||
`
|
||||
|
||||
const Identicon = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 1.125rem;
|
||||
background-color: ${({ theme }) => theme.silverGray};
|
||||
`
|
||||
|
||||
const NetworkIcon = styled(Activity)`
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
@ -99,41 +106,20 @@ const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
`
|
||||
|
||||
const walletModalInitialState = {
|
||||
open: false,
|
||||
error: undefined
|
||||
}
|
||||
|
||||
const WALLET_MODAL_ERROR = 'WALLET_MODAL_ERROR'
|
||||
const WALLET_MODAL_OPEN = 'WALLET_MODAL_OPEN'
|
||||
const WALLET_MODAL_OPEN_ERROR = 'WALLET_MODAL_OPEN_ERROR'
|
||||
const WALLET_MODAL_CLOSE = 'WALLET_MODAL_CLOSE'
|
||||
|
||||
function walletModalReducer(state, { type, payload }) {
|
||||
switch (type) {
|
||||
case WALLET_MODAL_ERROR: {
|
||||
const { error } = payload
|
||||
return { ...state, error }
|
||||
const IconWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > * {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
case WALLET_MODAL_OPEN: {
|
||||
return { ...state, open: true }
|
||||
}
|
||||
case WALLET_MODAL_OPEN_ERROR: {
|
||||
const { error } = payload || {}
|
||||
return { open: true, error }
|
||||
}
|
||||
case WALLET_MODAL_CLOSE: {
|
||||
return { ...state, open: false }
|
||||
}
|
||||
default: {
|
||||
throw Error(`Unexpected action type in walletModalReducer reducer: '${type}'.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Web3Status() {
|
||||
const { t } = useTranslation()
|
||||
const { active, account, connectorName, setConnector } = useWeb3Context()
|
||||
const { active, account, connector, error } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const ENSName = useENSName(account)
|
||||
|
||||
@ -143,137 +129,60 @@ export default function Web3Status() {
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
|
||||
const [{ open: walletModalIsOpen, error: walletModalError }, dispatch] = useReducer(
|
||||
walletModalReducer,
|
||||
walletModalInitialState
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// handle the logo we want to show with the account
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return <Identicon />
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
function setError(error) {
|
||||
dispatch({ type: WALLET_MODAL_ERROR, payload: { error } })
|
||||
}
|
||||
function openWalletModal(error) {
|
||||
dispatch({ type: WALLET_MODAL_OPEN, ...(error ? { payload: { error } } : {}) })
|
||||
}
|
||||
function closeWalletModal() {
|
||||
dispatch({ type: WALLET_MODAL_CLOSE })
|
||||
}
|
||||
|
||||
// janky logic to detect log{ins,outs}...
|
||||
useEffect(() => {
|
||||
// if the injected connector is not active...
|
||||
const { ethereum } = window
|
||||
if (connectorName !== 'Injected') {
|
||||
if (connectorName === 'Network' && ethereum && ethereum.on && ethereum.removeListener) {
|
||||
function tryToActivateInjected() {
|
||||
const library = new ethers.providers.Web3Provider(window.ethereum)
|
||||
// if calling enable won't pop an approve modal, then try to activate injected...
|
||||
library.listAccounts().then(accounts => {
|
||||
if (accounts.length >= 1) {
|
||||
setConnector('Injected', { suppressAndThrowErrors: true })
|
||||
.then(() => {
|
||||
setError()
|
||||
})
|
||||
.catch(error => {
|
||||
// ...and if the error is that they're on the wrong network, display it, otherwise eat it
|
||||
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||
setError(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ethereum.on('networkChanged', tryToActivateInjected)
|
||||
ethereum.on('accountsChanged', tryToActivateInjected)
|
||||
|
||||
return () => {
|
||||
if (ethereum.removeListener) {
|
||||
ethereum.removeListener('networkChanged', tryToActivateInjected)
|
||||
ethereum.removeListener('accountsChanged', tryToActivateInjected)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ...poll to check the accounts array, and if it's ever 0 i.e. the user logged out, update the connector
|
||||
if (ethereum) {
|
||||
const accountPoll = setInterval(() => {
|
||||
const library = new ethers.providers.Web3Provider(ethereum)
|
||||
library.listAccounts().then(accounts => {
|
||||
if (accounts.length === 0) {
|
||||
setConnector('Network')
|
||||
}
|
||||
})
|
||||
}, 750)
|
||||
|
||||
return () => {
|
||||
clearInterval(accountPoll)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [connectorName, setConnector])
|
||||
|
||||
function onClick() {
|
||||
if (walletModalError) {
|
||||
openWalletModal()
|
||||
} else if (connectorName === 'Network' && (window.ethereum || window.web3)) {
|
||||
setConnector('Injected', { suppressAndThrowErrors: true }).catch(error => {
|
||||
if (error.code === Connector.errorCodes.UNSUPPORTED_NETWORK) {
|
||||
setError(error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
openWalletModal()
|
||||
}
|
||||
}
|
||||
|
||||
const ref = useRef()
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = ''
|
||||
if (account) {
|
||||
ref.current.appendChild(Jazzicon(16, parseInt(account.slice(2, 10), 16)))
|
||||
}
|
||||
}
|
||||
}, [account, walletModalError])
|
||||
|
||||
function getWeb3Status() {
|
||||
if (walletModalError) {
|
||||
// this is ok because we're guaranteed that the error is a wrong network error
|
||||
if (account) {
|
||||
return (
|
||||
<Web3StatusError onClick={onClick}>
|
||||
<NetworkIcon />
|
||||
<Text>Wrong Network</Text>
|
||||
</Web3StatusError>
|
||||
<Web3StatusConnected onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions && <SpinnerWrapper src={Circle} alt="loader" />}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
{getStatusIcon()}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (!account) {
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusConnect onClick={onClick}>
|
||||
<Text>{t('Connect')}</Text>
|
||||
</Web3StatusConnect>
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnected onClick={onClick} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions && <SpinnerWrapper src={Circle} alt="loader" />}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
<Identicon ref={ref} />
|
||||
</Web3StatusConnected>
|
||||
<Web3StatusConnect onClick={toggleWalletModal} faded={!account}>
|
||||
<Text>{t('Connect to a Wallet')}</Text>
|
||||
</Web3StatusConnect>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!contextNetwork.active && !active) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
active && (
|
||||
<>
|
||||
{getWeb3Status()}
|
||||
<WalletModal
|
||||
isOpen={walletModalIsOpen}
|
||||
error={walletModalError}
|
||||
onDismiss={closeWalletModal}
|
||||
ENSName={ENSName}
|
||||
pendingTransactions={pending}
|
||||
confirmedTransactions={confirmed}
|
||||
/>
|
||||
<WalletModal ENSName={ENSName} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
115
src/connectors/Injected.js
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
InjectedConnector as InjectedConnectorCore,
|
||||
NoEthereumProviderError,
|
||||
UserRejectedRequestError
|
||||
} from '@web3-react/injected-connector'
|
||||
|
||||
export class InjectedConnector extends InjectedConnectorCore {
|
||||
async activate() {
|
||||
if (!window.ethereum) {
|
||||
throw new NoEthereumProviderError()
|
||||
}
|
||||
|
||||
if (window.ethereum.on) {
|
||||
window.ethereum.on('connect', this.handleConnect)
|
||||
window.ethereum.on('chainChanged', this.handleChainChanged)
|
||||
window.ethereum.on('networkChanged', this.handleNetworkChanged)
|
||||
window.ethereum.on('accountsChanged', this.handleAccountsChanged)
|
||||
window.ethereum.on('close', this.handleClose)
|
||||
}
|
||||
|
||||
// provides support for most dapp browsers
|
||||
let account = undefined
|
||||
if (window.ethereum.isMetaMask) {
|
||||
window.ethereum.autoRefreshOnNetworkChange = false
|
||||
account = await window.ethereum
|
||||
.send('eth_requestAccounts')
|
||||
.then(({ result: accounts }) => accounts[0])
|
||||
.catch(error => {
|
||||
if (error && error.code === 4001) {
|
||||
throw new UserRejectedRequestError()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
} else {
|
||||
account = await window.ethereum
|
||||
.enable()
|
||||
.then(accounts => accounts[0])
|
||||
.catch(error => {
|
||||
if (error && error.code === 4001) {
|
||||
throw new UserRejectedRequestError()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { provider: window.ethereum, account }
|
||||
}
|
||||
|
||||
async getChainId() {
|
||||
if (!window.ethereum) {
|
||||
throw new NoEthereumProviderError()
|
||||
}
|
||||
|
||||
if (window.ethereum.isMetaMask) {
|
||||
return window.ethereum.send('eth_chainId').then(({ result: chainId }) => chainId)
|
||||
} else {
|
||||
return window.ethereum.networkVersion ? window.ethereum.networkVersion : 1
|
||||
}
|
||||
}
|
||||
|
||||
async getAccount() {
|
||||
if (!window.ethereum) {
|
||||
throw new NoEthereumProviderError()
|
||||
}
|
||||
|
||||
if (window.ethereum.isMetaMask) {
|
||||
return window.ethereum.send('eth_accounts').then(({ result: accounts }) => accounts[0])
|
||||
} else {
|
||||
return window.ethereum.enable().then(accounts => accounts[0])
|
||||
}
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
if (window.ethereum && window.ethereum.removeListener) {
|
||||
window.ethereum.removeListener('connect', this.handleConnect)
|
||||
window.ethereum.removeListener('chainChanged', this.handleChainChanged)
|
||||
window.ethereum.removeListener('networkChanged', this.handleNetworkChanged)
|
||||
window.ethereum.removeListener('accountsChanged', this.handleAccountsChanged)
|
||||
window.ethereum.removeListener('close', this.handleClose)
|
||||
}
|
||||
}
|
||||
|
||||
async isAuthorized() {
|
||||
if (window.ethereum) {
|
||||
if (window.ethereum.isMetaMask) {
|
||||
return window.ethereum
|
||||
.send('eth_accounts')
|
||||
.then(({ result: accounts }) => {
|
||||
if (accounts.length > 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.catch(() => {
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
return window.ethereum
|
||||
.enable()
|
||||
.then(accounts => {
|
||||
if (accounts.length > 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.catch(() => {
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
15
src/connectors/Network.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { NetworkConnector as NetworkConnectorCore } from '@web3-react/network-connector'
|
||||
|
||||
export class NetworkConnector extends NetworkConnectorCore {
|
||||
pause() {
|
||||
if (this.active) {
|
||||
this.providers[this.currentChainId].stop()
|
||||
}
|
||||
}
|
||||
|
||||
resume() {
|
||||
if (this.active) {
|
||||
this.providers[this.currentChainId].start()
|
||||
}
|
||||
}
|
||||
}
|
30
src/connectors/index.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
|
||||
import { InjectedConnector } from './Injected'
|
||||
import { NetworkConnector } from './Network'
|
||||
|
||||
const POLLING_INTERVAL = 10000
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
supportedChainIds: [1]
|
||||
})
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: { 1: process.env.REACT_APP_NETWORK_URL },
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
})
|
||||
|
||||
export const walletconnect = new WalletConnectConnector({
|
||||
rpc: { 1: process.env.REACT_APP_NETWORK_URL },
|
||||
bridge: 'https://bridge.walletconnect.org',
|
||||
qrcode: false,
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
})
|
||||
|
||||
export const walletlink = new WalletLinkConnector({
|
||||
url: process.env.REACT_APP_NETWORK_URL,
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl:
|
||||
'https://mpng.pngfly.com/20181202/bex/kisspng-emoji-domain-unicorn-pin-badges-sticker-unicorn-tumblr-emoji-unicorn-iphoneemoji-5c046729264a77.5671679315437924251569.jpg'
|
||||
})
|
@ -1,3 +1,5 @@
|
||||
import { injected, walletconnect, walletlink } from '../connectors'
|
||||
|
||||
export const FACTORY_ADDRESSES = {
|
||||
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
||||
3: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
|
||||
@ -10,9 +12,63 @@ export const SUPPORTED_THEMES = {
|
||||
LIGHT: 'LIGHT'
|
||||
}
|
||||
|
||||
export const SUPPORTED_WALLETS = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
id: 'Injected',
|
||||
name: 'Injected',
|
||||
iconName: 'arrow-right.svg',
|
||||
description: 'Injected web3 provider.',
|
||||
color: '#010101'
|
||||
},
|
||||
METAMASK: {
|
||||
connector: injected,
|
||||
id: 'MetaMask',
|
||||
name: 'MetaMask',
|
||||
iconName: 'metamask.png',
|
||||
description: 'Easy-to-use browser extension.',
|
||||
color: '#E8831D'
|
||||
},
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
id: 'WalletConnect',
|
||||
name: 'Wallet Connect',
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
color: '#4196FC'
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
id: 'WalletLink',
|
||||
name: 'Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Use Coinbase Wallet app on mobile device',
|
||||
color: '#315CF5'
|
||||
}
|
||||
}
|
||||
|
||||
export const MOBILE_DEEP_LINKS = {
|
||||
COINBASE_LINK: {
|
||||
name: 'Open in Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Open in Coinbase Wallet app.',
|
||||
href: 'https://go.cb-w.com/mtUDhEZPy1',
|
||||
color: '#315CF5'
|
||||
},
|
||||
TRUST_WALLET_LINK: {
|
||||
name: 'Open in Trust Wallet',
|
||||
iconName: 'trustWallet.png',
|
||||
description: 'iOS and Android app.',
|
||||
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
|
||||
color: '#1C74CC'
|
||||
}
|
||||
}
|
||||
|
||||
// list of tokens that lock fund on adding liquidity - used to disable button
|
||||
export const brokenTokens = [
|
||||
'0xb8c77482e45f1f44de1745f52c74426c631bdd52',
|
||||
'0x95daaab98046846bf4b2853e23cba236fa394a31',
|
||||
'0x55296f69f40ea6d20e478533c15a6b08b654e758'
|
||||
'0xB8c77482e45F1F44dE1745F52C74426C631bDD52',
|
||||
'0x95dAaaB98046846bF4B2853e23cba236fa394A31',
|
||||
'0x55296f69f40Ea6d20E478533C15A6B08B654E758'
|
||||
]
|
||||
|
||||
export const NetworkContextName = 'NETWORK'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback } from 'react'
|
||||
import { ethers } from 'ethers'
|
||||
import { getTokenReserves, getMarketDetails, BigNumber } from '@uniswap/sdk'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
|
||||
import { useAllTokenDetails } from './Tokens'
|
||||
|
||||
@ -53,13 +53,13 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function useFetchAllBalances() {
|
||||
const { account, networkId, library } = useWeb3Context()
|
||||
const { library, chainId, account } = useWeb3React()
|
||||
|
||||
const allTokens = useAllTokenDetails()
|
||||
|
||||
const [state, { update }] = useAllBalancesContext()
|
||||
|
||||
const { allBalanceData } = safeAccess(state, [networkId, account]) || {}
|
||||
const { allBalanceData } = safeAccess(state, [chainId, account]) || {}
|
||||
|
||||
const getData = async () => {
|
||||
if (!!library && !!account) {
|
||||
@ -90,7 +90,7 @@ export function useFetchAllBalances() {
|
||||
}
|
||||
})
|
||||
)
|
||||
update(newBalances, networkId, account)
|
||||
update(newBalances, chainId, account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import {} from '@web3-react/core'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { safeAccess, isAddress, getTokenAllowance } from '../utils'
|
||||
import { useBlockNumber } from './Application'
|
||||
|
||||
@ -54,12 +55,12 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
||||
const { networkId, library } = useWeb3Context()
|
||||
const { library, chainId } = useWeb3React()
|
||||
|
||||
const globalBlockNumber = useBlockNumber()
|
||||
|
||||
const [state, { update }] = useAllowancesContext()
|
||||
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress, spenderAddress]) || {}
|
||||
const { value, blockNumber } = safeAccess(state, [chainId, address, tokenAddress, spenderAddress]) || {}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -67,7 +68,7 @@ export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
||||
isAddress(tokenAddress) &&
|
||||
isAddress(spenderAddress) &&
|
||||
(value === undefined || blockNumber !== globalBlockNumber) &&
|
||||
(networkId || networkId === 0) &&
|
||||
(chainId || chainId === 0) &&
|
||||
library
|
||||
) {
|
||||
let stale = false
|
||||
@ -75,12 +76,12 @@ export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
||||
getTokenAllowance(address, tokenAddress, spenderAddress, library)
|
||||
.then(value => {
|
||||
if (!stale) {
|
||||
update(networkId, address, tokenAddress, spenderAddress, value, globalBlockNumber)
|
||||
update(chainId, address, tokenAddress, spenderAddress, value, globalBlockNumber)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
update(networkId, address, tokenAddress, spenderAddress, null, globalBlockNumber)
|
||||
update(chainId, address, tokenAddress, spenderAddress, null, globalBlockNumber)
|
||||
}
|
||||
})
|
||||
|
||||
@ -88,7 +89,7 @@ export function useAddressAllowance(address, tokenAddress, spenderAddress) {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [address, tokenAddress, spenderAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
|
||||
}, [address, tokenAddress, spenderAddress, value, blockNumber, globalBlockNumber, chainId, library, update])
|
||||
|
||||
return value
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { safeAccess } from '../utils'
|
||||
import { getUSDPrice } from '../utils/price'
|
||||
|
||||
const BLOCK_NUMBER = 'BLOCK_NUMBER'
|
||||
const USD_PRICE = 'USD_PRICE'
|
||||
const WALLET_MODAL_OPEN = 'WALLET_MODAL_OPEN'
|
||||
|
||||
const UPDATE_BLOCK_NUMBER = 'UPDATE_BLOCK_NUMBER'
|
||||
const UPDATE_USD_PRICE = 'UPDATE_USD_PRICE'
|
||||
const TOGGLE_WALLET_MODAL = 'TOGGLE_WALLET_MODAL'
|
||||
|
||||
const ApplicationContext = createContext()
|
||||
|
||||
@ -38,6 +40,9 @@ function reducer(state, { type, payload }) {
|
||||
}
|
||||
}
|
||||
}
|
||||
case TOGGLE_WALLET_MODAL: {
|
||||
return { ...state, [WALLET_MODAL_OPEN]: !state[WALLET_MODAL_OPEN] }
|
||||
}
|
||||
default: {
|
||||
throw Error(`Unexpected action type in ApplicationContext reducer: '${type}'.`)
|
||||
}
|
||||
@ -47,7 +52,8 @@ function reducer(state, { type, payload }) {
|
||||
export default function Provider({ children }) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
[BLOCK_NUMBER]: {},
|
||||
[USD_PRICE]: {}
|
||||
[USD_PRICE]: {},
|
||||
[WALLET_MODAL_OPEN]: false
|
||||
})
|
||||
|
||||
const updateBlockNumber = useCallback((networkId, blockNumber) => {
|
||||
@ -58,9 +64,18 @@ export default function Provider({ children }) {
|
||||
dispatch({ type: UPDATE_USD_PRICE, payload: { networkId, USDPrice } })
|
||||
}, [])
|
||||
|
||||
const toggleWalletModal = useCallback(() => {
|
||||
dispatch({ type: TOGGLE_WALLET_MODAL })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ApplicationContext.Provider
|
||||
value={useMemo(() => [state, { updateBlockNumber, updateUSDPrice }], [state, updateBlockNumber, updateUSDPrice])}
|
||||
value={useMemo(() => [state, { updateBlockNumber, updateUSDPrice, toggleWalletModal }], [
|
||||
state,
|
||||
updateBlockNumber,
|
||||
updateUSDPrice,
|
||||
toggleWalletModal
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</ApplicationContext.Provider>
|
||||
@ -68,36 +83,29 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function Updater() {
|
||||
const { networkId, library } = useWeb3Context()
|
||||
const { library, chainId } = useWeb3React()
|
||||
|
||||
const globalBlockNumber = useBlockNumber()
|
||||
const [, { updateBlockNumber, updateUSDPrice }] = useApplicationContext()
|
||||
|
||||
// slow down polling interval
|
||||
// if (library && connectorName === 'Network' && library.pollingInterval !== 15) {
|
||||
// library.pollingInterval = 15
|
||||
// } else if (library && library.pollingInterval !== 5) {
|
||||
// library.pollingInterval = 5
|
||||
// }
|
||||
|
||||
// update usd price
|
||||
useEffect(() => {
|
||||
if (library && networkId === 1) {
|
||||
if (library && chainId === 1) {
|
||||
let stale = false
|
||||
|
||||
getUSDPrice(library)
|
||||
.then(([price]) => {
|
||||
if (!stale) {
|
||||
updateUSDPrice(networkId, price)
|
||||
updateUSDPrice(chainId, price)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
updateUSDPrice(networkId, null)
|
||||
updateUSDPrice(chainId, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [globalBlockNumber, library, networkId, updateUSDPrice])
|
||||
}, [globalBlockNumber, library, chainId, updateUSDPrice])
|
||||
|
||||
// update block number
|
||||
useEffect(() => {
|
||||
@ -109,12 +117,12 @@ export function Updater() {
|
||||
.getBlockNumber()
|
||||
.then(blockNumber => {
|
||||
if (!stale) {
|
||||
updateBlockNumber(networkId, blockNumber)
|
||||
updateBlockNumber(chainId, blockNumber)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
updateBlockNumber(networkId, null)
|
||||
updateBlockNumber(chainId, null)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -127,23 +135,35 @@ export function Updater() {
|
||||
library.removeListener('block', update)
|
||||
}
|
||||
}
|
||||
}, [networkId, library, updateBlockNumber])
|
||||
}, [chainId, library, updateBlockNumber])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function useBlockNumber() {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const [state] = useApplicationContext()
|
||||
|
||||
return safeAccess(state, [BLOCK_NUMBER, networkId])
|
||||
return safeAccess(state, [BLOCK_NUMBER, chainId])
|
||||
}
|
||||
|
||||
export function useUSDPrice() {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const [state] = useApplicationContext()
|
||||
|
||||
return safeAccess(state, [USD_PRICE, networkId])
|
||||
return safeAccess(state, [USD_PRICE, chainId])
|
||||
}
|
||||
|
||||
export function useWalletModalOpen() {
|
||||
const [state] = useApplicationContext()
|
||||
|
||||
return state[WALLET_MODAL_OPEN]
|
||||
}
|
||||
|
||||
export function useWalletModalToggle() {
|
||||
const [, { toggleWalletModal }] = useApplicationContext()
|
||||
|
||||
return toggleWalletModal
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { safeAccess, isAddress, getEtherBalance, getTokenBalance } from '../utils'
|
||||
import { useBlockNumber } from './Application'
|
||||
import { useTokenDetails } from './Tokens'
|
||||
@ -52,38 +52,38 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function useAddressBalance(address, tokenAddress) {
|
||||
const { networkId, library } = useWeb3Context()
|
||||
const { library, chainId } = useWeb3React()
|
||||
|
||||
const globalBlockNumber = useBlockNumber()
|
||||
|
||||
const [state, { update }] = useBalancesContext()
|
||||
const { value, blockNumber } = safeAccess(state, [networkId, address, tokenAddress]) || {}
|
||||
const { value, blockNumber } = safeAccess(state, [chainId, address, tokenAddress]) || {}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isAddress(address) &&
|
||||
(tokenAddress === 'ETH' || isAddress(tokenAddress)) &&
|
||||
(value === undefined || blockNumber !== globalBlockNumber) &&
|
||||
(networkId || networkId === 0) &&
|
||||
(chainId || chainId === 0) &&
|
||||
library
|
||||
) {
|
||||
let stale = false
|
||||
;(tokenAddress === 'ETH' ? getEtherBalance(address, library) : getTokenBalance(tokenAddress, address, library))
|
||||
.then(value => {
|
||||
if (!stale) {
|
||||
update(networkId, address, tokenAddress, value, globalBlockNumber)
|
||||
update(chainId, address, tokenAddress, value, globalBlockNumber)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
update(networkId, address, tokenAddress, null, globalBlockNumber)
|
||||
update(chainId, address, tokenAddress, null, globalBlockNumber)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [address, tokenAddress, value, blockNumber, globalBlockNumber, networkId, library, update])
|
||||
}, [address, tokenAddress, value, blockNumber, globalBlockNumber, chainId, library, update])
|
||||
|
||||
return value
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { ethers } from 'ethers'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import {
|
||||
isAddress,
|
||||
getTokenName,
|
||||
@ -558,10 +558,10 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function useTokenDetails(tokenAddress) {
|
||||
const { networkId, library } = useWeb3Context()
|
||||
const { library, chainId } = useWeb3React()
|
||||
|
||||
const [state, { update }] = useTokensContext()
|
||||
const allTokensInNetwork = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
|
||||
const allTokensInNetwork = { ...ETH, ...(safeAccess(state, [chainId]) || {}) }
|
||||
const { [NAME]: name, [SYMBOL]: symbol, [DECIMALS]: decimals, [EXCHANGE_ADDRESS]: exchangeAddress } =
|
||||
safeAccess(allTokensInNetwork, [tokenAddress]) || {}
|
||||
|
||||
@ -569,7 +569,7 @@ export function useTokenDetails(tokenAddress) {
|
||||
if (
|
||||
isAddress(tokenAddress) &&
|
||||
(name === undefined || symbol === undefined || decimals === undefined || exchangeAddress === undefined) &&
|
||||
(networkId || networkId === 0) &&
|
||||
(chainId || chainId === 0) &&
|
||||
library
|
||||
) {
|
||||
let stale = false
|
||||
@ -577,14 +577,14 @@ export function useTokenDetails(tokenAddress) {
|
||||
const namePromise = getTokenName(tokenAddress, library).catch(() => null)
|
||||
const symbolPromise = getTokenSymbol(tokenAddress, library).catch(() => null)
|
||||
const decimalsPromise = getTokenDecimals(tokenAddress, library).catch(() => null)
|
||||
const exchangeAddressPromise = getTokenExchangeAddressFromFactory(tokenAddress, networkId, library).catch(
|
||||
const exchangeAddressPromise = getTokenExchangeAddressFromFactory(tokenAddress, chainId, library).catch(
|
||||
() => null
|
||||
)
|
||||
|
||||
Promise.all([namePromise, symbolPromise, decimalsPromise, exchangeAddressPromise]).then(
|
||||
([resolvedName, resolvedSymbol, resolvedDecimals, resolvedExchangeAddress]) => {
|
||||
if (!stale) {
|
||||
update(networkId, tokenAddress, resolvedName, resolvedSymbol, resolvedDecimals, resolvedExchangeAddress)
|
||||
update(chainId, tokenAddress, resolvedName, resolvedSymbol, resolvedDecimals, resolvedExchangeAddress)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -592,16 +592,16 @@ export function useTokenDetails(tokenAddress) {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [tokenAddress, name, symbol, decimals, exchangeAddress, networkId, library, update])
|
||||
}, [tokenAddress, name, symbol, decimals, exchangeAddress, chainId, library, update])
|
||||
|
||||
return { name, symbol, decimals, exchangeAddress }
|
||||
}
|
||||
|
||||
export function useAllTokenDetails(requireExchange = true) {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const [state] = useTokensContext()
|
||||
const tokenDetails = { ...ETH, ...(safeAccess(state, [networkId]) || {}) }
|
||||
const tokenDetails = { ...ETH, ...(safeAccess(state, [chainId]) || {}) }
|
||||
|
||||
return requireExchange
|
||||
? Object.keys(tokenDetails)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useReducer, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
|
||||
import { useWeb3React } from '../hooks'
|
||||
import { safeAccess } from '../utils'
|
||||
import { useBlockNumber } from './Application'
|
||||
|
||||
@ -103,15 +103,15 @@ export default function Provider({ children }) {
|
||||
}
|
||||
|
||||
export function Updater() {
|
||||
const { networkId, library } = useWeb3Context()
|
||||
const { chainId, library } = useWeb3React()
|
||||
|
||||
const globalBlockNumber = useBlockNumber()
|
||||
|
||||
const [state, { check, finalize }] = useTransactionsContext()
|
||||
const allTransactions = safeAccess(state, [networkId]) || {}
|
||||
const allTransactions = safeAccess(state, [chainId]) || {}
|
||||
|
||||
useEffect(() => {
|
||||
if ((networkId || networkId === 0) && library) {
|
||||
if ((chainId || chainId === 0) && library) {
|
||||
let stale = false
|
||||
Object.keys(allTransactions)
|
||||
.filter(
|
||||
@ -123,14 +123,14 @@ export function Updater() {
|
||||
.then(receipt => {
|
||||
if (!stale) {
|
||||
if (!receipt) {
|
||||
check(networkId, hash, globalBlockNumber)
|
||||
check(chainId, hash, globalBlockNumber)
|
||||
} else {
|
||||
finalize(networkId, hash, receipt)
|
||||
finalize(chainId, hash, receipt)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
check(networkId, hash, globalBlockNumber)
|
||||
check(chainId, hash, globalBlockNumber)
|
||||
})
|
||||
})
|
||||
|
||||
@ -138,20 +138,20 @@ export function Updater() {
|
||||
stale = true
|
||||
}
|
||||
}
|
||||
}, [networkId, library, allTransactions, globalBlockNumber, check, finalize])
|
||||
}, [chainId, library, allTransactions, globalBlockNumber, check, finalize])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function useTransactionAdder() {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const [, { add }] = useTransactionsContext()
|
||||
|
||||
return useCallback(
|
||||
(response, customData = {}) => {
|
||||
if (!(networkId || networkId === 0)) {
|
||||
throw Error(`Invalid networkId '${networkId}`)
|
||||
if (!(chainId || chainId === 0)) {
|
||||
throw Error(`Invalid networkId '${chainId}`)
|
||||
}
|
||||
|
||||
const hash = safeAccess(response, ['hash'])
|
||||
@ -159,18 +159,18 @@ export function useTransactionAdder() {
|
||||
if (!hash) {
|
||||
throw Error('No transaction hash found.')
|
||||
}
|
||||
add(networkId, hash, { ...response, [CUSTOM_DATA]: customData })
|
||||
add(chainId, hash, { ...response, [CUSTOM_DATA]: customData })
|
||||
},
|
||||
[networkId, add]
|
||||
[chainId, add]
|
||||
)
|
||||
}
|
||||
|
||||
export function useAllTransactions() {
|
||||
const { networkId } = useWeb3Context()
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const [state] = useTransactionsContext()
|
||||
|
||||
return safeAccess(state, [networkId]) || {}
|
||||
return safeAccess(state, [chainId]) || {}
|
||||
}
|
||||
|
||||
export function usePendingApproval(tokenAddress) {
|
||||
|
@ -1,9 +1,81 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import { NetworkContextName } from '../constants'
|
||||
import ERC20_ABI from '../constants/abis/erc20'
|
||||
import { getContract, getFactoryContract, getExchangeContract, isAddress } from '../utils'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { injected } from '../connectors'
|
||||
|
||||
export function useWeb3React() {
|
||||
const context = useWeb3ReactCore()
|
||||
const contextNetwork = useWeb3ReactCore(NetworkContextName)
|
||||
|
||||
return context.active ? context : contextNetwork
|
||||
}
|
||||
|
||||
export function useEagerConnect() {
|
||||
const { activate, active } = useWeb3ReactCore() // specifically using useWeb3ReactCore because of what this hook does
|
||||
|
||||
const [tried, setTried] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
injected.isAuthorized().then(isAuthorized => {
|
||||
if (isAuthorized) {
|
||||
activate(injected, undefined, true).catch(() => {
|
||||
setTried(true)
|
||||
})
|
||||
} else {
|
||||
setTried(true)
|
||||
}
|
||||
})
|
||||
}, [activate]) // intentionally only running on mount (make sure it's only mounted once :))
|
||||
|
||||
// if the connection worked, wait until we get confirmation of that to flip the flag
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setTried(true)
|
||||
}
|
||||
}, [active])
|
||||
|
||||
return tried
|
||||
}
|
||||
|
||||
/**
|
||||
* Use for network and injected - logs user in
|
||||
* and out after checking what network theyre on
|
||||
*/
|
||||
export function useInactiveListener(suppress = false) {
|
||||
const { active, error, activate } = useWeb3ReactCore() // specifically using useWeb3React because of what this hook does
|
||||
|
||||
useEffect(() => {
|
||||
const { ethereum } = window
|
||||
|
||||
if (ethereum && ethereum.on && !active && !error && !suppress) {
|
||||
const handleNetworkChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(() => {})
|
||||
}
|
||||
|
||||
const handleAccountsChanged = accounts => {
|
||||
if (accounts.length > 0) {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
ethereum.on('networkChanged', handleNetworkChanged)
|
||||
ethereum.on('accountsChanged', handleAccountsChanged)
|
||||
|
||||
return () => {
|
||||
ethereum.removeListener('networkChanged', handleNetworkChanged)
|
||||
ethereum.removeListener('accountsChanged', handleAccountsChanged)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {}
|
||||
}, [active, error, suppress, activate])
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export function useDebounce(value, delay) {
|
||||
@ -51,7 +123,7 @@ export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false)
|
||||
}
|
||||
|
||||
export function useENSName(address) {
|
||||
const { library } = useWeb3Context()
|
||||
const { library } = useWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState()
|
||||
|
||||
@ -84,7 +156,7 @@ export function useENSName(address) {
|
||||
|
||||
// returns null on errors
|
||||
export function useContract(address, ABI, withSignerIfPossible = true) {
|
||||
const { library, account } = useWeb3Context()
|
||||
const { library, account } = useWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
try {
|
||||
@ -97,7 +169,7 @@ export function useContract(address, ABI, withSignerIfPossible = true) {
|
||||
|
||||
// returns null on errors
|
||||
export function useTokenContract(tokenAddress, withSignerIfPossible = true) {
|
||||
const { library, account } = useWeb3Context()
|
||||
const { library, account } = useWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
try {
|
||||
@ -110,7 +182,7 @@ export function useTokenContract(tokenAddress, withSignerIfPossible = true) {
|
||||
|
||||
// returns null on errors
|
||||
export function useFactoryContract(withSignerIfPossible = true) {
|
||||
const { networkId, library, account } = useWeb3Context()
|
||||
const { networkId, library, account } = useWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
try {
|
||||
@ -122,7 +194,7 @@ export function useFactoryContract(withSignerIfPossible = true) {
|
||||
}
|
||||
|
||||
export function useExchangeContract(exchangeAddress, withSignerIfPossible = true) {
|
||||
const { library, account } = useWeb3Context()
|
||||
const { library, account } = useWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
try {
|
||||
@ -155,3 +227,18 @@ export function useCopyClipboard(timeout = 500) {
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/usePrevious/
|
||||
export function usePrevious(value) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef()
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
|
28
src/index.js
@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import Web3Provider from 'web3-react'
|
||||
import { Web3ReactProvider, createWeb3ReactRoot } from '@web3-react/core'
|
||||
import { ethers } from 'ethers'
|
||||
|
||||
import ThemeProvider, { GlobalStyle } from './theme'
|
||||
import { NetworkContextName } from './constants'
|
||||
import LocalStorageContextProvider, { Updater as LocalStorageContextUpdater } from './contexts/LocalStorage'
|
||||
import ApplicationContextProvider, { Updater as ApplicationContextUpdater } from './contexts/Application'
|
||||
import TransactionContextProvider, { Updater as TransactionContextUpdater } from './contexts/Transactions'
|
||||
@ -11,13 +12,18 @@ import TokensContextProvider from './contexts/Tokens'
|
||||
import BalancesContextProvider from './contexts/Balances'
|
||||
import AllowancesContextProvider from './contexts/Allowances'
|
||||
import AllBalancesContextProvider from './contexts/AllBalances'
|
||||
|
||||
import App from './pages/App'
|
||||
import NetworkOnlyConnector from './NetworkOnlyConnector'
|
||||
import InjectedConnector from './InjectedConnector'
|
||||
|
||||
import ThemeProvider, { GlobalStyle } from './theme'
|
||||
import './i18n'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
function getLibrary(provider) {
|
||||
const library = new ethers.providers.Web3Provider(provider)
|
||||
library.pollingInterval = 10000
|
||||
return library
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
ReactGA.initialize('UA-128182339-1')
|
||||
} else {
|
||||
@ -25,10 +31,6 @@ if (process.env.NODE_ENV === 'production') {
|
||||
}
|
||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
||||
|
||||
const Network = new NetworkOnlyConnector({ providerURL: process.env.REACT_APP_NETWORK_URL || '' })
|
||||
const Injected = new InjectedConnector({ supportedNetworks: [Number(process.env.REACT_APP_NETWORK_ID || '1')] })
|
||||
const connectors = { Injected, Network }
|
||||
|
||||
function ContextProviders({ children }) {
|
||||
return (
|
||||
<LocalStorageContextProvider>
|
||||
@ -58,7 +60,8 @@ function Updaters() {
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Web3Provider connectors={connectors} libraryName="ethers.js">
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3ProviderNetwork getLibrary={getLibrary}>
|
||||
<ContextProviders>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
@ -68,6 +71,7 @@ ReactDOM.render(
|
||||
</>
|
||||
</ThemeProvider>
|
||||
</ContextProviders>
|
||||
</Web3Provider>,
|
||||
</Web3ProviderNetwork>
|
||||
</Web3ReactProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useReducer, useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { ethers } from 'ethers'
|
||||
import ReactGA from 'react-ga'
|
||||
@ -11,8 +10,7 @@ import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import OversizedPanel from '../../components/OversizedPanel'
|
||||
import ContextualInfo from '../../components/ContextualInfo'
|
||||
import { ReactComponent as Plus } from '../../assets/images/plus-blue.svg'
|
||||
|
||||
import { useExchangeContract } from '../../hooks'
|
||||
import { useWeb3React, useExchangeContract } from '../../hooks'
|
||||
import { brokenTokens } from '../../constants'
|
||||
import { amountFormatter, calculateGasMargin } from '../../utils'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
@ -201,7 +199,7 @@ function getMarketRate(reserveETH, reserveToken, decimals, invert = false) {
|
||||
|
||||
export default function AddLiquidity({ params }) {
|
||||
const { t } = useTranslation()
|
||||
const { library, active, account } = useWeb3Context()
|
||||
const { library, account, active } = useWeb3React()
|
||||
|
||||
// clear url of query
|
||||
useEffect(() => {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { withRouter } from 'react-router'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { ethers } from 'ethers'
|
||||
import styled from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactGA from 'react-ga'
|
||||
|
||||
import { useWeb3React, useFactoryContract } from '../../hooks'
|
||||
import { Button } from '../../theme'
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
import OversizedPanel from '../../components/OversizedPanel'
|
||||
import { useFactoryContract } from '../../hooks'
|
||||
import { useTokenDetails } from '../../contexts/Tokens'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
|
||||
@ -56,7 +56,8 @@ const Flex = styled.div`
|
||||
|
||||
function CreateExchange({ location, params }) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useWeb3Context()
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const factory = useFactoryContract()
|
||||
|
||||
const [tokenAddress, setTokenAddress] = useState({
|
||||
|
@ -126,6 +126,7 @@ function ModeSelector({ location: { pathname }, history }) {
|
||||
</LiquidityContainer>
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
maxHeight={50}
|
||||
onDismiss={() => {
|
||||
setModalIsOpen(false)
|
||||
}}
|
||||
|
@ -2,17 +2,15 @@ import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactGA from 'react-ga'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { useWeb3Context } from 'web3-react'
|
||||
import { ethers } from 'ethers'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useWeb3React, useExchangeContract } from '../../hooks'
|
||||
import { Button } from '../../theme'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import ContextualInfo from '../../components/ContextualInfo'
|
||||
import OversizedPanel from '../../components/OversizedPanel'
|
||||
import ArrowDown from '../../assets/svg/SVGArrowDown'
|
||||
|
||||
import { useExchangeContract } from '../../hooks'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
import { useTokenDetails } from '../../contexts/Tokens'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
@ -143,8 +141,8 @@ function calculateSlippageBounds(value) {
|
||||
}
|
||||
|
||||
export default function RemoveLiquidity({ params }) {
|
||||
const { library, account, active } = useWeb3Context()
|
||||
const { t } = useTranslation()
|
||||
const { library, account, active } = useWeb3React()
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
|
@ -21,16 +21,6 @@ const mediaWidthTemplates = Object.keys(MEDIA_WIDTHS).reduce((accumulator, size)
|
||||
return accumulator
|
||||
}, {})
|
||||
|
||||
const flexColumnNoWrap = css`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
`
|
||||
|
||||
const flexRowNoWrap = css`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
`
|
||||
|
||||
const white = '#FFFFFF'
|
||||
const black = '#000000'
|
||||
|
||||
@ -59,7 +49,7 @@ const theme = darkMode => ({
|
||||
// for setting css on <html>
|
||||
backgroundColor: darkMode ? '#333639' : white,
|
||||
|
||||
modalBackground: darkMode ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.3)',
|
||||
modalBackground: darkMode ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.5)',
|
||||
inputBackground: darkMode ? '#202124' : white,
|
||||
placeholderGray: darkMode ? '#5F5F5F' : '#E1E1E1',
|
||||
shadowColor: darkMode ? '#000' : '#2F80ED',
|
||||
@ -92,16 +82,29 @@ const theme = darkMode => ({
|
||||
warningYellow: '#FFE270',
|
||||
// pink
|
||||
uniswapPink: '#DC6BE5',
|
||||
//green
|
||||
connectedGreen: '#27AE60',
|
||||
|
||||
//branded
|
||||
metaMaskOrange: '#E8831D',
|
||||
|
||||
//specific
|
||||
textHover: darkMode ? theme.uniswapPink : theme.doveGray,
|
||||
|
||||
// connect button when loggedout
|
||||
buttonFaded: darkMode ? '#DC6BE5' : '#737373',
|
||||
|
||||
// media queries
|
||||
mediaWidth: mediaWidthTemplates,
|
||||
// css snippets
|
||||
flexColumnNoWrap,
|
||||
flexRowNoWrap
|
||||
flexColumnNoWrap: css`
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
`,
|
||||
flexRowNoWrap: css`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
`
|
||||
})
|
||||
|
||||
export const GlobalStyle = createGlobalStyle`
|
||||
|