Skip to content

Commit e204d49

Browse files
authored
Add TokenGate to core (#214)
* Add `TokenGate` to `core` * Add changeset * Add `ethers` to peer dependenices for the core package
1 parent 5d53bf1 commit e204d49

File tree

9 files changed

+154
-6
lines changed

9 files changed

+154
-6
lines changed

.changeset/clean-lobsters-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web3-ui/core': minor
3+
---
4+
5+
The core package now has a TokenGate that lets you conditionally render some content depending on whether a user owns some amount of a token or not.

packages/core/README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ This is the list of components the package currently provides:
3333

3434
- [Provider](#provider)
3535
- [ConnectWallet](#connectwallet)
36+
- [TokenGate](#tokengate)
3637

3738
---
3839

@@ -48,7 +49,7 @@ See [Getting Started](#getting-started) for an example.
4849

4950
### ConnectWallet
5051

51-
The `ConnectWallet` is a `Button` based component with the following behaviour:
52+
`ConnectWallet` is a `Button` based component with the following behaviour:
5253

5354
When wallet is connected,
5455

@@ -69,6 +70,28 @@ import { ConnectWallet } from '@web3-ui/core';
6970

7071
---
7172

73+
### TokenGate
74+
75+
`TokenGate` lets you conditionally render some content depending on whether the current connected user has enough amount of a specific token. The component only supports ERC20 tokens at the moment but support for other standards is coming soon.
76+
77+
#### Usage
78+
79+
```tsx
80+
import { TokenGate } from '@web3-ui/core';
81+
82+
<TokenGate
83+
tokenContractAddress="0x08149745590e9025b52b6801e9dd3E752e60F3A2"
84+
requiredQuantity={+ethers.utils.parseEther('1')} // the component expects the amount in wei.
85+
deniedContent={
86+
<p>This message will show up if the user doesn't have enough tokens.</p>
87+
}
88+
>
89+
<h1>This message will be visible if the user has enough tokens.</h1>
90+
</TokenGate>;
91+
```
92+
93+
---
94+
7295
## Hooks
7396

7497
This package exports all the hooks provided by the [`@web3-ui/hooks` package](https://npmjs.com/package/@web3-ui/hooks).

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
},
4747
"peerDependencies": {
4848
"react": "*",
49-
"react-dom": "*"
49+
"react-dom": "*",
50+
"ethers": "^5.5.1"
5051
},
5152
"devDependencies": {
5253
"@babel/core": "^7.12.7",
@@ -59,7 +60,6 @@
5960
"@web3-ui/hooks": "^0.8.0",
6061
"babel-loader": "^8.2.1",
6162
"classnames": "^2.2.6",
62-
"ethers": "^5.5.1",
6363
"identity-obj-proxy": "^3.0.0",
6464
"prettier": "^2.2.0",
6565
"react": "^17.0.2",

packages/core/src/components/Provider/Provider.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@ import React from 'react';
22
import { Provider as HooksProvider } from '@web3-ui/hooks';
33
import { Provider as ComponentsProvider } from '@web3-ui/components';
44

5-
export const Provider = ({ network, children }) => {
5+
interface ProviderProps {
6+
children: React.ReactNode;
7+
network: number;
8+
rpcUrl?: string;
9+
}
10+
11+
export const Provider = ({ network, children, rpcUrl }: ProviderProps) => {
612
return (
713
<ComponentsProvider>
8-
<HooksProvider network={network}>{children}</HooksProvider>
14+
<HooksProvider network={network} rpcUrl={rpcUrl}>
15+
{children}
16+
</HooksProvider>
917
</ComponentsProvider>
1018
);
1119
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Button } from '@chakra-ui/react';
2+
import { NETWORKS, useWallet } from '@web3-ui/hooks';
3+
import React from 'react';
4+
import { ConnectWallet, Provider } from '..';
5+
import { TokenGate } from './TokenGate';
6+
7+
export default {
8+
component: TokenGate,
9+
title: 'Core/TokenGate',
10+
decorators: [
11+
Story => {
12+
return (
13+
<Provider
14+
network={NETWORKS.rinkeby}
15+
rpcUrl="https://rinkeby.infura.io/v3/21bc321f21a54c528dc084f5ed7f8df7"
16+
>
17+
<ConnectWallet />
18+
<Story />
19+
</Provider>
20+
);
21+
}
22+
]
23+
};
24+
25+
export const Default = () => {
26+
const { correctNetwork, switchToCorrectNetwork } = useWallet();
27+
28+
return (
29+
<>
30+
{!correctNetwork && (
31+
<Button onClick={switchToCorrectNetwork}>
32+
Switch to correct network
33+
</Button>
34+
)}
35+
<TokenGate
36+
tokenContractAddress="0x08149745590e9025b52b6801e9dd3E752e60F3A2"
37+
deniedContent={<p>You don't have enough dUSDT</p>}
38+
>
39+
<div>You have more than 1 dUSDT.</div>
40+
</TokenGate>
41+
</>
42+
);
43+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import { TokenGate as PrivTokenGate } from '@web3-ui/components';
3+
import { useReadOnlyContract, useWallet } from '@web3-ui/hooks';
4+
import { ERC20ABI } from '@web3-ui/hooks/src/constants';
5+
import { BigNumber, ethers } from 'ethers';
6+
7+
export interface TokenGateProps {
8+
/**
9+
* The content that will be displayed if the user holds the required number of tokens.
10+
*/
11+
children: React.ReactNode;
12+
/**
13+
* The contract address of the token you want to gate your content using.
14+
*/
15+
tokenContractAddress: string;
16+
/**
17+
* The token quantity required to access child component in wei. Default=1 ether.
18+
*/
19+
requiredQuantity?: number;
20+
/**
21+
* Optional message that is displayed if access denied i.e. the user does not hold enough tokens. Default=null
22+
*/
23+
deniedContent?: React.ReactNode;
24+
}
25+
26+
/**
27+
* A 'token gate' that renders some content depending on whether the user has the required quantity of a token or not.
28+
*/
29+
export const TokenGate = ({
30+
children,
31+
tokenContractAddress,
32+
requiredQuantity = +ethers.utils.parseEther('1'),
33+
deniedContent = null
34+
}: TokenGateProps) => {
35+
const { connection } = useWallet();
36+
const { userAddress } = connection;
37+
const [tokenContract, isReady] = useReadOnlyContract(
38+
tokenContractAddress,
39+
ERC20ABI
40+
);
41+
const [balance, setBalance] = useState<BigNumber>();
42+
43+
const fetchBalance = useCallback(async () => {
44+
if (isReady && userAddress && tokenContract) {
45+
const balance: BigNumber = await tokenContract.balanceOf(userAddress);
46+
setBalance(balance);
47+
}
48+
}, [isReady, userAddress, tokenContract]);
49+
50+
useEffect(() => {
51+
fetchBalance();
52+
}, [fetchBalance]);
53+
54+
if (!balance) {
55+
return null;
56+
}
57+
58+
return (
59+
<PrivTokenGate
60+
walletBalance={+balance.toString()}
61+
deniedMessage={deniedContent}
62+
requiredQuantity={requiredQuantity}
63+
>
64+
{children}
65+
</PrivTokenGate>
66+
);
67+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TokenGate } from './TokenGate';

packages/core/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './ConnectWallet';
22
export * from './Provider';
3+
export * from './TokenGate';

packages/hooks/src/stories/UseContract.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ const ReadContract = () => {
167167
storiesOf('Hooks/useReadContract', module).add('Default', () => (
168168
<Provider
169169
network={NETWORKS.rinkeby}
170-
readOnlyProviderUrl="https://rinkeby.infura.io/v3/INFURA_ID"
170+
rpcUrl="https://rinkeby.infura.io/v3/21bc321f21a54c528dc084f5ed7f8df7"
171171
>
172172
<ReadContract />
173173
</Provider>

0 commit comments

Comments
 (0)