Skip to content

Commit

Permalink
Merge pull request #42 from oasisprotocol/ml/eip-6963
Browse files Browse the repository at this point in the history
Add EIP-6963 wallet support
  • Loading branch information
lubej authored Apr 19, 2024
2 parents 9ee11e7 + f2aa2ae commit 18e892c
Show file tree
Hide file tree
Showing 22 changed files with 770 additions and 116 deletions.
15 changes: 12 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { Web3ContextProvider } from './providers/Web3Provider'
import { ConnectWallet } from './pages/ConnectWallet'
import { WrapFormContextProvider } from './providers/WrapFormProvider'
import { Transaction } from './pages/Transaction'
import { EIP1193ContextProvider } from './providers/EIP1193Provider.tsx'
import { EIP6963ManagerContextProvider } from './providers/EIP6963ManagerProvider.tsx'
import { EIP6963ContextProvider } from './providers/EIP6963Provider.tsx'

const router = createHashRouter([
{
Expand Down Expand Up @@ -33,7 +36,13 @@ const router = createHashRouter([
])

export const App: FC = () => (
<Web3ContextProvider>
<RouterProvider router={router} />
</Web3ContextProvider>
<EIP6963ManagerContextProvider>
<EIP1193ContextProvider>
<EIP6963ContextProvider>
<Web3ContextProvider>
<RouterProvider router={router} />
</Web3ContextProvider>
</EIP6963ContextProvider>
</EIP1193ContextProvider>
</EIP6963ManagerContextProvider>
)
64 changes: 64 additions & 0 deletions src/components/WalletModal/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.walletModalContent {
display: flex;
flex-direction: column;
padding-bottom: 2rem;

h4 {
color: var(--gray-extra-dark);
margin-bottom: 2rem;
text-align: center;
}
}

.walletModalLogo {
align-self: center;
margin-bottom: 3.125rem;
}

.walletModalProviderList {
display: flex;
flex-direction: column;
gap: 0.5rem;

.providerOption {
background-color: var(--gray-medium-light);
border-radius: 46px;
}
}

.providerOptionBtn {
display: flex;
align-items: center;
padding: 0.5rem 2rem;
gap: 0.75rem;
background: none;
border: none;
font: inherit;
outline: inherit;
color: var(--gray-extra-dark);
cursor: pointer;
width: 100%;
border-radius: 46px;

&:hover,
&:focus {
background-color: var(--gray-extra-dark);
color: var(--white);
}
}

.providerOptionLogo {
object-fit: cover;
width: 45px;
height: 45px;
}

@media screen and (max-width: 1000px) {
.walletModal {
max-width: unset;
}

.walletModalLogo {
margin-bottom: 2rem;
}
}
74 changes: 74 additions & 0 deletions src/components/WalletModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { FC, useState } from 'react'
import { Modal, ModalProps } from '../Modal'
import { LogoIconRound } from '../icons/LogoIconRound.tsx'
import classes from './index.module.css'
import { useEIP6963ProviderInfoList } from '../../hooks/useEIP6963ProviderInfoList.ts'
import { EIP6963ProviderInfo } from '../../utils/types.ts'
import { useEIP6963 } from '../../hooks/useEIP6963.ts'

interface WalletModalProps extends Pick<ModalProps, 'isOpen' | 'closeModal'> {
next: () => void
}

interface ProviderOptionProps extends Pick<EIP6963ProviderInfo, 'rdns' | 'name' | 'icon'> {
isLoading: boolean
onSelectProvider: (rdns: string) => void
}

const ProviderOption: FC<ProviderOptionProps> = ({ rdns, name, icon, onSelectProvider, isLoading }) => {
if (!rdns) {
return null
}

return (
<div className={classes.providerOption}>
<button
className={classes.providerOptionBtn}
onClick={() => onSelectProvider(rdns)}
disabled={isLoading}
>
<img className={classes.providerOptionLogo} src={icon} alt={name} />
{name}
</button>
</div>
)
}

export const WalletModal: FC<WalletModalProps> = ({ isOpen, closeModal, next }) => {
const { setCurrentProviderByRdns } = useEIP6963()
const providerInfoList = useEIP6963ProviderInfoList()

const [isLoading, setIsLoading] = useState(false)

const onSelectProvider = (rdns: string) => {
setIsLoading(true)
setCurrentProviderByRdns(rdns)
next()
setIsLoading(false)
}

const providerInfoOptionsList = providerInfoList.map(({ uuid, rdns, name, icon }) => (
<ProviderOption
key={uuid}
rdns={rdns}
name={name}
icon={icon}
isLoading={isLoading}
onSelectProvider={onSelectProvider}
/>
))

return (
<Modal isOpen={isOpen} closeModal={closeModal} disableBackdropClick>
<div className={classes.walletModalContent}>
<div className={classes.walletModalLogo}>
<LogoIconRound />
</div>

<h4>Connect a wallet</h4>

<div className={classes.walletModalProviderList}>{providerInfoOptionsList}</div>
</div>
</Modal>
)
}
11 changes: 11 additions & 0 deletions src/hooks/useEIP1193.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext } from 'react'
import { EIP1193Context } from '../providers/EIP1193Context'

export const useEIP1193 = () => {
const value = useContext(EIP1193Context)
if (Object.keys(value).length === 0) {
throw new Error('[useEIP1193] Component not wrapped within a Provider')
}

return value
}
11 changes: 11 additions & 0 deletions src/hooks/useEIP6963.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext } from 'react'
import { EIP6963Context } from '../providers/EIP6963Context.ts'

export const useEIP6963 = () => {
const value = useContext(EIP6963Context)
if (Object.keys(value).length === 0) {
throw new Error('[useEIP6963] Component not wrapped within a Provider')
}

return value
}
11 changes: 11 additions & 0 deletions src/hooks/useEIP6963Manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useContext } from 'react'
import { EIP6963ManagerContext } from '../providers/EIP6963ManagerContext.ts'

