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*'], }