mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
feat(app): Add Create Seed tab and enhance Seed Blender workflow
This major update introduces a new "Create" tab for generating fresh BIP39 mnemonic seeds and significantly improves the entire application workflow, particularly the interaction with the Seed Blender. **✨ New Features & Enhancements** * **Create Seed Tab**: * Add a new "Create" tab as the default view for generating 12 or 24-word BIP39 seeds. * Implement a destination selector, allowing users to send the newly generated seed directly to the "Backup" tab for encryption or to the "Seed Blender" for advanced operations. * The UI automatically switches to the chosen destination tab after generation for a seamless workflow. * **Seed Blender Integration**: * Generated seeds sent to the Seed Blender are now automatically added to the list of inputs. * The Seed Blender's state is now preserved when switching between tabs, preventing data loss and allowing users to accumulate seeds from the Create tab. * **Global Reset Functionality**: * A "Reset All" button has been added to the main header for a global application reset. * This action clears all component states (including the Seed Blender's internal state), passwords, generated data, and the in-memory session key, returning the app to a fresh initial state. * **UI/UX Polish**: * The "Use This Seed for Backup" button in the Seed Blender has been restyled to match the application's cyberpunk aesthetic and its text clarified. * The "Create" tab UI is cleared automatically after a seed is generated and the user is navigated away, ensuring a clean slate for the next use. **🔒 Security Fixes** * **Auto-Clear Passwords**: Password and passphrase fields in both the "Backup" and "Restore" tabs are now automatically cleared from the UI and state after a successful encryption or decryption operation. This prevents sensitive data from lingering in the application. * **Robust Seed Generation**: The seed generation process now uses the secure `crypto.getRandomValues` Web API to generate entropy before converting it to a mnemonic. **🐛 Bug Fixes** * **Seed Blender State**: * Fixed a critical bug where the Seed Blender's internal state was lost when switching tabs. The component is now kept mounted but hidden via CSS. * Resolved an issue where a seed sent from the "Create" tab could be added multiple times to the blender. A `useRef` guard now prevents duplicates. * Corrected a race condition where transferring a blended seed to the "Backup" tab would clear the blender's state before the data could be used. The auto-clear has been removed in favor of the manual "Reset All" button.
This commit is contained in:
459
src/App.tsx
459
src/App.tsx
@@ -42,7 +42,7 @@ interface ClipboardEvent {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState<'backup' | 'restore' | 'seedblender'>('backup');
|
||||
const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create');
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const [backupMessagePassword, setBackupMessagePassword] = useState('');
|
||||
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
|
||||
@@ -76,7 +76,12 @@ function App() {
|
||||
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
|
||||
|
||||
const [seedQrFormat, setSeedQrFormat] = useState<'standard' | 'compact'>('standard');
|
||||
const [generatedSeed, setGeneratedSeed] = useState<string>('');
|
||||
const [seedWordCount, setSeedWordCount] = useState<12 | 24>(24);
|
||||
const [seedDestination, setSeedDestination] = useState<'backup' | 'seedblender'>('backup');
|
||||
const [detectedMode, setDetectedMode] = useState<EncryptionMode | null>(null);
|
||||
const [seedForBlender, setSeedForBlender] = useState<string>('');
|
||||
const [blenderResetKey, setBlenderResetKey] = useState(0);
|
||||
|
||||
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
|
||||
|
||||
@@ -246,6 +251,45 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const generateNewSeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Generate random entropy
|
||||
const entropyLength = seedWordCount === 12 ? 16 : 32; // 128 bits for 12 words, 256 for 24
|
||||
const entropy = new Uint8Array(entropyLength);
|
||||
crypto.getRandomValues(entropy);
|
||||
|
||||
// Convert to mnemonic using your existing lib
|
||||
const { entropyToMnemonic } = await import('./lib/seedblend');
|
||||
const newMnemonic = await entropyToMnemonic(entropy);
|
||||
|
||||
setGeneratedSeed(newMnemonic);
|
||||
|
||||
// Set mnemonic for backup if that's the destination
|
||||
if (seedDestination === 'backup') {
|
||||
setMnemonic(newMnemonic);
|
||||
} else if (seedDestination === 'seedblender') {
|
||||
setSeedForBlender(newMnemonic);
|
||||
}
|
||||
|
||||
// Auto-switch to chosen destination after generation
|
||||
setTimeout(() => {
|
||||
setActiveTab(seedDestination);
|
||||
// Reset Create tab state after switching
|
||||
setTimeout(() => {
|
||||
setGeneratedSeed('');
|
||||
}, 300);
|
||||
}, 1500);
|
||||
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Seed generation failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackup = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
@@ -298,6 +342,10 @@ function App() {
|
||||
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
|
||||
setEncryptedMnemonicCache(blob);
|
||||
setMnemonic(''); // Clear plaintext mnemonic
|
||||
|
||||
// Clear password after successful encryption (security best practice)
|
||||
setBackupMessagePassword('');
|
||||
setPrivateKeyPassphrase(''); // Also clear PGP passphrase if used
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Encryption failed');
|
||||
} finally {
|
||||
@@ -402,6 +450,12 @@ function App() {
|
||||
|
||||
// Temporarily display the mnemonic and then clear it
|
||||
setDecryptedRestoredMnemonic(result.w);
|
||||
|
||||
// Clear passwords after successful decryption (security best practice)
|
||||
setRestoreMessagePassword('');
|
||||
setPrivateKeyPassphrase('');
|
||||
// Also clear restore input after successful decrypt
|
||||
setRestoreInput('');
|
||||
setTimeout(() => {
|
||||
setDecryptedRestoredMnemonic(null);
|
||||
}, 10000); // Auto-clear after 10 seconds
|
||||
@@ -446,15 +500,37 @@ function App() {
|
||||
setShowLockConfirm(false);
|
||||
};
|
||||
|
||||
const handleRequestTabChange = (newTab: 'backup' | 'restore' | 'seedblender') => {
|
||||
if (activeTab === 'seedblender' && isBlenderDirty) {
|
||||
if (window.confirm("You have unsaved data in the Seed Blender. Are you sure you want to leave? All progress will be lost.")) {
|
||||
setActiveTab(newTab);
|
||||
setIsBlenderDirty(false); // Reset dirty state on leaving
|
||||
}
|
||||
// else: user cancelled, do nothing
|
||||
} else {
|
||||
setActiveTab(newTab);
|
||||
const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => {
|
||||
// Allow free navigation - no warnings
|
||||
// User can manually reset Seed Blender with "Reset All" button
|
||||
setActiveTab(newTab);
|
||||
};
|
||||
|
||||
const handleResetAll = () => {
|
||||
if (window.confirm("Reset entire app? This will clear all seeds, passwords, and generated data.")) {
|
||||
// Clear all state
|
||||
setMnemonic('');
|
||||
setGeneratedSeed('');
|
||||
setBackupMessagePassword('');
|
||||
setRestoreMessagePassword('');
|
||||
setPublicKeyInput('');
|
||||
setPrivateKeyInput('');
|
||||
setPrivateKeyPassphrase('');
|
||||
setQrPayload('');
|
||||
setRecipientFpr('');
|
||||
setRestoreInput('');
|
||||
setDecryptedRestoredMnemonic(null);
|
||||
setError('');
|
||||
setSeedForBlender('');
|
||||
setIsBlenderDirty(false);
|
||||
// Clear session
|
||||
destroySessionKey();
|
||||
setEncryptedMnemonicCache(null);
|
||||
// Go to Create tab (fresh start)
|
||||
// Force SeedBlender to remount (resets its internal state)
|
||||
setBlenderResetKey(prev => prev + 1);
|
||||
|
||||
setActiveTab('create');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -509,6 +585,7 @@ function App() {
|
||||
appVersion={__APP_VERSION__}
|
||||
isLocked={isReadOnly}
|
||||
onToggleLock={handleToggleLock}
|
||||
onResetAll={handleResetAll}
|
||||
/>
|
||||
<main className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 shadow-[0_0_30px_rgba(0,240,255,0.3)] p-8">
|
||||
@@ -540,136 +617,274 @@ function App() {
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-3 md:items-start">
|
||||
<div className="md:col-span-2 space-y-6">
|
||||
{activeTab === 'backup' ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
|
||||
<textarea
|
||||
className={`w-full h-32 p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden ${isReadOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
|
||||
textShadow: '0 0 5px rgba(0,240,255,0.5)'
|
||||
}}
|
||||
|
||||
data-sensitive="BIP39 Mnemonic"
|
||||
placeholder="Enter your 12 or 24 word seed phrase..."
|
||||
value={mnemonic}
|
||||
onChange={(e) => setMnemonic(e.target.value)}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PgpKeyInput
|
||||
label="PGP Public Key (Optional)"
|
||||
icon={FileKey}
|
||||
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK----- Paste or drag & drop your public key..."
|
||||
value={publicKeyInput}
|
||||
onChange={setPublicKeyInput}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</>
|
||||
) : activeTab === 'restore' ? (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{/* File Upload Zone */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging ? 'border-[#ff006e] bg-[#16213e] shadow-[0_0_30px_rgba(255,0,110,0.5)]' : 'border-[#00f0ff]/50 bg-[#16213e]'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<FileKey size={48} className="mx-auto text-[#00f0ff]" />
|
||||
<p className="text-sm text-[#00f0ff] font-medium" style={{ textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>
|
||||
Drag & drop QR image or text file
|
||||
</p>
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Supports: PNG/JPG (QR), TXT/ASC (PGP armor, hex, numeric)
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center pt-2">
|
||||
<label className="px-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-medium text-[#00f0ff] hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] cursor-pointer transition-all">
|
||||
📁 Browse Files
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.txt,.asc"
|
||||
className="hidden"
|
||||
onChange={(e) => e.target.files?.length && handleFileUpload(e.target.files[0])}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
className="px-4 py-2 bg-transparent text-[#00f0ff] rounded-lg font-medium border-2 border-[#00f0ff] hover:bg-[#00f0ff]/10 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)] transition-all flex items-center gap-2"
|
||||
>
|
||||
📷 Scan QR
|
||||
</button> </div>
|
||||
</div>
|
||||
<div className={activeTab === 'create' ? 'block' : 'hidden'}>
|
||||
<div className="space-y-6">
|
||||
<div className="text-center space-y-4">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[#16213e] border border-[#00f0ff]/50 rounded-lg">
|
||||
<span className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
Generate New Seed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Existing restore input textarea stays here */}
|
||||
<textarea
|
||||
className={`w-full p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden`}
|
||||
rows={6}
|
||||
placeholder="Or paste encrypted data here..."
|
||||
value={restoreInput}
|
||||
onChange={(e) => setRestoreInput(e.target.value)}
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
|
||||
textShadow: '0 0 5px rgba(0,240,255,0.5)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Auto-detection hint */}
|
||||
{detectedMode && (
|
||||
<p className="text-xs text-[#00f0ff] flex items-center gap-1">
|
||||
<Info size={14} />
|
||||
Detected: {detectedMode.toUpperCase()} format
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
Create a fresh BIP39 mnemonic for a new wallet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PgpKeyInput
|
||||
label="PGP Private Key (Optional)"
|
||||
icon={FileKey}
|
||||
data-sensitive="PGP Private Key"
|
||||
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK----- Paste or drag & drop your private key..."
|
||||
value={privateKeyInput}
|
||||
onChange={setPrivateKeyInput}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
{/* Word count selector */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
Seed Length
|
||||
</label>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<button
|
||||
onClick={() => setSeedWordCount(12)}
|
||||
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 12
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
|
||||
}`}
|
||||
style={seedWordCount === 12 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
>
|
||||
12 Words
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSeedWordCount(24)}
|
||||
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 24
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
|
||||
}`}
|
||||
style={seedWordCount === 24 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
>
|
||||
24 Words
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{privateKeyInput && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Private Key Passphrase</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
|
||||
{/* Destination selector */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
Send Generated Seed To
|
||||
</label>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'backup'
|
||||
? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
|
||||
}`}>
|
||||
<input
|
||||
type="password"
|
||||
data-sensitive="Message Password"
|
||||
className={`w-full pl-10 pr-4 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
|
||||
placeholder="Unlock private key..."
|
||||
value={privateKeyPassphrase}
|
||||
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
|
||||
readOnly={isReadOnly}
|
||||
type="radio"
|
||||
name="destination"
|
||||
value="backup"
|
||||
checked={seedDestination === 'backup'}
|
||||
onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')}
|
||||
className="hidden"
|
||||
/>
|
||||
<div className="text-center space-y-1">
|
||||
<div className={`text-lg font-bold ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
|
||||
style={seedDestination === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
|
||||
📦 Backup
|
||||
</div>
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Encrypt immediately
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'seedblender'
|
||||
? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
|
||||
}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="destination"
|
||||
value="seedblender"
|
||||
checked={seedDestination === 'seedblender'}
|
||||
onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')}
|
||||
className="hidden"
|
||||
/>
|
||||
<div className="text-center space-y-1">
|
||||
<div className={`text-lg font-bold ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
|
||||
style={seedDestination === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
|
||||
🎲 Seed Blender
|
||||
</div>
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Use for XOR blending
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate button */}
|
||||
<button
|
||||
onClick={generateNewSeed}
|
||||
disabled={loading || isReadOnly}
|
||||
className="w-full py-4 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold uppercase tracking-wider flex items-center justify-center gap-2 hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-[#ff006e]"
|
||||
style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
Generating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={20} />
|
||||
Generate {seedWordCount}-Word Seed
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Display generated seed */}
|
||||
{generatedSeed && (
|
||||
<div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] space-y-4 animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-bold text-[#39ff14] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}>
|
||||
<CheckCircle2 size={20} /> Generated Successfully
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4 bg-[#16213e] rounded-lg border border-[#39ff14]/50">
|
||||
<p className="font-mono text-sm text-[#39ff14] break-words leading-relaxed" style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}>
|
||||
{generatedSeed}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-[#6ef3f7] text-center">
|
||||
✨ Switching to {seedDestination === 'backup' ? 'Backup' : 'Seed Blender'} tab...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
</div>
|
||||
</div>
|
||||
<div className={activeTab === 'backup' ? 'block' : 'hidden'}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
|
||||
<textarea
|
||||
className={`w-full h-32 p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden ${isReadOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
|
||||
textShadow: '0 0 5px rgba(0,240,255,0.5)'
|
||||
}}
|
||||
|
||||
data-sensitive="BIP39 Mnemonic"
|
||||
placeholder="Enter your 12 or 24 word seed phrase..."
|
||||
value={mnemonic}
|
||||
onChange={(e) => setMnemonic(e.target.value)}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PgpKeyInput
|
||||
label="PGP Public Key (Optional)"
|
||||
icon={FileKey}
|
||||
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK----- Paste or drag & drop your public key..."
|
||||
value={publicKeyInput}
|
||||
onChange={setPublicKeyInput}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
<div className={activeTab === 'restore' ? 'block' : 'hidden'}>
|
||||
<div className="space-y-4">
|
||||
{/* File Upload Zone */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging ? 'border-[#ff006e] bg-[#16213e] shadow-[0_0_30px_rgba(255,0,110,0.5)]' : 'border-[#00f0ff]/50 bg-[#16213e]'
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<FileKey size={48} className="mx-auto text-[#00f0ff]" />
|
||||
<p className="text-sm text-[#00f0ff] font-medium" style={{ textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>
|
||||
Drag & drop QR image or text file
|
||||
</p>
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Supports: PNG/JPG (QR), TXT/ASC (PGP armor, hex, numeric)
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center pt-2">
|
||||
<label className="px-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-medium text-[#00f0ff] hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] cursor-pointer transition-all">
|
||||
📁 Browse Files
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.txt,.asc"
|
||||
className="hidden"
|
||||
onChange={(e) => e.target.files?.length && handleFileUpload(e.target.files[0])}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setShowQRScanner(true)}
|
||||
className="px-4 py-2 bg-transparent text-[#00f0ff] rounded-lg font-medium border-2 border-[#00f0ff] hover:bg-[#00f0ff]/10 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)] transition-all flex items-center gap-2"
|
||||
>
|
||||
📷 Scan QR
|
||||
</button> </div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Existing restore input textarea stays here */}
|
||||
<textarea
|
||||
className={`w-full p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden`}
|
||||
rows={6}
|
||||
placeholder="Or paste encrypted data here..."
|
||||
value={restoreInput}
|
||||
onChange={(e) => setRestoreInput(e.target.value)}
|
||||
style={{
|
||||
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
|
||||
textShadow: '0 0 5px rgba(0,240,255,0.5)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Auto-detection hint */}
|
||||
{detectedMode && (
|
||||
<p className="text-xs text-[#00f0ff] flex items-center gap-1">
|
||||
<Info size={14} />
|
||||
Detected: {detectedMode.toUpperCase()} format
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PgpKeyInput
|
||||
label="PGP Private Key (Optional)"
|
||||
icon={FileKey}
|
||||
data-sensitive="PGP Private Key"
|
||||
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK----- Paste or drag & drop your private key..."
|
||||
value={privateKeyInput}
|
||||
onChange={setPrivateKeyInput}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
|
||||
{privateKeyInput && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Private Key Passphrase</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
|
||||
<input
|
||||
type="password"
|
||||
data-sensitive="Message Password"
|
||||
className={`w-full pl-10 pr-4 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
|
||||
placeholder="Unlock private key..."
|
||||
value={privateKeyPassphrase}
|
||||
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={activeTab === 'seedblender' ? 'block' : 'hidden'}>
|
||||
<SeedBlender
|
||||
key={blenderResetKey}
|
||||
onDirtyStateChange={setIsBlenderDirty}
|
||||
setMnemonicForBackup={setMnemonic}
|
||||
requestTabChange={handleRequestTabChange}
|
||||
incomingSeed={seedForBlender}
|
||||
onSeedReceived={() => setSeedForBlender('')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Panel */}
|
||||
{activeTab !== 'seedblender' && (
|
||||
{activeTab !== 'seedblender' && activeTab !== 'create' && (
|
||||
<div className="space-y-2"> {/* Added space-y-2 wrapper */}
|
||||
<label className="text-sm font-semibold text-[#00f0ff] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
<Lock size={14} /> SECURITY OPTIONS
|
||||
|
||||
Reference in New Issue
Block a user