solvox / src /renderer /pages /OnboardingScreen.tsx
muthuk1's picture
🎨 Complete Coinbase design system rebuild: white canvas, single blue accent, pill CTAs, editorial spacing, hairline borders, dark hero bands, asset rows, mono numbers, 96px section rhythm
5f5514e verified
import React, { useState } from 'react';
import { StepIndicator } from '../components/ui/index';
interface Props { onComplete: (pk: string) => void; }
type Step = 'welcome' | 'choose' | 'import' | 'pin' | 'done';
const LABELS = ['Welcome', 'Wallet', 'Secure', 'Done'];
const idx = (s: Step) => s === 'welcome' ? 0 : s === 'choose' || s === 'import' ? 1 : s === 'pin' ? 2 : 3;
export default function OnboardingScreen({ onComplete }: Props) {
const [step, setStep] = useState<Step>('welcome');
const [mnemonic, setMnemonic] = useState('');
const [pin, setPin] = useState(''); const [pinC, setPinC] = useState('');
const [pk, setPk] = useState('');
const [err, setErr] = useState('');
const [busy, setBusy] = useState(false);
const create = async () => {
setBusy(true); setErr('');
try {
if (window.solvox) { const r = await window.solvox.wallet.create(); if (r.success && r.publicKey) { setPk(r.publicKey); setStep('pin'); } else setErr(r.error || 'Failed'); }
else { setPk('Dev' + Date.now()); setStep('pin'); }
} catch (e: any) { setErr(e.message); }
setBusy(false);
};
const imp = async () => {
if (!mnemonic.trim()) return setErr('Enter recovery phrase');
const w = mnemonic.trim().split(/\s+/); if (w.length !== 12 && w.length !== 24) return setErr('Must be 12 or 24 words');
setBusy(true); setErr('');
try {
if (window.solvox) { const r = await window.solvox.wallet.import(mnemonic.trim()); if (r.success && r.publicKey) { setPk(r.publicKey); setStep('pin'); } else setErr(r.error || 'Invalid'); }
else { setPk('DevImp' + Date.now()); setStep('pin'); }
} catch (e: any) { setErr(e.message); }
setBusy(false);
};
const setP = async () => {
if (pin.length < 6) return setErr('Min 6 digits');
if (pin !== pinC) return setErr('PINs don\'t match');
setBusy(true); setErr('');
try { if (window.solvox) { const r = await window.solvox.auth.setPin(pin); if (!r.success) { setErr(r.error || 'Failed'); setBusy(false); return; } } setStep('done'); }
catch (e: any) { setErr(e.message); }
setBusy(false);
};
return (
<div className="h-screen flex items-center justify-center bg-canvas">
<div className="w-full max-w-md px-6">
{step !== 'welcome' && <StepIndicator steps={LABELS} current={idx(step)} />}
{step === 'welcome' && (
<div className="text-center page-enter">
<div className="w-14 h-14 mx-auto rounded-full bg-primary flex items-center justify-center mb-6">
<span className="text-on-primary font-bold text-title-lg">SV</span>
</div>
<h1 className="display-text text-display-lg text-ink mb-3">SolVox</h1>
<p className="text-body-md text-body mb-2">Voice-First Private AI Wallet</p>
<p className="text-body-sm text-muted mb-10">Powered by QVAC SDK · 100% Local AI · Zero Cloud</p>
<div className="grid grid-cols-3 gap-3 mb-10">
{[{ t: 'Voice Control', d: 'Talk to your wallet' }, { t: 'Local AI', d: '6 QVAC modules' }, { t: 'Self-Custody', d: 'Your keys only' }].map(f => (
<div key={f.t} className="card text-center" style={{ padding: '20px' }}>
<div className="text-title-sm text-ink">{f.t}</div>
<div className="text-caption text-muted mt-1">{f.d}</div>
</div>
))}
</div>
<button onClick={() => setStep('choose')} className="btn-primary-lg">Get started</button>
</div>
)}
{step === 'choose' && (
<div className="space-y-4 page-enter">
<h2 className="display-text text-display-sm text-ink text-center mb-6">Set up your wallet</h2>
<button onClick={create} disabled={busy} className="w-full card text-left hover:border-primary/30 hover:shadow-soft transition-all">
<div className="text-title-md text-ink">Create new wallet</div>
<div className="text-body-sm text-body mt-1">Generate a fresh 24-word recovery phrase</div>
</button>
<button onClick={() => setStep('import')} className="w-full card text-left hover:border-primary/30 hover:shadow-soft transition-all">
<div className="text-title-md text-ink">Import existing wallet</div>
<div className="text-body-sm text-body mt-1">Use a 12 or 24 word recovery phrase</div>
</button>
{err && <div className="text-body-sm text-semantic-down text-center">{err}</div>}
</div>
)}
{step === 'import' && (
<div className="space-y-5 page-enter">
<h2 className="display-text text-display-sm text-ink text-center">Import recovery phrase</h2>
<p className="text-body-sm text-muted text-center">Stays on your device — never sent anywhere.</p>
<textarea value={mnemonic} onChange={e => setMnemonic(e.target.value)} placeholder="word1 word2 word3 …" rows={4} className="input-field font-mono text-body-sm resize-none" style={{ height: 'auto' }} />
{err && <div className="text-body-sm text-semantic-down">{err}</div>}
<div className="flex gap-3">
<button onClick={() => { setStep('choose'); setErr(''); }} className="btn-secondary flex-1">Back</button>
<button onClick={imp} disabled={busy} className="btn-primary flex-1 disabled:opacity-50">{busy ? 'Importing…' : 'Import'}</button>
</div>
</div>
)}
{step === 'pin' && (
<div className="space-y-5 page-enter">
<h2 className="display-text text-display-sm text-ink text-center">Set your PIN</h2>
<p className="text-body-sm text-muted text-center">Encrypts your wallet with AES-256-GCM.</p>
<input type="password" inputMode="numeric" value={pin} onChange={e => setPin(e.target.value.replace(/\D/g, ''))} placeholder="Enter PIN (min 6)" maxLength={12} className="input-field text-center text-title-lg tracking-[0.5em] font-mono" />
<input type="password" inputMode="numeric" value={pinC} onChange={e => setPinC(e.target.value.replace(/\D/g, ''))} placeholder="Confirm PIN" maxLength={12} className="input-field text-center text-title-lg tracking-[0.5em] font-mono" />
{pin.length >= 6 && pin === pinC && <div className="text-center"><span className="badge-pill-green badge-pill">PINs match</span></div>}
{err && <div className="text-body-sm text-semantic-down text-center">{err}</div>}
<button onClick={setP} disabled={busy || pin.length < 6} className="btn-primary w-full disabled:opacity-40">{busy ? 'Encrypting…' : 'Continue'}</button>
</div>
)}
{step === 'done' && (
<div className="text-center page-enter">
<div className="w-14 h-14 mx-auto rounded-full bg-semantic-up/10 flex items-center justify-center mb-6">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#05b169" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
<h2 className="display-text text-display-sm text-ink mb-2">You're all set</h2>
<p className="text-body-md text-body mb-4">Wallet encrypted & secured. AI runs 100% locally.</p>
<div className="bg-surface-soft rounded-xl p-3 font-mono text-caption text-muted break-all mb-6">{pk}</div>
<button onClick={() => onComplete(pk)} className="btn-primary-lg">Open SolVox</button>
</div>
)}
</div>
</div>
);
}