feat(v1.2.0): add QR scanner with camera/upload support

- Add QRScanner component with camera and image upload
- Add QR code download button with auto-naming (SeedPGP_DATE_TIME.png)
- Split state for backup/restore (separate public/private keys and passwords)
- Improve QR generation settings (margin: 4, errorCorrection: M)
- Fix Safari camera permissions and Continuity Camera support
- Add React timing fix for Html5Qrcode initialization

Features:
- Camera scanning with live preview
- Image file upload scanning
- Automatic SEEDPGP1 validation
- User-friendly error messages
- 512x512px high-quality QR generation
This commit is contained in:
LC mac
2026-01-28 23:54:02 +08:00
parent 3285bc383e
commit c55390228b
5 changed files with 576 additions and 284 deletions

View File

@@ -14,17 +14,22 @@ import {
} from 'lucide-react';
import { PgpKeyInput } from './components/PgpKeyInput';
import { QrDisplay } from './components/QrDisplay';
import QRScanner from './components/QRScanner';
import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp';
import type { SeedPgpPlaintext } from './lib/types';
import * as openpgp from 'openpgp';
console.log("OpenPGP.js version:", openpgp.config.versionString);
function App() {
const [activeTab, setActiveTab] = useState<'backup' | 'restore'>('backup');
const [mnemonic, setMnemonic] = useState('');
const [messagePassword, setMessagePassword] = useState('');
const [pgpKeyInput, setPgpKeyInput] = useState('');
const [backupMessagePassword, setBackupMessagePassword] = useState('');
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
const [publicKeyInput, setPublicKeyInput] = useState('');
const [privateKeyInput, setPrivateKeyInput] = useState('');
const [privateKeyPassphrase, setPrivateKeyPassphrase] = useState('');
const [hasBip39Passphrase, setHasBip39Passphrase] = useState(false);
const [qrPayload, setQrPayload] = useState('');
@@ -35,6 +40,7 @@ function App() {
const [loading, setLoading] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false);
const [copied, setCopied] = useState(false);
const [showQRScanner, setShowQRScanner] = useState(false);
const copyToClipboard = async (text: string) => {
try {
@@ -42,7 +48,6 @@ function App() {
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
} catch {
// Fallback for environments where Clipboard API is blocked
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
@@ -57,7 +62,6 @@ function App() {
}
};
const handleBackup = async () => {
setLoading(true);
setError('');
@@ -74,8 +78,8 @@ function App() {
const result = await encryptToSeedPgp({
plaintext,
publicKeyArmored: pgpKeyInput || undefined,
messagePassword: messagePassword || undefined,
publicKeyArmored: publicKeyInput || undefined,
messagePassword: backupMessagePassword || undefined, // Changed
});
setQrPayload(result.framed);
@@ -97,11 +101,12 @@ function App() {
try {
const result = await decryptSeedPgp({
frameText: restoreInput,
privateKeyArmored: pgpKeyInput || undefined,
privateKeyArmored: privateKeyInput || undefined,
privateKeyPassphrase: privateKeyPassphrase || undefined,
messagePassword: messagePassword || undefined,
messagePassword: restoreMessagePassword || undefined, // Changed
});
setRestoredData(result);
} catch (e) {
setError(e instanceof Error ? e.message : 'Decryption failed');
@@ -110,296 +115,322 @@ function App() {
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 text-slate-900 p-4 md:p-8">
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-200">
<>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 text-slate-900 p-4 md:p-8">
<div className="max-w-5xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-200">
{/* Header */}
<div className="bg-gradient-to-r from-slate-900 to-slate-800 p-6 text-white flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-600 rounded-lg shadow-lg">
<Shield size={28} />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">
SeedPGP <span className="text-blue-400 font-mono text-base ml-2">v1.1</span>
</h1>
<p className="text-xs text-slate-400 mt-0.5">OpenPGP-secured BIP39 backup</p>
</div>
</div>
<div className="flex bg-slate-800/50 rounded-lg p-1 backdrop-blur">
<button
onClick={() => {
setActiveTab('backup');
setError('');
setQrPayload('');
setRestoredData(null);
}}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'backup'
? 'bg-white text-slate-900 shadow-lg'
: 'text-slate-300 hover:text-white hover:bg-slate-700/50'
}`}
>
Backup
</button>
<button
onClick={() => {
setActiveTab('restore');
setError('');
setQrPayload('');
setRestoredData(null);
}}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'restore'
? 'bg-white text-slate-900 shadow-lg'
: 'text-slate-300 hover:text-white hover:bg-slate-700/50'
}`}
>
Restore
</button>
</div>
</div>
<div className="p-6 md:p-8 space-y-6">
{/* Error Display */}
{error && (
<div className="p-4 bg-red-50 border-l-4 border-red-500 rounded-r-xl flex gap-3 text-red-800 text-sm items-start animate-in slide-in-from-top-2">
<AlertCircle className="shrink-0 mt-0.5" size={20} />
{/* Header */}
<div className="bg-gradient-to-r from-slate-900 to-slate-800 p-6 text-white flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-600 rounded-lg shadow-lg">
<Shield size={28} />
</div>
<div>
<p className="font-bold mb-1">Error</p>
<p className="whitespace-pre-wrap">{error}</p>
<h1 className="text-2xl font-bold tracking-tight">
SeedPGP <span className="text-blue-400 font-mono text-base ml-2">v1.2</span>
</h1>
<p className="text-xs text-slate-400 mt-0.5">OpenPGP-secured BIP39 backup</p>
</div>
</div>
)}
{/* Info Banner */}
{recipientFpr && activeTab === 'backup' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3 text-blue-800 text-xs animate-in fade-in">
<Info size={16} className="shrink-0 mt-0.5" />
<div>
<strong>Recipient Key:</strong> <code className="bg-blue-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code>
</div>
<div className="flex bg-slate-800/50 rounded-lg p-1 backdrop-blur">
<button
onClick={() => {
setActiveTab('backup');
setError('');
setQrPayload('');
setRestoredData(null);
}}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'backup'
? 'bg-white text-slate-900 shadow-lg'
: 'text-slate-300 hover:text-white hover:bg-slate-700/50'
}`}
>
Backup
</button>
<button
onClick={() => {
setActiveTab('restore');
setError('');
setQrPayload('');
setRestoredData(null);
}}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'restore'
? 'bg-white text-slate-900 shadow-lg'
: 'text-slate-300 hover:text-white hover:bg-slate-700/50'
}`}
>
Restore
</button>
</div>
)}
</div>
{/* Main Content Grid */}
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-2 space-y-6">
{activeTab === 'backup' ? (
<>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">BIP39 Mnemonic</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
placeholder="Enter your 12 or 24 word seed phrase..."
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
/>
</div>
<div className="p-6 md:p-8 space-y-6">
{/* Error Display */}
{error && (
<div className="p-4 bg-red-50 border-l-4 border-red-500 rounded-r-xl flex gap-3 text-red-800 text-sm items-start animate-in slide-in-from-top-2">
<AlertCircle className="shrink-0 mt-0.5" size={20} />
<div>
<p className="font-bold mb-1">Error</p>
<p className="whitespace-pre-wrap">{error}</p>
</div>
</div>
)}
<PgpKeyInput
label="PGP Public Key (Optional)"
icon={FileKey}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#10;&#10;Paste or drag & drop your public key..."
value={pgpKeyInput}
onChange={setPgpKeyInput}
/>
</>
) : (
<>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">SEEDPGP1 Payload</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
placeholder="SEEDPGP1:0:ABCD:..."
value={restoreInput}
onChange={(e) => setRestoreInput(e.target.value)}
/>
</div>
{/* Info Banner */}
{recipientFpr && activeTab === 'backup' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3 text-blue-800 text-xs animate-in fade-in">
<Info size={16} className="shrink-0 mt-0.5" />
<div>
<strong>Recipient Key:</strong> <code className="bg-blue-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code>
</div>
</div>
)}
<PgpKeyInput
label="PGP Private Key (Optional)"
icon={FileKey}
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----&#10;&#10;Paste or drag & drop your private key..."
value={pgpKeyInput}
onChange={setPgpKeyInput}
/>
{pgpKeyInput && (
{/* Main Content Grid */}
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-2 space-y-6">
{activeTab === 'backup' ? (
<>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Private Key Passphrase</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
placeholder="Unlock private key..."
value={privateKeyPassphrase}
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
/>
<label className="text-sm font-semibold text-slate-700">BIP39 Mnemonic</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
placeholder="Enter your 12 or 24 word seed phrase..."
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
/>
</div>
<PgpKeyInput
label="PGP Public Key (Optional)"
icon={FileKey}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#10;&#10;Paste or drag & drop your public key..."
value={publicKeyInput}
onChange={setPublicKeyInput}
/>
</>
) : (
<>
<div className="flex gap-2">
<button
onClick={() => setShowQRScanner(true)}
className="flex-1 py-3 bg-gradient-to-r from-purple-600 to-purple-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 hover:from-purple-700 hover:to-purple-800 transition-all shadow-lg"
>
<QrCode size={18} />
Scan QR Code
</button>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">SEEDPGP1 Payload</label>
<textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none"
placeholder="SEEDPGP1:0:ABCD:..."
value={restoreInput}
onChange={(e) => setRestoreInput(e.target.value)}
/>
</div>
<PgpKeyInput
label="PGP Private Key (Optional)"
icon={FileKey}
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----&#10;&#10;Paste or drag & drop your private key..."
value={privateKeyInput}
onChange={setPrivateKeyInput}
/>
{privateKeyInput && (
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Private Key Passphrase</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
placeholder="Unlock private key..."
value={privateKeyPassphrase}
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
/>
</div>
</div>
)}
</>
)}
</div>
{/* Security Panel */}
<div className="space-y-6">
<div className="p-5 bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border-2 border-slate-200 shadow-inner space-y-4">
<h3 className="text-sm font-bold text-slate-800 uppercase tracking-wider flex items-center gap-2">
<Lock size={14} /> Security Options
</h3>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Message Password</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
placeholder="Optional password..."
value={activeTab === 'backup' ? backupMessagePassword : restoreMessagePassword}
onChange={(e) => activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
/>
</div>
<p className="text-[10px] text-slate-500 mt-1">Symmetric encryption password (SKESK)</p>
</div>
{activeTab === 'backup' && (
<div className="pt-3 border-t border-slate-300">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={hasBip39Passphrase}
onChange={(e) => setHasBip39Passphrase(e.target.checked)}
className="rounded text-blue-600 focus:ring-2 focus:ring-blue-500 transition-all"
/>
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
BIP39 25th word active
</span>
</label>
</div>
)}
</>
)}
</div>
{/* Security Panel */}
<div className="space-y-6">
<div className="p-5 bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border-2 border-slate-200 shadow-inner space-y-4">
<h3 className="text-sm font-bold text-slate-800 uppercase tracking-wider flex items-center gap-2">
<Lock size={14} /> Security Options
</h3>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Message Password</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input
type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all"
placeholder="Optional password..."
value={messagePassword}
onChange={(e) => setMessagePassword(e.target.value)}
/>
</div>
<p className="text-[10px] text-slate-500 mt-1">Symmetric encryption password (SKESK)</p>
</div>
{activeTab === 'backup' && (
<div className="pt-3 border-t border-slate-300">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={hasBip39Passphrase}
onChange={(e) => setHasBip39Passphrase(e.target.checked)}
className="rounded text-blue-600 focus:ring-2 focus:ring-blue-500 transition-all"
/>
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
BIP39 25th word active
</span>
</label>
</div>
{/* Action Button */}
{activeTab === 'backup' ? (
<button
onClick={handleBackup}
disabled={!mnemonic || loading}
className="w-full py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-blue-600 disabled:hover:to-blue-700"
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<QrCode size={20} />
)}
{loading ? 'Generating...' : 'Generate QR Backup'}
</button>
) : (
<button
onClick={handleRestore}
disabled={!restoreInput || loading}
className="w-full py-4 bg-gradient-to-r from-slate-800 to-slate-900 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-slate-900 hover:to-black transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<Unlock size={20} />
)}
{loading ? 'Decrypting...' : 'Decrypt & Restore'}
</button>
)}
</div>
{/* Action Button */}
{activeTab === 'backup' ? (
<button
onClick={handleBackup}
disabled={!mnemonic || loading}
className="w-full py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-blue-600 disabled:hover:to-blue-700"
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<QrCode size={20} />
)}
{loading ? 'Generating...' : 'Generate QR Backup'}
</button>
) : (
<button
onClick={handleRestore}
disabled={!restoreInput || loading}
className="w-full py-4 bg-gradient-to-r from-slate-800 to-slate-900 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-slate-900 hover:to-black transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<Unlock size={20} />
)}
{loading ? 'Decrypting...' : 'Decrypt & Restore'}
</button>
)}
</div>
</div>
{/* QR Output */}
{qrPayload && activeTab === 'backup' && (
<div className="pt-6 border-t border-slate-200 space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex justify-center">
<QrDisplay value={qrPayload} />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Raw payload (copy for backup)
</label>
<button
type="button"
onClick={() => copyToClipboard(qrPayload)}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-black transition-colors"
>
{copied ? <CheckCircle2 size={14} /> : <QrCode size={14} />}
{copied ? "Copied" : "Copy"}
</button>
{/* QR Output */}
{qrPayload && activeTab === 'backup' && (
<div className="pt-6 border-t border-slate-200 space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex justify-center">
<QrDisplay value={qrPayload} />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Raw payload (copy for backup)
</label>
<textarea
readOnly
value={qrPayload}
onFocus={(e) => e.currentTarget.select()}
className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-[11px] text-slate-500">
Tip: click the box to select all, or use Copy.
</p>
</div>
<button
type="button"
onClick={() => copyToClipboard(qrPayload)}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-900 text-white text-xs font-semibold hover:bg-black transition-colors"
>
{copied ? <CheckCircle2 size={14} /> : <QrCode size={14} />}
{copied ? "Copied" : "Copy"}
</button>
</div>
</div>
)}
{/* Restored Mnemonic */}
{restoredData && activeTab === 'restore' && (
<div className="pt-6 border-t border-slate-200 animate-in zoom-in-95">
<div className="p-6 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-2xl shadow-lg">
<div className="flex items-center justify-between mb-4">
<span className="font-bold text-green-700 flex items-center gap-2 text-lg">
<CheckCircle2 size={22} /> Mnemonic Recovered
</span>
<button
onClick={() => setShowMnemonic(!showMnemonic)}
className="p-2.5 hover:bg-green-100 rounded-xl transition-all text-green-700 hover:shadow"
>
{showMnemonic ? <EyeOff size={22} /> : <Eye size={22} />}
</button>
</div>
<div className={`p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm transition-all duration-300 ${showMnemonic ? 'blur-0' : 'blur-lg select-none'
}`}>
<p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words">
{restoredData.w}
<textarea
readOnly
value={qrPayload}
onFocus={(e) => e.currentTarget.select()}
className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-[11px] text-slate-500">
Tip: click the box to select all, or use Copy.
</p>
</div>
{restoredData.pp === 1 && (
<div className="mt-4 p-3 bg-orange-100 border border-orange-300 rounded-lg">
<p className="text-xs text-center text-orange-800 font-bold uppercase tracking-widest flex items-center justify-center gap-2">
<AlertCircle size={14} /> BIP39 Passphrase Required (25th Word)
</p>
</div>
)}
{restoredData.fpr && restoredData.fpr.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-xs text-blue-800">
<strong>Encrypted for keys:</strong> {restoredData.fpr.join(', ')}
</p>
</div>
)}
</div>
</div>
)}
)}
{/* Restored Mnemonic */}
{restoredData && activeTab === 'restore' && (
<div className="pt-6 border-t border-slate-200 animate-in zoom-in-95">
<div className="p-6 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-2xl shadow-lg">
<div className="flex items-center justify-between mb-4">
<span className="font-bold text-green-700 flex items-center gap-2 text-lg">
<CheckCircle2 size={22} /> Mnemonic Recovered
</span>
<button
onClick={() => setShowMnemonic(!showMnemonic)}
className="p-2.5 hover:bg-green-100 rounded-xl transition-all text-green-700 hover:shadow"
>
{showMnemonic ? <EyeOff size={22} /> : <Eye size={22} />}
</button>
</div>
<div className={`p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm transition-all duration-300 ${showMnemonic ? 'blur-0' : 'blur-lg select-none'
}`}>
<p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words">
{restoredData.w}
</p>
</div>
{restoredData.pp === 1 && (
<div className="mt-4 p-3 bg-orange-100 border border-orange-300 rounded-lg">
<p className="text-xs text-center text-orange-800 font-bold uppercase tracking-widest flex items-center justify-center gap-2">
<AlertCircle size={14} /> BIP39 Passphrase Required (25th Word)
</p>
</div>
)}
{restoredData.fpr && restoredData.fpr.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-xs text-blue-800">
<strong>Encrypted for keys:</strong> {restoredData.fpr.join(', ')}
</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center text-xs text-slate-500">
<p>SeedPGP v1.2 OpenPGP (RFC 4880) + Base45 (RFC 9285) + CRC16/CCITT-FALSE</p>
<p className="mt-1">Never share your private keys or seed phrases. Always verify on an airgapped device.</p>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center text-xs text-slate-500">
<p>SeedPGP v1.1 OpenPGP (RFC 4880) + Base45 (RFC 9285) + CRC16/CCITT-FALSE</p>
<p className="mt-1">Never share your private keys or seed phrases. Always verify on an airgapped device.</p>
</div>
</div>
{/* QR Scanner Modal */}
{showQRScanner && (
<QRScanner
onScanSuccess={(scannedText) => {
setRestoreInput(scannedText);
setShowQRScanner(false);
setError('');
}}
onClose={() => setShowQRScanner(false)}
/>
)}
</>
);
}
export default App;