Skip to content

Commit d09606c

Browse files
kevincodex1claude
andcommitted
wallet: a real connect picker; Coinbase Smart Wallet was never reachable
Every "Connect wallet" button looked for a connector with id "coinbaseWallet", but wagmi's Coinbase connector registers as "coinbaseWalletSDK". The lookup never matched, so the app always fell through to the generic injected connector. Coinbase Smart Wallet could not be chosen, and in a browser with no wallet extension the click failed with ProviderNotFound and no UI reacted: a dead button. That also hid that @coinbase/wallet-sdk (an optional peer of wagmi) was not installed; the production bundle carried a "Cannot find module" stub in its place. - lib/wallet-connectors.ts: pure module choosing which wallets to offer (browser-announced EIP-6963 wallets first, the generic browser wallet only when something is injected without announcing itself, Coinbase always) and turning connect failures into plain text without echoing raw errors. - components/WalletPicker.tsx: the connect dialog on the existing Sheet, one in-flight request at a time, errors shown inline with role=alert, closes on success. components/ConnectWallet.tsx: a button in the caller's styling that opens it. - Header, trade panel, launch form, posts composer and the /me dashboard all connect through the picker; no component calls useConnect any more except the picker, and a test pins that. - @coinbase/wallet-sdk ^4.3.7 installed (registry tarball, integrity matches, no eval or script/iframe injection; its origins were already in the CSP). Verified in headless Chrome against the dev server: no wallet shows Coinbase plus guidance and the Coinbase row waits on its popup; a fake EIP-6963 wallet is listed, connects, and the header shows the address chip; a bare window.ethereum is offered as "Browser wallet". Lint, typecheck, 279 tests and a production build pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146kzimBSuHTErfKYhMCaic
1 parent d82d7e5 commit d09606c

15 files changed

Lines changed: 370 additions & 54 deletions