export const useEIP6963Manager = () => {
const value = useContext(EIP6963ManagerContext)
if (Object.keys(value).length === 0) {
throw new Error('[useEIP6963Manager] Component not wrapped within a Provider')
}

return value
}
10 changes: 10 additions & 0 deletions src/hooks/useEIP6963ProviderInfoList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useEIP6963Manager } from './useEIP6963Manager.ts'
import { EIP6963ProviderInfo } from '../utils/types.ts'

export const useEIP6963ProviderInfoList = (): EIP6963ProviderInfo[] => {
const {
state: { providerList },
} = useEIP6963Manager()

return providerList.map(({ info }) => info)
}
2 changes: 1 addition & 1 deletion src/hooks/useWeb3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Web3Context } from '../providers/Web3Context'

export const useWeb3 = () => {
const value = useContext(Web3Context)
if (value === undefined) {
if (Object.keys(value).length === 0) {
throw new Error('[useWeb3] Component not wrapped within a Provider')
}

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useWrapForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WrapFormContext } from '../providers/WrapFormContext'

export const useWrapForm = () => {
const value = useContext(WrapFormContext)
if (value === undefined) {
if (Object.keys(value).length === 0) {
throw new Error('[useWrapForm] Component not wrapped within a Provider')
}

Expand Down
60 changes: 50 additions & 10 deletions src/pages/ConnectWallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,64 @@ import { UnknownNetworkError } from '../../utils/errors'
import { Alert } from '../../components/Alert'
import { METAMASK_HOME_PAGE } from '../../constants/config'
import { useWeb3 } from '../../hooks/useWeb3'
import { WalletModal } from '../../components/WalletModal'
import { ProviderType } from '../../providers/Web3Context.ts'
import { useEIP6963 } from '../../hooks/useEIP6963.ts'
import { useEIP1193 } from '../../hooks/useEIP1193.ts'

export const ConnectWallet: FC = () => {
const { connectWallet, switchNetwork, isMetaMaskInstalled } = useWeb3()
const {
state: { isEIP6963ProviderAvailable },
} = useEIP6963()
const { isEIP1193ProviderAvailable } = useEIP1193()
const {
state: { providerType },
connectWallet,
switchNetwork,
isProviderAvailable,
} = useWeb3()
const [isLoading, setIsLoading] = useState(false)
const [isWalletModalOpen, setIsWalletModalOpen] = useState(false)
const [error, setError] = useState('')
const [hasMetaMaskWallet, setHasMetaMaskWallet] = useState(true)
const [providerAvailable, setProviderAvailable] = useState(true)
const [isUnknownNetwork, setIsUnknownNetwork] = useState(false)

useEffect(() => {
const init = async () => {
setIsLoading(true)
setHasMetaMaskWallet(await isMetaMaskInstalled())
setProviderAvailable(await isProviderAvailable())
setIsLoading(false)
}

init()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [window.ethereum])
}, [])

useEffect(() => {
const setAvailable = async () => {
setProviderAvailable((await isEIP1193ProviderAvailable()) || isEIP6963ProviderAvailable)
}

setAvailable()
}, [isEIP1193ProviderAvailable, isEIP6963ProviderAvailable])

const handleProviderConnectionType = () => {
// This is async version of EIP-6963
if (isEIP6963ProviderAvailable) {
setIsWalletModalOpen(true)

return
}

handleConnectWallet(ProviderType.EIP1193)
}

const handleConnectWallet = async (walletProviderType: ProviderType = providerType) => {
setIsWalletModalOpen(false)

const handleConnectWallet = async () => {
setIsLoading(true)
try {
await connectWallet()
await connectWallet(walletProviderType)
} catch (ex) {
if (ex instanceof UnknownNetworkError) {
setIsUnknownNetwork(true)
Expand All @@ -53,7 +88,7 @@ export const ConnectWallet: FC = () => {

return (
<>
{!hasMetaMaskWallet && (
{!providerAvailable && (
<div>
<p className={classes.subHeader}>
Quickly wrap your ROSE into wROSE and vice versa with the (un)wrap ROSE tool.
Expand All @@ -68,15 +103,15 @@ export const ConnectWallet: FC = () => {
</a>
<Button
variant="secondary"
onClick={() => setHasMetaMaskWallet(true)}
onClick={() => setProviderAvailable(true)}
disabled={isLoading}
fullWidth
>
Skip
</Button>
</div>
)}
{hasMetaMaskWallet && (
{providerAvailable && (
<>
{!isUnknownNetwork && (
<div>
Expand All @@ -86,7 +121,7 @@ export const ConnectWallet: FC = () => {
Please connect your wallet to get started.
</p>

<Button onClick={handleConnectWallet} disabled={isLoading} fullWidth>
<Button onClick={handleProviderConnectionType} disabled={isLoading} fullWidth>
Connect wallet
</Button>
{error && <Alert variant="danger">{error}</Alert>}
Expand All @@ -108,6 +143,11 @@ export const ConnectWallet: FC = () => {
)}
</>
)}
<WalletModal
isOpen={isWalletModalOpen}
closeModal={() => setIsWalletModalOpen(false)}
next={() => handleConnectWallet(ProviderType.EIP6963)}
/>
</>
)
}
11 changes: 11 additions & 0 deletions src/providers/EIP1193Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from 'react'
import { EIP1193Provider } from '../utils/types.ts'

export interface EIP1193ProviderContext {
isEIP1193ProviderAvailable: () => Promise<boolean>
connectWallet: (provider?: EIP1193Provider) => Promise<string>
switchNetwork: (chainId: number, provider?: EIP1193Provider) => void
addTokenToWallet: (wRoseContractAddress: string, provider?: EIP1193Provider) => Promise<void>
}

export const EIP1193Context = createContext<EIP1193ProviderContext>({} as EIP1193ProviderContext)
Loading

0 comments on commit 18e892c

Please sign in to comment.