Skip to content

Token gate #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-turtles-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@web3-ui/components': minor
---

Added a TokenGate component that restricts access to child components unless erc-20/erc-721 token quantity requirements are met.
110 changes: 110 additions & 0 deletions packages/components/src/components/TokenGate/TokenGate.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import { TokenGate } from '.';
import { NETWORKS, Provider, useTokenBalance, useWallet } from '@web3-ui/hooks';
import { Text } from '@chakra-ui/layout';
import { Button } from '@chakra-ui/react';
export default {
title: 'Components/TokenGate',
component: TokenGate,
parameters: {
// TODO: Fix window.ethereum is undefined breaking chromatic
chromatic: { disableSnapshot: true },
},
};

const Component = ({ ...props }) => {
/**
* requiredQuantity was done this way because when requiredQuantity is not passed to the component, the required quantity
* should default to 1 not 0
*/
const requiredQuantity = props.requiredQuantity === undefined ? 1 : props.requiredQuantity;
return (
<>
<TokenGate walletBalance={props.walletBalance} {...props}>
<Text>
{`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} `}
</Text>
</TokenGate>
</>
);
};

const WithUseWallet = ({ ...props }) => {
const { connected, connectWallet, connection } = useWallet();
const { formattedBalance, error } = useTokenBalance({
// GTC token contract address
tokenAddress: '0xde30da39c46104798bb5aa3fe8b9e0e1f348163f',
accountAddress: connection.userAddress!,
});
// TokenGate only returned if there is a connection and a balance. Done this way to accomplish rendering the loading state.
// Using the loading state from useTokenBalance would not work because loading status changes simultaneously with connected status
if (connected && formattedBalance) {
return (
<>
<TokenGate
walletBalance={Number(formattedBalance)}
requiredQuantity={props.requiredQuantity}
{...props}
>
<Text>
{`This is the child component of TokenGate. You were able to access this component because you hold at least ${
props.requiredQuantity === undefined ? 1 : props.requiredQuantity
} of the required token: GTC`}
</Text>
</TokenGate>
</>
);
}

if (error) {
return <Text>Error occured while trying to fetch balance.</Text>;
}
// Using the loading state from useTokenBalance hook does not work here because connected status and loading status change simultaneously.
return !connected ? (
<Button onClick={connectWallet}>Connect Wallet</Button>
) : (
<Text>Loading...</Text>
);
};

export const Default = () => <Component walletBalance={1} />;

export const UsingWeb3Hooks = () => {
return (
<Provider network={NETWORKS.mainnet}>
<WithUseWallet requiredQuantity={0} />
</Provider>
);
};

export const AccessGrantedDefault = () => <Component walletBalance={150} requiredQuantity={100} />;

export const AccessDeniedDefault = () => (
<Component walletBalance={0} requiredQuantity={1000} label='Denied' />
);

/**
* Example of custom access denied node for the deniedMessage prop
*/
const DeniedAccess = props => (
<div>
<h1>This is a custom component for when access is denied</h1>
<ul>
<li>Make sure your wallet is connected</li>
<li>Verify you are connected to the correct address</li>
<li>
{`Make sure you hold the number of tokens required to access this component:
${props.requiredQuantity === undefined ? 1 : props.requiredQuantity}`}
</li>
<li>Not providing a "deniedMessage" will return null when access is denied</li>
</ul>
</div>
);

export const AccessDeniedWithCustomMessage = () => (
<Component
requiredQuantity={1000}
deniedMessage={<DeniedAccess requiredQuantity={1000} />}
label='Denied With Message'
/>
);
35 changes: 35 additions & 0 deletions packages/components/src/components/TokenGate/TokenGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { ReactNode } from 'react';
export interface TokenGateProps {
/**
* The balance of the required token held in wallet
*/
walletBalance: number;
/**
* The token quantity required to access child component. Default=1
*/
requiredQuantity?: number;
/**
* Child nodes
*/
children: ReactNode;
/**
* Optional message if access denied
*/
deniedMessage?: ReactNode;
}
export const TokenGate: React.FC<TokenGateProps> = ({
walletBalance,
requiredQuantity = 1,
children,
deniedMessage,
}) => {
// return children within simple container
return (
// verify token quantity in wallet is greater than required amount(optional, defaults to 1)
walletBalance >= requiredQuantity ? (
<>{children}</>
) : deniedMessage ? (
<>{deniedMessage}</>
) : null
);
};
1 change: 1 addition & 0 deletions packages/components/src/components/TokenGate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TokenGate';