diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ffad4114..46a1f001 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -21,10 +21,10 @@ import { ROUTE_SEND, ROUTE_BATCH, ROUTE_TOKEN_TIP, ROUTE_SCHEDULE, ROUTE_SCHEDULED_TIPS, ROUTE_FEED, ROUTE_LEADERBOARD, ROUTE_ACTIVITY, ROUTE_PROFILE, ROUTE_ADDRESS_BOOK, ROUTE_BLOCK, ROUTE_STATS, ROUTE_ADMIN, ROUTE_TELEMETRY, ROUTE_REFUNDS, - ROUTE_NOTIFICATION_PREFERENCES, + ROUTE_NOTIFICATION_PREFERENCES, ROUTE_ENCRYPTION, DEFAULT_AUTHENTICATED_ROUTE, ROUTE_META, } from './config/routes'; -import { Zap, Radio, Trophy, User, BarChart3, Users, ShieldBan, Coins, UserCircle, Shield, Gauge, Calendar, Clock, BookUser, RotateCcw, BellCog } from 'lucide-react'; +import { Zap, Radio, Trophy, User, BarChart3, Users, ShieldBan, Coins, UserCircle, Shield, Gauge, Calendar, Clock, BookUser, RotateCcw, BellCog, Lock } from 'lucide-react'; import { activateDemo, deactivateDemo } from './lib/demo-utils'; import { useNotificationPreferences } from './context/NotificationPreferencesContext'; @@ -47,6 +47,7 @@ const AdminDashboard = lazy(() => import('./components/AdminDashboard')); const TelemetryDashboard = lazy(() => import('./components/TelemetryDashboard')); const RefundManager = lazy(() => import('./components/RefundManager')); const NotificationPreferencesPage = lazy(() => import('./components/NotificationPreferences')); +const EncryptionSettings = lazy(() => import('./components/EncryptionSettings')); function App() { const [userData, setUserData] = useState(null); @@ -175,6 +176,7 @@ function App() { { path: ROUTE_BLOCK, label: 'Block', icon: ShieldBan }, { path: ROUTE_REFUNDS, label: 'Refunds', icon: RotateCcw }, { path: ROUTE_NOTIFICATION_PREFERENCES, label: 'Notifications', icon: BellCog }, + { path: ROUTE_ENCRYPTION, label: 'Encryption', icon: Lock }, { path: ROUTE_STATS, label: 'Stats', icon: BarChart3 }, ]; @@ -421,6 +423,20 @@ function App() { ) } /> + + {/* Encryption settings */} + + ) : ( + + + + ) + } + /> {/* Root and fallback */} } /> diff --git a/frontend/src/components/EncryptionSettings.jsx b/frontend/src/components/EncryptionSettings.jsx new file mode 100644 index 00000000..22007175 --- /dev/null +++ b/frontend/src/components/EncryptionSettings.jsx @@ -0,0 +1,151 @@ +import { useState } from 'react'; +import { useEncryption } from '../hooks/useEncryption'; +import { useSenderAddress } from '../hooks/useSenderAddress'; +import { Lock, RefreshCw, Copy, Check } from 'lucide-react'; + +export default function EncryptionSettings({ addToast }) { + const senderAddress = useSenderAddress(); + const { keys, loading, error, regenerateKeys } = useEncryption(senderAddress); + const [copied, setCopied] = useState(false); + const [regenerating, setRegenerating] = useState(false); + + const handleCopyPublicKey = async () => { + if (!keys?.publicKey) return; + + try { + await navigator.clipboard.writeText(keys.publicKey); + setCopied(true); + addToast('Public key copied to clipboard', 'success'); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + addToast('Failed to copy public key', 'error'); + } + }; + + const handleRegenerateKeys = async () => { + if (!confirm('Are you sure you want to regenerate your encryption keys? You will not be able to decrypt old messages.')) { + return; + } + + setRegenerating(true); + try { + await regenerateKeys(); + addToast('Encryption keys regenerated successfully', 'success'); + } catch (err) { + addToast('Failed to regenerate keys', 'error'); + } finally { + setRegenerating(false); + } + }; + + if (!senderAddress) { + return ( +
+
+

+ Connect your wallet to manage encryption settings +

+
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+
+
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+
+
+ ); + } + + return ( +
+
+
+
+ +
+
+

Encryption Settings

+

Manage your message encryption keys

+
+
+ +
+
+

+ Your encryption keys are stored locally in your browser. They are used to encrypt and decrypt private tip messages. +

+
+ +
+ +
+
+ {keys?.publicKey ? keys.publicKey.slice(0, 100) + '...' : 'No key available'} +
+ +
+

+ Share this key with others so they can send you encrypted messages +

+
+ +
+ +
+
+
+ + {keys?.privateKey ? 'Private key is stored securely' : 'No private key available'} + +
+
+

+ Your private key never leaves your browser and is used to decrypt messages sent to you +

+
+ +
+ +

+ Warning: Regenerating keys will prevent you from decrypting old messages +

+
+
+
+
+ ); +} diff --git a/frontend/src/components/SendTip.jsx b/frontend/src/components/SendTip.jsx index ecb28986..04dc922d 100644 --- a/frontend/src/components/SendTip.jsx +++ b/frontend/src/components/SendTip.jsx @@ -21,10 +21,12 @@ import { useStxPrice } from '../hooks/useStxPrice'; import { useSenderAddress } from '../hooks/useSenderAddress'; import { useContractFee } from '../hooks/useContractFee'; import { useTransactionFeeEstimate } from '../hooks/useTransactionFeeEstimate'; +import { useEncryption } from '../hooks/useEncryption'; import { analytics } from '../lib/analytics'; import ConfirmDialog from './ui/confirm-dialog'; import TxStatus from './ui/tx-status'; import AddressBook from './AddressBook'; +import { Lock, Unlock } from 'lucide-react'; const MIN_TIP_STX = 0.001; // minimum tip in STX const MAX_TIP_STX = 10000; // maximum tip in STX @@ -67,10 +69,12 @@ export default function SendTip({ addToast }) { const [amountError, setAmountError] = useState(''); const [cooldown, setCooldown] = useState(0); const [showAddressBook, setShowAddressBook] = useState(false); + const [encryptMessage, setEncryptMessage] = useState(false); const cooldownRef = useRef(null); const walletSenderAddress = useSenderAddress(); const senderAddress = demoEnabled ? getDemoData().mockWalletAddress : walletSenderAddress; + const { encrypt, canEncryptFor } = useEncryption(senderAddress); const realBalanceState = useBalance(senderAddress); const { displayBalance: balance, sendTipInDemo, pendingTransaction } = useSendTipWithDemo(realBalanceState.balance); const balanceLoading = demoEnabled ? false : realBalanceState.loading; @@ -93,6 +97,7 @@ export default function SendTip({ addToast }) { } = useTransactionFeeEstimate(); const isRecipientHighRisk = !canProceedWithRecipient(recipient, blockedWarning); + const canEncrypt = recipient && canEncryptFor(recipient); useEffect(() => { return () => { @@ -205,13 +210,25 @@ export default function SendTip({ addToast }) { setLoading(true); try { + let finalMessage = message || 'Thanks!'; + + if (encryptMessage && message && canEncrypt) { + try { + finalMessage = await encrypt(message, recipient.trim()); + } catch (encryptError) { + addToast('Failed to encrypt message. Sending unencrypted.', 'warning'); + finalMessage = message; + } + } + if (demoEnabled) { - const result = await sendTipInDemo(recipient.trim(), amount, message, category); + const result = await sendTipInDemo(recipient.trim(), amount, finalMessage, category); setPendingTx(result); setRecipient(''); setAmount(''); setMessage(''); setCategory(0); + setEncryptMessage(false); startCooldown(); addToast('Demo tip sent successfully.', 'success'); setLoading(false); @@ -226,7 +243,7 @@ export default function SendTip({ addToast }) { const functionArgs = [ principalCV(recipient.trim()), uintCV(microSTX), - stringUtf8CV(message || 'Thanks!'), + stringUtf8CV(finalMessage), uintCV(category) ]; @@ -246,6 +263,7 @@ export default function SendTip({ addToast }) { setAmount(''); setMessage(''); setCategory(0); + setEncryptMessage(false); notifyTipSent(); startCooldown(); analytics.trackTipConfirmed(); @@ -262,7 +280,6 @@ export default function SendTip({ addToast }) { console.error('Failed to send tip:', msg); analytics.trackTipFailed(); - // Provide a more specific message for post-condition failures if (msg.toLowerCase().includes('post-condition') || msg.toLowerCase().includes('postcondition')) { addToast('Transaction rejected by post-condition check. Your funds are safe.', 'error'); } else { @@ -360,13 +377,46 @@ export default function SendTip({ addToast }) { {/* Message */}
- +
+ + {message && ( + + )} +