mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 18:07:50 +08:00
add recovery kit
This commit is contained in:
@@ -24,8 +24,8 @@ interface HeaderProps {
|
||||
sessionItems: StorageItem[];
|
||||
events: ClipboardEvent[];
|
||||
onOpenClipboardModal: () => void;
|
||||
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
|
||||
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
||||
activeTab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery';
|
||||
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery') => void;
|
||||
appVersion: string;
|
||||
isNetworkBlocked: boolean;
|
||||
onToggleNetwork: () => void;
|
||||
@@ -113,7 +113,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</div>
|
||||
|
||||
{/* ROW 3: Navigation Tabs */}
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
<button
|
||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'create'
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
@@ -157,6 +157,17 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
Blender
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'test-recovery'
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
||||
}`}
|
||||
style={activeTab === 'test-recovery' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
onClick={() => onRequestTabChange('test-recovery')}
|
||||
>
|
||||
🧪 Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -4,9 +4,11 @@ import QRCode from 'qrcode';
|
||||
|
||||
interface QrDisplayProps {
|
||||
value: string | Uint8Array;
|
||||
encryptionMode?: 'pgp' | 'krux' | 'seedqr';
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
export const QrDisplay: React.FC<QrDisplayProps> = ({ value, encryptionMode = 'pgp', fingerprint }) => {
|
||||
const [dataUrl, setDataUrl] = useState('');
|
||||
const [debugInfo, setDebugInfo] = useState('');
|
||||
|
||||
@@ -94,8 +96,15 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
|
||||
if (!dataUrl) return null;
|
||||
|
||||
const metadata = {
|
||||
format: encryptionMode.toUpperCase(),
|
||||
created: new Date().toLocaleDateString(),
|
||||
recovery_url: 'github.com/kccleoc/seedpgp-web',
|
||||
fingerprint: fingerprint || 'N/A',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-4 border-[#00f0ff] rounded-xl shadow-[0_0_40px_rgba(0,240,255,0.6)] p-4 bg-[#0a0a0f] space-y-4">
|
||||
<div className="border-4 border-[#00f0ff] rounded-xl shadow-[0_0_40px_rgba(0,240,255,0.6)] p-4 bg-[#0a0a0f] space-y-4 qr-container">
|
||||
<div className="bg-[#16213e] p-6 rounded-lg inline-block shadow-[0_0_20px_rgba(0,240,255,0.3)] border-2 border-[#00f0ff]/30">
|
||||
<img src={dataUrl} alt="QR Code" className="w-full h-auto" />
|
||||
</div>
|
||||
@@ -106,6 +115,27 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* NEW: Metadata below QR */}
|
||||
<div className="bg-[#0a0a0f] border border-[#00f0ff]/30 rounded-lg p-3 text-xs font-mono qr-metadata">
|
||||
<div className="grid grid-cols-2 gap-2 text-[#6ef3f7]">
|
||||
<div>Format:</div>
|
||||
<div className="text-[#00f0ff] font-bold">{metadata.format}</div>
|
||||
|
||||
<div>Created:</div>
|
||||
<div className="text-[#00f0ff]">{metadata.created}</div>
|
||||
|
||||
{fingerprint && (
|
||||
<>
|
||||
<div>PGP Key:</div>
|
||||
<div className="text-[#00f0ff] break-all">{metadata.fingerprint.slice(0, 16)}...</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>Recovery Guide:</div>
|
||||
<div className="text-[#00f0ff]">{metadata.recovery_url}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#00f0ff] hover:bg-[#00f0ff]/80 text-[#0a0a0f] rounded-lg transition-all hover:shadow-[0_0_15px_rgba(0,240,255,0.5)]"
|
||||
@@ -114,6 +144,10 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
Download QR Code
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-[#6ef3f7] text-center">
|
||||
💡 Screenshot this entire area (QR + metadata) for easy identification
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png
|
||||
</p>
|
||||
|
||||
403
src/components/TestRecovery.tsx
Normal file
403
src/components/TestRecovery.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock } from 'lucide-react';
|
||||
import { generateRecoveryKit } from '../lib/recoveryKit';
|
||||
import { encryptToSeed, decryptFromSeed } from '../lib/seedpgp';
|
||||
import { entropyToMnemonic } from '../lib/seedblend';
|
||||
|
||||
type TestStep = 'intro' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete';
|
||||
|
||||
export const TestRecovery: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState<TestStep>('intro');
|
||||
const [dummySeed, setDummySeed] = useState('');
|
||||
const [testPassword, setTestPassword] = useState('TestPassword123!');
|
||||
const [recoveredSeed, setRecoveredSeed] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [encryptedBackup, setEncryptedBackup] = useState<string>('');
|
||||
|
||||
const generateDummySeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Generate a random 12-word BIP39 mnemonic for testing
|
||||
const entropy = crypto.getRandomValues(new Uint8Array(16));
|
||||
const mnemonic = await entropyToMnemonic(entropy);
|
||||
|
||||
setDummySeed(mnemonic);
|
||||
setCurrentStep('encrypt');
|
||||
} catch (err: any) {
|
||||
setError(`Failed to generate dummy seed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const encryptDummySeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Encrypt using the same logic as real backups
|
||||
const result = await encryptToSeed({
|
||||
plaintext: dummySeed,
|
||||
messagePassword: testPassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
|
||||
// Store encrypted backup
|
||||
setEncryptedBackup(result.framed as string);
|
||||
setCurrentStep('download');
|
||||
} catch (err: any) {
|
||||
setError(`Failed to encrypt dummy seed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadRecoveryKit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Generate and download recovery kit with test backup
|
||||
const kitBlob = await generateRecoveryKit({
|
||||
encryptedData: encryptedBackup,
|
||||
encryptionMode: 'pgp',
|
||||
encryptionMethod: 'password',
|
||||
qrImageDataUrl: undefined, // No QR image for test
|
||||
});
|
||||
|
||||
// Trigger download
|
||||
const url = URL.createObjectURL(kitBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `seedpgp-test-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
alert('✅ Recovery kit downloaded! Now let\'s test if you can recover the seed.');
|
||||
setCurrentStep('clear');
|
||||
} catch (err: any) {
|
||||
setError(`Failed to generate recovery kit: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearDummySeed = () => {
|
||||
// Clear the dummy seed from state (simulating app unavailability)
|
||||
setDummySeed('');
|
||||
setRecoveredSeed('');
|
||||
alert('✅ Dummy seed cleared. Now follow the recovery instructions to get it back!');
|
||||
setCurrentStep('recover');
|
||||
};
|
||||
|
||||
const recoverFromBackup = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Decrypt using recovery instructions
|
||||
const result = await decryptFromSeed({
|
||||
frameText: encryptedBackup,
|
||||
messagePassword: testPassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
|
||||
setRecoveredSeed(result.w);
|
||||
setCurrentStep('verify');
|
||||
} catch (err: any) {
|
||||
setError(`❌ Recovery failed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyRecovery = () => {
|
||||
if (recoveredSeed === dummySeed) {
|
||||
setCurrentStep('complete');
|
||||
alert('🎉 SUCCESS! You successfully recovered the seed phrase!');
|
||||
} else {
|
||||
alert('❌ FAILED: Recovered seed does not match original. Try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const resetTest = () => {
|
||||
setCurrentStep('intro');
|
||||
setDummySeed('');
|
||||
setTestPassword('TestPassword123!');
|
||||
setRecoveredSeed('');
|
||||
setEncryptedBackup('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 p-6">
|
||||
<h2 className="text-2xl font-bold text-[#00f0ff] mb-4">
|
||||
🧪 Test Your Recovery Ability
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-[#1a1a2e] border-2 border-[#ff006e] rounded-lg text-[#ff006e] text-sm shadow-[0_0_20px_rgba(255,0,110,0.3)] flex gap-3 items-start mb-4">
|
||||
<AlertCircle className="shrink-0 mt-0.5" size={20} />
|
||||
<div>
|
||||
<p className="font-bold mb-1">Error</p>
|
||||
<p className="whitespace-pre-wrap">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'intro' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[#6ef3f7]">
|
||||
This drill will help you practice recovering a seed phrase from an encrypted backup.
|
||||
You'll learn the recovery process without risking your real funds.
|
||||
</p>
|
||||
|
||||
<div className="bg-[#0a0a0f] border border-[#ff006e] rounded-lg p-4">
|
||||
<h3 className="text-[#ff006e] font-bold mb-2">What You'll Do:</h3>
|
||||
<ol className="text-sm text-[#6ef3f7] space-y-1 list-decimal list-inside">
|
||||
<li>Generate a dummy test seed</li>
|
||||
<li>Encrypt it with a test password</li>
|
||||
<li>Download the recovery kit</li>
|
||||
<li>Clear the seed from your browser</li>
|
||||
<li>Follow recovery instructions to decrypt</li>
|
||||
<li>Verify you got the correct seed back</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={generateDummySeed}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold uppercase flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<PlayCircle size={20} />
|
||||
)}
|
||||
{loading ? 'Generating...' : 'Start Test Recovery Drill'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'generate' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 1: Dummy Seed Generated</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Test Seed (DO NOT USE FOR REAL FUNDS):</p>
|
||||
<p className="font-mono text-sm text-[#00f0ff]">{dummySeed}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={encryptDummySeed}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-[#00f0ff] text-[#0a0a0f] rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Lock size={20} />
|
||||
)}
|
||||
{loading ? 'Encrypting...' : 'Next: Encrypt This Seed'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'encrypt' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 2: Seed Encrypted</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Test Password:</p>
|
||||
<p className="font-mono text-sm text-[#00f0ff]">{testPassword}</p>
|
||||
<p className="text-xs text-[#6ef3f7] mt-2">Seed has been encrypted with PGP using password-based encryption.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadRecoveryKit}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] text-[#0a0a0f] rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Package size={20} />
|
||||
)}
|
||||
{loading ? 'Generating...' : 'Next: Download Recovery Kit'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'download' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 3: Recovery Kit Downloaded</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
The recovery kit ZIP file has been downloaded to your computer. It contains:
|
||||
</p>
|
||||
<ul className="text-xs text-[#6ef3f7] list-disc list-inside mt-2 space-y-1">
|
||||
<li>Encrypted backup file</li>
|
||||
<li>Recovery scripts (Python/Bash)</li>
|
||||
<li>Personalized instructions</li>
|
||||
<li>BIP39 wordlist</li>
|
||||
<li>Metadata file</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearDummySeed}
|
||||
className="w-full py-3 bg-[#ff006e] text-white rounded-xl font-bold"
|
||||
>
|
||||
Next: Clear Seed & Test Recovery
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'clear' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 4: Seed Cleared</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
The dummy seed has been cleared from browser memory. This simulates what would happen if:
|
||||
</p>
|
||||
<ul className="text-xs text-[#6ef3f7] list-disc list-inside mt-2 space-y-1">
|
||||
<li>The SeedPGP website goes down</li>
|
||||
<li>You lose access to this browser</li>
|
||||
<li>You need to recover from the backup alone</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={recoverFromBackup}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Unlock size={20} />
|
||||
)}
|
||||
{loading ? 'Decrypting...' : 'Next: Recover Seed from Backup'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'recover' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 5: Seed Recovered</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Recovered Seed:</p>
|
||||
<p className="font-mono text-sm text-[#00f0ff]">{recoveredSeed}</p>
|
||||
<p className="text-xs text-[#6ef3f7] mt-2">
|
||||
The seed has been successfully decrypted from the backup using the test password.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={verifyRecovery}
|
||||
className="w-full py-3 bg-[#39ff14] text-[#0a0a0f] rounded-xl font-bold"
|
||||
>
|
||||
Next: Verify Recovery
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'verify' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 6: Verification</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
Comparing original seed with recovered seed...
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<p className="text-xs text-[#6ef3f7] mb-1">Original:</p>
|
||||
<p className="font-mono text-xs text-[#00f0ff] truncate">{dummySeed}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#6ef3f7] mb-1">Recovered:</p>
|
||||
<p className="font-mono text-xs text-[#00f0ff] truncate">{recoveredSeed}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (recoveredSeed === dummySeed) {
|
||||
setCurrentStep('complete');
|
||||
alert('🎉 SUCCESS! You successfully recovered the seed phrase!');
|
||||
} else {
|
||||
alert('❌ FAILED: Recovered seed does not match original. Try again.');
|
||||
}
|
||||
}}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#39ff14] to-[#00ff88] text-[#0a0a0f] rounded-xl font-bold"
|
||||
>
|
||||
Verify Match
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'complete' && (
|
||||
<div className="space-y-4 text-center">
|
||||
<CheckCircle2 className="text-[#39ff14] mx-auto" size={64} />
|
||||
<h3 className="text-2xl font-bold text-[#39ff14]">🎉 Test Passed!</h3>
|
||||
<p className="text-[#6ef3f7]">
|
||||
You've successfully proven you can recover a seed phrase from an encrypted backup.
|
||||
You're ready to trust this system with real funds.
|
||||
</p>
|
||||
<div className="bg-[#0a0a0f] border border-[#39ff14] rounded-lg p-4 mt-4">
|
||||
<h4 className="text-[#39ff14] font-bold mb-2">Key Takeaways:</h4>
|
||||
<ul className="text-sm text-[#6ef3f7] space-y-1 text-left">
|
||||
<li>✅ You can decrypt backups without the SeedPGP website</li>
|
||||
<li>✅ The recovery kit contains everything needed</li>
|
||||
<li>✅ You understand the recovery process</li>
|
||||
<li>✅ Your real backups are recoverable</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetTest}
|
||||
className="py-3 px-6 bg-[#16213e] border-2 border-[#00f0ff] text-[#00f0ff] rounded-xl font-bold flex items-center justify-center gap-2 mx-auto"
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Run Test Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="mt-6 pt-4 border-t border-[#00f0ff]/20">
|
||||
<div className="flex justify-between text-xs text-[#6ef3f7] mb-2">
|
||||
<span>Progress:</span>
|
||||
<span>
|
||||
{currentStep === 'intro' && '0/7'}
|
||||
{currentStep === 'generate' && '1/7'}
|
||||
{currentStep === 'encrypt' && '2/7'}
|
||||
{currentStep === 'download' && '3/7'}
|
||||
{currentStep === 'clear' && '4/7'}
|
||||
{currentStep === 'recover' && '5/7'}
|
||||
{currentStep === 'verify' && '6/7'}
|
||||
{currentStep === 'complete' && '7/7'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#0a0a0f] rounded-full h-2">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: currentStep === 'intro' ? '0%' :
|
||||
currentStep === 'generate' ? '14%' :
|
||||
currentStep === 'encrypt' ? '28%' :
|
||||
currentStep === 'download' ? '42%' :
|
||||
currentStep === 'clear' ? '57%' :
|
||||
currentStep === 'recover' ? '71%' :
|
||||
currentStep === 'verify' ? '85%' :
|
||||
'100%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user