diff --git a/.changeset/nice-baths-design.md b/.changeset/nice-baths-design.md new file mode 100644 index 0000000..385876f --- /dev/null +++ b/.changeset/nice-baths-design.md @@ -0,0 +1,7 @@ +--- +"@everipedia/iq-login": patch +--- + +Add `useEnsureCorrectChain` hook to standardize wallet network enforcement in dApps. + +This hook introduces a unified status state machine (`idle` → `wrong-network` → `switching` → `correct`) for managing connected wallet chain state. It includes utilities for programmatic network switching, dismissal handling, and an optional `onStatusChange` callback for reacting to status transitions. Documentation has been added to the README with usage examples and API reference. diff --git a/README.md b/README.md index 9a53b0b..323b3df 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,82 @@ if (token && address) { } ``` +## 🔗 Chain Enforcement Hook + +Use `useEnsureCorrectChain` to ensure the connected wallet is on the correct network. It exposes a single `status` flow instead of multiple booleans: + +``` +idle → wrong-network → switching → correct +``` + +| Status | Meaning | +|---|---| +| `"idle"` | Wallet not connected or state dismissed | +| `"wrong-network"` | Connected to an unsupported chain | +| `"switching"` | Chain switch in progress | +| `"correct"` | On the required chain | + +### Basic Usage + +```tsx +import { useEnsureCorrectChain } from '@everipedia/iq-login/client'; + +function MyComponent() { + const { status, switchToCorrectChain, targetChain, dismiss } = useEnsureCorrectChain({ + requiredChainId: 252, // e.g. Fraxtal + }); + + if (status === "wrong-network") { + return ( +
+

Please switch to {targetChain?.name}

+ + +
+ ); + } + + if (status === "switching") { + return

Switching network...

; + } + + return

Connected to the correct network!

; +} +``` + +### With Status Callback + +Use `onStatusChange` to react to transitions — e.g. to open/close a modal: + +```tsx +const { status, switchToCorrectChain } = useEnsureCorrectChain({ + requiredChainId: 252, + onStatusChange: (status, chainName) => { + if (status === "wrong-network") openSwitchModal(); + if (status === "correct") closeSwitchModal(); + }, +}); +``` + +### API Reference + +**Options:** + +| Prop | Type | Description | +|---|---|---| +| `requiredChainId` | `number` | The chain ID your app requires | +| `onStatusChange` | `(status, chainName?) => void` | Optional callback on every status transition | + +**Returns:** + +| Field | Type | Description | +|---|---|---| +| `status` | `ChainStatus` | Current status (`"idle"`, `"wrong-network"`, `"switching"`, `"correct"`) | +| `switchToCorrectChain` | `() => Promise` | Trigger a chain switch | +| `dismiss` | `() => void` | Dismiss the wrong-network state | +| `targetChain` | `Chain \| undefined` | Target chain object from wagmi config | +| `isConnected` | `boolean` | Whether the wallet is connected | + ## 🎨 Styling The package uses Tailwind CSS and Shadcn UI Theme. Visit https://ui.shadcn.com/themes for theme customization. diff --git a/src/client.ts b/src/client.ts index 857b5ab..0a5e5d7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -12,6 +12,12 @@ export { Login } from "./components/login-element"; // =============== export { useAuth } from "./hooks/use-auth"; export { useWeb3Auth } from "./hooks/use-web-3-auth"; +export { + useEnsureCorrectChain, + type ChainStatus, + type UseEnsureCorrectChainOptions, + type UseEnsureCorrectChainReturn, +} from "./hooks/use-ensure-correct-chain"; // =============== // Config diff --git a/src/hooks/use-ensure-correct-chain.ts b/src/hooks/use-ensure-correct-chain.ts new file mode 100644 index 0000000..912aed3 --- /dev/null +++ b/src/hooks/use-ensure-correct-chain.ts @@ -0,0 +1,95 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useAccount, useSwitchChain } from "wagmi"; + +export type ChainStatus = "idle" | "correct" | "wrong-network" | "switching"; + +export interface UseEnsureCorrectChainOptions { + /** The chain ID the app requires */ + requiredChainId: number; + /** Called when status transitions (e.g. to open/close a modal) */ + onStatusChange?: (status: ChainStatus, targetChainName?: string) => void; +} + +export interface UseEnsureCorrectChainReturn { + /** Current chain-matching status */ + status: ChainStatus; + /** Dismiss the wrong-network state (user chose to stay) */ + dismiss: () => void; + /** Attempt to programmatically switch to the required chain */ + switchToCorrectChain: () => Promise; + /** The target chain object from the wagmi config, if found */ + targetChain: ReturnType["chains"][number] | undefined; + /** Whether the wallet is connected */ + isConnected: boolean; +} + +export const useEnsureCorrectChain = ({ + requiredChainId, + onStatusChange, +}: UseEnsureCorrectChainOptions): UseEnsureCorrectChainReturn => { + const { chainId, isConnected } = useAccount(); + const { switchChainAsync, chains } = useSwitchChain(); + const [status, setStatus] = useState("idle"); + + const targetChain = chains.find((c) => c.id === requiredChainId); + + const transition = useCallback( + (next: ChainStatus) => { + setStatus(next); + onStatusChange?.(next, targetChain?.name); + }, + [onStatusChange, targetChain?.name], + ); + + useEffect(() => { + if (!isConnected || !chainId) { + transition("idle"); + return; + } + + transition(chainId === requiredChainId ? "correct" : "wrong-network"); + }, [chainId, isConnected, requiredChainId, transition]); + + const switchToCorrectChain = useCallback(async () => { + if (!isConnected) { + console.error("Cannot switch chain, wallet not connected."); + return; + } + + if (chainId === requiredChainId) return; + + if (!targetChain) { + console.error( + `Cannot switch chain, target chain with ID ${requiredChainId} is not configured.`, + ); + return; + } + + try { + transition("switching"); + await switchChainAsync({ chainId: requiredChainId }); + } catch (error) { + console.error("Failed to switch network:", error); + transition("wrong-network"); + } + }, [ + isConnected, + chainId, + requiredChainId, + targetChain, + switchChainAsync, + transition, + ]); + + const dismiss = useCallback(() => transition("idle"), [transition]); + + return { + status, + dismiss, + switchToCorrectChain, + targetChain, + isConnected, + }; +};