mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
fix: guard short trader ID, i18n setup page, simplify onboarding UX
- main.go: prevent panic when trader ID < 8 chars - SetupPage: add zh/en i18n labels - BeginnerOnboardingPage: show private key by default, simplify code
This commit is contained in:
@@ -118,8 +118,12 @@ func main() {
|
||||
if t.IsRunning {
|
||||
status = "✅ Running"
|
||||
}
|
||||
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
|
||||
t.Name, t.ID[:8], status, t.AIModelID, t.ExchangeID)
|
||||
idShort := t.ID
|
||||
if len(idShort) > 8 {
|
||||
idShort = idShort[:8]
|
||||
}
|
||||
logger.Infof(" • %s [%s] %s - AI Model: %s, Exchange: %s",
|
||||
t.Name, idShort, status, t.AIModelID, t.ExchangeID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,62 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { Eye, EyeOff, Globe } from 'lucide-react'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { DeepVoidBackground } from '../common/DeepVoidBackground'
|
||||
import { invalidateSystemConfig } from '../../lib/config'
|
||||
import { OnboardingModeSelector } from '../auth/OnboardingModeSelector'
|
||||
import type { UserMode } from '../../lib/onboarding'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import type { Language } from '../../i18n/translations'
|
||||
|
||||
const labels = {
|
||||
zh: {
|
||||
welcome: '欢迎使用 NOFX',
|
||||
subtitle: '创建账号开始使用',
|
||||
email: '邮箱',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
password: '密码',
|
||||
passwordPlaceholder: '至少 8 个字符',
|
||||
passwordError: '密码至少需要 8 个字符',
|
||||
submit: '开始使用',
|
||||
submitting: '创建中...',
|
||||
setupFailed: '创建失败,请重试',
|
||||
singleUser: '单用户系统 — 这是唯一的账号',
|
||||
},
|
||||
en: {
|
||||
welcome: 'Welcome to NOFX',
|
||||
subtitle: 'Create your account to get started',
|
||||
email: 'Email',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
password: 'Password',
|
||||
passwordPlaceholder: 'At least 8 characters',
|
||||
passwordError: 'Password must be at least 8 characters',
|
||||
submit: 'Get Started',
|
||||
submitting: 'Creating account...',
|
||||
setupFailed: 'Setup failed, please try again',
|
||||
singleUser: 'Single-user system — this is the only account',
|
||||
},
|
||||
id: {
|
||||
welcome: 'Selamat Datang di NOFX',
|
||||
subtitle: 'Buat akun untuk memulai',
|
||||
email: 'Email',
|
||||
emailPlaceholder: 'you@example.com',
|
||||
password: 'Kata Sandi',
|
||||
passwordPlaceholder: 'Minimal 8 karakter',
|
||||
passwordError: 'Kata sandi minimal 8 karakter',
|
||||
submit: 'Mulai',
|
||||
submitting: 'Membuat akun...',
|
||||
setupFailed: 'Gagal membuat akun, coba lagi',
|
||||
singleUser: 'Sistem pengguna tunggal — ini satu-satunya akun',
|
||||
},
|
||||
} as const
|
||||
|
||||
const langOptions: { value: Language; label: string }[] = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
{ value: 'id', label: 'Bahasa' },
|
||||
]
|
||||
|
||||
export function SetupPage() {
|
||||
const { language } = useLanguage()
|
||||
const { language, setLanguage } = useLanguage()
|
||||
const { register } = useAuth()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -17,11 +65,13 @@ export function SetupPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [mode, setMode] = useState<UserMode>('beginner')
|
||||
|
||||
const l = labels[language as keyof typeof labels] || labels.en
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
if (password.length < 8) {
|
||||
setError('Password must be at least 8 characters')
|
||||
setError(l.passwordError)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
@@ -30,40 +80,100 @@ export function SetupPage() {
|
||||
if (result.success) {
|
||||
invalidateSystemConfig()
|
||||
} else {
|
||||
setError(result.message || 'Setup failed, please try again')
|
||||
setError(result.message || l.setupFailed)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DeepVoidBackground disableAnimation>
|
||||
<div className="flex-1 flex items-center justify-center px-4 py-16">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="relative min-h-screen w-full overflow-hidden bg-[#0a0a0f]">
|
||||
{/* Decorative background - simulates the main app behind a modal */}
|
||||
|
||||
{/* Grid */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute inset-x-0 bottom-0 h-[60vh] bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)] opacity-40" style={{ transform: 'perspective(500px) rotateX(60deg) translateY(80px) scale(2)' }} />
|
||||
</div>
|
||||
|
||||
{/* Glow spots */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute top-[10%] left-[15%] w-[500px] h-[500px] bg-nofx-gold/8 rounded-full blur-[150px]" />
|
||||
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-indigo-500/6 rounded-full blur-[140px]" />
|
||||
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-emerald-500/4 rounded-full blur-[120px]" />
|
||||
</div>
|
||||
|
||||
{/* Faux UI elements in background to simulate the app */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.06]">
|
||||
{/* Fake header bar */}
|
||||
<div className="h-14 border-b border-white/20 flex items-center px-6 gap-4">
|
||||
<div className="w-8 h-8 rounded-lg bg-white/40" />
|
||||
<div className="h-3 w-20 rounded bg-white/30" />
|
||||
<div className="h-3 w-16 rounded bg-white/20 ml-4" />
|
||||
<div className="h-3 w-16 rounded bg-white/20" />
|
||||
<div className="h-3 w-16 rounded bg-white/20" />
|
||||
</div>
|
||||
{/* Fake content cards */}
|
||||
<div className="p-6 grid grid-cols-4 gap-4 mt-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="h-24 rounded-xl border border-white/15 bg-white/5" />
|
||||
))}
|
||||
</div>
|
||||
<div className="px-6 mt-2">
|
||||
<div className="h-64 rounded-xl border border-white/15 bg-white/5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blur overlay */}
|
||||
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
|
||||
|
||||
{/* Language switcher */}
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<div className="flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm px-2 py-1.5">
|
||||
<Globe className="h-3.5 w-3.5 text-zinc-500" />
|
||||
{langOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => setLanguage(opt.value)}
|
||||
className={`rounded-lg px-2 py-1 text-xs font-medium transition ${
|
||||
language === opt.value
|
||||
? 'bg-nofx-gold/15 text-nofx-gold'
|
||||
: 'text-zinc-500 hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal card */}
|
||||
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 py-16">
|
||||
<div className="w-full max-w-sm animate-[fadeInUp_0.4s_ease-out]">
|
||||
|
||||
{/* Logo + Title */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="flex justify-center mb-5">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-3 bg-nofx-gold/15 rounded-full blur-2xl" />
|
||||
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
|
||||
<div className="absolute -inset-4 bg-nofx-gold/20 rounded-full blur-2xl" />
|
||||
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10 drop-shadow-[0_0_15px_rgba(240,185,11,0.3)]" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white mb-1.5">Welcome to NOFX</h1>
|
||||
<p className="text-zinc-500 text-sm">Create your account to get started</p>
|
||||
<h1 className="text-2xl font-bold text-white mb-1.5">{l.welcome}</h1>
|
||||
<p className="text-zinc-500 text-sm">{l.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-8 shadow-2xl">
|
||||
<div className="bg-zinc-900/80 backdrop-blur-2xl border border-white/10 rounded-2xl p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.5),0_0_40px_-10px_rgba(240,185,11,0.08)]">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">Email</label>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.email}</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder="you@example.com"
|
||||
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder={l.emailPlaceholder}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
@@ -71,14 +181,14 @@ export function SetupPage() {
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">Password</label>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.password}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-zinc-950/80 border border-zinc-700/80 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder="At least 8 characters"
|
||||
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
|
||||
placeholder={l.passwordPlaceholder}
|
||||
required
|
||||
/>
|
||||
<button
|
||||
@@ -108,18 +218,25 @@ export function SetupPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
||||
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2 shadow-[0_0_20px_rgba(240,185,11,0.2)]"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Get Started'}
|
||||
{loading ? l.submitting : l.submit}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-zinc-600 mt-6">
|
||||
Single-user system — this is the only account
|
||||
{l.singleUser}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
|
||||
<style>{`
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Copy, Eye, EyeOff, RefreshCw, Shield, Wallet, Sparkles } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Copy, Eye, EyeOff, RefreshCw, Shield, Wallet } from 'lucide-react'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { toast } from 'sonner'
|
||||
import { DeepVoidBackground } from '../components/common/DeepVoidBackground'
|
||||
@@ -13,64 +13,33 @@ export function BeginnerOnboardingPage() {
|
||||
const [data, setData] = useState<BeginnerOnboardingResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [showPrivateKey, setShowPrivateKey] = useState(false)
|
||||
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 hints = useMemo(
|
||||
() =>
|
||||
isZh
|
||||
? [
|
||||
'这是你的专属 Base 钱包,只用于后续调用大模型。',
|
||||
'请保存私钥。丢失后无法恢复。',
|
||||
'只往这个地址充值 Base 链 USDC,不要充到别的链。',
|
||||
]
|
||||
: [
|
||||
'This dedicated Base wallet is only used to pay for model calls.',
|
||||
'Save the private key now. It cannot be recovered later.',
|
||||
'Deposit USDC on Base only. Do not send funds from another chain.',
|
||||
],
|
||||
[isZh]
|
||||
)
|
||||
|
||||
const copyText = async (value: string, label: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
@@ -87,177 +56,131 @@ export function BeginnerOnboardingPage() {
|
||||
|
||||
return (
|
||||
<DeepVoidBackground disableAnimation>
|
||||
<div className="mx-auto flex min-h-screen max-w-5xl items-center px-4 py-12">
|
||||
<div className="grid w-full gap-8 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-zinc-950/70 p-8 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-nofx-gold/15 text-nofx-gold">
|
||||
<Shield className="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.28em] text-nofx-gold/80">
|
||||
{isZh ? '新手保护' : 'Beginner Guard'}
|
||||
</div>
|
||||
<h1 className="mt-1 text-3xl font-bold text-white">
|
||||
{isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mx-auto flex h-screen max-w-4xl flex-col justify-center px-4">
|
||||
{/* Header - compact */}
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-nofx-gold/15 text-nofx-gold">
|
||||
<Shield className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.3em] text-nofx-gold/80">
|
||||
{isZh ? '新手保护' : 'Beginner Guard'}
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-white">
|
||||
{isZh ? '钱包已经帮你准备好了' : 'Your wallet is ready'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="ml-auto text-xs text-zinc-500">
|
||||
Claw402 + DeepSeek · {isZh ? '按次付费' : 'Pay per call'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="max-w-xl text-sm leading-7 text-zinc-300">
|
||||
{isZh
|
||||
? '我们已经为你生成了一个专属钱包,并默认接入 Claw402 + DeepSeek。你现在只需要保存私钥,然后往这个地址充值 Base 链 USDC,后面调用大模型时会自动从这里扣费。'
|
||||
: 'We generated a dedicated wallet for you and preconfigured Claw402 + DeepSeek. Save the private key, then deposit Base USDC to this address so future model calls can be paid automatically.'}
|
||||
</p>
|
||||
{error ? (
|
||||
<div className="mb-4 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-2 text-sm text-red-300">{error}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 grid gap-3">
|
||||
{hints.map((hint) => (
|
||||
<div
|
||||
key={hint}
|
||||
className="flex items-start gap-3 rounded-2xl border border-white/8 bg-white/5 px-4 py-3"
|
||||
>
|
||||
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-nofx-gold" />
|
||||
<div className="text-sm leading-6 text-zinc-300">{hint}</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Main card */}
|
||||
<section className="rounded-[24px] border border-white/10 bg-zinc-950/70 shadow-2xl backdrop-blur-xl">
|
||||
{loading ? (
|
||||
<div className="flex h-[400px] items-center justify-center text-sm text-zinc-400">
|
||||
{isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 rounded-[24px] border border-sky-500/20 bg-sky-500/5 p-5">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-sky-300">
|
||||
<Wallet className="h-4 w-4" />
|
||||
<span>{isZh ? '为什么要充值?' : 'Why fund this wallet?'}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-zinc-300">
|
||||
{isZh
|
||||
? '这里只负责大模型调用费用,不会自动替你充值交易所。先充少量 USDC 就够了,通常 $5-$10 可以用很久。'
|
||||
: 'This wallet only covers LLM usage costs. It does not fund your exchange automatically. A small amount of USDC is enough to get started, usually $5-$10.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-6 rounded-2xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-black/60 p-8 shadow-2xl backdrop-blur-xl">
|
||||
{loading ? (
|
||||
<div className="flex min-h-[420px] items-center justify-center text-sm text-zinc-400">
|
||||
{isZh ? '正在准备你的 Base 钱包...' : 'Preparing your Base wallet...'}
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/5 p-5">
|
||||
<div className="text-xs uppercase tracking-[0.28em] text-zinc-500">
|
||||
{isZh ? '默认模型' : 'Default Model'}
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-white">Claw402 + DeepSeek</div>
|
||||
<div className="mt-2 text-sm text-zinc-400">
|
||||
{isZh ? '按次付费,无需 API Key' : 'Pay per call, no API key needed'}
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="grid gap-0 lg:grid-cols-[280px_1fr]">
|
||||
{/* Left: QR + Balance */}
|
||||
<div className="flex flex-col items-center border-b border-white/8 p-6 lg:border-b-0 lg:border-r">
|
||||
<div className="rounded-2xl bg-white p-2.5">
|
||||
<QRCodeSVG value={data.address} size={120} level="M" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-[24px] border border-white/10 bg-white p-5 text-center">
|
||||
<div className="inline-flex rounded-2xl bg-white p-3">
|
||||
<QRCodeSVG value={data.address} size={180} level="M" />
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-semibold text-zinc-900">
|
||||
{isZh ? '充值地址(Base 链 USDC)' : 'Deposit Address (Base USDC)'}
|
||||
</div>
|
||||
<div className="mt-2 break-all rounded-2xl bg-zinc-100 px-3 py-3 font-mono text-xs text-zinc-700">
|
||||
{data.address}
|
||||
<div className="mt-3 text-xs font-medium text-zinc-400">
|
||||
{isZh ? '充值地址(Base USDC)' : 'Deposit (Base USDC)'}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-xl border border-emerald-500/20 bg-emerald-500/8 px-3 py-2">
|
||||
<div>
|
||||
<div className="text-lg font-bold text-emerald-300">{data.balance_usdc} USDC</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyText(data.address, isZh ? '地址' : 'Address')}
|
||||
className="mt-3 inline-flex items-center gap-2 rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-zinc-800"
|
||||
onClick={() => void loadOnboarding(false)}
|
||||
disabled={refreshingBalance}
|
||||
className="rounded-lg border border-emerald-500/20 p-1.5 text-emerald-400 transition hover:bg-emerald-500/10 disabled:opacity-50"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
{isZh ? '复制地址' : 'Copy address'}
|
||||
<RefreshCw className={`h-3 w-3 ${refreshingBalance ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-left">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-emerald-700">
|
||||
{isZh ? '当前余额' : 'Current Balance'}
|
||||
</div>
|
||||
<div className="mt-1 text-2xl font-bold text-emerald-900">
|
||||
{data.balance_usdc} USDC
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-emerald-700/80">
|
||||
{isZh ? 'Base 链钱包余额' : 'Base wallet balance'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadOnboarding(false)}
|
||||
disabled={refreshingBalance}
|
||||
className="inline-flex items-center gap-2 rounded-xl border border-emerald-300 bg-white px-3 py-2 text-xs font-semibold text-emerald-800 transition hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 ${refreshingBalance ? 'animate-spin' : ''}`} />
|
||||
{isZh ? '刷新余额' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] text-zinc-600">
|
||||
{isZh ? '$5-$10 可以用很久' : '$5-$10 lasts a long time'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[24px] border border-amber-500/20 bg-amber-500/8 p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-amber-200">
|
||||
{isZh ? '钱包私钥' : 'Wallet Private Key'}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-amber-100/75">
|
||||
{isZh ? '请先备份,再进入下一步。' : 'Back this up before you continue.'}
|
||||
</div>
|
||||
{/* Right: Address + Key + Action */}
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
{/* Address */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-zinc-400">
|
||||
<Wallet className="h-3.5 w-3.5 text-nofx-gold" />
|
||||
{isZh ? '钱包地址' : 'Wallet Address'}
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1 truncate rounded-lg bg-black/30 px-3 py-2 font-mono text-xs text-zinc-300">
|
||||
{data.address}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPrivateKey((prev) => !prev)}
|
||||
className="rounded-xl border border-amber-400/20 px-3 py-2 text-amber-200 transition hover:bg-amber-400/10"
|
||||
onClick={() => copyText(data.address, isZh ? '地址' : 'Address')}
|
||||
className="shrink-0 rounded-lg bg-white/10 p-2 text-zinc-400 transition hover:bg-white/15 hover:text-white"
|
||||
>
|
||||
{showPrivateKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 break-all rounded-2xl bg-black/25 px-3 py-3 font-mono text-xs text-amber-50">
|
||||
{showPrivateKey ? data.private_key : '0x' + '•'.repeat(64)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyText(data.private_key, isZh ? '私钥' : 'Private key')}
|
||||
className="mt-3 inline-flex items-center gap-2 rounded-xl border border-amber-300/20 px-4 py-2 text-sm font-semibold text-amber-100 transition hover:bg-amber-400/10"
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
{isZh ? '复制私钥' : 'Copy private key'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/5 p-4 text-xs leading-6 text-zinc-400">
|
||||
<div>
|
||||
{data.env_saved
|
||||
? isZh
|
||||
? `已同步保存到环境文件:${data.env_path || '.env'}`
|
||||
: `Also saved to env: ${data.env_path || '.env'}`
|
||||
: isZh
|
||||
? '当前运行环境没有成功写回 .env,但产品已完成默认配置。'
|
||||
: 'The app is configured, but this runtime could not write back to .env.'}
|
||||
{/* Private Key */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-amber-300/80">
|
||||
<Shield className="h-3.5 w-3.5" />
|
||||
{isZh ? '私钥 — 请立即备份' : 'Private Key — back up now'}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPrivateKey((p) => !p)}
|
||||
className="ml-auto rounded-lg p-1 text-amber-300/60 transition hover:text-amber-200"
|
||||
>
|
||||
{showPrivateKey ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="min-w-0 flex-1 truncate rounded-lg bg-amber-500/8 border border-amber-500/15 px-3 py-2 font-mono text-xs text-amber-100">
|
||||
{showPrivateKey ? data.private_key : '0x' + '•'.repeat(64)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyText(data.private_key, isZh ? '私钥' : 'Private key')}
|
||||
className="shrink-0 rounded-lg bg-amber-500/10 border border-amber-500/15 p-2 text-amber-300 transition hover:bg-amber-500/20"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{data.env_warning ? <div className="mt-2 text-amber-300">{data.env_warning}</div> : null}
|
||||
</div>
|
||||
|
||||
{/* Tips */}
|
||||
<div className="rounded-lg bg-white/3 border border-white/5 px-3 py-2 text-[11px] leading-5 text-zinc-500">
|
||||
{isZh
|
||||
? '• 此钱包仅用于大模型调用费用,不会自动充值交易所 • 私钥丢失后无法恢复 • 只充 Base 链 USDC'
|
||||
: '• This wallet only covers LLM costs, not exchange funding • Private key cannot be recovered • Base USDC only'}
|
||||
</div>
|
||||
|
||||
{/* Continue */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleContinue}
|
||||
className="w-full rounded-2xl bg-nofx-gold px-5 py-4 text-sm font-bold text-black transition hover:bg-yellow-400"
|
||||
className="mt-auto w-full rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-black transition hover:bg-yellow-400"
|
||||
>
|
||||
{isZh ? '我已保存,进入下一步' : 'I saved it, continue'}
|
||||
{isZh ? '我已保存,进入下一步 →' : 'I saved it, continue →'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
</DeepVoidBackground>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user