🦞 My dApp
{/* DUPLICATE! Delete this. */}Description of the app
...--- name: frontend-ux description: Frontend UX rules for Ethereum dApps that prevent the most common AI agent UI bugs. Mandatory patterns for onchain buttons, token approval flows, address display, USD values, RPC configuration, and pre-publish metadata. Built around Scaffold-ETH 2 but the patterns apply to any Ethereum frontend. Use when building any dApp frontend. --- # Frontend UX Rules ## What You Probably Got Wrong **"The button works."** Working is not the standard. Does it disable during the transaction? Does it show a spinner? Does it stay disabled until the chain confirms? Does it show an error if the user rejects? AI agents skip all of this, every time. **"I used wagmi hooks."** Wrong hooks. Scaffold-ETH 2 wraps wagmi with `useTransactor` which **waits for transaction confirmation** — not just wallet signing. Raw wagmi's `writeContractAsync` resolves the moment the user clicks Confirm in MetaMask, BEFORE the tx is mined. Your button re-enables while the transaction is still pending. **"I showed the address."** As raw hex? That's not showing it. `
` gives you ENS resolution, blockie avatars, copy-to-clipboard, and block explorer links. Raw `0x1234...5678` is unacceptable. --- ## Rule 1: Every Onchain Button — Loader + Disable > ⚠️ **THIS IS THE #1 BUG AI AGENTS SHIP.** The user clicks Approve, signs in their wallet, comes back to the app, and the Approve button is clickable again — so they click it again, send a duplicate transaction, and now two approvals are pending. **The button MUST be disabled and show a spinner from the moment they click until the transaction confirms onchain.** Not until the wallet closes. Not until the signature is sent. Until the BLOCK CONFIRMS. ANY button that triggers a blockchain transaction MUST: 1. **Disable immediately** on click 2. **Show a spinner** ("Approving...", "Staking...", etc.) 3. **Stay disabled** until the state update confirms the action completed 4. **Show success/error feedback** when done ```typescript // ✅ CORRECT: Separate loading state PER ACTION const [isApproving, setIsApproving] = useState(false); const [isStaking, setIsStaking] = useState(false); ``` **❌ NEVER use a single shared `isLoading` for multiple buttons.** Each button gets its own loading state. A shared state causes the WRONG loading text to appear when UI conditionally switches between buttons. ### Scaffold Hooks Only — Never Raw Wagmi ```typescript // ❌ WRONG: Raw wagmi — resolves after signing, not confirmation const { writeContractAsync } = useWriteContract(); await writeContractAsync({...}); // Returns immediately after MetaMask signs! // ✅ CORRECT: Scaffold hooks — waits for tx to be mined const { writeContractAsync } = useScaffoldWriteContract("MyContract"); await writeContractAsync({...}); // Waits for actual onchain confirmation ``` **Why:** `useScaffoldWriteContract` uses `useTransactor` internally, which waits for block confirmation. Raw wagmi doesn't — your UI will show "success" while the transaction is still in the mempool. --- ## Rule 2: Four-State Flow — Connect → Network → Approve → Action When a user needs to interact with the app, there are FOUR states. Show exactly ONE big, obvious button at a time: ``` 1. Not connected? → Big "Connect Wallet" button (NOT text saying "connect your wallet to play") 2. Wrong network? → Big "Switch to Base" button 3. Not enough approved? → "Approve" button (with loader per Rule 1) 4. Enough approved? → "Stake" / "Deposit" / action button ``` > **NEVER show a text prompt like "Connect your wallet to play" or "Please connect to continue."** Show a button. The user should always have exactly one thing to click. ```typescript const { data: allowance } = useScaffoldReadContract({ contractName: "Token", functionName: "allowance", args: [address, contractAddress], }); const needsApproval = !allowance || allowance < amount; const wrongNetwork = chain?.id !== targetChainId; const notConnected = !address; {notConnected ? (0x1234...5678
``` `` handles ENS resolution, blockie avatars, copy-to-clipboard, truncation, and block explorer links. Raw hex is unacceptable. ### Address Input — Always `Contract:
Description of the app
...