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:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "seedpgp-web",
|
"name": "seedpgp-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.4",
|
"version": "1.4.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
459
src/App.tsx
459
src/App.tsx
@@ -42,7 +42,7 @@ interface ClipboardEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState<'backup' | 'restore' | 'seedblender'>('backup');
|
const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create');
|
||||||
const [mnemonic, setMnemonic] = useState('');
|
const [mnemonic, setMnemonic] = useState('');
|
||||||
const [backupMessagePassword, setBackupMessagePassword] = useState('');
|
const [backupMessagePassword, setBackupMessagePassword] = useState('');
|
||||||
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
|
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
|
||||||
@@ -76,7 +76,12 @@ function App() {
|
|||||||
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
|
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
|
||||||
|
|
||||||
const [seedQrFormat, setSeedQrFormat] = useState<'standard' | 'compact'>('standard');
|
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 [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'];
|
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 () => {
|
const handleBackup = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -298,6 +342,10 @@ function App() {
|
|||||||
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
|
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
|
||||||
setEncryptedMnemonicCache(blob);
|
setEncryptedMnemonicCache(blob);
|
||||||
setMnemonic(''); // Clear plaintext mnemonic
|
setMnemonic(''); // Clear plaintext mnemonic
|
||||||
|
|
||||||
|
// Clear password after successful encryption (security best practice)
|
||||||
|
setBackupMessagePassword('');
|
||||||
|
setPrivateKeyPassphrase(''); // Also clear PGP passphrase if used
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Encryption failed');
|
setError(e instanceof Error ? e.message : 'Encryption failed');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -402,6 +450,12 @@ function App() {
|
|||||||
|
|
||||||
// Temporarily display the mnemonic and then clear it
|
// Temporarily display the mnemonic and then clear it
|
||||||
setDecryptedRestoredMnemonic(result.w);
|
setDecryptedRestoredMnemonic(result.w);
|
||||||
|
|
||||||
|
// Clear passwords after successful decryption (security best practice)
|
||||||
|
setRestoreMessagePassword('');
|
||||||
|
setPrivateKeyPassphrase('');
|
||||||
|
// Also clear restore input after successful decrypt
|
||||||
|
setRestoreInput('');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setDecryptedRestoredMnemonic(null);
|
setDecryptedRestoredMnemonic(null);
|
||||||
}, 10000); // Auto-clear after 10 seconds
|
}, 10000); // Auto-clear after 10 seconds
|
||||||
@@ -446,15 +500,37 @@ function App() {
|
|||||||
setShowLockConfirm(false);
|
setShowLockConfirm(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRequestTabChange = (newTab: 'backup' | 'restore' | 'seedblender') => {
|
const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => {
|
||||||
if (activeTab === 'seedblender' && isBlenderDirty) {
|
// Allow free navigation - no warnings
|
||||||
if (window.confirm("You have unsaved data in the Seed Blender. Are you sure you want to leave? All progress will be lost.")) {
|
// User can manually reset Seed Blender with "Reset All" button
|
||||||
setActiveTab(newTab);
|
setActiveTab(newTab);
|
||||||
setIsBlenderDirty(false); // Reset dirty state on leaving
|
};
|
||||||
}
|
|
||||||
// else: user cancelled, do nothing
|
const handleResetAll = () => {
|
||||||
} else {
|
if (window.confirm("Reset entire app? This will clear all seeds, passwords, and generated data.")) {
|
||||||
setActiveTab(newTab);
|
// 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__}
|
appVersion={__APP_VERSION__}
|
||||||
isLocked={isReadOnly}
|
isLocked={isReadOnly}
|
||||||
onToggleLock={handleToggleLock}
|
onToggleLock={handleToggleLock}
|
||||||
|
onResetAll={handleResetAll}
|
||||||
/>
|
/>
|
||||||
<main className="max-w-7xl mx-auto px-6 py-4">
|
<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">
|
<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 */}
|
{/* Main Content Grid */}
|
||||||
<div className="grid gap-6 md:grid-cols-3 md:items-start">
|
<div className="grid gap-6 md:grid-cols-3 md:items-start">
|
||||||
<div className="md:col-span-2 space-y-6">
|
<div className="md:col-span-2 space-y-6">
|
||||||
{activeTab === 'backup' ? (
|
<div className={activeTab === 'create' ? 'block' : 'hidden'}>
|
||||||
<>
|
<div className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="text-center space-y-4">
|
||||||
<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>
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[#16213e] border border-[#00f0ff]/50 rounded-lg">
|
||||||
<textarea
|
<span className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||||
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' : ''
|
Generate New Seed
|
||||||
}`}
|
</span>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Existing restore input textarea stays here */}
|
<p className="text-sm text-[#6ef3f7]">
|
||||||
<textarea
|
Create a fresh BIP39 mnemonic for a new wallet
|
||||||
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`}
|
</p>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<PgpKeyInput
|
{/* Word count selector */}
|
||||||
label="PGP Private Key (Optional)"
|
<div className="space-y-3">
|
||||||
icon={FileKey}
|
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||||
data-sensitive="PGP Private Key"
|
Seed Length
|
||||||
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK----- Paste or drag & drop your private key..."
|
</label>
|
||||||
value={privateKeyInput}
|
<div className="flex gap-4 justify-center">
|
||||||
onChange={setPrivateKeyInput}
|
<button
|
||||||
readOnly={isReadOnly}
|
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 && (
|
{/* Destination selector */}
|
||||||
<div className="space-y-2">
|
<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)' }}>Private Key Passphrase</label>
|
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||||
<div className="relative">
|
Send Generated Seed To
|
||||||
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
|
</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
|
<input
|
||||||
type="password"
|
type="radio"
|
||||||
data-sensitive="Message Password"
|
name="destination"
|
||||||
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' : ''
|
value="backup"
|
||||||
}`}
|
checked={seedDestination === 'backup'}
|
||||||
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
|
onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')}
|
||||||
placeholder="Unlock private key..."
|
className="hidden"
|
||||||
value={privateKeyPassphrase}
|
|
||||||
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
<SeedBlender
|
||||||
|
key={blenderResetKey}
|
||||||
onDirtyStateChange={setIsBlenderDirty}
|
onDirtyStateChange={setIsBlenderDirty}
|
||||||
setMnemonicForBackup={setMnemonic}
|
setMnemonicForBackup={setMnemonic}
|
||||||
requestTabChange={handleRequestTabChange}
|
requestTabChange={handleRequestTabChange}
|
||||||
|
incomingSeed={seedForBlender}
|
||||||
|
onSeedReceived={() => setSeedForBlender('')}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Security Panel */}
|
{/* Security Panel */}
|
||||||
{activeTab !== 'seedblender' && (
|
{activeTab !== 'seedblender' && activeTab !== 'create' && (
|
||||||
<div className="space-y-2"> {/* Added space-y-2 wrapper */}
|
<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)' }}>
|
<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
|
<Lock size={14} /> SECURITY OPTIONS
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Shield, Lock } from 'lucide-react';
|
import { Shield, Lock, RefreshCw } from 'lucide-react';
|
||||||
import SecurityBadge from './badges/SecurityBadge';
|
import SecurityBadge from './badges/SecurityBadge';
|
||||||
import StorageBadge from './badges/StorageBadge';
|
import StorageBadge from './badges/StorageBadge';
|
||||||
import ClipboardBadge from './badges/ClipboardBadge';
|
import ClipboardBadge from './badges/ClipboardBadge';
|
||||||
import EditLockBadge from './badges/EditLockBadge';
|
import EditLockBadge from './badges/EditLockBadge';
|
||||||
|
|
||||||
interface StorageItem {
|
interface StorageItem {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
size: number;
|
size: number;
|
||||||
isSensitive: boolean;
|
isSensitive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClipboardEvent {
|
interface ClipboardEvent {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
field: string;
|
field: string;
|
||||||
length: number;
|
length: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
@@ -25,29 +25,31 @@ interface HeaderProps {
|
|||||||
sessionItems: StorageItem[];
|
sessionItems: StorageItem[];
|
||||||
events: ClipboardEvent[];
|
events: ClipboardEvent[];
|
||||||
onOpenClipboardModal: () => void;
|
onOpenClipboardModal: () => void;
|
||||||
activeTab: 'backup' | 'restore' | 'seedblender';
|
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
|
||||||
onRequestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
|
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
||||||
encryptedMnemonicCache: any;
|
encryptedMnemonicCache: any;
|
||||||
handleLockAndClear: () => void;
|
handleLockAndClear: () => void;
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
onToggleLock: () => void;
|
onToggleLock: () => void;
|
||||||
|
onResetAll: () => void; // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({
|
const Header: React.FC<HeaderProps> = ({
|
||||||
onOpenSecurityModal,
|
onOpenSecurityModal,
|
||||||
onOpenStorageModal,
|
onOpenStorageModal,
|
||||||
localItems,
|
localItems,
|
||||||
sessionItems,
|
sessionItems,
|
||||||
events,
|
events,
|
||||||
onOpenClipboardModal,
|
onOpenClipboardModal,
|
||||||
activeTab,
|
activeTab,
|
||||||
onRequestTabChange,
|
onRequestTabChange,
|
||||||
encryptedMnemonicCache,
|
encryptedMnemonicCache,
|
||||||
handleLockAndClear,
|
handleLockAndClear,
|
||||||
appVersion,
|
appVersion,
|
||||||
isLocked,
|
isLocked,
|
||||||
onToggleLock
|
onToggleLock,
|
||||||
|
onResetAll
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff]/30 backdrop-blur-sm">
|
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff]/30 backdrop-blur-sm">
|
||||||
@@ -81,34 +83,52 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
{/* Right: Action Buttons */}
|
{/* Right: Action Buttons */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{encryptedMnemonicCache && (
|
{encryptedMnemonicCache && (
|
||||||
<button
|
<button
|
||||||
onClick={handleLockAndClear}
|
onClick={handleLockAndClear}
|
||||||
className="flex items-center gap-2 text-sm text-[#ff006e] bg-[#16213e] px-3 py-1.5 rounded-lg hover:bg-[#ff006e]/20 border-2 border-[#ff006e]/50 transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
className="flex items-center gap-2 text-sm text-[#ff006e] bg-[#16213e] px-3 py-1.5 rounded-lg hover:bg-[#ff006e]/20 border-2 border-[#ff006e]/50 transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
||||||
>
|
>
|
||||||
<Lock size={16} />
|
<Lock size={16} />
|
||||||
<span>Lock/Clear</span>
|
<span>Lock/Clear</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
{/* Reset All button - left side */}
|
||||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'backup' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
<button
|
||||||
style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
onClick={onResetAll}
|
||||||
onClick={() => onRequestTabChange('backup')}
|
className="px-4 py-2 bg-[#16213e] border-2 border-[#ff006e] text-[#ff006e] rounded-lg font-medium hover:bg-[#ff006e] hover:text-white transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
Backup
|
<RefreshCw size={16} />
|
||||||
</button> <button
|
Reset All
|
||||||
|
</button>
|
||||||
|
{encryptedMnemonicCache && (
|
||||||
|
<div className="h-8 w-px bg-[#00f0ff]/30 mx-2"></div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'create' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||||
|
style={activeTab === 'create' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||||
|
onClick={() => onRequestTabChange('create')}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'backup' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||||
|
style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||||
|
onClick={() => onRequestTabChange('backup')}
|
||||||
|
>
|
||||||
|
Backup
|
||||||
|
</button> <button
|
||||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'restore' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'restore' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||||
style={activeTab === 'restore' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
style={activeTab === 'restore' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||||
onClick={() => onRequestTabChange('restore')}
|
onClick={() => onRequestTabChange('restore')}
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'seedblender' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'seedblender' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||||
style={activeTab === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
style={activeTab === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||||
onClick={() => onRequestTabChange('seedblender')}
|
onClick={() => onRequestTabChange('seedblender')}
|
||||||
>
|
>
|
||||||
Seed Blender
|
Seed Blender
|
||||||
</button> </div>
|
</button> </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile: Stack monitoring badges */}
|
{/* Mobile: Stack monitoring badges */}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @description This component provides a full UI for the multi-step seed blending process,
|
* @description This component provides a full UI for the multi-step seed blending process,
|
||||||
* handling various input formats, per-row decryption, and final output actions.
|
* handling various input formats, per-row decryption, and final output actions.
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock, Key, ArrowRight } from 'lucide-react';
|
import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock, Key, ArrowRight } from 'lucide-react';
|
||||||
import QRScanner from './QRScanner';
|
import QRScanner from './QRScanner';
|
||||||
import { decryptFromSeed, detectEncryptionMode } from '../lib/seedpgp';
|
import { decryptFromSeed, detectEncryptionMode } from '../lib/seedpgp';
|
||||||
@@ -53,298 +53,346 @@ const createNewEntry = (): MnemonicEntry => ({
|
|||||||
interface SeedBlenderProps {
|
interface SeedBlenderProps {
|
||||||
onDirtyStateChange: (isDirty: boolean) => void;
|
onDirtyStateChange: (isDirty: boolean) => void;
|
||||||
setMnemonicForBackup: (mnemonic: string) => void;
|
setMnemonicForBackup: (mnemonic: string) => void;
|
||||||
requestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
|
requestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
||||||
|
incomingSeed?: string; // NEW: seed from Create tab
|
||||||
|
onSeedReceived?: () => void; // NEW: callback after seed added
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestTabChange }: SeedBlenderProps) {
|
export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestTabChange, incomingSeed, onSeedReceived }: SeedBlenderProps) {
|
||||||
const [entries, setEntries] = useState<MnemonicEntry[]>([createNewEntry()]);
|
const processedSeedsRef = useRef<Set<string>>(new Set());
|
||||||
const [showQRScanner, setShowQRScanner] = useState(false);
|
const [entries, setEntries] = useState<MnemonicEntry[]>([createNewEntry()]);
|
||||||
const [scanTargetIndex, setScanTargetIndex] = useState<number | null>(null);
|
const [showQRScanner, setShowQRScanner] = useState(false);
|
||||||
const [blendedResult, setBlendedResult] = useState<{ blendedEntropy: Uint8Array; blendedMnemonic12: string; blendedMnemonic24?: string; } | null>(null);
|
const [scanTargetIndex, setScanTargetIndex] = useState<number | null>(null);
|
||||||
const [xorStrength, setXorStrength] = useState<{ isWeak: boolean; uniqueBytes: number; } | null>(null);
|
const [blendedResult, setBlendedResult] = useState<{ blendedEntropy: Uint8Array; blendedMnemonic12: string; blendedMnemonic24?: string; } | null>(null);
|
||||||
const [blendError, setBlendError] = useState<string>('');
|
const [xorStrength, setXorStrength] = useState<{ isWeak: boolean; uniqueBytes: number; } | null>(null);
|
||||||
const [blending, setBlending] = useState(false);
|
const [blendError, setBlendError] = useState<string>('');
|
||||||
const [diceRolls, setDiceRolls] = useState('');
|
const [blending, setBlending] = useState(false);
|
||||||
const [diceStats, setDiceStats] = useState<DiceStats | null>(null);
|
const [diceRolls, setDiceRolls] = useState('');
|
||||||
const [dicePatternWarning, setDicePatternWarning] = useState<string | null>(null);
|
const [diceStats, setDiceStats] = useState<DiceStats | null>(null);
|
||||||
const [diceOnlyMnemonic, setDiceOnlyMnemonic] = useState<string | null>(null);
|
const [dicePatternWarning, setDicePatternWarning] = useState<string | null>(null);
|
||||||
const [finalMnemonic, setFinalMnemonic] = useState<string | null>(null);
|
const [diceOnlyMnemonic, setDiceOnlyMnemonic] = useState<string | null>(null);
|
||||||
const [mixing, setMixing] = useState(false);
|
const [finalMnemonic, setFinalMnemonic] = useState<string | null>(null);
|
||||||
const [showFinalQR, setShowFinalQR] = useState(false);
|
const [mixing, setMixing] = useState(false);
|
||||||
|
const [showFinalQR, setShowFinalQR] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isDirty = entries.some(e => e.rawInput.length > 0) || diceRolls.length > 0;
|
const isDirty = entries.some(e => e.rawInput.length > 0) || diceRolls.length > 0;
|
||||||
onDirtyStateChange(isDirty);
|
onDirtyStateChange(isDirty);
|
||||||
}, [entries, diceRolls, onDirtyStateChange]);
|
}, [entries, diceRolls, onDirtyStateChange]);
|
||||||
|
|
||||||
const handleLockAndClear = () => {
|
const addSeedEntry = (seed: string) => {
|
||||||
setEntries([createNewEntry()]);
|
setEntries(currentEntries => {
|
||||||
setBlendedResult(null);
|
const emptyEntryIndex = currentEntries.findIndex(e => !e.rawInput.trim());
|
||||||
setXorStrength(null);
|
if (emptyEntryIndex !== -1) {
|
||||||
setBlendError('');
|
return currentEntries.map((entry, index) =>
|
||||||
setDiceRolls('');
|
index === emptyEntryIndex
|
||||||
setDiceStats(null);
|
? { ...entry, rawInput: seed, decryptedMnemonic: seed, isValid: null, error: null }
|
||||||
setDicePatternWarning(null);
|
: entry
|
||||||
setDiceOnlyMnemonic(null);
|
);
|
||||||
setFinalMnemonic(null);
|
} else {
|
||||||
setShowFinalQR(false);
|
const newEntry = createNewEntry();
|
||||||
};
|
newEntry.rawInput = seed;
|
||||||
|
newEntry.decryptedMnemonic = seed;
|
||||||
useEffect(() => {
|
return [...currentEntries, newEntry];
|
||||||
const processEntries = async () => {
|
|
||||||
setBlending(true);
|
|
||||||
setBlendError('');
|
|
||||||
const validMnemonics = entries.map(e => e.decryptedMnemonic).filter((m): m is string => m !== null && m.length > 0);
|
|
||||||
|
|
||||||
const validityPromises = entries.map(async (entry) => {
|
|
||||||
if (!entry.rawInput.trim()) return { isValid: null, error: null };
|
|
||||||
if (entry.isEncrypted && !entry.decryptedMnemonic) return { isValid: null, error: null };
|
|
||||||
|
|
||||||
const textToValidate = entry.decryptedMnemonic || entry.rawInput;
|
|
||||||
try {
|
|
||||||
await mnemonicToEntropy(textToValidate.trim());
|
|
||||||
return { isValid: true, error: null };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { isValid: false, error: e.message || "Invalid mnemonic" };
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const newValidationResults = await Promise.all(validityPromises);
|
|
||||||
setEntries(currentEntries => currentEntries.map((e, i) => ({
|
|
||||||
...e,
|
|
||||||
isValid: newValidationResults[i]?.isValid ?? e.isValid,
|
|
||||||
error: newValidationResults[i]?.error ?? e.error,
|
|
||||||
})));
|
|
||||||
|
|
||||||
if (validMnemonics.length > 0) {
|
|
||||||
try {
|
|
||||||
const result = await blendMnemonicsAsync(validMnemonics);
|
|
||||||
setBlendedResult(result);
|
|
||||||
setXorStrength(checkXorStrength(result.blendedEntropy));
|
|
||||||
} catch (e: any) { setBlendError(e.message); setBlendedResult(null); }
|
|
||||||
} else {
|
|
||||||
setBlendedResult(null);
|
|
||||||
}
|
|
||||||
setBlending(false);
|
|
||||||
};
|
};
|
||||||
debounce(processEntries, 300)();
|
|
||||||
}, [JSON.stringify(entries.map(e => e.decryptedMnemonic))]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const processDice = async () => {
|
if (incomingSeed && incomingSeed.trim()) {
|
||||||
setDiceStats(calculateDiceStats(diceRolls));
|
// Check if we've already processed this exact seed
|
||||||
setDicePatternWarning(detectBadPatterns(diceRolls).message || null);
|
if (!processedSeedsRef.current.has(incomingSeed)) {
|
||||||
if (diceRolls.length >= 50) {
|
const isDuplicate = entries.some(e => e.decryptedMnemonic === incomingSeed);
|
||||||
try {
|
if (!isDuplicate) {
|
||||||
const outputByteLength = (blendedResult && blendedResult.blendedEntropy.length >= 32) ? 32 : 16;
|
addSeedEntry(incomingSeed);
|
||||||
const diceOnlyEntropy = await hkdfExtractExpand(diceToBytes(diceRolls), outputByteLength, new TextEncoder().encode('dice-only'));
|
processedSeedsRef.current.add(incomingSeed);
|
||||||
setDiceOnlyMnemonic(await entropyToMnemonic(diceOnlyEntropy));
|
}
|
||||||
} catch { setDiceOnlyMnemonic(null); }
|
}
|
||||||
} else { setDiceOnlyMnemonic(null); }
|
// Always notify parent to clear the incoming seed
|
||||||
|
onSeedReceived?.();
|
||||||
|
}
|
||||||
|
}, [incomingSeed]);
|
||||||
|
|
||||||
|
const handleLockAndClear = () => {
|
||||||
|
setEntries([createNewEntry()]);
|
||||||
|
setBlendedResult(null);
|
||||||
|
setXorStrength(null);
|
||||||
|
setBlendError('');
|
||||||
|
setDiceRolls('');
|
||||||
|
setDiceStats(null);
|
||||||
|
setDicePatternWarning(null);
|
||||||
|
setDiceOnlyMnemonic(null);
|
||||||
|
setFinalMnemonic(null);
|
||||||
|
setShowFinalQR(false);
|
||||||
};
|
};
|
||||||
debounce(processDice, 200)();
|
|
||||||
}, [diceRolls, blendedResult]);
|
|
||||||
|
|
||||||
const updateEntry = (index: number, newProps: Partial<MnemonicEntry>) => {
|
useEffect(() => {
|
||||||
setEntries(currentEntries => currentEntries.map((entry, i) => i === index ? { ...entry, ...newProps } : entry));
|
const processEntries = async () => {
|
||||||
};
|
setBlending(true);
|
||||||
const handleAddEntry = () => setEntries([...entries, createNewEntry()]);
|
setBlendError('');
|
||||||
const handleRemoveEntry = (id: number) => {
|
const validMnemonics = entries.map(e => e.decryptedMnemonic).filter((m): m is string => m !== null && m.length > 0);
|
||||||
if (entries.length > 1) setEntries(entries.filter(e => e.id !== id));
|
|
||||||
else setEntries([createNewEntry()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScan = (index: number) => {
|
const validityPromises = entries.map(async (entry) => {
|
||||||
setScanTargetIndex(index);
|
if (!entry.rawInput.trim()) return { isValid: null, error: null };
|
||||||
setShowQRScanner(true);
|
if (entry.isEncrypted && !entry.decryptedMnemonic) return { isValid: null, error: null };
|
||||||
};
|
|
||||||
|
|
||||||
const handleScanSuccess = useCallback(async (scannedData: string | Uint8Array) => {
|
const textToValidate = entry.decryptedMnemonic || entry.rawInput;
|
||||||
if (scanTargetIndex === null) return;
|
try {
|
||||||
|
await mnemonicToEntropy(textToValidate.trim());
|
||||||
const scannedText = typeof scannedData === 'string'
|
return { isValid: true, error: null };
|
||||||
? scannedData
|
} catch (e: any) {
|
||||||
: Array.from(scannedData).map(b => b.toString(16).padStart(2, '0')).join('');
|
return { isValid: false, error: e.message || "Invalid mnemonic" };
|
||||||
|
}
|
||||||
const mode = detectEncryptionMode(scannedText);
|
|
||||||
let mnemonic = scannedText;
|
|
||||||
let error: string | null = null;
|
|
||||||
let inputType: 'text' | 'seedpgp' | 'krux' | 'seedqr' = 'text';
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (mode === 'seedqr') {
|
|
||||||
mnemonic = await decodeSeedQR(scannedText);
|
|
||||||
inputType = 'seedqr';
|
|
||||||
updateEntry(scanTargetIndex, {
|
|
||||||
rawInput: mnemonic,
|
|
||||||
decryptedMnemonic: mnemonic,
|
|
||||||
isEncrypted: false,
|
|
||||||
passwordRequired: false,
|
|
||||||
inputType,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
} else if (mode === 'pgp' || mode === 'krux') {
|
|
||||||
inputType = (mode === 'pgp' ? 'seedpgp' : mode);
|
|
||||||
updateEntry(scanTargetIndex, {
|
|
||||||
rawInput: scannedText,
|
|
||||||
decryptedMnemonic: null,
|
|
||||||
isEncrypted: true,
|
|
||||||
passwordRequired: true,
|
|
||||||
inputType,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
} else { // text or un-recognized
|
|
||||||
updateEntry(scanTargetIndex, {
|
|
||||||
rawInput: scannedText,
|
|
||||||
decryptedMnemonic: scannedText,
|
|
||||||
isEncrypted: false,
|
|
||||||
passwordRequired: false,
|
|
||||||
inputType: 'text',
|
|
||||||
error: null,
|
|
||||||
});
|
});
|
||||||
|
const newValidationResults = await Promise.all(validityPromises);
|
||||||
|
setEntries(currentEntries => currentEntries.map((e, i) => ({
|
||||||
|
...e,
|
||||||
|
isValid: newValidationResults[i]?.isValid ?? e.isValid,
|
||||||
|
error: newValidationResults[i]?.error ?? e.error,
|
||||||
|
})));
|
||||||
|
|
||||||
|
if (validMnemonics.length > 0) {
|
||||||
|
try {
|
||||||
|
const result = await blendMnemonicsAsync(validMnemonics);
|
||||||
|
setBlendedResult(result);
|
||||||
|
setXorStrength(checkXorStrength(result.blendedEntropy));
|
||||||
|
} catch (e: any) { setBlendError(e.message); setBlendedResult(null); }
|
||||||
|
} else {
|
||||||
|
setBlendedResult(null);
|
||||||
|
}
|
||||||
|
setBlending(false);
|
||||||
|
};
|
||||||
|
debounce(processEntries, 300)();
|
||||||
|
}, [JSON.stringify(entries.map(e => e.decryptedMnemonic))]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const processDice = async () => {
|
||||||
|
setDiceStats(calculateDiceStats(diceRolls));
|
||||||
|
setDicePatternWarning(detectBadPatterns(diceRolls).message || null);
|
||||||
|
if (diceRolls.length >= 50) {
|
||||||
|
try {
|
||||||
|
const outputByteLength = (blendedResult && blendedResult.blendedEntropy.length >= 32) ? 32 : 16;
|
||||||
|
const diceOnlyEntropy = await hkdfExtractExpand(diceToBytes(diceRolls), outputByteLength, new TextEncoder().encode('dice-only'));
|
||||||
|
setDiceOnlyMnemonic(await entropyToMnemonic(diceOnlyEntropy));
|
||||||
|
} catch { setDiceOnlyMnemonic(null); }
|
||||||
|
} else { setDiceOnlyMnemonic(null); }
|
||||||
|
};
|
||||||
|
debounce(processDice, 200)();
|
||||||
|
}, [diceRolls, blendedResult]);
|
||||||
|
|
||||||
|
const updateEntry = (index: number, newProps: Partial<MnemonicEntry>) => {
|
||||||
|
setEntries(currentEntries => currentEntries.map((entry, i) => i === index ? { ...entry, ...newProps } : entry));
|
||||||
|
};
|
||||||
|
const handleAddEntry = () => setEntries([...entries, createNewEntry()]);
|
||||||
|
const handleRemoveEntry = (id: number) => {
|
||||||
|
if (entries.length > 1) setEntries(entries.filter(e => e.id !== id));
|
||||||
|
else setEntries([createNewEntry()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScan = (index: number) => {
|
||||||
|
setScanTargetIndex(index);
|
||||||
|
setShowQRScanner(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScanSuccess = useCallback(async (scannedData: string | Uint8Array) => {
|
||||||
|
if (scanTargetIndex === null) return;
|
||||||
|
|
||||||
|
const scannedText = typeof scannedData === 'string'
|
||||||
|
? scannedData
|
||||||
|
: Array.from(scannedData).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
const mode = detectEncryptionMode(scannedText);
|
||||||
|
let mnemonic = scannedText;
|
||||||
|
let error: string | null = null;
|
||||||
|
let inputType: 'text' | 'seedpgp' | 'krux' | 'seedqr' = 'text';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mode === 'seedqr') {
|
||||||
|
mnemonic = await decodeSeedQR(scannedText);
|
||||||
|
inputType = 'seedqr';
|
||||||
|
updateEntry(scanTargetIndex, {
|
||||||
|
rawInput: mnemonic,
|
||||||
|
decryptedMnemonic: mnemonic,
|
||||||
|
isEncrypted: false,
|
||||||
|
passwordRequired: false,
|
||||||
|
inputType,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} else if (mode === 'pgp' || mode === 'krux') {
|
||||||
|
inputType = (mode === 'pgp' ? 'seedpgp' : mode);
|
||||||
|
updateEntry(scanTargetIndex, {
|
||||||
|
rawInput: scannedText,
|
||||||
|
decryptedMnemonic: null,
|
||||||
|
isEncrypted: true,
|
||||||
|
passwordRequired: true,
|
||||||
|
inputType,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} else { // text or un-recognized
|
||||||
|
updateEntry(scanTargetIndex, {
|
||||||
|
rawInput: scannedText,
|
||||||
|
decryptedMnemonic: scannedText,
|
||||||
|
isEncrypted: false,
|
||||||
|
passwordRequired: false,
|
||||||
|
inputType: 'text',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e.message || "Failed to process QR code";
|
||||||
|
updateEntry(scanTargetIndex, { rawInput: scannedText, error });
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
setShowQRScanner(false);
|
||||||
error = e.message || "Failed to process QR code";
|
}, [scanTargetIndex]);
|
||||||
updateEntry(scanTargetIndex, { rawInput: scannedText, error });
|
|
||||||
}
|
|
||||||
setShowQRScanner(false);
|
|
||||||
}, [scanTargetIndex]);
|
|
||||||
|
|
||||||
const handleScanClose = useCallback(() => {
|
const handleScanClose = useCallback(() => {
|
||||||
setShowQRScanner(false);
|
setShowQRScanner(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleScanError = useCallback((errMsg: string) => {
|
const handleScanError = useCallback((errMsg: string) => {
|
||||||
if (scanTargetIndex !== null) {
|
if (scanTargetIndex !== null) {
|
||||||
updateEntry(scanTargetIndex, { error: errMsg });
|
updateEntry(scanTargetIndex, { error: errMsg });
|
||||||
}
|
|
||||||
}, [scanTargetIndex]);
|
|
||||||
|
|
||||||
const handleDecrypt = async (index: number) => {
|
|
||||||
const entry = entries[index];
|
|
||||||
if (!entry.isEncrypted || !entry.passwordInput) return;
|
|
||||||
try {
|
|
||||||
let mnemonic: string;
|
|
||||||
if (entry.inputType === 'krux') {
|
|
||||||
mnemonic = (await decryptFromKrux({ kefData: entry.rawInput, passphrase: entry.passwordInput })).mnemonic;
|
|
||||||
} else { // seedpgp
|
|
||||||
mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w;
|
|
||||||
}
|
}
|
||||||
updateEntry(index, { rawInput: mnemonic, decryptedMnemonic: mnemonic, isEncrypted: false, passwordRequired: false, error: null });
|
}, [scanTargetIndex]);
|
||||||
} catch(e: any) {
|
|
||||||
updateEntry(index, { error: e.message || "Decryption failed" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinalMix = async () => {
|
const handleDecrypt = async (index: number) => {
|
||||||
if (!blendedResult) return;
|
const entry = entries[index];
|
||||||
setMixing(true);
|
if (!entry.isEncrypted || !entry.passwordInput) return;
|
||||||
try {
|
try {
|
||||||
const outputBits = blendedResult.blendedEntropy.length >= 32 ? 256 : 128;
|
let mnemonic: string;
|
||||||
const result = await mixWithDiceAsync(blendedResult.blendedEntropy, diceRolls, outputBits);
|
if (entry.inputType === 'krux') {
|
||||||
setFinalMnemonic(result.finalMnemonic);
|
mnemonic = (await decryptFromKrux({ kefData: entry.rawInput, passphrase: entry.passwordInput })).mnemonic;
|
||||||
} catch(e) { setFinalMnemonic(null); } finally { setMixing(false); }
|
} else { // seedpgp
|
||||||
};
|
mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w;
|
||||||
|
}
|
||||||
|
updateEntry(index, { rawInput: mnemonic, decryptedMnemonic: mnemonic, isEncrypted: false, passwordRequired: false, error: null });
|
||||||
|
} catch (e: any) {
|
||||||
|
updateEntry(index, { error: e.message || "Decryption failed" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleTransfer = () => {
|
const handleFinalMix = async () => {
|
||||||
if (!finalMnemonic) return;
|
if (!blendedResult) return;
|
||||||
setMnemonicForBackup(finalMnemonic);
|
setMixing(true);
|
||||||
handleLockAndClear();
|
try {
|
||||||
requestTabChange('backup');
|
const outputBits = blendedResult.blendedEntropy.length >= 32 ? 256 : 128;
|
||||||
};
|
const result = await mixWithDiceAsync(blendedResult.blendedEntropy, diceRolls, outputBits);
|
||||||
|
setFinalMnemonic(result.finalMnemonic);
|
||||||
|
} catch (e) { setFinalMnemonic(null); } finally { setMixing(false); }
|
||||||
|
};
|
||||||
|
|
||||||
const getBorderColor = (isValid: boolean | null) => {
|
const handleTransfer = () => {
|
||||||
if (isValid === true) return 'border-[#39ff14] focus:ring-[#39ff14]';
|
if (!finalMnemonic) return;
|
||||||
if (isValid === false) return 'border-[#ff006e] focus:ring-[#ff006e]';
|
// Set mnemonic for backup
|
||||||
return 'border-[#00f0ff]/50 focus:ring-[#00f0ff]';
|
setMnemonicForBackup(finalMnemonic);
|
||||||
};
|
// Switch to backup tab
|
||||||
|
requestTabChange('backup');
|
||||||
|
// DON'T auto-clear - user can use "Reset All" button if they want to start fresh
|
||||||
|
// This preserves the blended seed in case user wants to come back and export QR
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const getBorderColor = (isValid: boolean | null) => {
|
||||||
<>
|
if (isValid === true) return 'border-[#39ff14] focus:ring-[#39ff14]';
|
||||||
<div className="space-y-6 pb-20">
|
if (isValid === false) return 'border-[#ff006e] focus:ring-[#ff006e]';
|
||||||
<div className="flex items-center justify-between">
|
return 'border-[#00f0ff]/50 focus:ring-[#00f0ff]';
|
||||||
<h2 className="text-2xl font-bold text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Seed Blender</h2>
|
};
|
||||||
<button onClick={handleLockAndClear} className="flex items-center gap-2 text-sm text-[#ff006e] bg-[#16213e] px-3 py-1.5 rounded-lg hover:bg-[#ff006e]/20 border-2 border-[#ff006e]/50"><Lock size={16} /><span>Lock/Clear</span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
return (
|
||||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 1: Input Mnemonics</h3>
|
<>
|
||||||
<div className="space-y-4">
|
<div className="space-y-6 pb-20">
|
||||||
{entries.map((entry, index) => (
|
<div className="mb-6">
|
||||||
<div key={entry.id} className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/20">
|
<h2 className="text-lg font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||||
{entry.passwordRequired ? (
|
Seed Blender
|
||||||
<div className="space-y-2">
|
</h2>
|
||||||
<div className="flex items-center justify-between"><label className="text-sm font-semibold text-[#00f0ff]">Decrypt {entry.inputType.toUpperCase()} Mnemonic</label><button onClick={() => updateEntry(index, createNewEntry())} className="text-xs text-[#6ef3f7] hover:text-[#00f0ff]">× Cancel</button></div>
|
</div>
|
||||||
<p className="text-xs text-[#6ef3f7] truncate">Payload: <code className="text-[#9d84b7]">{entry.rawInput.substring(0, 40)}...</code></p>
|
|
||||||
<div className="flex gap-2"><input type="password" placeholder="Enter passphrase to decrypt..." value={entry.passwordInput} onChange={(e) => updateEntry(index, { passwordInput: e.target.value })} className="w-full p-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)]" /><button onClick={() => handleDecrypt(index)} className="px-4 bg-[#ff006e] text-white rounded-lg font-semibold hover:bg-[#ff4d8f] hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"><Key size={16}/></button></div>
|
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
||||||
{entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>}
|
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 1: Input Mnemonics</h3>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
) : (
|
{entries.map((entry, index) => (
|
||||||
<div className="space-y-1">
|
<div key={entry.id} className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/20">
|
||||||
<div className="flex flex-col sm:flex-row items-start gap-2">
|
{entry.passwordRequired ? (
|
||||||
<div className="relative w-full">
|
<div className="space-y-2">
|
||||||
<textarea value={entry.rawInput} onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })} placeholder={`Mnemonic #${index + 1} (12 or 24 words)`} className={`w-full h-28 sm:h-24 p-3 pr-10 bg-[#16213e] border-2 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] ${getBorderColor(entry.isValid)}`} />
|
<div className="flex items-center justify-between"><label className="text-sm font-semibold text-[#00f0ff]">Decrypt {entry.inputType.toUpperCase()} Mnemonic</label><button onClick={() => updateEntry(index, createNewEntry())} className="text-xs text-[#6ef3f7] hover:text-[#00f0ff]">× Cancel</button></div>
|
||||||
{entry.isValid === true && <CheckCircle2 className="absolute top-3 right-3 text-[#39ff14]" />}
|
<p className="text-xs text-[#6ef3f7] truncate">Payload: <code className="text-[#9d84b7]">{entry.rawInput.substring(0, 40)}...</code></p>
|
||||||
{entry.isValid === false && <AlertTriangle className="absolute top-3 right-3 text-[#ff006e]" />}
|
<div className="flex gap-2"><input type="password" placeholder="Enter passphrase to decrypt..." value={entry.passwordInput} onChange={(e) => updateEntry(index, { passwordInput: e.target.value })} className="w-full p-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)]" /><button onClick={() => handleDecrypt(index)} className="px-4 bg-[#ff006e] text-white rounded-lg font-semibold hover:bg-[#ff4d8f] hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"><Key size={16} /></button></div>
|
||||||
|
{entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex flex-col sm:flex-row items-start gap-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<textarea value={entry.rawInput} onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })} placeholder={`Mnemonic #${index + 1} (12 or 24 words)`} className={`w-full h-28 sm:h-24 p-3 pr-10 bg-[#16213e] border-2 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] ${getBorderColor(entry.isValid)}`} />
|
||||||
|
{entry.isValid === true && <CheckCircle2 className="absolute top-3 right-3 text-[#39ff14]" />}
|
||||||
|
{entry.isValid === false && <AlertTriangle className="absolute top-3 right-3 text-[#ff006e]" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button onClick={() => handleScan(index)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><QrCode size={20} /></button>
|
||||||
|
<button onClick={() => handleRemoveEntry(entry.id)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><X size={20} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
))}
|
||||||
<button onClick={() => handleScan(index)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><QrCode size={20} /></button>
|
<button onClick={handleAddEntry} className="w-full py-2.5 bg-[#1a1a2e] hover:bg-[#16213e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50"><Plus size={16} /> Add Another Mnemonic</button>
|
||||||
<button onClick={() => handleRemoveEntry(entry.id)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><X size={20} /></button>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 min-h-[10rem]">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 2: Blended Preview</h3>
|
||||||
|
{blending ? <p className="text-sm text-[#6ef3f7]">Blending...</p> : !blendError && blendedResult ? (<div className="space-y-4 animate-in fade-in">{xorStrength?.isWeak && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><div><span className="font-bold">Weak XOR Result:</span> Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.</div></div>)}<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (12-word)</label><p data-sensitive="Blended Mnemonic (12-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic12}</p></div>{blendedResult.blendedMnemonic24 && (<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (24-word)</label><p data-sensitive="Blended Mnemonic (24-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic24}</p></div>)}</div>) : (<p className="text-sm text-[#6ef3f7]">{blendError || 'Previews will appear here once you enter one or more valid mnemonics.'}</p>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 3: Input Dice Rolls</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="Enter 99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-lg font-mono text-[#00f0ff] placeholder-[#9d84b7]" />
|
||||||
|
{dicePatternWarning && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><p><span className="font-bold">Warning:</span> {dicePatternWarning}</p></div>)}
|
||||||
|
{diceStats && diceStats.length > 0 && (<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Rolls</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.length}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Entropy (bits)</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.estimatedEntropyBits.toFixed(1)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Mean</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.mean.toFixed(2)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Chi-Square</p><p className={`text-lg font-bold ${diceStats.chiSquare > 11.07 ? 'text-[#ff006e]' : 'text-[#00f0ff]'}`}>{diceStats.chiSquare.toFixed(2)}</p></div></div>)}
|
||||||
|
{diceOnlyMnemonic && (<div className="space-y-1 pt-2"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Dice-Only Preview Mnemonic</label><p data-sensitive="Dice-Only Preview Mnemonic" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{diceOnlyMnemonic}</p></div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/50 shadow-[0_0_20px_rgba(0,240,255,0.3)]">
|
||||||
|
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 4: Generate Final Mnemonic</h3>
|
||||||
|
{finalMnemonic ? (
|
||||||
|
<div className="p-4 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-2xl shadow-[0_0_20px_rgba(57,255,20,0.3)]">
|
||||||
|
<div className="flex items-center justify-between mb-4"><span className="font-bold text-[#39ff14] flex items-center gap-2 text-lg" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}><CheckCircle2 size={22} /> Final Mnemonic Generated</span><button onClick={() => setFinalMnemonic(null)} className="p-2.5 hover:bg-[#16213e] rounded-xl transition-all text-[#39ff14] hover:shadow-[0_0_15px_rgba(57,255,20,0.5)] flex items-center gap-2"><EyeOff size={22} /> Hide</button></div>
|
||||||
|
<div className="p-6 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]"><p data-sensitive="Final Blended Mnemonic" className="font-mono text-center text-lg break-words text-[#39ff14]">{finalMnemonic}</p></div>
|
||||||
|
<div className="mt-4 p-3 bg-[#ff006e]/10 text-[#ff006e] rounded-lg text-xs flex gap-2 border-2 border-[#ff006e]/30"><AlertTriangle size={16} className="shrink-0 mt-0.5" /><span><strong>Security Warning:</strong> Write this down immediately. Do not save it digitally.</span></div>
|
||||||
|
<div className="grid grid-cols-2 gap-3 mt-4">
|
||||||
|
<button onClick={() => setShowFinalQR(true)} className="w-full py-2.5 bg-[#1a1a2e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50 hover:bg-[#16213e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)]"><QrCode size={16} /> Export as QR</button>
|
||||||
|
<button
|
||||||
|
onClick={handleTransfer}
|
||||||
|
className="w-full py-2.5 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-lg 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)' }}
|
||||||
|
disabled={!finalMnemonic}
|
||||||
|
>
|
||||||
|
<ArrowRight size={20} />
|
||||||
|
Send to Backup Tab
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>}
|
) : (
|
||||||
|
<><p className="text-sm text-[#6ef3f7] mb-4">Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final mnemonic.</p><button onClick={handleFinalMix} disabled={!blendedResult || !diceRolls || diceRolls.length < 50 || mixing} className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#0066ff] text-[#16213e] rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)]">{mixing ? <RefreshCw className="animate-spin" size={20} /> : <Sparkles size={20} />}{mixing ? 'Generating...' : 'Mix Mnemonic + Dice'}</button></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showQRScanner && <QRScanner
|
||||||
|
onScanSuccess={handleScanSuccess}
|
||||||
|
onClose={handleScanClose}
|
||||||
|
onError={handleScanError}
|
||||||
|
/>}
|
||||||
|
{showFinalQR && finalMnemonic && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowFinalQR(false)}>
|
||||||
|
<div className="bg-[#16213e] rounded-2xl p-4 border-2 border-[#00f0ff]/50" onClick={e => e.stopPropagation()}>
|
||||||
|
<QrDisplay value={finalMnemonic} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<button onClick={handleAddEntry} className="w-full py-2.5 bg-[#1a1a2e] hover:bg-[#16213e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50"><Plus size={16} /> Add Another Mnemonic</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 min-h-[10rem]">
|
|
||||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 2: Blended Preview</h3>
|
|
||||||
{blending ? <p className="text-sm text-[#6ef3f7]">Blending...</p> : !blendError && blendedResult ? (<div className="space-y-4 animate-in fade-in">{xorStrength?.isWeak && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><div><span className="font-bold">Weak XOR Result:</span> Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.</div></div>)}<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (12-word)</label><p data-sensitive="Blended Mnemonic (12-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic12}</p></div>{blendedResult.blendedMnemonic24 && (<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (24-word)</label><p data-sensitive="Blended Mnemonic (24-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic24}</p></div>)}</div>) : (<p className="text-sm text-[#6ef3f7]">{blendError || 'Previews will appear here once you enter one or more valid mnemonics.'}</p>)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
|
||||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 3: Input Dice Rolls</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="Enter 99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-lg font-mono text-[#00f0ff] placeholder-[#9d84b7]" />
|
|
||||||
{dicePatternWarning && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><p><span className="font-bold">Warning:</span> {dicePatternWarning}</p></div>)}
|
|
||||||
{diceStats && diceStats.length > 0 && (<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Rolls</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.length}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Entropy (bits)</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.estimatedEntropyBits.toFixed(1)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Mean</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.mean.toFixed(2)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Chi-Square</p><p className={`text-lg font-bold ${diceStats.chiSquare > 11.07 ? 'text-[#ff006e]' : 'text-[#00f0ff]'}`}>{diceStats.chiSquare.toFixed(2)}</p></div></div>)}
|
|
||||||
{diceOnlyMnemonic && (<div className="space-y-1 pt-2"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Dice-Only Preview Mnemonic</label><p data-sensitive="Dice-Only Preview Mnemonic" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{diceOnlyMnemonic}</p></div>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/50 shadow-[0_0_20px_rgba(0,240,255,0.3)]">
|
|
||||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 4: Generate Final Mnemonic</h3>
|
|
||||||
{finalMnemonic ? (
|
|
||||||
<div className="p-4 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-2xl shadow-[0_0_20px_rgba(57,255,20,0.3)]">
|
|
||||||
<div className="flex items-center justify-between mb-4"><span className="font-bold text-[#39ff14] flex items-center gap-2 text-lg" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}><CheckCircle2 size={22} /> Final Mnemonic Generated</span><button onClick={() => setFinalMnemonic(null)} className="p-2.5 hover:bg-[#16213e] rounded-xl transition-all text-[#39ff14] hover:shadow-[0_0_15px_rgba(57,255,20,0.5)] flex items-center gap-2"><EyeOff size={22} /> Hide</button></div>
|
|
||||||
<div className="p-6 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]"><p data-sensitive="Final Blended Mnemonic" className="font-mono text-center text-lg break-words text-[#39ff14]">{finalMnemonic}</p></div>
|
|
||||||
<div className="mt-4 p-3 bg-[#ff006e]/10 text-[#ff006e] rounded-lg text-xs flex gap-2 border-2 border-[#ff006e]/30"><AlertTriangle size={16} className="shrink-0 mt-0.5" /><span><strong>Security Warning:</strong> Write this down immediately. Do not save it digitally.</span></div>
|
|
||||||
<div className="grid grid-cols-2 gap-3 mt-4">
|
|
||||||
<button onClick={() => setShowFinalQR(true)} className="w-full py-2.5 bg-[#1a1a2e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50 hover:bg-[#16213e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)]"><QrCode size={16}/> Export as QR</button>
|
|
||||||
<button onClick={handleTransfer} className="w-full py-2.5 bg-[#00f0ff] text-[#16213e] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff] hover:bg-[#00f0ff]/80 hover:shadow-[0_0_15px_rgba(0,240,255,0.5)]"><ArrowRight size={16}/> Transfer to Backup</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<><p className="text-sm text-[#6ef3f7] mb-4">Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final mnemonic.</p><button onClick={handleFinalMix} disabled={!blendedResult || !diceRolls || diceRolls.length < 50 || mixing} className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#0066ff] text-[#16213e] rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)]">{mixing ? <RefreshCw className="animate-spin" size={20} /> : <Sparkles size={20} />}{mixing ? 'Generating...' : 'Mix Mnemonic + Dice'}</button></>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
{showQRScanner && <QRScanner
|
|
||||||
onScanSuccess={handleScanSuccess}
|
|
||||||
onClose={handleScanClose}
|
|
||||||
onError={handleScanError}
|
|
||||||
/>}
|
|
||||||
{showFinalQR && finalMnemonic && (
|
|
||||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowFinalQR(false)}>
|
|
||||||
<div className="bg-[#16213e] rounded-2xl p-4 border-2 border-[#00f0ff]/50" onClick={e => e.stopPropagation()}>
|
|
||||||
<QrDisplay value={finalMnemonic} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user