From 3a113aaa8fae68477955cc357d4d54c3e1ec5715 Mon Sep 17 00:00:00 2001 From: k Date: Fri, 29 May 2026 14:34:51 +0800 Subject: [PATCH] fix: wallet/trades UX + SEO + commit Next.js 16 routing migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-launch audit findings (frontend): - trades: unlock private history in-page. TradesPageClient only read a CACHED view envelope, so a connected user who hadn't visited Settings first saw "load settings once to unlock" with no way forward. Added an explicit "Unlock trade history" button that mints the view envelope via getOrCreateViewEnvelope (one signature, no gas) and reloads — no detour. - wagmi: broaden connector from injected({target:'metaMask'}) to generic injected(). The MetaMask pin made Rabby / Coinbase ext / Brave / Frame / OKX and non-MetaMask browsers fail to connect. wagmi v2 EIP-6963 discovery covers all injected wallets; still uses the native provider path (not the MM SDK). - sitemap: drop /settings. It was emitted in sitemap.xml while robots.ts disallows it — a self-contradicting crawler signal. Private pages stay out of both. - docs: note the app runs on port 3001 (package.json), not 3000. The CLAUDE.md verify sweep targeted 3000 and would have reported every page as down. - routing: COMMIT the Next.js 16 migration that was sitting uncommitted — delete middleware.ts, add root proxy.ts (next-intl createMiddleware), update next-env.d.ts. Vercel deploys from git, so leaving proxy.ts untracked would have shipped the old routing. `next build` confirms "Proxy (Middleware)" is active and all routes compile. tsc clean. Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 8 +- app/[locale]/trades/TradesPageClient.tsx | 119 +++++++++++++++-------- app/sitemap.ts | 4 +- lib/wagmi.ts | 16 +-- next-env.d.ts | 2 +- middleware.ts => proxy.ts | 5 +- 6 files changed, 101 insertions(+), 53 deletions(-) rename middleware.ts => proxy.ts (68%) diff --git a/CLAUDE.md b/CLAUDE.md index e35a4dd..c9aab8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -245,13 +245,15 @@ npx tsc --noEmit # Production build (catches missing pages, stale imports) rm -rf .next && npx next build -# Dev server +# Dev server — NOTE: this app runs on port 3001 (see package.json: +# `next dev -p 3001` / `next start -p 3001`), NOT the Next.js default 3000. +# Any external doc, script, or healthcheck must target 3001. npm run dev -# Pages-200 sweep (every locale page returns 200) +# Pages-200 sweep (every locale page returns 200) — port 3001 for p in /en /en/trump /en/macro /en/kol /en/trades /en/posts /en/settings \ /en/methodology /en/glossary /en/case-studies /en/privacy /en/terms; do - echo "$p $(curl -so /dev/null -w '%{http_code}' http://localhost:3000$p)" + echo "$p $(curl -so /dev/null -w '%{http_code}' http://localhost:3001$p)" done ``` diff --git a/app/[locale]/trades/TradesPageClient.tsx b/app/[locale]/trades/TradesPageClient.tsx index d34051d..9f5a8c8 100644 --- a/app/[locale]/trades/TradesPageClient.tsx +++ b/app/[locale]/trades/TradesPageClient.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' import { useLocale } from 'next-intl' @@ -8,7 +8,7 @@ import { useAccount, useSignMessage } from 'wagmi' import type { BotTrade, TrumpPost } from '@/types' import { getTrades, getPosts } from '@/lib/api' import { useDashboardStore } from '@/store/dashboard' -import { getCachedViewEnvelope } from '@/lib/signedRequest' +import { getCachedViewEnvelope, getOrCreateViewEnvelope, type SignedEnvelope } from '@/lib/signedRequest' import TradeTable from '@/components/trades/TradeTable' import OpenPositions from '@/components/positions/OpenPositions' import PageHint from '@/components/ui/PageHint' @@ -17,6 +17,7 @@ export default function TradesPageClient() { const intlLocale = useLocale() const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json const { address, isConnected } = useAccount() + const { signMessageAsync } = useSignMessage() const { isSubscribed, hlApiKeySet } = useDashboardStore() const pathname = usePathname() const locale = pathname.split('/')[1] || 'en' @@ -26,53 +27,76 @@ export default function TradesPageClient() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(true) const [loadErr, setLoadErr] = useState('') + const [needsUnlock, setNeedsUnlock] = useState(false) + const [unlocking, setUnlocking] = useState(false) - useEffect(() => { setMounted(true) }, []) - + const aliveRef = useRef(true) useEffect(() => { - let cancelled = false + setMounted(true) + aliveRef.current = true + return () => { aliveRef.current = false } + }, []) + + // Load public posts always; load private trades only if we have a view + // envelope. `forcedEnv` lets the in-page Unlock button pass a freshly-minted + // one so the user never has to detour through the Settings page first. + async function loadAll(forcedEnv?: SignedEnvelope) { if (!address || !isConnected) { - setTrades([]) - setPosts([]) - setLoading(false) - setLoadErr('') + setTrades([]); setPosts([]); setLoading(false); setLoadErr(''); setNeedsUnlock(false) return } setLoading(true) - ;(async () => { - let failed = false - try { - const env = getCachedViewEnvelope('view_trades', address) - ?? getCachedViewEnvelope('view_user', address) - const [t, p] = await Promise.all([ - env - ? getTrades(address, env, 100, 1).catch(e => { - failed = true - setLoadErr(e instanceof Error ? e.message : (isZh ? '交易加载失败' : 'Failed to load trades')) - return [] as BotTrade[] - }) - : Promise.resolve([] as BotTrade[]), - getPosts(500, 1).catch(() => [] as TrumpPost[]), - ]) - if (!cancelled) { - setTrades(t) - setPosts(p) - if (!env) { + const env = forcedEnv + ?? getCachedViewEnvelope('view_trades', address) + ?? getCachedViewEnvelope('view_user', address) + let failed = false + try { + const [t, p] = await Promise.all([ + env + ? getTrades(address, env, 100, 1).catch(e => { failed = true - setLoadErr('Load your settings once on the Settings page to unlock private trade history.') - } else if (!failed) { - setLoadErr('') - } - } - } finally { - if (!cancelled) setLoading(false) - } - })() - return () => { cancelled = true } - // isZh intentionally excluded: compile-time constant (always false). + setLoadErr(e instanceof Error ? e.message : 'Failed to load trades') + return [] as BotTrade[] + }) + : Promise.resolve([] as BotTrade[]), + getPosts(500, 1).catch(() => [] as TrumpPost[]), + ]) + if (!aliveRef.current) return + setTrades(t) + setPosts(p) + setNeedsUnlock(!env) + if (env && !failed) setLoadErr('') + } finally { + if (aliveRef.current) setLoading(false) + } + } + + useEffect(() => { + // On navigation we only use a cached envelope — never auto-popup the + // wallet. If none is cached the user unlocks explicitly via the button. + void loadAll() // eslint-disable-next-line react-hooks/exhaustive-deps }, [address, isConnected]) + // Mint a view envelope right here (one signature) and reload — no Settings detour. + async function handleUnlock() { + if (!address) return + setUnlocking(true) + setLoadErr('') + try { + const env = await getOrCreateViewEnvelope({ + action: 'view_trades', wallet: address, signMessageAsync, + }) + await loadAll(env) + } catch (e) { + // User rejected the signature or it failed — keep the Unlock CTA visible. + const msg = e instanceof Error ? e.message : 'Signature cancelled' + if (aliveRef.current) setLoadErr(/reject|denied|cancel/i.test(msg) ? '' : msg) + } finally { + if (aliveRef.current) setUnlocking(false) + } + } + const needsSetup = mounted && isConnected && (!isSubscribed || !hlApiKeySet) return ( @@ -113,6 +137,23 @@ export default function TradesPageClient() { + {mounted && isConnected && !loading && needsUnlock && ( +
+
+ 🔒 Your trade history is private. Sign once to unlock it on this device + (valid for a few minutes, no transaction, no gas). +
+ +
+ )} + {!loading && loadErr && (
diff --git a/app/sitemap.ts b/app/sitemap.ts index 5379dfd..5b99399 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -28,7 +28,9 @@ const routes: Array<{ { path: '/methodology', priority: 0.8, freq: 'monthly' }, { path: '/glossary', priority: 0.8, freq: 'monthly' }, { path: '/case-studies', priority: 0.8, freq: 'monthly' }, - { path: '/settings', priority: 0.5, freq: 'monthly' }, + // NOTE: /settings is intentionally NOT listed — it's user-personalized and + // robots.ts disallows it. Listing it here while disallowing in robots is a + // self-contradicting signal to crawlers. Keep private pages out of both. { path: '/privacy', priority: 0.3, freq: 'yearly' }, { path: '/terms', priority: 0.3, freq: 'yearly' }, { path: '/contact', priority: 0.4, freq: 'monthly' }, diff --git a/lib/wagmi.ts b/lib/wagmi.ts index 47f746f..7f38a08 100644 --- a/lib/wagmi.ts +++ b/lib/wagmi.ts @@ -7,13 +7,15 @@ export const chains = [mainnet] as const export const config = createConfig({ chains, connectors: [ - injected({ - target: 'metaMask', - // Use the native injected provider path in desktop browsers. - // This avoids the MetaMask SDK account-sync bug we hit in the - // in-app browser and keeps connect to a single requestAccounts flow. - shimDisconnect: false, - }), + // Generic injected connector — NOT pinned to target:'metaMask'. Pinning to + // MetaMask made every other injected wallet (Rabby, Coinbase Wallet ext, + // Brave, Frame, Trust, OKX…) fail to connect, and broke in browsers where + // window.ethereum isn't MetaMask. wagmi v2 auto-discovers all injected + // wallets via EIP-6963 (multiInjectedProviderDiscovery, on by default), so + // a generic injected() connector covers them. This still uses the native + // injected provider path (NOT the MetaMask SDK), so it avoids the SDK + // account-sync bug that motivated the original pin. + injected({ shimDisconnect: false }), ], transports: { [mainnet.id]: http(), diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/middleware.ts b/proxy.ts similarity index 68% rename from middleware.ts rename to proxy.ts index f2a56e2..d95adbd 100644 --- a/middleware.ts +++ b/proxy.ts @@ -1,5 +1,6 @@ import createMiddleware from 'next-intl/middleware' -import { locales, defaultLocale } from './i18n' + +import { defaultLocale, locales } from './i18n' export default createMiddleware({ locales, @@ -9,6 +10,6 @@ export default createMiddleware({ export const config = { // Only match locale-prefixed routes. `/` is served by `app/page.tsx` - // (the landing page) and must not be intercepted by the i18n middleware. + // and should not be intercepted by the locale proxy. matcher: ['/(en)/:path*'], }