Add 10 major features to complete the desktop wallet: - Staking: Stake SYN tokens for rewards with pool management - DEX/Swap: Built-in token swap interface with liquidity pools - Address Book: Save and manage frequently used addresses - DApp Browser: Interact with decentralized applications - Hardware Wallet: Ledger/Trezor support for secure signing - Multi-sig Wallets: Require multiple signatures for transactions - Price Charts: Market data and real-time price tracking - Notifications: Push notifications for transactions and alerts - QR Scanner: Generate and parse payment QR codes - Backup/Export: Encrypted wallet backup and recovery Includes Tauri backend commands for all features, Zustand stores for state management, and complete UI pages with navigation.
377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
import { useState } from 'react';
|
||
import { open, save } from '@tauri-apps/plugin-dialog';
|
||
import {
|
||
Download,
|
||
Upload,
|
||
FileJson,
|
||
Shield,
|
||
AlertCircle,
|
||
Check,
|
||
Clock,
|
||
HardDrive,
|
||
Lock,
|
||
} from 'lucide-react';
|
||
import { useBackupStore } from '../../store/backup';
|
||
|
||
export default function BackupPage() {
|
||
const {
|
||
isExporting,
|
||
isImporting,
|
||
lastExport,
|
||
lastHistoryExport,
|
||
error,
|
||
clearError,
|
||
exportWallet,
|
||
importWallet,
|
||
exportHistory,
|
||
} = useBackupStore();
|
||
|
||
const [exportPassword, setExportPassword] = useState('');
|
||
const [confirmPassword, setConfirmPassword] = useState('');
|
||
const [importPassword, setImportPassword] = useState('');
|
||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||
const [exportSuccess, setExportSuccess] = useState(false);
|
||
const [importSuccess, setImportSuccess] = useState(false);
|
||
|
||
const handleExportWallet = async () => {
|
||
if (!exportPassword || exportPassword !== confirmPassword) return;
|
||
|
||
try {
|
||
const path = await save({
|
||
defaultPath: `synor-wallet-backup-${Date.now()}.enc`,
|
||
filters: [{ name: 'Encrypted Backup', extensions: ['enc'] }],
|
||
});
|
||
|
||
if (path) {
|
||
await exportWallet(exportPassword, path);
|
||
setExportPassword('');
|
||
setConfirmPassword('');
|
||
setExportSuccess(true);
|
||
setTimeout(() => setExportSuccess(false), 5000);
|
||
}
|
||
} catch {
|
||
// Error handled by store
|
||
}
|
||
};
|
||
|
||
const handleSelectFile = async () => {
|
||
try {
|
||
const selected = await open({
|
||
multiple: false,
|
||
filters: [{ name: 'Encrypted Backup', extensions: ['enc'] }],
|
||
});
|
||
|
||
if (selected && typeof selected === 'string') {
|
||
setSelectedFile(selected);
|
||
}
|
||
} catch {
|
||
// User cancelled
|
||
}
|
||
};
|
||
|
||
const handleImportWallet = async () => {
|
||
if (!selectedFile || !importPassword) return;
|
||
|
||
try {
|
||
await importWallet(selectedFile, importPassword);
|
||
setSelectedFile(null);
|
||
setImportPassword('');
|
||
setImportSuccess(true);
|
||
setTimeout(() => setImportSuccess(false), 5000);
|
||
} catch {
|
||
// Error handled by store
|
||
}
|
||
};
|
||
|
||
const handleExportHistory = async (format: 'json' | 'csv') => {
|
||
try {
|
||
const path = await save({
|
||
defaultPath: `synor-history-${Date.now()}.${format}`,
|
||
filters: [
|
||
format === 'json'
|
||
? { name: 'JSON', extensions: ['json'] }
|
||
: { name: 'CSV', extensions: ['csv'] },
|
||
],
|
||
});
|
||
|
||
if (path) {
|
||
await exportHistory(path, format);
|
||
}
|
||
} catch {
|
||
// Error handled by store
|
||
}
|
||
};
|
||
|
||
const passwordsMatch = exportPassword && exportPassword === confirmPassword;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white">Backup & Export</h1>
|
||
<p className="text-gray-400 mt-1">
|
||
Securely backup your wallet and export transaction history
|
||
</p>
|
||
</div>
|
||
|
||
{/* Error Alert */}
|
||
{error && (
|
||
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
|
||
<AlertCircle className="text-red-400" size={20} />
|
||
<p className="text-red-400 flex-1">{error}</p>
|
||
<button onClick={clearError} className="text-red-400 hover:text-red-300">
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Success Alerts */}
|
||
{exportSuccess && (
|
||
<div className="flex items-center gap-3 p-4 bg-green-900/20 border border-green-800 rounded-lg">
|
||
<Check className="text-green-400" size={20} />
|
||
<p className="text-green-400">Wallet backup exported successfully!</p>
|
||
</div>
|
||
)}
|
||
|
||
{importSuccess && (
|
||
<div className="flex items-center gap-3 p-4 bg-green-900/20 border border-green-800 rounded-lg">
|
||
<Check className="text-green-400" size={20} />
|
||
<p className="text-green-400">Wallet imported successfully!</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* Export Wallet */}
|
||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="p-2 bg-synor-600/20 rounded-lg">
|
||
<Download className="text-synor-400" size={24} />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">Export Wallet</h2>
|
||
<p className="text-sm text-gray-400">
|
||
Create an encrypted backup of your wallet
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">
|
||
Encryption Password
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={exportPassword}
|
||
onChange={(e) => setExportPassword(e.target.value)}
|
||
placeholder="Enter a strong password"
|
||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">
|
||
Confirm Password
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
placeholder="Confirm password"
|
||
className={`w-full px-4 py-2 bg-gray-800 border rounded-lg text-white placeholder-gray-500 focus:outline-none ${
|
||
confirmPassword && !passwordsMatch
|
||
? 'border-red-500'
|
||
: 'border-gray-700 focus:border-synor-500'
|
||
}`}
|
||
/>
|
||
{confirmPassword && !passwordsMatch && (
|
||
<p className="text-xs text-red-400 mt-1">Passwords do not match</p>
|
||
)}
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleExportWallet}
|
||
disabled={!passwordsMatch || isExporting}
|
||
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{isExporting ? (
|
||
<>
|
||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||
Exporting...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Download size={18} />
|
||
Export Encrypted Backup
|
||
</>
|
||
)}
|
||
</button>
|
||
|
||
{lastExport && (
|
||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||
<Clock size={14} />
|
||
Last export: {new Date(lastExport.createdAt).toLocaleString()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Import Wallet */}
|
||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="p-2 bg-purple-600/20 rounded-lg">
|
||
<Upload className="text-purple-400" size={24} />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">Import Wallet</h2>
|
||
<p className="text-sm text-gray-400">
|
||
Restore from an encrypted backup file
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">Backup File</label>
|
||
<button
|
||
onClick={handleSelectFile}
|
||
className="w-full flex items-center justify-between px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-left hover:border-gray-600 transition-colors"
|
||
>
|
||
<span className={selectedFile ? 'text-white' : 'text-gray-500'}>
|
||
{selectedFile
|
||
? selectedFile.split('/').pop()
|
||
: 'Select backup file...'}
|
||
</span>
|
||
<HardDrive size={18} className="text-gray-400" />
|
||
</button>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm text-gray-400 mb-1">
|
||
Decryption Password
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={importPassword}
|
||
onChange={(e) => setImportPassword(e.target.value)}
|
||
placeholder="Enter backup password"
|
||
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleImportWallet}
|
||
disabled={!selectedFile || !importPassword || isImporting}
|
||
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{isImporting ? (
|
||
<>
|
||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||
Importing...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Upload size={18} />
|
||
Import Backup
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Export History */}
|
||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="p-2 bg-green-600/20 rounded-lg">
|
||
<FileJson className="text-green-400" size={24} />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">Export History</h2>
|
||
<p className="text-sm text-gray-400">
|
||
Export your transaction history for records
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<p className="text-sm text-gray-400">
|
||
Export your complete transaction history for tax purposes, accounting, or
|
||
personal records.
|
||
</p>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => handleExportHistory('csv')}
|
||
disabled={isExporting}
|
||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
|
||
>
|
||
Export CSV
|
||
</button>
|
||
<button
|
||
onClick={() => handleExportHistory('json')}
|
||
disabled={isExporting}
|
||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
|
||
>
|
||
Export JSON
|
||
</button>
|
||
</div>
|
||
|
||
{lastHistoryExport && (
|
||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||
<Clock size={14} />
|
||
Last export: {lastHistoryExport.transactionCount} transactions on{' '}
|
||
{new Date(lastHistoryExport.createdAt).toLocaleDateString()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Security Info */}
|
||
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="p-2 bg-yellow-600/20 rounded-lg">
|
||
<Shield className="text-yellow-400" size={24} />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-lg font-semibold text-white">Security Tips</h2>
|
||
<p className="text-sm text-gray-400">Keep your backup safe</p>
|
||
</div>
|
||
</div>
|
||
|
||
<ul className="space-y-3 text-sm">
|
||
<li className="flex items-start gap-2">
|
||
<Lock size={16} className="text-synor-400 mt-0.5" />
|
||
<span className="text-gray-300">
|
||
Use a strong, unique password for your backup
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<Lock size={16} className="text-synor-400 mt-0.5" />
|
||
<span className="text-gray-300">
|
||
Store backups in multiple secure locations
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<Lock size={16} className="text-synor-400 mt-0.5" />
|
||
<span className="text-gray-300">
|
||
Never share your backup file or password
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<Lock size={16} className="text-synor-400 mt-0.5" />
|
||
<span className="text-gray-300">
|
||
Consider using cold storage for large amounts
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<Lock size={16} className="text-synor-400 mt-0.5" />
|
||
<span className="text-gray-300">
|
||
Create a new backup after important changes
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|