Commit 015c9f85 authored by Ian Lapham's avatar Ian Lapham Committed by Noah Zinsmeister

Hosted wallets (#528)

* formnatic setup

* stable with new hosted

* stable version, fortmatic and portis

* fix safari bug

* cleanup

* add white version of arrow

* update options to tabbable buttons

* update injected version

* style tweaks

* style tweaks, remove portis connector
parent a11dc4cd
...@@ -7,9 +7,11 @@ import Transaction from './Transaction' ...@@ -7,9 +7,11 @@ import Transaction from './Transaction'
import { SUPPORTED_WALLETS } from '../../constants' import { SUPPORTED_WALLETS } from '../../constants'
import { ReactComponent as Close } from '../../assets/images/x.svg' import { ReactComponent as Close } from '../../assets/images/x.svg'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink } from '../../utils'
import { injected, walletconnect, walletlink } from '../../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import Identicon from '../Identicon' import Identicon from '../Identicon'
import { Link } from '../../theme' import { Link } from '../../theme'
...@@ -115,14 +117,16 @@ const GreenCircle = styled.div` ...@@ -115,14 +117,16 @@ const GreenCircle = styled.div`
width: 8px; width: 8px;
margin-left: 12px; margin-left: 12px;
margin-right: 2px; margin-right: 2px;
margin-top: -6px;
background-color: ${({ theme }) => theme.connectedGreen}; background-color: ${({ theme }) => theme.connectedGreen};
border-radius: 50%; border-radius: 50%;
} }
` `
const GreenText = styled.div` const CircleWrapper = styled.div`
color: ${({ theme }) => theme.connectedGreen}; color: ${({ theme }) => theme.connectedGreen};
display: flex;
justify-content: center;
align-items: center;
` `
const LowerSection = styled.div` const LowerSection = styled.div`
...@@ -208,6 +212,20 @@ const TransactionListWrapper = styled.div` ...@@ -208,6 +212,20 @@ const TransactionListWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
` `
const WalletAction = styled.div`
color: ${({ theme }) => theme.chaliceGray};
margin-left: 16px;
font-weight: 400;
:hover {
cursor: pointer;
text-decoration: underline;
}
`
const MainWalletAction = styled(WalletAction)`
color: ${({ theme }) => theme.royalBlue};
`
function renderTransactions(transactions, pending) { function renderTransactions(transactions, pending) {
return ( return (
<TransactionListWrapper> <TransactionListWrapper>
...@@ -228,14 +246,13 @@ export default function AccountDetails({ ...@@ -228,14 +246,13 @@ export default function AccountDetails({
const { chainId, account, connector } = useWeb3React() const { chainId, account, connector } = useWeb3React()
function formatConnectorName() { function formatConnectorName() {
const isMetaMask = window.ethereum && window.ethereum.isMetaMask const isMetaMask = window.ethereum && window.ethereum.isMetaMask ? true : false
const name = Object.keys(SUPPORTED_WALLETS) const name = Object.keys(SUPPORTED_WALLETS)
.filter( .filter(
k => k =>
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK')) SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
) )
.map(k => SUPPORTED_WALLETS[k].name)[0] .map(k => SUPPORTED_WALLETS[k].name)[0]
return <WalletName>{name}</WalletName> return <WalletName>{name}</WalletName>
} }
...@@ -258,6 +275,27 @@ export default function AccountDetails({ ...@@ -258,6 +275,27 @@ export default function AccountDetails({
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()} <img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
</IconWrapper> </IconWrapper>
) )
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
</IconWrapper>
)
} else if (connector === portis) {
return (
<>
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
}}
>
Show Portis
</MainWalletAction>
</IconWrapper>
</>
)
} }
} }
...@@ -273,11 +311,22 @@ export default function AccountDetails({ ...@@ -273,11 +311,22 @@ export default function AccountDetails({
<InfoCard> <InfoCard>
<AccountGroupingRow> <AccountGroupingRow>
{getStatusIcon()} {getStatusIcon()}
<GreenText> <div>
{connector !== injected && connector !== walletlink && (
<WalletAction
onClick={() => {
connector.close()
}}
>
Disconnect
</WalletAction>
)}
<CircleWrapper>
<GreenCircle> <GreenCircle>
<div /> <div />
</GreenCircle> </GreenCircle>
</GreenText> </CircleWrapper>
</div>
</AccountGroupingRow> </AccountGroupingRow>
<AccountGroupingRow> <AccountGroupingRow>
{ENSName ? ( {ENSName ? (
...@@ -298,7 +347,8 @@ export default function AccountDetails({ ...@@ -298,7 +347,8 @@ export default function AccountDetails({
</AccountGroupingRow> </AccountGroupingRow>
</InfoCard> </InfoCard>
</YourAccount> </YourAccount>
{!isMobile && (
{!(isMobile && (window.web3 || window.ethereum)) && (
<ConnectButtonRow> <ConnectButtonRow>
<OptionButton <OptionButton
onClick={() => { onClick={() => {
......
...@@ -3,42 +3,67 @@ import styled from 'styled-components' ...@@ -3,42 +3,67 @@ import styled from 'styled-components'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import { Link } from '../../theme' import { Link } from '../../theme'
const InfoCard = styled.div` const InfoCard = styled.button`
background-color: ${({ theme }) => theme.backgroundColor}; background-color: ${({ theme, active }) => (active ? theme.activeGray : theme.backgroundColor)};
padding: 1rem; padding: 1rem;
border: 1px solid ${({ theme }) => theme.placeholderGray}; outline: none;
border-radius: 20px; border: 1px solid;
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)}; border-radius: 12px;
width: 100% !important;
: 0 4px 8px 0 ${({ theme, clickable }) => (clickable ? transparentize(0.95, theme.shadowColor) : 'none')};
&:focus {
box-shadow: 0 0 0 1px ${({ theme }) => theme.royalBlue};
}
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.placeholderGray)};
` `
const OptionCard = styled(InfoCard)` const OptionCard = styled(InfoCard)`
display: grid; display: flex;
grid-template-columns: 1fr 48px; flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 2rem; margin-top: 2rem;
padding: 1rem; padding: 1rem;
${({ theme }) => theme.mediaWidth.upToMedium`
height: 40px;
grid-template-columns: 1fr 40px;
padding: 0.6rem 1rem;
`};
` `
const OptionCardLeft = styled.div` const OptionCardLeft = styled.div`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
height: 100%;
justify-content: center; justify-content: center;
height: 100%;
` `
const OptionCardClickable = styled(OptionCard)` const OptionCardClickable = styled(OptionCard)`
margin-top: 0; margin-top: 0;
margin-bottom: 1rem;
&:hover { &:hover {
cursor: pointer; cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
border: 1px solid ${({ theme }) => theme.malibuBlue}; border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.malibuBlue}` : ``)};
}
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
`
const GreenCircle = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
&:first-child {
height: 8px;
width: 8px;
margin-right: 8px;
background-color: ${({ theme }) => theme.connectedGreen};
border-radius: 50%;
} }
` `
const CircleWrapper = styled.div`
color: ${({ theme }) => theme.connectedGreen};
display: flex;
justify-content: center;
align-items: center;
`
const HeaderText = styled.div` const HeaderText = styled.div`
${({ theme }) => theme.flexRowNoWrap};
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : props.color)}; color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : props.color)};
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
...@@ -62,22 +87,44 @@ const IconWrapper = styled.div` ...@@ -62,22 +87,44 @@ const IconWrapper = styled.div`
justify-content: center; justify-content: center;
& > img, & > img,
span { span {
height: ${({ size }) => (size ? size + 'px' : '32px')}; height: ${({ size }) => (size ? size + 'px' : '24px')};
width: ${({ size }) => (size ? size + 'px' : '32px')}; width: ${({ size }) => (size ? size + 'px' : '24px')};
} }
${({ theme }) => theme.mediaWidth.upToMedium` ${({ theme }) => theme.mediaWidth.upToMedium`
align-items: flex-end; align-items: flex-end;
`}; `};
` `
export default function Option({ link = null, size = null, onClick = null, color, header, subheader = null, icon }) { export default function Option({
link = null,
clickable = true,
size = null,
onClick = null,
color,
header,
subheader = null,
icon,
active = false
}) {
const content = ( const content = (
<OptionCardClickable onClick={onClick}> <OptionCardClickable onClick={onClick} clickable={clickable && !active} active={active}>
<OptionCardLeft> <OptionCardLeft>
<HeaderText color={color}>{header}</HeaderText> <HeaderText color={color}>
{' '}
{active ? (
<CircleWrapper>
<GreenCircle>
<div />
</GreenCircle>
</CircleWrapper>
) : (
''
)}
{header}
</HeaderText>
{subheader && <SubHeader>{subheader}</SubHeader>} {subheader && <SubHeader>{subheader}</SubHeader>}
</OptionCardLeft> </OptionCardLeft>
<IconWrapper size={size}> <IconWrapper size={size} active={active}>
<img src={icon} alt={'Icon'} /> <img src={icon} alt={'Icon'} />
</IconWrapper> </IconWrapper>
</OptionCardClickable> </OptionCardClickable>
......
import React from 'react'
import styled from 'styled-components'
import Option from './Option'
import { SUPPORTED_WALLETS } from '../../constants'
import WalletConnectData from './WalletConnectData'
import { walletconnect, injected } from '../../connectors'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { darken } from 'polished'
const PendingSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
width: 100%;
& > * {
width: 100%;
}
`
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
margin-right: 1rem;
svg {
path {
color: ${({ theme }) => theme.placeholderGray};
}
}
`
const LoadingMessage = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
justify-content: flex-start;
border-radius: 12px;
margin-bottom: 20px;
color: ${({ theme, error }) => (error ? theme.salmonRed : 'inherit')};
border: 1px solid ${({ theme, error }) => (error ? theme.salmonRed : theme.placeholderGray)};
& > * {
padding: 1rem;
}
`
const ErrorGroup = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
justify-content: flex-start;
`
const ErrorButton = styled.div`
border-radius: 8px;
font-size: 12px;
color: ${({ theme }) => theme.textColor};
background-color: ${({ theme }) => theme.placeholderGray};
margin-left: 1rem;
padding: 0.5rem;
font-weight: 600;
user-select: none;
&:hover {
cursor: pointer;
background-color: ${({ theme }) => darken(0.1, theme.placeholderGray)};
}
`
export default function PendingView({ uri = '', size, connector, error = false, tryActivation }) {
const isMetamask = window.ethereum && window.ethereum.isMetaMask
return (
<PendingSection>
<LoadingMessage error={error}>
<div>
{!error && <SpinnerWrapper src={Circle} />}
{error ? (
<ErrorGroup>
<div>Error connecting.</div>
<ErrorButton
onClick={() => {
tryActivation(connector)
}}
>
Try Again
</ErrorButton>
</ErrorGroup>
) : connector === walletconnect ? (
'Scan QR code with a compatible wallet...'
) : (
'Waiting for connection...'
)}
</div>
</LoadingMessage>
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
{Object.keys(SUPPORTED_WALLETS).map(key => {
const option = SUPPORTED_WALLETS[key]
if (option.connector === connector) {
if (option.connector === injected && isMetamask && option.name !== 'MetaMask') {
return null
} else {
return (
<Option
key={key}
clickable={false}
color={option.color}
header={option.name}
subheader={option.description}
icon={require('../../assets/images/' + option.iconName)}
/>
)
}
}
return true
})}
</PendingSection>
)
}
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import QRCode from 'qrcode.react' import QRCode from 'qrcode.react'
import Option from './Option'
import { useDarkModeManager } from '../../contexts/LocalStorage' 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` const QRCodeWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 280px; border-radius: 12px;
height: 280px;
border-radius: 20px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid ${({ theme }) => theme.placeholderGray};
` `
export default function QrCode({ uri, size, header, subheader, icon }) { export default function WalletConnectData({ uri = '', size }) {
const [isDark] = useDarkModeManager() const [isDark] = useDarkModeManager()
return ( return (
<QRSection>
<h5>Scan QR code with a compatible wallet</h5>
<QRCodeWrapper> <QRCodeWrapper>
{uri && ( {uri && (
<QRCode size={size} value={uri} bgColor={isDark ? '#333639' : 'white'} fgColor={isDark ? 'white' : 'black'} /> <QRCode size={size} value={uri} bgColor={isDark ? '#333639' : 'white'} fgColor={isDark ? 'white' : 'black'} />
)} )}
</QRCodeWrapper> </QRCodeWrapper>
<Option color={'#4196FC'} header={header} subheader={subheader} icon={icon} />
</QRSection>
) )
} }
...@@ -6,16 +6,16 @@ import { URI_AVAILABLE } from '@web3-react/walletconnect-connector' ...@@ -6,16 +6,16 @@ import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
import Modal from '../Modal' import Modal from '../Modal'
import AccountDetails from '../AccountDetails' import AccountDetails from '../AccountDetails'
import QrCode from './QrCode' import PendingView from './PendingView'
import Option from './Option' import Option from './Option'
import { SUPPORTED_WALLETS, MOBILE_DEEP_LINKS } from '../../constants' import { SUPPORTED_WALLETS } from '../../constants'
import { usePrevious } from '../../hooks' import { usePrevious } from '../../hooks'
import { Link } from '../../theme' import { Link } from '../../theme'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import MetamaskIcon from '../../assets/images/metamask.png' import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg' import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, walletconnect } from '../../connectors' import { injected, walletconnect, fortmatic } from '../../connectors'
import { useWalletModalToggle, useWalletModalOpen } from '../../contexts/Application' import { useWalletModalToggle, useWalletModalOpen } from '../../contexts/Application'
import { OVERLAY_READY } from '../../connectors/Fortmatic'
const CloseIcon = styled.div` const CloseIcon = styled.div`
position: absolute; position: absolute;
...@@ -107,8 +107,9 @@ const HoverText = styled.div` ...@@ -107,8 +107,9 @@ const HoverText = styled.div`
const WALLET_VIEWS = { const WALLET_VIEWS = {
OPTIONS: 'options', OPTIONS: 'options',
OPTIONS_SECONDARY: 'options_secondary',
ACCOUNT: 'account', ACCOUNT: 'account',
WALLET_CONNECT: 'walletConnect' PENDING: 'pending'
} }
export default function WalletModal({ pendingTransactions, confirmedTransactions, ENSName }) { export default function WalletModal({ pendingTransactions, confirmedTransactions, ENSName }) {
...@@ -116,12 +117,17 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -116,12 +117,17 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT) const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
const [pendingWallet, setPendingWallet] = useState()
const [pendingError, setPendingError] = useState()
const walletModalOpen = useWalletModalOpen() const walletModalOpen = useWalletModalOpen()
const toggleWalletModal = useWalletModalToggle() const toggleWalletModal = useWalletModalToggle()
// always reset to account view // always reset to account view
useEffect(() => { useEffect(() => {
if (walletModalOpen) { if (walletModalOpen) {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT) setWalletView(WALLET_VIEWS.ACCOUNT)
} }
}, [walletModalOpen]) }, [walletModalOpen])
...@@ -131,7 +137,7 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -131,7 +137,7 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
useEffect(() => { useEffect(() => {
const activateWC = uri => { const activateWC = uri => {
setUri(uri) setUri(uri)
setWalletView(WALLET_VIEWS.WALLET_CONNECT) // setWalletView(WALLET_VIEWS.PENDING)
} }
walletconnect.on(URI_AVAILABLE, activateWC) walletconnect.on(URI_AVAILABLE, activateWC)
return () => { return () => {
...@@ -148,33 +154,48 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -148,33 +154,48 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
} }
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious]) }, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
const tryActivation = async connector => {
setPendingWallet(connector) // set wallet for pending view
setWalletView(WALLET_VIEWS.PENDING)
activate(connector, undefined, true).catch(e => {
setPendingError(true)
})
}
// close wallet modal if fortmatic modal is active
useEffect(() => {
fortmatic.on(OVERLAY_READY, () => {
toggleWalletModal()
})
}, [toggleWalletModal])
// get wallets user can switch too, depending on device/browser // get wallets user can switch too, depending on device/browser
function getOptions() { function getOptions() {
const isMetamask = window.ethereum && window.ethereum.isMetaMask const isMetamask = window.ethereum && window.ethereum.isMetaMask
return Object.keys(SUPPORTED_WALLETS).map(key => {
if (isMobile && !window.web3 && !window.ethereum) { const option = SUPPORTED_WALLETS[key]
return Object.keys(MOBILE_DEEP_LINKS).map(key => { // check for mobile options
const option = MOBILE_DEEP_LINKS[key] if (isMobile) {
if (!window.web3 && !window.ethereum && option.mobile) {
return ( return (
<Option <Option
onClick={() => {
option.connector !== connector && !option.href && tryActivation(option.connector)
}}
key={key} key={key}
active={option.connector && option.connector === connector}
color={option.color} color={option.color}
header={option.name}
link={option.href} link={option.href}
subheader={option.description} header={option.name}
subheader={null}
icon={require('../../assets/images/' + option.iconName)} 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 return null
} }
// overwrite injected when needed
if (option.connector === injected) { if (option.connector === injected) {
// don't show injected if there's no injected provider // don't show injected if there's no injected provider
if (!(window.web3 || window.ethereum)) { if (!(window.web3 || window.ethereum)) {
...@@ -184,7 +205,7 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -184,7 +205,7 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
key={key} key={key}
color={'#E8831D'} color={'#E8831D'}
header={'Install Metamask'} header={'Install Metamask'}
subheader={'Easy to use browser extension.'} subheader={null}
link={'https://metamask.io/'} link={'https://metamask.io/'}
icon={MetamaskIcon} icon={MetamaskIcon}
/> />
...@@ -203,21 +224,26 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -203,21 +224,26 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
} }
} }
// return rest of options
return ( return (
!isMobile &&
!option.mobileOnly && (
<Option <Option
onClick={() => { onClick={() => {
activate(option.connector) option.connector !== connector && !option.href && tryActivation(option.connector)
}} }}
key={key} key={key}
active={option.connector === connector}
color={option.color} color={option.color}
link={option.href}
header={option.name} header={option.name}
subheader={walletView === WALLET_VIEWS.OPTIONS ? null : option.description} subheader={null} //use option.descriptio to bring back multi-line
icon={require('../../assets/images/' + option.iconName)} icon={require('../../assets/images/' + option.iconName)}
/> />
) )
)
}) })
} }
}
function getModalContent() { function getModalContent() {
if (error) { if (error) {
...@@ -254,13 +280,15 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -254,13 +280,15 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
<CloseColor alt={'close icon'} /> <CloseColor alt={'close icon'} />
</CloseIcon> </CloseIcon>
{walletView !== WALLET_VIEWS.ACCOUNT ? ( {walletView !== WALLET_VIEWS.ACCOUNT ? (
<HeaderRow <HeaderRow color="blue">
color="blue" <HoverText
onClick={() => { onClick={() => {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT) setWalletView(WALLET_VIEWS.ACCOUNT)
}} }}
> >
<HoverText>Back</HoverText> Back
</HoverText>
</HeaderRow> </HeaderRow>
) : ( ) : (
<HeaderRow> <HeaderRow>
...@@ -268,25 +296,25 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions ...@@ -268,25 +296,25 @@ export default function WalletModal({ pendingTransactions, confirmedTransactions
</HeaderRow> </HeaderRow>
)} )}
<ContentWrapper> <ContentWrapper>
{walletView === WALLET_VIEWS.WALLET_CONNECT ? ( {walletView === WALLET_VIEWS.PENDING ? (
<QrCode <PendingView
uri={uri} uri={uri}
header={'Connect with Wallet Connect'}
subheader={'Open protocol supported by major mobile wallets'}
size={220} size={220}
icon={WalletConnectIcon} connector={pendingWallet}
error={pendingError}
tryActivation={tryActivation}
/> />
) : !account ? (
getOptions()
) : ( ) : (
<OptionGrid>{getOptions()}</OptionGrid> <OptionGrid>{getOptions()}</OptionGrid>
)} )}
{walletView !== WALLET_VIEWS.PENDING && (
<Blurb> <Blurb>
<span>New to Ethereum? &nbsp;</span>{' '} <span>New to Ethereum? &nbsp;</span>{' '}
<Link href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use"> <Link href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
Learn more about wallets Learn more about wallets
</Link> </Link>
</Blurb> </Blurb>
)}
</ContentWrapper> </ContentWrapper>
</UpperSection> </UpperSection>
) )
......
...@@ -12,9 +12,11 @@ import { useAllTransactions } from '../../contexts/Transactions' ...@@ -12,9 +12,11 @@ import { useAllTransactions } from '../../contexts/Transactions'
import { useWalletModalToggle } from '../../contexts/Application' import { useWalletModalToggle } from '../../contexts/Application'
import { Spinner } from '../../theme' import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg' import Circle from '../../assets/images/circle.svg'
import { injected, walletconnect, walletlink } from '../../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import { NetworkContextName } from '../../constants' import { NetworkContextName } from '../../constants'
import Identicon from '../Identicon' import Identicon from '../Identicon'
...@@ -147,6 +149,18 @@ export default function Web3Status() { ...@@ -147,6 +149,18 @@ export default function Web3Status() {
<img src={CoinbaseWalletIcon} alt={''} /> <img src={CoinbaseWalletIcon} alt={''} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} />
</IconWrapper>
)
} }
} }
......
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
export const OVERLAY_READY = 'OVERLAY_READY'
const chainIdToNetwork = {
1: 'mainnet',
3: 'ropsten',
4: 'rinkeby',
42: 'kovan'
}
export class FortmaticConnector extends FortmaticConnectorCore {
async activate() {
if (!this.fortmatic) {
const { default: Fortmatic } = await import('fortmatic')
this.fortmatic = new Fortmatic(
this.apiKey,
this.chainId === 1 || this.chainId === 4 ? undefined : chainIdToNetwork[this.chainId]
)
}
const provider = this.fortmatic.getProvider()
const pollForOverlayReady = new Promise(resolve => {
const interval = setInterval(() => {
if (provider.overlayReady) {
clearInterval(interval)
this.emit(OVERLAY_READY)
resolve()
}
}, 200)
})
const [account] = await Promise.all([provider.enable().then(accounts => accounts[0]), pollForOverlayReady])
return { provider: this.fortmatic.getProvider(), chainId: this.chainId, account }
}
}
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
}
}
import { InjectedConnector } from '@web3-react/injected-connector'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector' import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { WalletLinkConnector } from '@web3-react/walletlink-connector' import { WalletLinkConnector } from '@web3-react/walletlink-connector'
import { PortisConnector } from '@web3-react/portis-connector'
import { InjectedConnector } from './Injected'
import { NetworkConnector } from './Network' import { NetworkConnector } from './Network'
import { FortmaticConnector } from './Fortmatic'
const POLLING_INTERVAL = 10000 const POLLING_INTERVAL = 10000
export const injected = new InjectedConnector({
supportedChainIds: [1]
})
export const network = new NetworkConnector({ export const network = new NetworkConnector({
urls: { 1: process.env.REACT_APP_NETWORK_URL }, urls: { 1: process.env.REACT_APP_NETWORK_URL },
pollingInterval: POLLING_INTERVAL pollingInterval: POLLING_INTERVAL
}) })
export const injected = new InjectedConnector({
supportedChainIds: [1]
})
export const walletconnect = new WalletConnectConnector({ export const walletconnect = new WalletConnectConnector({
rpc: { 1: process.env.REACT_APP_NETWORK_URL }, rpc: { 1: process.env.REACT_APP_NETWORK_URL },
bridge: 'https://bridge.walletconnect.org', bridge: 'https://bridge.walletconnect.org',
...@@ -22,6 +24,16 @@ export const walletconnect = new WalletConnectConnector({ ...@@ -22,6 +24,16 @@ export const walletconnect = new WalletConnectConnector({
pollingInterval: POLLING_INTERVAL pollingInterval: POLLING_INTERVAL
}) })
export const fortmatic = new FortmaticConnector({
apiKey: process.env.REACT_APP_FORTMATIC_KEY,
chainId: 1
})
export const portis = new PortisConnector({
dAppId: process.env.REACT_APP_PORTIS_ID,
networks: [1]
})
export const walletlink = new WalletLinkConnector({ export const walletlink = new WalletLinkConnector({
url: process.env.REACT_APP_NETWORK_URL, url: process.env.REACT_APP_NETWORK_URL,
appName: 'Uniswap', appName: 'Uniswap',
......
import { injected, walletconnect, walletlink } from '../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../connectors'
export const FACTORY_ADDRESSES = { export const FACTORY_ADDRESSES = {
1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95', 1: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
...@@ -15,52 +15,72 @@ export const SUPPORTED_THEMES = { ...@@ -15,52 +15,72 @@ export const SUPPORTED_THEMES = {
export const SUPPORTED_WALLETS = { export const SUPPORTED_WALLETS = {
INJECTED: { INJECTED: {
connector: injected, connector: injected,
id: 'Injected',
name: 'Injected', name: 'Injected',
iconName: 'arrow-right.svg', iconName: 'arrow-right.svg',
description: 'Injected web3 provider.', description: 'Injected web3 provider.',
color: '#010101' href: null,
color: '#010101',
primary: true
}, },
METAMASK: { METAMASK: {
connector: injected, connector: injected,
id: 'MetaMask',
name: 'MetaMask', name: 'MetaMask',
iconName: 'metamask.png', iconName: 'metamask.png',
description: 'Easy-to-use browser extension.', description: 'Easy-to-use browser extension.',
href: null,
color: '#E8831D' color: '#E8831D'
}, },
WALLET_CONNECT: { WALLET_CONNECT: {
connector: walletconnect, connector: walletconnect,
id: 'WalletConnect',
name: 'Wallet Connect', name: 'Wallet Connect',
iconName: 'walletConnectIcon.svg', iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...', description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC' color: '#4196FC'
}, },
WALLET_LINK: { WALLET_LINK: {
connector: walletlink, connector: walletlink,
id: 'WalletLink',
name: 'Coinbase Wallet', name: 'Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg', iconName: 'coinbaseWalletIcon.svg',
description: 'Use Coinbase Wallet app on mobile device', description: 'Use Coinbase Wallet app on mobile device',
href: null,
color: '#315CF5' color: '#315CF5'
} },
}
export const MOBILE_DEEP_LINKS = {
COINBASE_LINK: { COINBASE_LINK: {
name: 'Open in Coinbase Wallet', name: 'Open in Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg', iconName: 'coinbaseWalletIcon.svg',
description: 'Open in Coinbase Wallet app.', description: 'Open in Coinbase Wallet app.',
href: 'https://go.cb-w.com/mtUDhEZPy1', href: 'https://go.cb-w.com/mtUDhEZPy1',
color: '#315CF5' color: '#315CF5',
mobile: true,
mobileOnly: true
}, },
TRUST_WALLET_LINK: { TRUST_WALLET_LINK: {
name: 'Open in Trust Wallet', name: 'Open in Trust Wallet',
iconName: 'trustWallet.png', iconName: 'trustWallet.png',
description: 'iOS and Android app.', description: 'iOS and Android app.',
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap', href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
color: '#1C74CC' color: '#1C74CC',
mobile: true,
mobileOnly: true
},
FORTMATIC: {
connector: fortmatic,
name: 'Fortmatic',
iconName: 'fortmaticIcon.png',
description: 'Login using Fortmatic hosted wallet',
href: null,
color: '#6748FF',
mobile: true
},
Portis: {
connector: portis,
name: 'Portis',
iconName: 'portisIcon.png',
description: 'Login using Portis hosted wallet',
href: null,
color: '#4A6C9B',
mobile: true
} }
} }
......
import { useState, useMemo, useCallback, useEffect, useRef } from 'react' import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core' import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { isMobile } from 'react-device-detect'
import { NetworkContextName } from '../constants' import { NetworkContextName } from '../constants'
import ERC20_ABI from '../constants/abis/erc20' import ERC20_ABI from '../constants/abis/erc20'
...@@ -25,9 +26,15 @@ export function useEagerConnect() { ...@@ -25,9 +26,15 @@ export function useEagerConnect() {
activate(injected, undefined, true).catch(() => { activate(injected, undefined, true).catch(() => {
setTried(true) setTried(true)
}) })
} else {
if (isMobile && window.ethereum) {
activate(injected, undefined, true).catch(() => {
setTried(true)
})
} else { } else {
setTried(true) setTried(true)
} }
}
}) })
}, [activate]) // intentionally only running on mount (make sure it's only mounted once :)) }, [activate]) // intentionally only running on mount (make sure it's only mounted once :))
...@@ -52,7 +59,7 @@ export function useInactiveListener(suppress = false) { ...@@ -52,7 +59,7 @@ export function useInactiveListener(suppress = false) {
const { ethereum } = window const { ethereum } = window
if (ethereum && ethereum.on && !active && !error && !suppress) { if (ethereum && ethereum.on && !active && !error && !suppress) {
const handleNetworkChanged = () => { const handleChainChanged = () => {
// eat errors // eat errors
activate(injected, undefined, true).catch(() => {}) activate(injected, undefined, true).catch(() => {})
} }
...@@ -64,14 +71,23 @@ export function useInactiveListener(suppress = false) { ...@@ -64,14 +71,23 @@ export function useInactiveListener(suppress = false) {
} }
} }
const handleNetworkChanged = () => {
// eat errors
activate(injected, undefined, true).catch(() => {})
}
ethereum.on('chainChanged', handleChainChanged)
ethereum.on('networkChanged', handleNetworkChanged) ethereum.on('networkChanged', handleNetworkChanged)
ethereum.on('accountsChanged', handleAccountsChanged) ethereum.on('accountsChanged', handleAccountsChanged)
return () => { return () => {
if (ethereum.removeListener) {
ethereum.removeListener('chainChanged', handleChainChanged)
ethereum.removeListener('networkChanged', handleNetworkChanged) ethereum.removeListener('networkChanged', handleNetworkChanged)
ethereum.removeListener('accountsChanged', handleAccountsChanged) ethereum.removeListener('accountsChanged', handleAccountsChanged)
} }
} }
}
return () => {} return () => {}
}, [active, error, suppress, activate]) }, [active, error, suppress, activate])
......
...@@ -61,6 +61,7 @@ const theme = darkMode => ({ ...@@ -61,6 +61,7 @@ const theme = darkMode => ({
chaliceGray: darkMode ? '#7B7B7B' : '#AEAEAE', chaliceGray: darkMode ? '#7B7B7B' : '#AEAEAE',
doveGray: darkMode ? '#C4C4C4' : '#737373', doveGray: darkMode ? '#C4C4C4' : '#737373',
mineshaftGray: darkMode ? '#E1E1E1' : '#2B2B2B', mineshaftGray: darkMode ? '#E1E1E1' : '#2B2B2B',
activeGray: darkMode ? '#292C2F' : '#F7F8FA',
buttonOutlineGrey: darkMode ? '#FAFAFA' : '#F2F2F2', buttonOutlineGrey: darkMode ? '#FAFAFA' : '#F2F2F2',
tokenRowHover: darkMode ? '#404040' : '#F2F2F2', tokenRowHover: darkMode ? '#404040' : '#F2F2F2',
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment