diff --git a/app/[locale]/analytics/AnalyticsPageClient.tsx b/app/[locale]/analytics/AnalyticsPageClient.tsx index 6dbcb41..a8966ca 100644 --- a/app/[locale]/analytics/AnalyticsPageClient.tsx +++ b/app/[locale]/analytics/AnalyticsPageClient.tsx @@ -1,12 +1,12 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { useLocale } from 'next-intl' -import { useAccount } from 'wagmi' +import { useAccount, useSignMessage } from 'wagmi' import { getPerformance, getTrades, getSignalAccuracy } from '@/lib/api' import type { BotPerformance, BotTrade } from '@/types' import type { SignalAccuracy } from '@/lib/api' -import { getCachedViewEnvelope } from '@/lib/signedRequest' +import { getCachedViewEnvelope, getOrCreateViewEnvelope, type SignedEnvelope } from '@/lib/signedRequest' import PageHint from '@/components/ui/PageHint' import InfoTip from '@/components/ui/InfoTip' @@ -63,47 +63,70 @@ export default function AnalyticsPageClient() { const locale = 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 [perf, setPerf] = useState(null) const [trades, setTrades] = useState([]) const [accuracy, setAccuracy] = useState(null) const [period, setPeriod] = useState('30d') const [privateLocked, setPrivateLocked] = useState(false) + const [unlocking, setUnlocking] = useState(false) + const [unlockErr, setUnlockErr] = useState('') + const aliveRef = useRef(true) useEffect(() => { - let cancelled = false + aliveRef.current = true + return () => { aliveRef.current = false } + }, []) + + // `forcedEnv` (a freshly-minted view_user) lets the in-page Unlock button + // load private data without a detour through the Settings page. Public + // signal-accuracy always loads regardless. + async function loadAll(forcedEnv?: SignedEnvelope) { if (!address || !isConnected) { - setPerf(null) - setTrades([]) - setAccuracy(null) - setPrivateLocked(false) + setPerf(null); setTrades([]); setAccuracy(null); setPrivateLocked(false) return } - ;(async () => { - try { - const sharedEnv = getCachedViewEnvelope('view_user', address) - const perfEnv = getCachedViewEnvelope('view_performance', address) ?? sharedEnv - const tradesEnv = getCachedViewEnvelope('view_trades', address) ?? sharedEnv - if (!cancelled) setPrivateLocked(!perfEnv && !tradesEnv) - const [p, t, a] = await Promise.all([ - perfEnv ? getPerformance(address, perfEnv).catch(() => null) : Promise.resolve(null), - tradesEnv ? getTrades(address, tradesEnv, 100, 1).catch(() => []) : Promise.resolve([]), - getSignalAccuracy().catch(() => null), - ]) - if (!cancelled) { - setPerf(p) - setTrades(t) - setAccuracy(a) - } - } catch { - if (!cancelled) { - setPerf(null) - setTrades([]) - } - } - })() - return () => { cancelled = true } + const sharedEnv = forcedEnv ?? getCachedViewEnvelope('view_user', address) + const perfEnv = getCachedViewEnvelope('view_performance', address) ?? sharedEnv + const tradesEnv = getCachedViewEnvelope('view_trades', address) ?? sharedEnv + if (aliveRef.current) setPrivateLocked(!perfEnv && !tradesEnv) + try { + const [p, t, a] = await Promise.all([ + perfEnv ? getPerformance(address, perfEnv).catch(() => null) : Promise.resolve(null), + tradesEnv ? getTrades(address, tradesEnv, 100, 1).catch(() => []) : Promise.resolve([]), + getSignalAccuracy().catch(() => null), + ]) + if (aliveRef.current) { setPerf(p); setTrades(t); setAccuracy(a) } + } catch { + if (aliveRef.current) { setPerf(null); setTrades([]) } + } + } + + useEffect(() => { + // Navigation only uses a cached envelope — never auto-popup the wallet. + void loadAll() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [address, isConnected]) + // Mint a view_user envelope (one signature) — unlocks both /performance and + // /trades (each accepts view_user as fallback) — then reload. + async function handleUnlock() { + if (!address) return + setUnlocking(true) + setUnlockErr('') + try { + const env = await getOrCreateViewEnvelope({ + action: 'view_user', wallet: address, signMessageAsync, + }) + await loadAll(env) + } catch (e) { + const msg = e instanceof Error ? e.message : 'Signature cancelled' + if (aliveRef.current) setUnlockErr(/reject|denied|cancel/i.test(msg) ? '' : msg) + } finally { + if (aliveRef.current) setUnlocking(false) + } + } + const filteredTrades = trades.filter((trade) => inPeriod(trade.closed_at, period)) const pricedTrades = filteredTrades.filter( (t) => t.pnl_usd !== null && t.pnl_usd !== undefined, @@ -157,8 +180,17 @@ export default function AnalyticsPageClient() {
Private performance is locked.
- Load your settings once to unlock cached trade history and wallet-specific performance. Public signal-accuracy data below remains available. + Sign once to unlock your wallet-specific performance and trade history + on this device (valid a few minutes, no transaction, no gas). Public + signal-accuracy data below is always available.
+ + {unlockErr && ( +
● {unlockErr}
+ )}
)}