synor/apps/desktop-wallet/src/pages/Backup/BackupPage.tsx
Gulshan Yadav 63c52b26b2 feat(desktop-wallet): add comprehensive wallet features
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.
2026-02-02 09:57:55 +05:30

377 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}