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