diff --git a/.changeset/two-turtles-march.md b/.changeset/two-turtles-march.md new file mode 100644 index 00000000..5b576807 --- /dev/null +++ b/.changeset/two-turtles-march.md @@ -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. diff --git a/packages/components/src/components/TokenGate/TokenGate.stories.tsx b/packages/components/src/components/TokenGate/TokenGate.stories.tsx new file mode 100644 index 00000000..e4f01aa3 --- /dev/null +++ b/packages/components/src/components/TokenGate/TokenGate.stories.tsx @@ -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 ( + <> + + + {`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} `} + + + + ); +}; + +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 ( + <> + + + {`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`} + + + + ); + } + + if (error) { + return Error occured while trying to fetch balance.; + } + // Using the loading state from useTokenBalance hook does not work here because connected status and loading status change simultaneously. + return !connected ? ( + + ) : ( + Loading... + ); +}; + +export const Default = () => ; + +export const UsingWeb3Hooks = () => { + return ( + + + + ); +}; + +export const AccessGrantedDefault = () => ; + +export const AccessDeniedDefault = () => ( + +); + +/** + * Example of custom access denied node for the deniedMessage prop + */ +const DeniedAccess = props => ( +
+

This is a custom component for when access is denied

+
    +
  • Make sure your wallet is connected
  • +
  • Verify you are connected to the correct address
  • +
  • + {`Make sure you hold the number of tokens required to access this component: + ${props.requiredQuantity === undefined ? 1 : props.requiredQuantity}`} +
  • +
  • Not providing a "deniedMessage" will return null when access is denied
  • +
+
+); + +export const AccessDeniedWithCustomMessage = () => ( + } + label='Denied With Message' + /> +); diff --git a/packages/components/src/components/TokenGate/TokenGate.tsx b/packages/components/src/components/TokenGate/TokenGate.tsx new file mode 100644 index 00000000..ecb78b8a --- /dev/null +++ b/packages/components/src/components/TokenGate/TokenGate.tsx @@ -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 = ({ + 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 + ); +}; diff --git a/packages/components/src/components/TokenGate/index.ts b/packages/components/src/components/TokenGate/index.ts new file mode 100644 index 00000000..81ad74eb --- /dev/null +++ b/packages/components/src/components/TokenGate/index.ts @@ -0,0 +1 @@ +export * from './TokenGate';