| 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> |
| ); |
| } |
|
|