Skip to content

Commit aa9b2f6

Browse files
Token gate (#121)
* Version Packages (#109) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fixed tokenQuantity default * storybook working with web3hooks * completed default, web3 hook, and denied access story * changed deniedMessage from string to ReactNode * updated requiredQuantity logic in story * added ERC20ABI from hooks/src/constants * added ERC20ABI from hooks/src/constants * changeset added * removed logic from TokenGate component. Changed stories to work with updated component * added whitespace back to Address.test.tsx to keep history clean. Updated docstring for TokenGate and removed console.log from file. Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent da536ed commit aa9b2f6

File tree

4 files changed

+151
-0
lines changed

4 files changed

+151
-0
lines changed

.changeset/two-turtles-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@web3-ui/components': minor
3+
---
4+
5+
Added a TokenGate component that restricts access to child components unless erc-20/erc-721 token quantity requirements are met.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react';
2+
import { TokenGate } from '.';
3+
import { NETWORKS, Provider, useTokenBalance, useWallet } from '@web3-ui/hooks';
4+
import { Text } from '@chakra-ui/layout';
5+
import { Button } from '@chakra-ui/react';
6+
export default {
7+
title: 'Components/TokenGate',
8+
component: TokenGate,
9+
parameters: {
10+
// TODO: Fix window.ethereum is undefined breaking chromatic
11+
chromatic: { disableSnapshot: true },
12+
},
13+
};
14+
15+
const Component = ({ ...props }) => {
16+
/**
17+
* requiredQuantity was done this way because when requiredQuantity is not passed to the component, the required quantity
18+
* should default to 1 not 0
19+
*/
20+
const requiredQuantity = props.requiredQuantity === undefined ? 1 : props.requiredQuantity;
21+
return (
22+
<>
23+
<TokenGate walletBalance={props.walletBalance} {...props}>
24+
<Text>
25+
{`This is the child component of TokenGate. You were able to access this component because you hold at least ${requiredQuantity} token. Your token balance: ${props.walletBalance} `}
26+
</Text>
27+
</TokenGate>
28+
</>
29+
);
30+
};
31+
32+
const WithUseWallet = ({ ...props }) => {
33+
const { connected, connectWallet, connection } = useWallet();
34+
const { formattedBalance, error } = useTokenBalance({
35+
// GTC token contract address
36+
tokenAddress: '0xde30da39c46104798bb5aa3fe8b9e0e1f348163f',
37+
accountAddress: connection.userAddress!,
38+
});
39+
// TokenGate only returned if there is a connection and a balance. Done this way to accomplish rendering the loading state.
40+
// Using the loading state from useTokenBalance would not work because loading status changes simultaneously with connected status
41+
if (connected && formattedBalance) {
42+
return (
43+
<>
44+
<TokenGate
45+
walletBalance={Number(formattedBalance)}
46+
requiredQuantity={props.requiredQuantity}
47+
{...props}
48+
>
49+
<Text>
50+
{`This is the child component of TokenGate. You were able to access this component because you hold at least ${
51+
props.requiredQuantity === undefined ? 1 : props.requiredQuantity
52+
} of the required token: GTC`}
53+
</Text>
54+
</TokenGate>
55+
</>
56+
);
57+
}
58+
59+
if (error) {
60+
return <Text>Error occured while trying to fetch balance.</Text>;
61+
}
62+
// Using the loading state from useTokenBalance hook does not work here because connected status and loading status change simultaneously.
63+
return !connected ? (
64+
<Button onClick={connectWallet}>Connect Wallet</Button>
65+
) : (
66+
<Text>Loading...</Text>
67+
);
68+
};
69+
70+
export const Default = () => <Component walletBalance={1} />;
71+
72+
export const UsingWeb3Hooks = () => {
73+
return (
74+
<Provider network={NETWORKS.mainnet}>
75+
<WithUseWallet requiredQuantity={0} />
76+
</Provider>
77+
);
78+
};
79+
80+
export const AccessGrantedDefault = () => <Component walletBalance={150} requiredQuantity={100} />;
81+
82+
export const AccessDeniedDefault = () => (
83+
<Component walletBalance={0} requiredQuantity={1000} label='Denied' />
84+
);
85+
86+
/**
87+
* Example of custom access denied node for the deniedMessage prop
88+
*/
89+
const DeniedAccess = props => (
90+
<div>
91+
<h1>This is a custom component for when access is denied</h1>
92+
<ul>
93+
<li>Make sure your wallet is connected</li>
94+
<li>Verify you are connected to the correct address</li>
95+
<li>
96+
{`Make sure you hold the number of tokens required to access this component:
97+
${props.requiredQuantity === undefined ? 1 : props.requiredQuantity}`}
98+
</li>
99+
<li>Not providing a "deniedMessage" will return null when access is denied</li>
100+
</ul>
101+
</div>
102+
);
103+
104+
export const AccessDeniedWithCustomMessage = () => (
105+
<Component
106+
requiredQuantity={1000}
107+
deniedMessage={<DeniedAccess requiredQuantity={1000} />}
108+
label='Denied With Message'
109+
/>
110+
);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { ReactNode } from 'react';
2+
export interface TokenGateProps {
3+
/**
4+
* The balance of the required token held in wallet
5+
*/
6+
walletBalance: number;
7+
/**
8+
* The token quantity required to access child component. Default=1
9+
*/
10+
requiredQuantity?: number;
11+
/**
12+
* Child nodes
13+
*/
14+
children: ReactNode;
15+
/**
16+
* Optional message if access denied
17+
*/
18+
deniedMessage?: ReactNode;
19+
}
20+
export const TokenGate: React.FC<TokenGateProps> = ({
21+
walletBalance,
22+
requiredQuantity = 1,
23+
children,
24+
deniedMessage,
25+
}) => {
26+
// return children within simple container
27+
return (
28+
// verify token quantity in wallet is greater than required amount(optional, defaults to 1)
29+
walletBalance >= requiredQuantity ? (
30+
<>{children}</>
31+
) : deniedMessage ? (
32+
<>{deniedMessage}</>
33+
) : null
34+
);
35+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './TokenGate';

0 commit comments

Comments
 (0)