solvox / src /renderer /pages /LockScreen.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, useRef, useEffect } from 'react';
interface Props { onUnlock: (pk: string) => void; }
export default function LockScreen({ onUnlock }: Props) {
const [pin, setPin] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [bio, setBio] = useState(false);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => { ref.current?.focus(); if (window.solvox) window.solvox.auth.biometricAvailable().then(b => { setBio(b); if (b) doBio(); }); }, []);
const doBio = async () => {
if (!window.solvox) return; setLoading(true);
const r = await window.solvox.auth.biometric('Unlock SolVox');
if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
else setError(r.error || 'Failed');
setLoading(false);
};
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (pin.length < 6) return setError('PIN must be at least 6 digits');
setLoading(true); setError('');
try {
if (window.solvox) {
const r = await window.solvox.auth.unlock(pin);
if (r.success) { const pk = await window.solvox.wallet.getPublicKey(); if (pk) onUnlock(pk); }
else setError(r.remainingAttempts !== undefined ? `${r.error} (${r.remainingAttempts} left)` : r.error || 'Failed');
} else onUnlock('Dev' + Date.now());
} catch (e: any) { setError(e.message); }
setLoading(false); setPin('');
};
const dots = Array.from({ length: 8 }, (_, i) => i < pin.length);
return (
<div className="h-screen flex flex-col items-center justify-center bg-canvas">
<div className="w-full max-w-xs">
{/* Logo */}
<div className="text-center mb-12">
<div className="w-12 h-12 mx-auto rounded-full bg-primary flex items-center justify-center mb-4">
<span className="text-on-primary font-bold text-title-md">SV</span>
</div>
<h1 className="display-text text-display-sm text-ink mb-1">SolVox</h1>
<p className="text-body-sm text-muted">Voice-First AI Wallet</p>
</div>
{/* PIN Dots */}
<div className="flex justify-center gap-3 mb-6">
{dots.map((f, i) => (
<div key={i} className={`w-3 h-3 rounded-full transition-all duration-200 ${f ? 'bg-primary' : 'bg-surface-strong'}`} />
))}
</div>
<form onSubmit={submit}>
<input ref={ref} type="password" inputMode="numeric" value={pin} onChange={e => setPin(e.target.value.replace(/\D/g, ''))}
maxLength={8} className="sr-only" disabled={loading} />
<div onClick={() => ref.current?.focus()} className="bg-surface-soft rounded-xl p-4 text-center cursor-text mb-4 border border-hairline hover:border-primary/30 transition-colors">
<div className="text-title-lg font-mono text-ink h-8 flex items-center justify-center tracking-[0.5em]">
{pin ? '●'.repeat(pin.length) : <span className="text-body-sm text-muted tracking-normal">Tap to enter PIN</span>}
</div>
</div>
{error && <div className="text-body-sm text-semantic-down text-center mb-4 bg-semantic-down/5 rounded-md p-2">{error}</div>}
<button type="submit" disabled={loading || pin.length < 6} className="btn-primary w-full disabled:opacity-40">{loading ? 'Unlocking…' : 'Unlock'}</button>
</form>
{bio && <button onClick={doBio} className="btn-secondary w-full mt-3">Use Touch ID</button>}
<div className="mt-8 text-center text-caption text-muted-soft flex items-center justify-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-semantic-up" />
Encrypted locally · No cloud · QVAC on-device
</div>
</div>
</div>
);
}