From 053b4e5997b4d78e15fa72ce142a3d09e009f0da Mon Sep 17 00:00:00 2001 From: luchobonatti Date: Mon, 8 Jul 2024 12:02:29 -0300 Subject: [PATCH 1/2] feat: fix TransactionButton component to handle wallet connection and transaction sending --- src/hooks/useWeb3Status.tsx | 39 +++---- .../home/Examples/demos/TransactionButton.tsx | 98 ++++++++-------- src/pageComponents/home/Examples/index.tsx | 2 +- src/sharedComponents/TransactionButton.tsx | 47 +++----- src/sharedComponents/WalletStatusVerifier.tsx | 106 ++++++++++++++++++ 5 files changed, 191 insertions(+), 101 deletions(-) create mode 100644 src/sharedComponents/WalletStatusVerifier.tsx diff --git a/src/hooks/useWeb3Status.tsx b/src/hooks/useWeb3Status.tsx index b90471fd..7f2d7fb4 100644 --- a/src/hooks/useWeb3Status.tsx +++ b/src/hooks/useWeb3Status.tsx @@ -1,4 +1,3 @@ -import { useChainIsSupported } from 'connectkit' import { Address, Chain } from 'viem' import { UseBalanceReturnType, @@ -7,20 +6,18 @@ import { useAccount, useBalance, useChainId, - useConnect, useDisconnect, usePublicClient, useSwitchChain, useWalletClient, } from 'wagmi' -import { injected } from 'wagmi/connectors' +import { chains, ChainsIds } from '@/src/lib/networks.config' import { RequiredNonNull } from '@/src/types/utils' export type AppWeb3Status = { - appChainId: Chain['id'] readOnlyClient: UsePublicClientReturnType - supportedChains: readonly [Chain, ...Chain[]] + appChainId: ChainsIds } export type WalletWeb3Status = { @@ -29,13 +26,13 @@ export type WalletWeb3Status = { connectingWallet: boolean switchingChain: boolean isWalletConnected: boolean - isWalletNetworkSupported: boolean | null - walletChainId: number | undefined walletClient: UseWalletClientReturnType['data'] + isWalletSynced: boolean + walletChainId: Chain['id'] | undefined } export type Web3Actions = { - switchChain: (chainId: Chain['id']) => void + switchChain: (chainId?: ChainsIds) => void disconnect: () => void } @@ -53,35 +50,33 @@ export const useWeb3Status = () => { isConnected: isWalletConnected, isConnecting: connectingWallet, } = useAccount() - const appChainId = useChainId() - const isWalletNetworkSupported = useChainIsSupported(walletChainId) - const { chains: supportedChains, isPending: switchingChain, switchChain } = useSwitchChain() + const appChainId = useChainId() as ChainsIds + const { isPending: switchingChain, switchChain } = useSwitchChain() const readOnlyClient = usePublicClient() const { data: walletClient } = useWalletClient() const { data: balance } = useBalance() - const { connect } = useConnect() const { disconnect } = useDisconnect() - const appWeb3Status = { - supportedChains, - appChainId, + const isWalletSynced = isWalletConnected && walletChainId === appChainId + + const appWeb3Status: AppWeb3Status = { readOnlyClient, + appChainId, } - const walletWeb3Status = { + const walletWeb3Status: WalletWeb3Status = { address, balance, - walletChainId, isWalletConnected, - isWalletNetworkSupported, connectingWallet, switchingChain, walletClient, + isWalletSynced, + walletChainId, } - const web3Actions = { - switchChain: (chainId: number) => switchChain({ chainId }), - connect: () => connect({ connector: injected() }), + const web3Actions: Web3Actions = { + switchChain: (chainId: number = chains[0].id) => switchChain({ chainId }), // default to the first chain in the config disconnect: disconnect, } @@ -96,7 +91,7 @@ export const useWeb3Status = () => { export const useWeb3StatusConnected = () => { const context = useWeb3Status() - if (!context.address) { + if (!context.isWalletConnected) { throw new Error('Use useWeb3StatusConnected only when a wallet is connected') } return useWeb3Status() as RequiredNonNull diff --git a/src/pageComponents/home/Examples/demos/TransactionButton.tsx b/src/pageComponents/home/Examples/demos/TransactionButton.tsx index 328a645c..8200f881 100644 --- a/src/pageComponents/home/Examples/demos/TransactionButton.tsx +++ b/src/pageComponents/home/Examples/demos/TransactionButton.tsx @@ -4,53 +4,61 @@ import { useSendTransaction, useWriteContract } from 'wagmi' import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' import { TransactionButton } from '@/src/sharedComponents/TransactionButton' +import { withWalletStatusVerifier } from '@/src/sharedComponents/WalletStatusVerifier' -export const TransactionButtonDemo = () => { - const { address } = useWeb3StatusConnected() - const { sendTransactionAsync } = useSendTransaction() - const { writeContractAsync } = useWriteContract() +const TransactionButtonDemo = withWalletStatusVerifier( + () => { + const { address } = useWeb3StatusConnected() + const { sendTransactionAsync } = useSendTransaction() + const { writeContractAsync } = useWriteContract() - const handleOnMined = (receipt: TransactionReceipt) => { - alert(`Transaction completed! 🎉 \n hash: ${receipt.transactionHash}`) - } + const handleOnMined = (receipt: TransactionReceipt) => { + alert(`Transaction completed! 🎉 \n hash: ${receipt.transactionHash}`) + } - const handleSendTransaction = (): Promise => { - // Send native token - return sendTransactionAsync({ - chainId: sepolia.id, - to: address, - value: parseEther('0.1'), - }) - } + const handleSendTransaction = (): Promise => { + // Send native token + return sendTransactionAsync({ + chainId: sepolia.id, + to: address, + value: parseEther('0.1'), + }) + } - const handleWriteContract = (): Promise => { - // Send ERC20 token [USDC] - return writeContractAsync({ - chainId: sepolia.id, - abi: erc20Abi, - address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', // USDC - functionName: 'transfer', - args: [address, 100000000n], // 100 USDC - }) - } + const handleWriteContract = (): Promise => { + // Send ERC20 token [USDC] + return writeContractAsync({ + chainId: sepolia.id, + abi: erc20Abi, + address: '0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', // USDC + functionName: 'transfer', + args: [address, 100000000n], // 100 USDC + }) + } - return ( - <> - -
- - - ) -} + return ( + <> + +
+ + + ) + }, + { + chainId: sepolia.id, // this DEMO component is for sepolia chain + }, +) + +export default TransactionButtonDemo diff --git a/src/pageComponents/home/Examples/index.tsx b/src/pageComponents/home/Examples/index.tsx index 6c2a7ec9..38b70d43 100644 --- a/src/pageComponents/home/Examples/index.tsx +++ b/src/pageComponents/home/Examples/index.tsx @@ -21,7 +21,7 @@ import Hash from '@/src/pageComponents/home/Examples/demos/Hash' import HashInput from '@/src/pageComponents/home/Examples/demos/HashInput' import TokenDropdownDemo from '@/src/pageComponents/home/Examples/demos/TokenDropdown' import TokenInput from '@/src/pageComponents/home/Examples/demos/TokenInput' -import { TransactionButtonDemo } from '@/src/pageComponents/home/Examples/demos/TransactionButton' +import TransactionButtonDemo from '@/src/pageComponents/home/Examples/demos/TransactionButton' import { ConnectWalletButton } from '@/src/providers/Web3Provider' const Wrapper = styled.section` diff --git a/src/sharedComponents/TransactionButton.tsx b/src/sharedComponents/TransactionButton.tsx index 514466c5..00add383 100644 --- a/src/sharedComponents/TransactionButton.tsx +++ b/src/sharedComponents/TransactionButton.tsx @@ -1,17 +1,15 @@ -import { ReactElement, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Button } from 'db-ui-toolkit' import { Chain, Hash, TransactionReceipt } from 'viem' import { useWaitForTransactionReceipt } from 'wagmi' -import { useWeb3Status } from '@/src/hooks/useWeb3Status' -import { ConnectWalletButton } from '@/src/providers/Web3Provider' +import { useWeb3StatusConnected } from '@/src/hooks/useWeb3Status' interface TransactionButtonProps { transaction: () => Promise chain?: Chain onMined?: (receipt: TransactionReceipt) => void - fallback?: ReactElement disabled?: boolean label?: string labelSending?: string @@ -24,9 +22,8 @@ interface TransactionButtonProps { * * @component * @param {Function} props.transaction - The function that initiates the transaction. - * @param {Function} props.fallback - The fallback component to be rendered when the wallet is not connected. (default: ConnectButton) - * @param {Function} props.onMined - The callback function to be called when the transaction is mined. * @param {Chain} props.chain - The chain where the transaction will be sent. + * @param {Function} props.onMined - The callback function to be called when the transaction is mined. * @param {boolean} props.disabled - The flag to disable the button. * @param {string} props.label - The label for the button. * @param {string} props.labelSending - The label for the button when the transaction is pending. @@ -35,27 +32,23 @@ interface TransactionButtonProps { */ export const TransactionButton = ({ - chain, disabled, - fallback = , label = 'Send Transaction', labelSending = 'Sending...', onMined, transaction, }: TransactionButtonProps) => { - const { - isWalletConnected, - isWalletNetworkSupported, - supportedChains, - switchChain, - switchingChain, - walletChainId, - } = useWeb3Status() + const { isWalletSynced } = useWeb3StatusConnected() + + if (!isWalletSynced) { + throw new Error( + 'TransactionButton component must be used inside a WalletStatusVerifier component or withWalletStatusVerifier HoC', + ) + } + const [hash, setHash] = useState() const [isPending, setIsPending] = useState(false) - const isCorrectChain = chain ? walletChainId === chain.id : isWalletNetworkSupported - const { data: receipt } = useWaitForTransactionReceipt({ hash: hash, }) @@ -79,22 +72,10 @@ export const TransactionButton = ({ } } - if (!isWalletConnected) { - return fallback - } - const inputProps = { - disabled: isPending || switchingChain || disabled, - onClick: isCorrectChain - ? handleSendTransaction - : () => switchChain((chain || supportedChains[0]).id), + disabled: isPending || disabled, + onClick: handleSendTransaction, } - const buttonLabel = isPending - ? labelSending - : !isCorrectChain - ? `Switch to ${(chain || supportedChains[0]).name}` - : label - - return + return } diff --git a/src/sharedComponents/WalletStatusVerifier.tsx b/src/sharedComponents/WalletStatusVerifier.tsx new file mode 100644 index 00000000..071eaa17 --- /dev/null +++ b/src/sharedComponents/WalletStatusVerifier.tsx @@ -0,0 +1,106 @@ +import { ComponentType, FC, ReactElement } from 'react' + +import { Button } from 'db-ui-toolkit' +import { extractChain } from 'viem' + +import { useWeb3Status } from '@/src/hooks/useWeb3Status' +import { chains, ChainsIds } from '@/src/lib/networks.config' +import { ConnectWalletButton } from '@/src/providers/Web3Provider' + +interface WalletStatusVerifierProps { + chainId?: ChainsIds + fallback?: ReactElement + children?: ReactElement + labelSwitchChain?: string +} + +/** + * WalletStatusVerifier Component + * + * This component checks the wallet connection and chain synchronization status. + * If the wallet is not connected, it displays a fallback component (default: ConnectWalletButton) + * If the wallet is connected but not synced with the correct chain, it provides an option to switch chain. + * + * @param {Object} props - Component props + * @param {Chain['id']} [props.chainId] - The chain ID to check for synchronization + * @param {ReactElement} [props.fallback] - The fallback component to render if the wallet is not connected + * @param {ReactElement} props.children - The children components to render if the wallet is connected and synced + * @example + * + * <>Components that requires a connected and synced wallet + * + * @returns {FC} The WalletStatusVerifier component + */ +const WalletStatusVerifier: FC = ({ + chainId, + children, + fallback = , + labelSwitchChain = 'Switch to', +}) => { + const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = + useWeb3Status() + + const chainToSwitch = extractChain({ chains, id: chainId || appChainId || chains[0].id }) + + if (!isWalletConnected) { + return fallback + } + + if (!isWalletSynced || walletChainId !== chainId) { + return ( + + ) + } + + return children +} + +/** + * WalletStatusVerifier HOC + * + * + * @param {Object} props - HOC props + * @param {Chain['id']} [props.chainId] - The chain ID to check for synchronization + * @param {ReactElement} [props.fallback] - The fallback component to render if the wallet is not connected + * @param {ReactElement} WrappedComponent - The component to render if the wallet is connected and synced + * @example + * const ComponentWithConection = withWalletStatusVerifier(MyComponent); + * @returns {FC} The WalletStatusVerifier HOC + */ +const withWalletStatusVerifier =

( + WrappedComponent: ComponentType

, + { + chainId, + fallback = , + labelSwitchChain = 'Switch to', + }: WalletStatusVerifierProps = {}, +): FC

=> { + const ComponentWithVerifier: FC

= (props: P) => { + const { appChainId, isWalletConnected, isWalletSynced, switchChain, walletChainId } = + useWeb3Status() + + const chainToSwitch = extractChain({ chains, id: chainId || appChainId || chains[0].id }) + + if (!isWalletConnected) { + return fallback + } + + if (!isWalletSynced || walletChainId !== chainId) { + return ( + + ) + } + + return + } + + ComponentWithVerifier.displayName = `withWalletStatusVerifier(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})` + + return ComponentWithVerifier +} + +export { WalletStatusVerifier, withWalletStatusVerifier } From a9c381aa9df9544cff2feeff1404db430f843825 Mon Sep 17 00:00:00 2001 From: Gabito Esmiapodo <4015436+gabitoesmiapodo@users.noreply.github.com> Date: Mon, 8 Jul 2024 13:52:37 -0300 Subject: [PATCH 2/2] Update src/hooks/useWeb3Status.tsx Co-authored-by: Fernando Greco --- src/hooks/useWeb3Status.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWeb3Status.tsx b/src/hooks/useWeb3Status.tsx index 7f2d7fb4..82e3f7ed 100644 --- a/src/hooks/useWeb3Status.tsx +++ b/src/hooks/useWeb3Status.tsx @@ -12,7 +12,7 @@ import { useWalletClient, } from 'wagmi' -import { chains, ChainsIds } from '@/src/lib/networks.config' +import { chains, type ChainsIds } from '@/src/lib/networks.config' import { RequiredNonNull } from '@/src/types/utils' export type AppWeb3Status = {