Skip to content

Commit

Permalink
improve SSR support (Uniswap#431)
Browse files Browse the repository at this point in the history
* add connectEagerly to top 4 connectors

* some cleanup

* fix tests
  • Loading branch information
NoahZinsmeister authored Feb 24, 2022
1 parent 1fd5b07 commit aeb26ff
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 319 deletions.
4 changes: 2 additions & 2 deletions packages/eip1193/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('EIP1193', () => {
})

test('fails silently', async () => {
connector = new EIP1193(actions, mockProvider)
connector = new EIP1193(actions, mockProvider, true)
await yieldThread()

expect(store.getState()).toEqual({
Expand All @@ -136,7 +136,7 @@ describe('EIP1193', () => {
mockProvider.chainId = chainId
mockProvider.accounts = accounts

connector = new EIP1193(actions, mockProvider)
connector = new EIP1193(actions, mockProvider, true)
await yieldThread()

expect(store.getState()).toEqual({
Expand Down
40 changes: 25 additions & 15 deletions packages/eip1193/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,49 @@ export class EIP1193 extends Connector {
* @param provider - An EIP-1193 ({@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md}) provider.
* @param connectEagerly - A flag indicating whether connection should be initiated when the class is constructed.
*/
constructor(actions: Actions, provider: Provider, connectEagerly = true) {
constructor(actions: Actions, provider: Provider, connectEagerly = false) {
super(actions)

if (connectEagerly && typeof window === 'undefined') {
throw new Error('connectEagerly = true is invalid for SSR, instead use the connectEagerly method in a useEffect')
}

this.provider = provider

this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) })
})

this.provider.on('disconnect', (error: ProviderRpcError): void => {
this.actions.reportError(error)
})

this.provider.on('chainChanged', (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) })
})

this.provider.on('accountsChanged', (accounts: string[]): void => {
this.actions.update({ accounts })
})

if (connectEagerly) {
const cancelActivation = this.actions.startActivation()
if (connectEagerly) void this.connectEagerly()
}

/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation()

Promise.all([
this.provider.request({ method: 'eth_chainId' }) as Promise<string>,
this.provider.request({ method: 'eth_accounts' }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
this.actions.update({ chainId: parseChainId(chainId), accounts })
})
.catch((error) => {
console.debug('Could not connect eagerly', error)
cancelActivation()
})
}
return Promise.all([
this.provider.request({ method: 'eth_chainId' }) as Promise<string>,
this.provider.request({ method: 'eth_accounts' }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
this.actions.update({ chainId: parseChainId(chainId), accounts })
})
.catch((error) => {
console.debug('Could not connect eagerly', error)
cancelActivation()
})
}

/** {@inheritdoc Connector.activate} */
Expand Down
6 changes: 6 additions & 0 deletions packages/example/components/connectors/MetaMaskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { hooks, metaMask } from '../../connectors/metaMask'
import { Accounts } from '../Accounts'
import { Card } from '../Card'
Expand All @@ -18,6 +19,11 @@ export default function MetaMaskCard() {
const provider = useProvider()
const ENSNames = useENSNames(provider)

// attempt to connect eagerly on mount
useEffect(() => {
void metaMask.connectEagerly()
}, [])

return (
<Card>
<div>
Expand Down
6 changes: 6 additions & 0 deletions packages/example/components/connectors/NetworkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { hooks, network } from '../../connectors/network'
import { Accounts } from '../Accounts'
import { Card } from '../Card'
Expand All @@ -18,6 +19,11 @@ export default function NetworkCard() {
const provider = useProvider()
const ENSNames = useENSNames(provider)

// attempt to connect eagerly on mount
useEffect(() => {
void network.activate()
}, [])

return (
<Card>
<div>
Expand Down
6 changes: 6 additions & 0 deletions packages/example/components/connectors/WalletConnectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { hooks, walletConnect } from '../../connectors/walletConnect'
import { Accounts } from '../Accounts'
import { Card } from '../Card'
Expand All @@ -18,6 +19,11 @@ export default function WalletConnectCard() {
const provider = useProvider()
const ENSNames = useENSNames(provider)

// attempt to connect eagerly on mount
useEffect(() => {
void walletConnect.connectEagerly()
}, [])

return (
<Card>
<div>
Expand Down
6 changes: 6 additions & 0 deletions packages/example/components/connectors/WalletLinkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from 'react'
import { hooks, walletLink } from '../../connectors/walletLink'
import { Accounts } from '../Accounts'
import { Card } from '../Card'
Expand All @@ -18,6 +19,11 @@ export default function WalletLinkCard() {
const provider = useProvider()
const ENSNames = useENSNames(provider)

// attempt to connect eagerly on mount
useEffect(() => {
void walletLink.connectEagerly()
}, [])

return (
<Card>
<div>
Expand Down
13 changes: 5 additions & 8 deletions packages/example/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import dynamic from 'next/dynamic'

const PriorityExample = dynamic(() => import('../components/connectors/PriorityExample'), { ssr: false })

const MetaMaskCard = dynamic(() => import('../components/connectors/MetaMaskCard'), { ssr: false })
const WalletConnectCard = dynamic(() => import('../components/connectors/WalletConnectCard'), { ssr: false })
const WalletLinkCard = dynamic(() => import('../components/connectors/WalletLinkCard'), { ssr: false })
const NetworkCard = dynamic(() => import('../components/connectors/NetworkCard'), { ssr: false })
import MetaMaskCard from '../components/connectors/MetaMaskCard'
import NetworkCard from '../components/connectors/NetworkCard'
import PriorityExample from '../components/connectors/PriorityExample'
import WalletConnectCard from '../components/connectors/WalletConnectCard'
import WalletLinkCard from '../components/connectors/WalletLinkCard'

export default function Home() {
return (
Expand Down
94 changes: 47 additions & 47 deletions packages/metamask/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,36 +28,39 @@ export class MetaMask extends Connector {
* @param connectEagerly - A flag indicating whether connection should be initiated when the class is constructed.
* @param options - Options to pass to `@metamask/detect-provider`
*/
constructor(actions: Actions, connectEagerly = true, options?: Parameters<typeof detectEthereumProvider>[0]) {
constructor(actions: Actions, connectEagerly = false, options?: Parameters<typeof detectEthereumProvider>[0]) {
super(actions)
this.options = options

if (connectEagerly) {
this.eagerConnection = this.initialize(true)
if (connectEagerly && typeof window === 'undefined') {
throw new Error('connectEagerly = true is invalid for SSR, instead use the connectEagerly method in a useEffect')
}

this.options = options

if (connectEagerly) void this.connectEagerly()
}

private async initialize(connectEagerly: boolean): Promise<void> {
let cancelActivation: () => void
if (connectEagerly) {
cancelActivation = this.actions.startActivation()
}
private async isomorphicInitialize(): Promise<void> {
if (this.eagerConnection) return this.eagerConnection

return import('@metamask/detect-provider')
await (this.eagerConnection = import('@metamask/detect-provider')
.then((m) => m.default(this.options))
.then((provider) => {
this.provider = (provider as Provider) ?? undefined
if (provider) {
this.provider = provider as Provider

if (this.provider) {
this.provider.on('connect', ({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) })
})

this.provider.on('disconnect', (error: ProviderRpcError): void => {
this.actions.reportError(error)
})

this.provider.on('chainChanged', (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) })
})

this.provider.on('accountsChanged', (accounts: string[]): void => {
if (accounts.length === 0) {
// handle this edge case by disconnecting
Expand All @@ -66,28 +69,32 @@ export class MetaMask extends Connector {
this.actions.update({ accounts })
}
})
}
}))
}

if (connectEagerly) {
return Promise.all([
this.provider.request({ method: 'eth_chainId' }) as Promise<string>,
this.provider.request({ method: 'eth_accounts' }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
if (accounts.length) {
this.actions.update({ chainId: parseChainId(chainId), accounts })
} else {
throw new Error('No accounts returned')
}
})
.catch((error) => {
console.debug('Could not connect eagerly', error)
cancelActivation()
})
}
} else if (connectEagerly) {
cancelActivation()
/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation()

await this.isomorphicInitialize()
if (!this.provider) return cancelActivation()

return Promise.all([
this.provider.request({ method: 'eth_chainId' }) as Promise<string>,
this.provider.request({ method: 'eth_accounts' }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
if (accounts.length) {
this.actions.update({ chainId: parseChainId(chainId), accounts })
} else {
throw new Error('No accounts returned')
}
})
.catch((error) => {
console.debug('Could not connect eagerly', error)
cancelActivation()
})
}

/**
Expand All @@ -100,36 +107,29 @@ export class MetaMask extends Connector {
* specified parameters first, before being prompted to switch.
*/
public async activate(desiredChainIdOrChainParameters?: number | AddEthereumChainParameter): Promise<void> {
const desiredChainId =
typeof desiredChainIdOrChainParameters === 'number'
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId

this.actions.startActivation()

if (!this.eagerConnection) {
this.eagerConnection = this.initialize(false)
}
await this.eagerConnection

if (!this.provider) {
return this.actions.reportError(new NoMetaMaskError())
}
await this.isomorphicInitialize()
if (!this.provider) return this.actions.reportError(new NoMetaMaskError())

return Promise.all([
this.provider.request({ method: 'eth_chainId' }) as Promise<string>,
this.provider.request({ method: 'eth_requestAccounts' }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
const receivedChainId = parseChainId(chainId)
const desiredChainId =
typeof desiredChainIdOrChainParameters === 'number'
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId

// if there's no desired chain, or it's equal to the received, update
if (!desiredChainId || receivedChainId === desiredChainId) {
if (!desiredChainId || receivedChainId === desiredChainId)
return this.actions.update({ chainId: receivedChainId, accounts })
}

// if we're here, we can try to switch networks
const desiredChainIdHex = `0x${desiredChainId.toString(16)}`

// if we're here, we can try to switch networks
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: 'wallet_switchEthereumChain',
Expand Down
8 changes: 4 additions & 4 deletions packages/network/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.mock('@ethersproject/experimental', () => ({
const chainId = '0x1'
const accounts: string[] = []

describe('Url', () => {
describe('Network', () => {
let store: Web3ReactStore
let connector: Network
let mockConnector: MockEip1193Bridge
Expand All @@ -26,7 +26,7 @@ describe('Url', () => {
beforeEach(() => {
let actions: Actions
;[store, actions] = createWeb3ReactStoreAndActions()
connector = new Network(actions, { 1: 'https://mock.url' })
connector = new Network(actions, { 1: 'https://mock.url' }, true)
})

beforeEach(async () => {
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('Url', () => {
beforeEach(() => {
let actions: Actions
;[store, actions] = createWeb3ReactStoreAndActions()
connector = new Network(actions, { 1: ['https://1.mock.url', 'https://2.mock.url'] })
connector = new Network(actions, { 1: ['https://1.mock.url', 'https://2.mock.url'] }, true)
})

beforeEach(async () => {
Expand All @@ -114,7 +114,7 @@ describe('Url', () => {
beforeEach(() => {
let actions: Actions
;[store, actions] = createWeb3ReactStoreAndActions()
connector = new Network(actions, { 1: 'https://mainnet.mock.url', 2: 'https://testnet.mock.url' })
connector = new Network(actions, { 1: 'https://mainnet.mock.url', 2: 'https://testnet.mock.url' }, true)
})

beforeEach(async () => {
Expand Down
Loading

0 comments on commit aeb26ff

Please sign in to comment.