‎app/package-lock.json‎

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎app/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"dependencies": {
1515
"@aws-sdk/client-s3": "^3.1127.0",
1616
"@base-ui/react": "^1.8.0",
17+
"@coinbase/wallet-sdk": "^4.3.7",
1718
"@tanstack/react-query": "^5.91.3",
1819
"clsx": "^2.1.1",
1920
"lightweight-charts": "^5.2.1",

‎app/src/components/ConnectButton.tsx‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22

33
import { useSyncExternalStore } from "react";
4-
import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi";
4+
import { useAccount, useDisconnect, useSwitchChain } from "wagmi";
55
import { CHAINS } from "@/lib/chainPublic";
6+
import ConnectWallet from "./ConnectWallet";
67
import WalletMenu from "./WalletMenu";
78

89
/**
@@ -11,7 +12,6 @@ import WalletMenu from "./WalletMenu";
1112
*/
1213
export default function ConnectButton({ block = false, onNavigate }: { block?: boolean; onNavigate?: () => void }) {
1314
const { address, isConnected, chainId, connector: activeConnector } = useAccount();
14-
const { connect, connectors, isPending } = useConnect();
1515
const { disconnectAsync, isPending: disconnecting } = useDisconnect();
1616
const { switchChainAsync, isPending: switching } = useSwitchChain();
1717
// Wallet state only exists on the client; render a neutral pill during SSR/hydration.
@@ -27,17 +27,8 @@ export default function ConnectButton({ block = false, onNavigate }: { block?: b
2727
if (!mounted) return <div className={`${base} border border-line text-faint`}>Wallet</div>;
2828

2929
if (!isConnected || !address) {
30-
const connector = connectors.find((c) => c.id === "coinbaseWallet") ?? connectors[0];
31-
return (
32-
<button
33-
onClick={() => connector && connect({ connector })}
34-
disabled={isPending || !connector}
35-
// a quiet hairline utility (the ThemeToggle recipe): only the launch CTA is loud
36-
className={`${base} border border-line text-body hover:text-ink hover:border-line-strong`}
37-
>
38-
{isPending ? "Connecting…" : "Connect wallet"}
39-
</button>
40-
);
30+
// a quiet hairline utility (the ThemeToggle recipe): only the launch CTA is loud
31+
return <ConnectWallet className={`${base} border border-line text-body hover:text-ink hover:border-line-strong`}>Connect wallet</ConnectWallet>;
4132
}
4233
return (
4334
<WalletMenu address={address} chainId={chainId} connectorName={activeConnector?.name}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import WalletPicker from "./WalletPicker";
5+
6+
/**
7+
* A "Connect wallet" button in the caller's own styling that opens the wallet picker.
8+
* Every connect entry point (header, trade panel, launch form, posts, dashboard) goes through
9+
* this so wallet choice and error handling live in one place.
10+
*/
11+
export default function ConnectWallet({ className, children, disabled = false }: { className?: string; children: React.ReactNode; disabled?: boolean }) {
12+
const [open, setOpen] = useState(false);
13+
return (
14+
<>
15+
<button type="button" onClick={() => setOpen(true)} disabled={disabled} aria-haspopup="dialog" aria-expanded={open} className={className}>
16+
{children}
17+
</button>
18+
{open ? <WalletPicker onClose={() => setOpen(false)} /> : null}
19+
</>
20+
);
21+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"use client";
2+
3+
import { useState, useSyncExternalStore } from "react";
4+
import { useConnect } from "wagmi";
5+
import { Wallet } from "lucide-react";
6+
import { NO_WALLET_NOTE, connectErrorMessage, walletChoices, type WalletChoice } from "@/lib/wallet-connectors";
7+
import Sheet from "./Sheet";
8+
9+
/**
10+
* The connect dialog: every wallet the browser announced, the generic browser wallet when one is
11+
* injected without announcing itself, and Coinbase (passkey Smart Wallet or the Coinbase app).
12+
* Connection errors are shown here instead of vanishing into hook state. Presentation only:
13+
* no signing, no transactions.
14+
*/
15+
export default function WalletPicker({ onClose }: { onClose: () => void }) {
16+
const { connect, connectors, isPending, reset } = useConnect({ mutation: { onSuccess: onClose } });
17+
const [error, setError] = useState<string | null>(null);
18+
const [pendingId, setPendingId] = useState<string | null>(null);
19+
const hasInjectedProvider = useSyncExternalStore(
20+
() => () => {},
21+
() => Boolean((window as { ethereum?: unknown }).ethereum),
22+
() => false,
23+
);
24+
const choices = walletChoices(connectors, { hasInjectedProvider });
25+
26+
function pick(choice: WalletChoice) {
27+
const connector = connectors.find((c) => c.id === choice.id);
28+
if (!connector || isPending) return;
29+
setError(null);
30+
reset();
31+
setPendingId(connector.id);
32+
connect({ connector }, { onError: (e) => setError(connectErrorMessage(e)), onSettled: () => setPendingId(null) });
33+
}
34+
35+
return (
36+
<Sheet title="Connect a wallet" onClose={onClose}>
37+
<ul className="flex flex-col gap-2" aria-label="Wallets">
38+
{choices.map((choice) => {
39+
const busy = isPending && pendingId === choice.id;
40+
return (
41+
<li key={choice.id}>
42+
<button
43+
type="button"
44+
onClick={() => pick(choice)}
45+
disabled={isPending}
46+
aria-busy={busy || undefined}
47+
className="w-full min-h-14 px-3.5 py-2.5 inline-flex items-center gap-3.5 rounded-xl bg-card border border-line-strong text-left hover:border-ink/40 hover:bg-paper transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
48+
>
49+
<span className="h-9 w-9 shrink-0 inline-flex items-center justify-center rounded-lg bg-paper border border-line overflow-hidden" aria-hidden>
50+
{choice.icon ? (
51+
// the wallet's own EIP-6963 icon, a data: URI; nothing to optimize
52+
// eslint-disable-next-line @next/next/no-img-element
53+
<img src={choice.icon} alt="" width={24} height={24} className="h-6 w-6" />
54+
) : (
55+
<Wallet size={18} strokeWidth={1.75} />
56+
)}
57+
</span>
58+
<span className="min-w-0 flex-1">
59+
<span className="block text-sm font-semibold text-ink truncate">{choice.name}</span>
60+
<span className="block text-xs text-muted truncate">{busy ? "Waiting for the wallet…" : choice.hint}</span>
61+
</span>
62+
</button>
63+
</li>
64+
);
65+
})}
66+
</ul>
67+
{!hasInjectedProvider && choices.every((c) => c.kind === "coinbase") ? <p className="mt-3 text-xs text-muted leading-relaxed">{NO_WALLET_NOTE}</p> : null}
68+
{choices.length === 0 ? <p className="text-sm text-muted">No wallet option is available right now. Reload the page and try again.</p> : null}
69+
<p role="alert" aria-live="assertive" className={`mt-3 text-[13px] leading-relaxed text-down-ink ${error ? "" : "sr-only"}`}>{error ?? ""}</p>
70+
<p className="mt-4 text-[11px] text-muted leading-relaxed">Connecting lets this site read your address. Transactions and edits always need your signature.</p>
71+
</Sheet>
72+
);
73+
}

‎app/src/components/launchpad/LaunchForm.tsx‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useEffect, useMemo, useRef, useState } from "react";
44
import { useRouter } from "next/navigation";
5-
import { useAccount, useBalance, useConfig, useConnect, useReadContract, useSwitchChain } from "wagmi";
5+
import { useAccount, useBalance, useConfig, useReadContract, useSwitchChain } from "wagmi";
66
import { getPublicClient, getWalletClient } from "wagmi/actions";
77
import { isAddress, maxUint160, maxUint256, parseEventLogs, parseUnits, zeroAddress, type Address, type Hex, type PublicClient, type WalletClient } from "viem";
88
import TokenAvatar from "./TokenAvatar";
@@ -21,6 +21,7 @@ import { CHAINS, CHAIN_LABELS, CHAIN_KEYS, BUILDER_DATA_SUFFIX, explorerTx, shor
2121
import { friendlyError } from "@/lib/errors";
2222
import { Spinner } from "@/components/Skeleton";
2323
import { startNav } from "@/components/RouteProgress";
24+
import WalletPicker from "@/components/WalletPicker";
2425

2526
/**
2627
* Launch flow — honest states, nothing claimed before the chain says so:
@@ -157,7 +158,7 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
157158
}, [chain, quoteKey, stockQ]);
158159
const config = useConfig();
159160
const { address, isConnected, chainId } = useAccount();
160-
const { connect, connectors, isPending: connecting } = useConnect();
161+
const [pickerOpen, setPickerOpen] = useState(false);
161162
const { switchChainAsync, isPending: switching } = useSwitchChain();
162163

163164
const [name, setName] = useState("");
@@ -685,16 +686,14 @@ export default function LaunchForm({ ethUsd, gitlawbUsd = null, initialChain = "
685686
configured={cfg.configured}
686687
connected={isConnected}
687688
onChain={onChain}
688-
connecting={connecting || switching}
689+
connecting={switching}
689690
valid={valid}
690691
phase={phase}
691-
onConnect={() => {
692-
const c = connectors.find((c) => c.id === "coinbaseWallet") ?? connectors[0];
693-
if (c) connect({ connector: c });
694-
}}
692+
onConnect={() => setPickerOpen(true)}
695693
onSwitch={() => void switchChainAsync({ chainId: CHAIN.id })}
696694
label={initialBuyRaw ? "Launch + optional buy" : "Launch for free, gas only"}
697695
/>
696+
{pickerOpen ? <WalletPicker onClose={() => setPickerOpen(false)} /> : null}
698697
<PhaseNote phase={phase} chain={chain} />
699698
<p className="text-xs text-muted leading-relaxed">
700699
One transaction on {CHAIN_LABEL}: deploys the token, creates the Uniswap v4 pool ({quote.symbol} / your token), locks 100% of the supply in it forever, and registers the fee routing. Cost: gas only, usually a few cents.

‎app/src/components/launchpad/MeDashboard.module.css‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
.dashboard button:disabled { opacity: .5; cursor: not-allowed; }
1717
.readOnly { display: flex; align-items: flex-start; gap: 8px; margin-top: 20px; max-width: 330px; color: var(--color-muted); font-size: 11px; line-height: 1.75; text-wrap: pretty; }
1818
.readOnly svg { margin-top: 2px; flex-shrink: 0; }
19-
.connectError { color: var(--color-down-ink); margin-top: 12px; font-size: 13px; line-height: 1.5; }
2019
.welcomeGuide { padding: 40px; border-left: 1px solid var(--color-line); }
2120
.capabilities { margin-top: 8px; }
2221
.capabilities li { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 12px; padding-block: 26px; border-bottom: 1px solid var(--color-line); }

‎app/src/components/launchpad/MeDashboard.tsx‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import Link from "next/link";
44
import { useCallback, useEffect, useState } from "react";
55
import { ArrowDownLeft, ArrowRight, ArrowUpRight, Coins, Layers3, LockKeyhole, RefreshCw, Wallet } from "lucide-react";
6-
import { useAccount, useConfig, useConnect } from "wagmi";
6+
import { useAccount, useConfig } from "wagmi";
77
import { getPublicClient, getWalletClient } from "wagmi/actions";
88
import type { Address } from "viem";
99
import TokenAvatar from "./TokenAvatar";
@@ -23,6 +23,7 @@ import { ago } from "@/lib/launchpad/time";
2323
import { BUILDER_DATA_SUFFIX, CHAINS, CHAIN_SHORT, explorerTx, shortAddr, type ChainKey } from "@/lib/chainPublic";
2424
import { friendlyError } from "@/lib/errors";
2525
import { SkRow, SkStat } from "@/components/Skeleton";
26+
import ConnectWallet from "@/components/ConnectWallet";
2627
import styles from "./MeDashboard.module.css";
2728

2829
type Me = { wallet: string; ethUsd: number | null; launches: LaunchRow[]; tokens: (LaunchRow & { my_buys: number; my_sells: number; my_last_trade: string })[]; trades: WalletTrade[] };
@@ -43,7 +44,6 @@ export default function MeDashboard() {
4344
}
4445

4546
function WalletDashboard({ address, isConnected }: { address: Address | undefined; isConnected: boolean }) {
46-
const { connect, connectors, isPending: connecting, error: connectError } = useConnect();
4747
const config = useConfig();
4848
const [me, setMe] = useState<Me | null>(null);
4949
const [err, setErr] = useState<string | null>(null);
@@ -151,7 +151,6 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
151151
}
152152

153153
if (!isConnected || !address) {
154-
const c = connectors.find((x) => x.id === "coinbaseWallet") ?? connectors[0];
155154
return (
156155
<div className={styles.dashboard}>
157156
<section className={styles.welcome} aria-labelledby="wallet-welcome">
@@ -160,10 +159,9 @@ function WalletDashboard({ address, isConnected }: { address: Address | undefine
160159
<p className={styles.eyebrow}>Start with your wallet</p>
161160
<h2 id="wallet-welcome">Your wallet.<br /><span>Your workspace.</span></h2>
162161
<p className={styles.welcomeCopy}>Bring your launches, fees and trading activity into one view. No new account to create.</p>
163-
<button type="button" onClick={() => c && connect({ connector: c })} disabled={connecting || !c} className={styles.connectButton}>
164-
{connecting ? "Connecting…" : "Connect wallet"}<ArrowRight size={16} aria-hidden="true" />
165-
</button>
166-
{connectError ? <p role="alert" className={styles.connectError}>{friendlyError(connectError)}</p> : null}
162+
<ConnectWallet className={styles.connectButton}>
163+
Connect wallet<ArrowRight size={16} aria-hidden="true" />
164+
</ConnectWallet>
167165
<p className={styles.readOnly}><LockKeyhole size={14} aria-hidden="true" />Connecting lets you read. Transactions and edits need your signature.</p>
168166
</div>
169167
<div className={styles.welcomeGuide}>

‎app/src/components/launchpad/Posts.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import Link from "next/link";
44
import { useCallback, useEffect, useRef, useState } from "react";
5-
import { useAccount, useConfig, useConnect } from "wagmi";
5+
import { useAccount, useConfig } from "wagmi";
66
import { getWalletClient } from "wagmi/actions";
77
import WalletAvatar from "@/components/WalletAvatar";
88
import ChainBadge from "./ChainBadge";
@@ -14,6 +14,7 @@ import type { PostRow } from "@/lib/launchpad/postsServer";
1414
import { ago, nowMs } from "@/lib/launchpad/time";
1515
import { CHAINS, CHAIN_SHORT, shortAddr, type ChainKey } from "@/lib/chainPublic";
1616
import { friendlyError } from "@/lib/errors";
17+
import ConnectWallet from "@/components/ConnectWallet";
1718
import { ChevronDown, MessageSquare } from "lucide-react";
1819

1920
function nonce(): string {
@@ -41,7 +42,6 @@ async function signed(config: ReturnType<typeof useConfig>, chain: ChainKey, mes
4142
*/
4243
export default function TokenComments({ chain, token, symbol, launcher, embedded = false }: { chain: ChainKey; token: string; symbol: string; launcher: string; embedded?: boolean }) {
4344
const { address, isConnected } = useAccount();
44-
const { connect, connectors, isPending: connecting } = useConnect();
4545
const config = useConfig();
4646
const { subscribe } = useLive();
4747
const [posts, setPosts] = useState<PostRow[]>([]);
@@ -147,7 +147,6 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded
147147

148148
const top = posts.filter((p) => p.parent_id === null);
149149
const replies = (id: number) => posts.filter((p) => p.parent_id === id).slice().reverse();
150-
const c = connectors.find((x) => x.id === "coinbaseWallet") ?? connectors[0];
151150

152151
return (
153152
<section className={embedded ? "overflow-hidden bg-paper" : "rounded-2xl bg-card border border-line overflow-hidden"} id="comments">
@@ -169,9 +168,7 @@ export default function TokenComments({ chain, token, symbol, launcher, embedded
169168
) : !isConnected ? (
170169
<div className="flex items-center justify-between gap-3 flex-wrap">
171170
<p className="text-sm text-muted">Connect a wallet to post. Free signature, no transaction. Holders, traders and creators can post.</p>
172-
<button type="button" onClick={() => c && connect({ connector: c })} disabled={connecting || !c} className={btn.secondarySm}>
173-
{connecting ? "Connecting…" : "Connect"}
174-
</button>
171+
<ConnectWallet className={btn.secondarySm}>Connect</ConnectWallet>
175172
</div>
176173
) : (
177174
<div className="space-y-2">

0 commit comments

Comments
 (0)