diff --git a/src/App.tsx b/src/App.tsx
index b7d06aa..38d1204 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -45,6 +45,7 @@ function App() {
const [backupMessagePassword, setBackupMessagePassword] = useState('');
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
+ const [isBlenderDirty, setIsBlenderDirty] = useState(false);
const [publicKeyInput, setPublicKeyInput] = useState('');
const [privateKeyInput, setPrivateKeyInput] = useState('');
@@ -350,6 +351,18 @@ 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);
+ }
+ };
+
return (
@@ -361,7 +374,7 @@ function App() {
events={clipboardEvents}
onOpenClipboardModal={() => setShowClipboardModal(true)}
activeTab={activeTab}
- setActiveTab={setActiveTab}
+ onRequestTabChange={handleRequestTabChange}
encryptedMnemonicCache={encryptedMnemonicCache}
handleLockAndClear={handleLockAndClear}
appVersion={__APP_VERSION__}
@@ -478,151 +491,156 @@ function App() {
)}
>
) : (
-
+
)}
- {/* Security Panel */}
- {/* Added space-y-2 wrapper */}
-
- SECURITY OPTIONS
-
+ {/* Security Panel */}
+ {activeTab !== 'seedblender' && (
+
{/* Added space-y-2 wrapper */}
+
+ SECURITY OPTIONS
+
+
+
+ {/* Removed h3 */}
+
+ {/* Encryption Mode Toggle */}
+
+
Encryption Mode
+
setEncryptionMode(e.target.value as 'pgp' | 'krux')}
+ disabled={isReadOnly}
+ className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all"
+ >
+ PGP (Asymmetric)
+ Krux KEF (Passphrase)
+
+
+ {encryptionMode === 'pgp'
+ ? 'Uses PGP keys or password'
+ : 'Uses passphrase only (Krux compatible)'}
+
+
-
- {/* Removed h3 */}
-
- {/* Encryption Mode Toggle */}
-
-
Encryption Mode
-
setEncryptionMode(e.target.value as 'pgp' | 'krux')}
- disabled={isReadOnly}
- className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all"
- >
- PGP (Asymmetric)
- Krux KEF (Passphrase)
-
-
- {encryptionMode === 'pgp'
- ? 'Uses PGP keys or password'
- : 'Uses passphrase only (Krux compatible)'}
-
-
-
- {/* Krux-specific fields */}
- {encryptionMode === 'krux' && activeTab === 'backup' && (
- <>
-
-
Krux Label
-
- setKruxLabel(e.target.value)}
- readOnly={isReadOnly}
- />
-
-
Label for identification (max 252 bytes)
-
-
-
-
PBKDF2 Iterations
-
- setKruxIterations(Number(e.target.value))}
- min={10000}
- step={10000}
- readOnly={isReadOnly}
- />
-
-
Higher = more secure but slower (default: 200,000)
-
- >
- )}
-
-
-
Message Password
-
-
- activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
- readOnly={isReadOnly}
- />
-
-
- {encryptionMode === 'krux'
- ? 'Required passphrase for Krux encryption'
- : 'Symmetric encryption password (SKESK)'}
-
-
-
-
- {activeTab === 'backup' && (
-
-
- setHasBip39Passphrase(e.target.checked)}
- disabled={isReadOnly}
- className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
- />
-
- BIP39 25th word active
-
-
-
- )}
-
-
-
- {/* Action Button */}
- {activeTab === 'backup' ? (
-
- {loading ? (
-
- ) : (
-
- )}
- {loading ? 'Generating...' : 'Generate QR Backup'}
-
- ) : (
-
- {loading ? (
-
- ) : (
-
- )}
- {loading ? 'Decrypting...' : 'Decrypt & Restore'}
-
- )}
-
-
+ {/* Krux-specific fields */}
+ {encryptionMode === 'krux' && activeTab === 'backup' && (
+ <>
+
+
Krux Label
+
+ setKruxLabel(e.target.value)}
+ readOnly={isReadOnly}
+ />
+
+
Label for identification (max 252 bytes)
+
+
+
+
PBKDF2 Iterations
+
+ setKruxIterations(Number(e.target.value))}
+ min={10000}
+ step={10000}
+ readOnly={isReadOnly}
+ />
+
+
Higher = more secure but slower (default: 200,000)
+
+ >
+ )}
+
+
+
Message Password
+
+
+ activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
+ readOnly={isReadOnly}
+ />
+
+
+ {encryptionMode === 'krux'
+ ? 'Required passphrase for Krux encryption'
+ : 'Symmetric encryption password (SKESK)'}
+
+
+
+
+ {activeTab === 'backup' && (
+
+
+ setHasBip39Passphrase(e.target.checked)}
+ disabled={isReadOnly}
+ className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
+ />
+
+ BIP39 25th word active
+
+
+
+ )}
+
+
+
+ {/* Action Button */}
+ {activeTab === 'backup' ? (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Generating...' : 'Generate QR Backup'}
+
+ ) : (
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Decrypting...' : 'Decrypt & Restore'}
+
+ )}
+
+ )}
{/* QR Output */}
{qrPayload && activeTab === 'backup' && (
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 28f6808..053b1e7 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -26,7 +26,7 @@ interface HeaderProps {
events: ClipboardEvent[];
onOpenClipboardModal: () => void;
activeTab: 'backup' | 'restore' | 'seedblender';
- setActiveTab: (tab: 'backup' | 'restore' | 'seedblender') => void;
+ onRequestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
encryptedMnemonicCache: any;
handleLockAndClear: () => void;
appVersion: string;
@@ -42,7 +42,7 @@ const Header: React.FC = ({
events,
onOpenClipboardModal,
activeTab,
- setActiveTab,
+ onRequestTabChange,
encryptedMnemonicCache,
handleLockAndClear,
appVersion,
@@ -91,19 +91,19 @@ const Header: React.FC = ({
)}
setActiveTab('backup')}
+ onClick={() => onRequestTabChange('backup')}
>
Backup
setActiveTab('restore')}
+ onClick={() => onRequestTabChange('restore')}
>
Restore
setActiveTab('seedblender')}
+ onClick={() => onRequestTabChange('seedblender')}
>
Seed Blender
diff --git a/src/components/QRScanner.tsx b/src/components/QRScanner.tsx
index 3d65d54..ddcd6d9 100644
--- a/src/components/QRScanner.tsx
+++ b/src/components/QRScanner.tsx
@@ -40,12 +40,11 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
aspectRatio: 1.0,
},
(decodedText) => {
- if (decodedText.startsWith('SEEDPGP1:')) {
+ // Stop scanning after the first successful detection
+ if (decodedText) {
setSuccess(true);
onScanSuccess(decodedText);
stopCamera();
- } else {
- setError('QR code found, but not a valid SEEDPGP1 frame');
}
},
() => {
@@ -89,13 +88,9 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
// Try scanning with verbose mode
const decodedText = await html5QrCode.scanFile(file, true);
- if (decodedText.startsWith('SEEDPGP1:')) {
- setSuccess(true);
- onScanSuccess(decodedText);
- html5QrCode.clear();
- } else {
- setError(`Found QR code, but not SEEDPGP format: ${decodedText.substring(0, 30)}...`);
- }
+ setSuccess(true);
+ onScanSuccess(decodedText);
+ html5QrCode.clear();
} catch (err: any) {
console.error('File scan error:', err);
diff --git a/src/components/SeedBlender.tsx b/src/components/SeedBlender.tsx
index 282d4e3..444064b 100644
--- a/src/components/SeedBlender.tsx
+++ b/src/components/SeedBlender.tsx
@@ -1,10 +1,15 @@
/**
* @file SeedBlender.tsx
* @summary Main component for the Seed Blending feature.
+ * @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.
*/
import { useState, useEffect } from 'react';
-import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock } from 'lucide-react';
+import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock, Key, ArrowRight } from 'lucide-react';
import QRScanner from './QRScanner';
+import { decryptFromSeed, detectEncryptionMode } from '../lib/seedpgp';
+import { decryptFromKrux } from '../lib/krux';
+import { QrDisplay } from './QrDisplay';
import {
blendMnemonicsAsync,
checkXorStrength,
@@ -18,7 +23,6 @@ import {
mixWithDiceAsync,
} from '../lib/seedblend';
-// A simple debounce function
function debounce any>(func: F, waitFor: number) {
let timeout: ReturnType | null = null;
return (...args: Parameters): void => {
@@ -27,33 +31,53 @@ function debounce any>(func: F, waitFor: number) {
};
}
-export function SeedBlender() {
- // Step 1 State
- const [mnemonics, setMnemonics] = useState(['']);
- const [validity, setValidity] = useState>([null]);
+interface MnemonicEntry {
+ id: number;
+ rawInput: string;
+ decryptedMnemonic: string | null;
+ isEncrypted: boolean;
+ inputType: 'text' | 'seedpgp' | 'krux';
+ passwordRequired: boolean;
+ passwordInput: string;
+ error: string | null;
+ isValid: boolean | null;
+}
+
+let nextId = 0;
+const createNewEntry = (): MnemonicEntry => ({
+ id: nextId++, rawInput: '', decryptedMnemonic: null, isEncrypted: false,
+ inputType: 'text', passwordRequired: false, passwordInput: '', error: null, isValid: null,
+});
+
+interface SeedBlenderProps {
+ onDirtyStateChange: (isDirty: boolean) => void;
+ setMnemonicForBackup: (mnemonic: string) => void;
+ requestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
+}
+
+export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestTabChange }: SeedBlenderProps) {
+ const [entries, setEntries] = useState([createNewEntry()]);
const [showQRScanner, setShowQRScanner] = useState(false);
const [scanTargetIndex, setScanTargetIndex] = useState(null);
-
- // Step 2 State
const [blendedResult, setBlendedResult] = useState<{ blendedEntropy: Uint8Array; blendedMnemonic12: string; blendedMnemonic24?: string; } | null>(null);
const [xorStrength, setXorStrength] = useState<{ isWeak: boolean; uniqueBytes: number; } | null>(null);
const [blendError, setBlendError] = useState('');
const [blending, setBlending] = useState(false);
-
- // Step 3 State
const [diceRolls, setDiceRolls] = useState('');
const [diceStats, setDiceStats] = useState(null);
const [dicePatternWarning, setDicePatternWarning] = useState(null);
const [diceOnlyMnemonic, setDiceOnlyMnemonic] = useState(null);
-
- // Step 4 State
const [finalMnemonic, setFinalMnemonic] = useState(null);
const [mixing, setMixing] = useState(false);
+ const [showFinalQR, setShowFinalQR] = useState(false);
+
+ useEffect(() => {
+ const isDirty = entries.some(e => e.rawInput.length > 0) || diceRolls.length > 0;
+ onDirtyStateChange(isDirty);
+ }, [entries, diceRolls, onDirtyStateChange]);
- // Main clear function
const handleLockAndClear = () => {
- setMnemonics(['']);
- setValidity([null]);
+ setEntries([createNewEntry()]);
setBlendedResult(null);
setXorStrength(null);
setBlendError('');
@@ -62,108 +86,65 @@ export function SeedBlender() {
setDicePatternWarning(null);
setDiceOnlyMnemonic(null);
setFinalMnemonic(null);
- }
+ setShowFinalQR(false);
+ };
- // Effect to validate and blend mnemonics (Step 2)
useEffect(() => {
- const processMnemonics = async () => {
+ const processEntries = async () => {
setBlending(true);
setBlendError('');
+ const validMnemonics = entries.map(e => e.decryptedMnemonic).filter((m): m is string => m !== null && m.length > 0);
- const filledMnemonics = mnemonics.map(m => m.trim()).filter(m => m.length > 0);
- if (filledMnemonics.length === 0) {
- setBlendedResult(null);
- setXorStrength(null);
- setValidity(mnemonics.map(() => null));
- setBlending(false);
- return;
- }
-
- const newValidity: Array = [...mnemonics].fill(null);
- const validMnemonics: string[] = [];
-
- await Promise.all(mnemonics.map(async (mnemonic, index) => {
- if (mnemonic.trim()) {
- try {
- await mnemonicToEntropy(mnemonic.trim());
- newValidity[index] = true;
- validMnemonics.push(mnemonic.trim());
- } catch (e) {
- newValidity[index] = false;
- }
+ const validityPromises = entries.map(async (entry) => {
+ if (!entry.rawInput.trim()) return null;
+ if (entry.isEncrypted && !entry.decryptedMnemonic) return null; // Cannot validate until decrypted
+ const textToValidate = entry.decryptedMnemonic || entry.rawInput;
+ try {
+ await mnemonicToEntropy(textToValidate.trim());
+ return true;
+ } catch {
+ return false;
}
- }));
+ });
+ const newValidity = await Promise.all(validityPromises);
+ setEntries(currentEntries => currentEntries.map((e, i) => ({ ...e, isValid: newValidity[i] })));
- setValidity(newValidity);
-
if (validMnemonics.length > 0) {
try {
const result = await blendMnemonicsAsync(validMnemonics);
- const strength = checkXorStrength(result.blendedEntropy);
setBlendedResult(result);
- setXorStrength(strength);
- } catch (e: any) {
- setBlendError(e.message);
- setBlendedResult(null);
- setXorStrength(null);
- }
+ setXorStrength(checkXorStrength(result.blendedEntropy));
+ } catch (e: any) { setBlendError(e.message); setBlendedResult(null); }
} else {
setBlendedResult(null);
- setXorStrength(null);
}
setBlending(false);
};
-
- debounce(processMnemonics, 300)();
- }, [mnemonics]);
+ debounce(processEntries, 300)();
+ }, [JSON.stringify(entries.map(e => e.decryptedMnemonic))]);
- // Effect to process dice rolls (Step 3)
useEffect(() => {
const processDice = async () => {
setDiceStats(calculateDiceStats(diceRolls));
-
- const pattern = detectBadPatterns(diceRolls);
- setDicePatternWarning(pattern.message || null);
-
+ setDicePatternWarning(detectBadPatterns(diceRolls).message || null);
if (diceRolls.length >= 50) {
try {
- const diceBytes = diceToBytes(diceRolls);
- const outputByteLength = blendedResult?.blendedEntropy.length === 32 ? 32 : 16;
- const diceOnlyEntropy = await hkdfExtractExpand(diceBytes, outputByteLength, new TextEncoder().encode('dice-only'));
- const mnemonic = await entropyToMnemonic(diceOnlyEntropy);
- setDiceOnlyMnemonic(mnemonic);
- } catch (e) {
- setDiceOnlyMnemonic(null);
- }
- } else {
- setDiceOnlyMnemonic(null);
- }
+ 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 handleAddMnemonic = () => {
- setMnemonics([...mnemonics, '']);
- setValidity([...validity, null]);
+ const updateEntry = (index: number, newProps: Partial) => {
+ setEntries(currentEntries => currentEntries.map((entry, i) => i === index ? { ...entry, ...newProps } : entry));
};
-
- const handleMnemonicChange = (index: number, value: string) => {
- const newMnemonics = [...mnemonics];
- newMnemonics[index] = value;
- setMnemonics(newMnemonics);
- };
-
- const handleRemoveMnemonic = (index: number) => {
- if (mnemonics.length > 1) {
- const newMnemonics = mnemonics.filter((_, i) => i !== index);
- const newValidity = validity.filter((_, i) => i !== index);
- setMnemonics(newMnemonics);
- setValidity(newValidity);
- } else {
- setMnemonics(['']);
- setValidity([null]);
- }
+ 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) => {
@@ -172,243 +153,135 @@ export function SeedBlender() {
};
const handleScanSuccess = (scannedText: string) => {
- if (scanTargetIndex !== null) {
- handleMnemonicChange(scanTargetIndex, scannedText);
- }
+ if (scanTargetIndex === null) return;
+ const mode = detectEncryptionMode(scannedText);
+ const isEncrypted = mode === 'pgp' || mode === 'krux';
+ updateEntry(scanTargetIndex, {
+ rawInput: scannedText, isEncrypted, inputType: isEncrypted ? mode : 'text',
+ passwordRequired: isEncrypted, decryptedMnemonic: isEncrypted ? null : scannedText, error: null,
+ });
setShowQRScanner(false);
- setScanTargetIndex(null);
+ };
+
+ 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({ kefHex: entry.rawInput, passphrase: entry.passwordInput })).mnemonic;
+ } else { // seedpgp
+ mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w;
+ }
+ updateEntry(index, { decryptedMnemonic: mnemonic, isEncrypted: false, passwordRequired: false, error: null });
+ } catch(e: any) {
+ updateEntry(index, { error: e.message || "Decryption failed" });
+ }
};
const handleFinalMix = async () => {
if (!blendedResult) return;
setMixing(true);
try {
- const outputBits = blendedResult.blendedEntropy.length === 32 ? 256 : 128;
+ const outputBits = blendedResult.blendedEntropy.length >= 32 ? 256 : 128;
const result = await mixWithDiceAsync(blendedResult.blendedEntropy, diceRolls, outputBits);
setFinalMnemonic(result.finalMnemonic);
- } catch(e) {
- // handle error
- } finally {
- setMixing(false);
- }
+ } catch(e) { setFinalMnemonic(null); } finally { setMixing(false); }
};
- const handleClearFinal = () => {
- setFinalMnemonic(null);
- }
+ const handleTransfer = () => {
+ if (!finalMnemonic) return;
+ setMnemonicForBackup(finalMnemonic);
+ handleLockAndClear();
+ requestTabChange('backup');
+ };
const getBorderColor = (isValid: boolean | null) => {
if (isValid === true) return 'border-green-500 focus:ring-green-500';
if (isValid === false) return 'border-red-500 focus:ring-red-500';
return 'border-slate-200 focus:ring-teal-500';
- }
+ };
return (
<>
-
- {/* Header */}
+
Seed Blender
-
-
- Lock/Clear
-
+ Lock/Clear
- {/* Step 1: Input Mnemonics */}
Step 1: Input Mnemonics
- {mnemonics.map((mnemonic, index) => (
-
-
-
-
- handleScan(index)}
- className="p-3 h-full bg-purple-600/20 text-purple-300 hover:bg-purple-600/50 hover:text-white rounded-md transition-colors flex items-center justify-center"
- aria-label="Scan QR Code"
- >
-
-
- handleRemoveMnemonic(index)}
- className="p-3 h-full bg-red-600/20 text-red-400 hover:bg-red-600/50 hover:text-white rounded-md transition-colors flex items-center justify-center"
- aria-label="Remove Mnemonic"
- >
-
-
-
-
- ))}
-
- Add Another Mnemonic
-
-
-
-
- {/* Step 2: Blended Preview */}
-
-
Step 2: Blended Preview
- {blending &&
Blending...
}
- {blendError &&
{blendError}
}
-
- {!blending && !blendError && blendedResult && (
-
- {xorStrength?.isWeak && (
-
-
-
-
Weak XOR Result: Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.
+ {entries.map((entry, index) => (
+
+ {entry.passwordRequired ? (
+
+
Decrypt {entry.inputType.toUpperCase()} Mnemonic updateEntry(index, createNewEntry())} className="text-xs text-slate-400 hover:text-white">× Cancel
+
Payload: {entry.rawInput.substring(0, 40)}...
+
updateEntry(index, { passwordInput: e.target.value })} className="w-full p-2 bg-slate-50 border-2 border-slate-200 rounded-lg text-sm font-mono text-slate-900 focus:outline-none focus:ring-2 focus:ring-purple-500" /> handleDecrypt(index)} className="px-4 bg-purple-600 text-white rounded-lg font-semibold hover:bg-purple-700">
+ {entry.error &&
{entry.error}
}
+
+ ) : (
+
+
+
+ handleScan(index)} className="p-3 h-full bg-purple-600/20 text-purple-300 hover:bg-purple-600/50 hover:text-white rounded-md">
+ handleRemoveEntry(entry.id)} className="p-3 h-full bg-red-600/20 text-red-400 hover:bg-red-600/50 hover:text-white rounded-md">
)}
-
-
-
Blended Mnemonic (12-word)
-
- {blendedResult.blendedMnemonic12}
-
-
-
- {blendedResult.blendedMnemonic24 && (
-
-
Blended Mnemonic (24-word)
-
- {blendedResult.blendedMnemonic24}
-
-
- )}
-
- )}
-
- {!blending && !blendError && !blendedResult && (
-
Previews will appear here once you enter one or more valid mnemonics.
- )}
+
+ ))}
+
Add Another Mnemonic
+
+
+
+
+
Step 2: Blended Preview
+ {blending ?
Blending...
: !blendError && blendedResult ? (
{xorStrength?.isWeak && (
Weak XOR Result: Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.
)}
Blended Mnemonic (12-word) {blendedResult.blendedMnemonic12}
{blendedResult.blendedMnemonic24 && (
Blended Mnemonic (24-word) {blendedResult.blendedMnemonic24}
)}
) : (
{blendError || 'Previews will appear here once you enter one or more valid mnemonics.'}
)}
- {/* Step 3: Input Dice Rolls */}
- {/* Step 4: Final Mnemonic */}
Step 4: Generate Final Mnemonic
-
- {!finalMnemonic ? (
- <>
-
- Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final, hardened mnemonic.
-
-
- {mixing ? (
-
- ) : (
-
- )}
- {mixing ? 'Generating...' : 'Mix Mnemonic + Dice'}
-
- >
- ) : (
-
-
-
- Final Mnemonic Generated
-
-
- Hide & Clear
-
-
-
-
-
-
-
- Security Warning: Write this mnemonic down immediately on paper or metal. Do not save it digitally. Clear when done.
-
+ {finalMnemonic ? (
+
+
Final Mnemonic Generated setFinalMnemonic(null)} className="p-2.5 hover:bg-green-100 rounded-xl">
+
+
Security Warning: Write this down immediately. Do not save it digitally.
+
+
setShowFinalQR(true)} className="w-full py-2.5 bg-slate-700 text-white rounded-lg font-semibold flex items-center justify-center gap-2"> Export as QR
+
Transfer to Backup
+ ) : (
+ <>
Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final mnemonic.
{mixing ? : }{mixing ? 'Generating...' : 'Mix Mnemonic + Dice'} >
)}
- {/* QR Scanner Modal */}
- {showQRScanner && (
-
setShowQRScanner(false)}
- />
+ {showQRScanner && setShowQRScanner(false)} />}
+ {showFinalQR && finalMnemonic && (
+ setShowFinalQR(false)}>
+
e.stopPropagation()}>
+
+
+
)}
>
);
-}
+}
\ No newline at end of file