synor/apps/desktop-wallet/src/pages/QRScanner/QRScannerPage.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

405 lines
14 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 {
QrCode,
Camera,
Copy,
Check,
AlertCircle,
Download,
Upload,
RefreshCw,
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
interface PaymentRequest {
address: string;
amount?: number;
label?: string;
message?: string;
}
export default function QRScannerPage() {
const [activeTab, setActiveTab] = useState<'generate' | 'scan'>('generate');
const [address, setAddress] = useState('');
const [amount, setAmount] = useState('');
const [label, setLabel] = useState('');
const [message, setMessage] = useState('');
const [qrCodeData, setQrCodeData] = useState<string | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [copiedUri, setCopiedUri] = useState(false);
const [scannedData, setScannedData] = useState<PaymentRequest | null>(null);
const [error, setError] = useState<string | null>(null);
const generatePaymentUri = () => {
let uri = `synor:${address}`;
const params: string[] = [];
if (amount) params.push(`amount=${amount}`);
if (label) params.push(`label=${encodeURIComponent(label)}`);
if (message) params.push(`message=${encodeURIComponent(message)}`);
if (params.length > 0) {
uri += '?' + params.join('&');
}
return uri;
};
const handleGenerateQR = async () => {
if (!address) return;
setIsGenerating(true);
setError(null);
try {
const uri = generatePaymentUri();
const qrData = await invoke<string>('qr_generate', {
data: uri,
size: 256,
});
setQrCodeData(qrData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to generate QR code');
} finally {
setIsGenerating(false);
}
};
const handlePasteUri = async () => {
try {
const text = await navigator.clipboard.readText();
parsePaymentUri(text);
} catch {
setError('Failed to read clipboard');
}
};
const parsePaymentUri = async (uri: string) => {
setError(null);
try {
const parsed = await invoke<PaymentRequest>('qr_parse_payment', { uri });
setScannedData(parsed);
} catch (err) {
setError(err instanceof Error ? err.message : 'Invalid payment URI');
}
};
const copyUri = () => {
const uri = generatePaymentUri();
navigator.clipboard.writeText(uri);
setCopiedUri(true);
setTimeout(() => setCopiedUri(false), 2000);
};
const downloadQR = () => {
if (!qrCodeData) return;
const link = document.createElement('a');
link.download = `synor-payment-${Date.now()}.png`;
link.href = qrCodeData;
link.click();
};
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-white">QR Code</h1>
<p className="text-gray-400 mt-1">Generate and scan payment QR codes</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={() => setError(null)} className="text-red-400 hover:text-red-300">
×
</button>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b border-gray-800">
<button
onClick={() => setActiveTab('generate')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'generate'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
Generate QR
</button>
<button
onClick={() => setActiveTab('scan')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'scan'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
Parse Payment
</button>
</div>
{/* Generate Tab */}
{activeTab === 'generate' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Form */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold text-white mb-4">Payment Details</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">
Receiving Address *
</label>
<input
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="synor1..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono text-sm placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">
Amount (SYN) - Optional
</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
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">
Label - Optional
</label>
<input
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="e.g., Coffee Shop"
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">
Message - Optional
</label>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="e.g., Payment for order #123"
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={handleGenerateQR}
disabled={!address || isGenerating}
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"
>
{isGenerating ? (
<>
<RefreshCw size={18} className="animate-spin" />
Generating...
</>
) : (
<>
<QrCode size={18} />
Generate QR Code
</>
)}
</button>
</div>
</div>
{/* QR Display */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold text-white mb-4">QR Code</h2>
<div className="flex flex-col items-center">
{qrCodeData ? (
<>
<div className="p-4 bg-white rounded-xl">
<img
src={qrCodeData}
alt="Payment QR Code"
className="w-64 h-64"
/>
</div>
<div className="mt-4 w-full">
<label className="block text-sm text-gray-400 mb-1">
Payment URI
</label>
<div className="flex gap-2">
<input
type="text"
value={generatePaymentUri()}
readOnly
className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm font-mono"
/>
<button
onClick={copyUri}
className="px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
>
{copiedUri ? (
<Check size={18} className="text-green-400" />
) : (
<Copy size={18} className="text-gray-400" />
)}
</button>
</div>
</div>
<button
onClick={downloadQR}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white transition-colors"
>
<Download size={18} />
Download QR Code
</button>
</>
) : (
<div className="w-64 h-64 bg-gray-800 rounded-xl flex items-center justify-center">
<div className="text-center text-gray-500">
<QrCode size={48} className="mx-auto mb-2 opacity-50" />
<p>Enter details to generate</p>
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* Scan Tab */}
{activeTab === 'scan' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Input */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold text-white mb-4">Parse Payment URI</h2>
<div className="space-y-4">
<p className="text-sm text-gray-400">
Paste a Synor payment URI to decode its contents. The URI format is:
</p>
<code className="block p-3 bg-gray-800 rounded-lg text-sm text-gray-300 break-all">
synor:{'<address>'}?amount={'<amount>'}&label={'<label>'}
</code>
<div>
<label className="block text-sm text-gray-400 mb-1">
Payment URI
</label>
<textarea
placeholder="Paste synor:... URI here"
rows={3}
onChange={(e) => {
if (e.target.value) {
parsePaymentUri(e.target.value);
} else {
setScannedData(null);
}
}}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono text-sm placeholder-gray-500 focus:outline-none focus:border-synor-500 resize-none"
/>
</div>
<button
onClick={handlePasteUri}
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
>
<Upload size={18} />
Paste from Clipboard
</button>
<div className="pt-4 border-t border-gray-800">
<p className="text-sm text-gray-500">
Note: Camera scanning is not available in desktop apps. You can use
your phone's camera to scan QR codes and copy the resulting URI here.
</p>
</div>
</div>
</div>
{/* Parsed Result */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<h2 className="text-lg font-semibold text-white mb-4">Payment Request</h2>
{scannedData ? (
<div className="space-y-4">
<div className="p-4 bg-green-900/20 border border-green-800 rounded-lg">
<div className="flex items-center gap-2 text-green-400 mb-2">
<Check size={18} />
<span className="font-medium">Valid Payment Request</span>
</div>
</div>
<div>
<label className="block text-sm text-gray-500 mb-1">Address</label>
<code className="block p-3 bg-gray-800 rounded-lg text-sm text-white break-all">
{scannedData.address}
</code>
</div>
{scannedData.amount !== undefined && (
<div>
<label className="block text-sm text-gray-500 mb-1">Amount</label>
<p className="text-2xl font-bold text-white">
{scannedData.amount} SYN
</p>
</div>
)}
{scannedData.label && (
<div>
<label className="block text-sm text-gray-500 mb-1">Label</label>
<p className="text-white">{scannedData.label}</p>
</div>
)}
{scannedData.message && (
<div>
<label className="block text-sm text-gray-500 mb-1">Message</label>
<p className="text-white">{scannedData.message}</p>
</div>
)}
<button
onClick={() => {
// Navigate to send page with pre-filled data
window.location.href = `/send?to=${scannedData.address}${
scannedData.amount ? `&amount=${scannedData.amount}` : ''
}`;
}}
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"
>
Send Payment
</button>
</div>
) : (
<div className="h-64 flex items-center justify-center text-gray-500">
<div className="text-center">
<Camera size={48} className="mx-auto mb-2 opacity-50" />
<p>Paste a payment URI to decode</p>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}