From 55db7473182b5ce3feaee5a4146140fed02f5f8c Mon Sep 17 00:00:00 2001 From: Zavier Date: Sat, 28 Mar 2026 16:09:04 +0800 Subject: [PATCH] feat: refine beginner wallet onboarding modal (#1438) Co-authored-by: Codex --- web/src/App.tsx | 15 +- web/src/pages/BeginnerOnboardingPage.tsx | 338 ++++++++++++++--------- 2 files changed, 217 insertions(+), 136 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 3173fb97..34fbef5c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -65,6 +65,7 @@ function App() { const path = window.location.pathname const hash = window.location.hash.slice(1) // 去掉 # + if (path === '/welcome') return 'traders' if (path === '/traders' || hash === 'traders') return 'traders' if (path === '/strategy' || hash === 'strategy') return 'strategy' if (path === '/strategy-market' || hash === 'strategy-market') return 'strategy-market' @@ -157,7 +158,9 @@ function App() { const params = new URLSearchParams(window.location.search) const traderParam = params.get('trader') - if (path === '/traders' || hash === 'traders') { + if (path === '/welcome') { + setCurrentPage('traders') + } else if (path === '/traders' || hash === 'traders') { setCurrentPage('traders') } else if (path === '/strategy' || hash === 'strategy') { setCurrentPage('strategy') @@ -337,7 +340,9 @@ function App() { // Set current page based on route for consistent navigation state useEffect(() => { - if (route === '/competition') { + if (route === '/welcome') { + setCurrentPage('traders') + } else if (route === '/competition') { setCurrentPage('competition') } else if (route === '/traders') { setCurrentPage('traders') @@ -346,6 +351,9 @@ function App() { } }, [route]) + const showBeginnerOnboarding = + route === '/welcome' && (!!user || hasPersistedAuth) && getUserMode() === 'beginner' + // Show loading spinner while checking auth or config if (isLoading || configLoading) { return ( @@ -391,7 +399,6 @@ function App() { window.location.href = '/traders' return null } - return } if (route === '/faq') { return ( @@ -695,6 +702,8 @@ function App() { onClose={() => setLoginOverlayOpen(false)} featureName={loginOverlayFeature} /> + + {showBeginnerOnboarding && } ) } diff --git a/web/src/pages/BeginnerOnboardingPage.tsx b/web/src/pages/BeginnerOnboardingPage.tsx index 17672eb0..a961f631 100644 --- a/web/src/pages/BeginnerOnboardingPage.tsx +++ b/web/src/pages/BeginnerOnboardingPage.tsx @@ -1,8 +1,13 @@ -import { useEffect, useRef, useState } from 'react' -import { Copy, Eye, EyeOff, RefreshCw, Shield, Wallet } from 'lucide-react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { + ArrowRight, + Copy, + RefreshCw, + Shield, + Wallet, +} from 'lucide-react' import { QRCodeSVG } from 'qrcode.react' import { toast } from 'sonner' -import { DeepVoidBackground } from '../components/common/DeepVoidBackground' import { useLanguage } from '../contexts/LanguageContext' import { api } from '../lib/api' import type { BeginnerOnboardingResponse } from '../types' @@ -13,33 +18,55 @@ export function BeginnerOnboardingPage() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') - const [showPrivateKey, setShowPrivateKey] = useState(true) const [refreshingBalance, setRefreshingBalance] = useState(false) const hasRequestedRef = useRef(false) const isZh = language === 'zh' const loadOnboarding = async (showLoading: boolean) => { - if (showLoading) setLoading(true) - else setRefreshingBalance(true) + if (showLoading) { + setLoading(true) + } else { + setRefreshingBalance(true) + } + setError('') try { const result = await api.prepareBeginnerOnboarding() setData(result) setBeginnerWalletAddress(result.address) } catch (err) { - setError(err instanceof Error ? err.message : isZh ? '新手钱包准备失败' : 'Failed to prepare beginner wallet') + setError( + err instanceof Error + ? err.message + : isZh + ? '新手钱包准备失败' + : 'Failed to prepare beginner wallet' + ) } finally { - if (showLoading) setLoading(false) - else setRefreshingBalance(false) + if (showLoading) { + setLoading(false) + } else { + setRefreshingBalance(false) + } } } useEffect(() => { - if (hasRequestedRef.current) return + if (hasRequestedRef.current) { + return + } hasRequestedRef.current = true void loadOnboarding(true) }, []) + const noticeText = useMemo( + () => + isZh + ? '此钱包仅用于大模型调用费用,不会自动充到交易所。私钥丢失后无法恢复,只充 Base 链 USDC。' + : 'This wallet only pays for model calls. It does not fund your exchange automatically. The private key cannot be recovered, and you should only deposit Base USDC.', + [isZh] + ) + const copyText = async (value: string, label: string) => { try { await navigator.clipboard.writeText(value) @@ -55,133 +82,178 @@ export function BeginnerOnboardingPage() { } return ( - -
- {/* Header - compact */} -
-
- -
-
-
- {isZh ? '新手保护' : 'Beginner Guard'} +
+
+
+
+
+
+
+ +
+
+
+ {isZh ? '新手保护' : 'Beginner Guard'} +
+

+ {isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'} +

+
+
+ +
+ Claw402 + DeepSeek · + {isZh ? '按次付费' : 'Pay per call'}
-

- {isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'} -

-
- Claw402 + DeepSeek · {isZh ? '按次付费' : 'Pay per call'} + +
+ {loading ? ( +
+ {isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'} +
+ ) : data ? ( +
+
+
+
+ +
+ +
+ {isZh ? '充值地址(Base USDC)' : 'Deposit address (Base USDC)'} +
+ +
+
+
+ {data.balance_usdc} + USDC +
+
+ +
+ +
+ {isZh ? '$5-$10 可以用很久' : '$5-$10 usually lasts a long time'} +
+
+
+ +
+
+
+
+ + {isZh ? '钱包地址' : 'Wallet address'} +
+
+
+
{data.address}
+
+ +
+
+ +
+
+ + {isZh ? '私钥,请立即备份' : 'Private key, back it up now'} +
+
+
+
{data.private_key}
+
+
+ +
+
+
+ +
+ + {noticeText} +
+ + {data.env_warning ? ( +
+ {data.env_warning} +
+ ) : null} + + {error ? ( +
+ {error} +
+ ) : null} + + + + {data.env_saved ? ( +
+ {isZh + ? `钱包信息已同步保存到 ${data.env_path || '.env'}` + : `Wallet details were also saved to ${data.env_path || '.env'}`} +
+ ) : null} +
+
+
+ ) : null}
- - {error ? ( -
{error}
- ) : null} - - {/* Main card */} -
- {loading ? ( -
- {isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'} -
- ) : data ? ( -
- {/* Left: QR + Balance */} -
-
- -
-
- {isZh ? '充值地址(Base USDC)' : 'Deposit (Base USDC)'} -
-
-
-
{data.balance_usdc} USDC
-
- -
-
- {isZh ? '$5-$10 可以用很久' : '$5-$10 lasts a long time'} -
-
- - {/* Right: Address + Key + Action */} -
- {/* Address */} -
-
- - {isZh ? '钱包地址' : 'Wallet Address'} -
-
-
- {data.address} -
- -
-
- - {/* Private Key */} -
-
- - {isZh ? '私钥 — 请立即备份' : 'Private Key — back up now'} - -
-
-
- {showPrivateKey ? data.private_key : '0x' + '•'.repeat(64)} -
- -
-
- - {/* Tips */} -
- {isZh - ? '• 此钱包仅用于大模型调用费用,不会自动充值交易所 • 私钥丢失后无法恢复 • 只充 Base 链 USDC' - : '• This wallet only covers LLM costs, not exchange funding • Private key cannot be recovered • Base USDC only'} -
- - {/* Continue */} - -
-
- ) : null} -
- +
) }