Week 4 • Wallet Standard Connection Flow
Wallet Standard Connection Flow
Build a beginner-safe wallet flow with explicit connection states and clear UX behavior.
Step 1: 0-to-1 Theory
Primitives
connector: the concrete wallet option (Phantom, Solflare, etc.)session: connected wallet statestatus: runtime wallet state (idle, connecting, connected, error)
Mental Model
Your app does not “log into” a wallet. It asks the user’s wallet app for permission to:
- expose a public address (safe)
- sign messages/transactions (sensitive)
Wallet UX is good when the user always knows which state they are in and what they can do next.
Invariants
- Never show “Connected” before connection promise resolves.
- User rejection is not a crash; it is a normal path.
- All signer-required actions must be disabled when disconnected.
Quick Checks
- Why is explicit status text critical in wallet UX?
- Why should rejection and network errors use different messages?
Step 2: Real-World Implementation (Code Solution)
Create components/solana/WalletPanel.tsx:
"use client";
import React from "react";
import {
useWalletConnection,
useConnectWallet,
useDisconnectWallet,
} from "@solana/react-hooks";
// UI helper: keep the address readable for beginners.
function shortAddress(addr?: string) {
if (!addr) return "";
return `${addr.slice(0, 4)}...${addr.slice(-4)}`;
}
export function WalletPanel() {
// wallet: connected wallet session (address + capabilities)
// connectors: available Wallet Standard wallets in the browser
// status: explicit connection state so the UI never guesses
const { wallet, connectors, status } = useWalletConnection();
const connectWallet = useConnectWallet();
const disconnectWallet = useDisconnectWallet();
const onConnect = async (connectorId: string) => {
try {
// This triggers the wallet's connect prompt (user must approve).
await connectWallet.mutateAsync({ connectorId });
} catch (e) {
// keep UX deterministic; show a toast in your app shell if you have one
console.error("connect failed", e);
}
};
const onDisconnect = async () => {
try {
// Disconnect clears the wallet session from app state.
await disconnectWallet.mutateAsync();
} catch (e) {
console.error("disconnect failed", e);
}
};
return (
<div className="rounded-xl border border-zinc-700 p-4 space-y-3">
<div className="text-sm text-zinc-300">Status: {status}</div>
{wallet ? (
<>
<div className="text-sm text-zinc-200">Connected: {shortAddress(wallet.address)}</div>
<button onClick={onDisconnect} className="px-3 py-2 rounded bg-red-600 text-white">
Disconnect
</button>
</>
) : (
<div className="space-y-2">
<div className="text-sm text-zinc-300">Choose wallet:</div>
{connectors.map((c) => (
<button
key={c.id}
onClick={() => onConnect(c.id)}
className="mr-2 px-3 py-2 rounded bg-emerald-600 text-white"
>
{/* Show connector name so the user knows which wallet they are choosing. */}
Connect {c.name}
</button>
))}
</div>
)}
</div>
);
}Use it on a page:
import { WalletPanel } from "@/components/solana/WalletPanel";
export default function Page() {
return <WalletPanel />;
}Expected result:
- connect/disconnect flow works with explicit status changes.
Step 3: Mastery Test
- Easy: user clicks connect then rejects prompt. What should status and UI do?
- Medium: wallet disconnects during an in-flight tx. What UI guard should trigger?
- Hard: two tabs open same app; one disconnects wallet. How should the other tab update?