From 819448df34ed0042fafc9603c377a51aea1cb0f9 Mon Sep 17 00:00:00 2001
From: Andy Huang
Date: Tue, 5 May 2026 23:58:49 +1200
Subject: [PATCH 1/6] feat: formalise wallet connection as a site-wide feature
- Add useWallet hook centralising wagmi/AppKit state with SSR mount guard
- Add WalletButton component with connected/disconnected/loading states
- Add /profile page showing wallet address, network, and disconnect
- Add WalletButton to desktop navbar (was missing) and mobile menu
- Update navbar to use fixed+backdrop on non-home pages; fix anchor links to /#section
- Refactor ens-claim and admin to use useWallet, removing duplicated boilerplate
- Replace deprecated useAccount with useConnection, disconnect with mutate
- Add docs/wallet-connection.md for team reference
---
docs/wallet-connection.md | 180 +++++++++++++++++++++++++++++++
src/app/admin/page.tsx | 15 ++-
src/app/profile/page.tsx | 113 +++++++++++++++++++
src/components/ens-claim.tsx | 23 +---
src/components/navbar.tsx | 31 +++---
src/components/wallet-button.tsx | 55 ++++++++++
src/hooks/use-wallet.ts | 29 +++++
7 files changed, 401 insertions(+), 45 deletions(-)
create mode 100644 docs/wallet-connection.md
create mode 100644 src/app/profile/page.tsx
create mode 100644 src/components/wallet-button.tsx
create mode 100644 src/hooks/use-wallet.ts
diff --git a/docs/wallet-connection.md b/docs/wallet-connection.md
new file mode 100644
index 0000000..9f13d43
--- /dev/null
+++ b/docs/wallet-connection.md
@@ -0,0 +1,180 @@
+# Wallet Connection
+
+This document covers how wallet connectivity works in the WEB3UOA site and how to use it when building new features.
+
+## Stack
+
+| Library | Version | Role |
+|---|---|---|
+| [wagmi](https://wagmi.sh) | v3 | React hooks for wallet state |
+| [viem](https://viem.sh) | v2 | Low-level Ethereum client |
+| [@reown/appkit](https://reown.com/appkit) | v1 | Wallet connection modal UI |
+| [@tanstack/react-query](https://tanstack.com/query) | — | Async state management for wagmi |
+
+## Architecture
+
+```
+Web3Provider (src/components/web3-provider.tsx)
+ └─ WagmiProvider
+ └─ QueryClientProvider
+ └─ App (all pages and components)
+```
+
+`Web3Provider` wraps the entire app in `src/app/layout.tsx`, so wallet state is globally available — no prop drilling or per-page setup needed.
+
+### Supported networks
+
+- Ethereum Mainnet
+- Sepolia Testnet
+
+### Reown project ID
+
+The modal requires a `NEXT_PUBLIC_REOWN_PROJECT_ID` environment variable. A fallback dev ID is hardcoded for local development. For production, set the real project ID in your environment config.
+
+---
+
+## How to access wallet state
+
+**Always use the `useWallet` hook.** Never import wagmi/appkit hooks directly in components — `useWallet` centralises the SSR mounting guard and exposes everything you need.
+
+```ts
+import { useWallet } from "@/hooks/use-wallet";
+
+const { address, isConnected, chainId, isAdmin, connect, disconnect } = useWallet();
+```
+
+### Returned values
+
+| Field | Type | Description |
+|---|---|---|
+| `address` | `string \| undefined` | Connected wallet address |
+| `isConnected` | `boolean` | True only after client-side mount (safe for SSR) |
+| `chainId` | `number \| undefined` | Current network chain ID |
+| `isAdmin` | `boolean` | True if address is in the admin allowlist |
+| `connect` | `() => void` | Opens the Reown wallet modal |
+| `disconnect` | `() => void` | Disconnects the wallet |
+
+### Why `mounted` matters
+
+Wallet state is only available in the browser. Without the mounting guard, server-rendered HTML will mismatch the client, causing React hydration errors. `useWallet` handles this internally — `isConnected` will always be `false` on the server and during the first render, becoming accurate once the component mounts.
+
+If you need to suppress a layout shift during that first render (e.g. a button that changes label), check `mounted` directly:
+
+```ts
+const { mounted, isConnected } = useWallet();
+
+if (!mounted) return ;
+```
+
+---
+
+## The WalletButton component
+
+`src/components/wallet-button.tsx` is a ready-made button that handles all three states automatically:
+
+| State | What renders |
+|---|---|
+| Not mounted | Invisible placeholder (prevents layout shift) |
+| Disconnected | "Connect Wallet" button — opens the modal on click |
+| Connected | Truncated address (e.g. `0xAB…CD`) — links to `/profile` |
+
+Use it anywhere a connect/disconnect control is needed:
+
+```tsx
+import { WalletButton } from "@/components/wallet-button";
+
+
+```
+
+---
+
+## Building a wallet-gated feature
+
+The standard pattern for any page or section that requires a connected wallet:
+
+```tsx
+"use client";
+
+import { useWallet } from "@/hooks/use-wallet";
+import { WalletButton } from "@/components/wallet-button";
+
+export function MyFeature() {
+ const { address, isConnected, mounted } = useWallet();
+
+ if (!mounted) return null; // or a loading skeleton
+
+ if (!isConnected) {
+ return (
+
+
Connect your wallet to continue.
+
+
+ );
+ }
+
+ return
Connected as {address}
;
+}
+```
+
+---
+
+## Admin authentication
+
+The admin panel (`src/app/admin/page.tsx`) uses a **sign-to-authenticate** pattern. No private keys are involved — the user proves ownership of their address by signing a message, and the signature is verified server-side.
+
+### Flow
+
+1. User connects wallet (must be an address in the admin allowlist)
+2. User clicks "Sign Message" — `useSignMessage` from wagmi prompts their wallet
+3. The signature, address, and timestamp are sent as HTTP headers on all subsequent admin API requests
+4. The server verifies the signature before processing any request
+
+### Sending authenticated requests
+
+```ts
+import { useSignMessage } from "wagmi";
+
+const { signMessageAsync } = useSignMessage();
+
+const timestamp = Date.now().toString();
+const signature = await signMessageAsync({ message: `Admin Auth ${timestamp}` });
+
+const authHeaders = {
+ "x-admin-address": address,
+ "x-admin-signature": signature,
+ "x-admin-timestamp": timestamp,
+};
+
+// Pass authHeaders to any protected API route
+fetch("/api/admin/claims", { headers: authHeaders });
+```
+
+### Adding a new protected API route
+
+Check for the auth headers at the top of your route handler. See any existing route in `src/app/api/admin/` for the verification pattern.
+
+---
+
+## Profile page
+
+`/profile` (`src/app/profile/page.tsx`) is the wallet-connected user's home base. Currently it shows:
+
+- Connection status and network
+- Full wallet address with a copy button
+- Disconnect button
+
+As new features are added (event attendance, ENS claim status, etc.), they should be surfaced here.
+
+---
+
+## Key files
+
+| File | Purpose |
+|---|---|
+| `src/components/web3-provider.tsx` | App-level wagmi + Reown setup |
+| `src/hooks/use-wallet.ts` | **Shared hook — use this in all components** |
+| `src/components/wallet-button.tsx` | Reusable connect/profile button |
+| `src/app/profile/page.tsx` | User profile page |
+| `src/components/ens-claim.tsx` | ENS subname claim flow |
+| `src/app/admin/page.tsx` | Admin panel with sign-to-auth |
+| `src/lib/admin-auth.ts` | Admin address allowlist |
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 11b42d1..7d7b3e1 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -1,24 +1,21 @@
"use client";
-import { useState, useEffect } from "react";
-import { useAccount, useSignMessage } from "wagmi";
+import { useState } from "react";
+import { useWallet } from "@/hooks/use-wallet";
+import { useSignMessage } from "wagmi";
import { Button } from "@/components/ui/button";
+import { WalletButton } from "@/components/wallet-button";
export default function AdminPage() {
- const { address, isConnected } = useAccount();
+ const { address, isConnected, mounted } = useWallet();
const { signMessageAsync } = useSignMessage();
const [authHeader, setAuthHeader] = useState(null);
- const [mounted, setMounted] = useState(false);
const [claims, setClaims] = useState([]);
const [activeNames, setActiveNames] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
- useEffect(() => {
- setMounted(true);
- }, []);
-
const authenticate = async () => {
if (!address) return;
try {
@@ -137,7 +134,7 @@ export default function AdminPage() {
Connect owner wallet to access.