File size: 7,616 Bytes
945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 383d246 945e815 5f5514e 383d246 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 5f5514e 945e815 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 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>
);
}
|