repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
stars
latest
clone command
git clone gitlawb://did:key:z6MkvfHn...poLu/gitcatgit clone gitlawb://did:key:z6MkvfHn.../gitcata815108csync from playground1d ago| #1 | import { useState, useCallback } from 'react'; |
| #2 | import { |
| #3 | useAccount, |
| #4 | useConnect, |
| #5 | useDisconnect, |
| #6 | useWriteContract, |
| #7 | useWaitForTransactionReceipt, |
| #8 | } from 'wagmi'; |
| #9 | import { parseUnits, type Address } from 'viem'; |
| #10 | import { RECIPIENT, USDC_BASE } from './wagmi'; |
| #11 | |
| #12 | const ERC20_ABI = [ |
| #13 | { |
| #14 | name: 'transfer', |
| #15 | type: 'function', |
| #16 | stateMutability: 'nonpayable', |
| #17 | inputs: [ |
| #18 | { name: 'to', type: 'address' }, |
| #19 | { name: 'amount', type: 'uint256' }, |
| #20 | ], |
| #21 | outputs: [{ name: '', type: 'bool' }], |
| #22 | }, |
| #23 | ] as const; |
| #24 | |
| #25 | const PRESETS = ['1', '5', '50'] as const; |
| #26 | |
| #27 | export default function TipButton() { |
| #28 | const [panelOpen, setPanelOpen] = useState(false); |
| #29 | const [selectedAmount, setSelectedAmount] = useState<string>('5'); |
| #30 | const [customAmount, setCustomAmount] = useState(''); |
| #31 | const [txHash, setTxHash] = useState<`0x${string}` | undefined>(); |
| #32 | const [error, setError] = useState<string | null>(null); |
| #33 | |
| #34 | const { address, isConnected } = useAccount(); |
| #35 | const { connect, connectors, isPending: isConnecting } = useConnect(); |
| #36 | const { disconnect } = useDisconnect(); |
| #37 | const { writeContractAsync, isPending: isSending } = useWriteContract(); |
| #38 | |
| #39 | const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ |
| #40 | hash: txHash, |
| #41 | }); |
| #42 | |
| #43 | const amount = customAmount || selectedAmount; |
| #44 | |
| #45 | const handleSend = useCallback(async () => { |
| #46 | setError(null); |
| #47 | setTxHash(undefined); |
| #48 | |
| #49 | const value = parseFloat(amount); |
| #50 | if (!value || value <= 0) { |
| #51 | setError('Enter a valid amount'); |
| #52 | return; |
| #53 | } |
| #54 | |
| #55 | if (!isConnected) { |
| #56 | const injectedConnector = connectors.find((c) => c.id === 'injected'); |
| #57 | if (!injectedConnector) { |
| #58 | setError('No wallet found. Install MetaMask or another wallet extension.'); |
| #59 | return; |
| #60 | } |
| #61 | connect({ connector: injectedConnector }); |
| #62 | return; |
| #63 | } |
| #64 | |
| #65 | try { |
| #66 | const hash = await writeContractAsync({ |
| #67 | address: USDC_BASE as Address, |
| #68 | abi: ERC20_ABI, |
| #69 | functionName: 'transfer', |
| #70 | args: [RECIPIENT as Address, parseUnits(amount, 6)], |
| #71 | chainId: 8453, |
| #72 | }); |
| #73 | setTxHash(hash); |
| #74 | } catch (err: unknown) { |
| #75 | const msg = err instanceof Error ? err.message : 'Transaction failed'; |
| #76 | if (msg.includes('User rejected')) { |
| #77 | setError('Transaction cancelled'); |
| #78 | } else { |
| #79 | setError(msg.length > 80 ? msg.slice(0, 77) + '...' : msg); |
| #80 | } |
| #81 | } |
| #82 | }, [amount, isConnected, connect, connectors, writeContractAsync]); |
| #83 | |
| #84 | const handleReset = () => { |
| #85 | setTxHash(undefined); |
| #86 | setError(null); |
| #87 | setPanelOpen(false); |
| #88 | }; |
| #89 | |
| #90 | const explorerUrl = txHash ? `https://basescan.org/tx/${txHash}` : null; |
| #91 | |
| #92 | return ( |
| #93 | <> |
| #94 | {!panelOpen && ( |
| #95 | <button className="tip-trigger" onClick={() => setPanelOpen(true)}> |
| #96 | Support this project |
| #97 | </button> |
| #98 | )} |
| #99 | |
| #100 | {panelOpen && ( |
| #101 | <div className="tip-panel"> |
| #102 | {isSuccess ? ( |
| #103 | <div className="tip-success"> |
| #104 | <div className="tip-success-icon">Thank you!</div> |
| #105 | <p className="tip-success-msg"> |
| #106 | Sent {amount} USDC to the project. |
| #107 | </p> |
| #108 | {explorerUrl && ( |
| #109 | <a |
| #110 | className="tip-explorer-link" |
| #111 | href={explorerUrl} |
| #112 | target="_blank" |
| #113 | rel="noopener noreferrer" |
| #114 | > |
| #115 | View on BaseScan |
| #116 | </a> |
| #117 | )} |
| #118 | <button className="tip-close-btn" onClick={handleReset}> |
| #119 | Close |
| #120 | </button> |
| #121 | </div> |
| #122 | ) : ( |
| #123 | <> |
| #124 | <div className="tip-header"> |
| #125 | <span className="tip-title">Support this project</span> |
| #126 | {isConnected && address && ( |
| #127 | <button className="tip-disconnect" onClick={() => disconnect()}> |
| #128 | {address.slice(0, 6)}...{address.slice(-4)} |
| #129 | </button> |
| #130 | )} |
| #131 | </div> |
| #132 | |
| #133 | <div className="tip-presets"> |
| #134 | {PRESETS.map((p) => ( |
| #135 | <button |
| #136 | key={p} |
| #137 | className={`tip-preset ${selectedAmount === p && !customAmount ? 'active' : ''}`} |
| #138 | onClick={() => { |
| #139 | setSelectedAmount(p); |
| #140 | setCustomAmount(''); |
| #141 | setError(null); |
| #142 | }} |
| #143 | > |
| #144 | ${p} USDC |
| #145 | </button> |
| #146 | ))} |
| #147 | </div> |
| #148 | |
| #149 | <input |
| #150 | className="tip-custom-input" |
| #151 | type="number" |
| #152 | min="0" |
| #153 | step="any" |
| #154 | placeholder="Custom amount" |
| #155 | value={customAmount} |
| #156 | onChange={(e) => { |
| #157 | setCustomAmount(e.target.value); |
| #158 | setError(null); |
| #159 | }} |
| #160 | /> |
| #161 | |
| #162 | {error && <div className="tip-error">{error}</div>} |
| #163 | |
| #164 | <button |
| #165 | className="tip-send-btn" |
| #166 | onClick={handleSend} |
| #167 | disabled={isSending || isConnecting || isConfirming || !amount || parseFloat(amount) <= 0} |
| #168 | > |
| #169 | {isSending |
| #170 | ? 'Confirm in wallet...' |
| #171 | : isConfirming |
| #172 | ? 'Sending...' |
| #173 | : isConnecting |
| #174 | ? 'Connecting...' |
| #175 | : !isConnected |
| #176 | ? 'Connect Wallet & Send' |
| #177 | : `Send ${amount} USDC`} |
| #178 | </button> |
| #179 | |
| #180 | <button className="tip-close-btn" onClick={handleReset}> |
| #181 | Cancel |
| #182 | </button> |
| #183 | </> |
| #184 | )} |
| #185 | </div> |
| #186 | )} |
| #187 | </> |
| #188 | ); |
| #189 | } |
| #190 |