From caf6bff46903af2f0d26e5bf057160974b9a14ad Mon Sep 17 00:00:00 2001 From: surbhi Date: Mon, 20 Oct 2025 18:09:16 -0400 Subject: [PATCH] adding toast and displaying fees in the swap-form and modal --- src/components/providers.tsx | 15 ++- src/components/swap-form.tsx | 183 +++++++++++++++++---------- src/components/swap-review-modal.tsx | 33 ++++- src/components/ui/toast.tsx | 102 +++++++++++++++ 4 files changed, 259 insertions(+), 74 deletions(-) create mode 100644 src/components/ui/toast.tsx diff --git a/src/components/providers.tsx b/src/components/providers.tsx index c212127..142b62e 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -10,6 +10,7 @@ import { defineChain } from 'viem'; import { TranslationsProvider } from '@/providers/translations-provider'; import { Header } from '@/components/header'; +import { ToastProvider } from '@/components/ui/toast'; const mockchain = defineChain({ id: 31337, @@ -87,12 +88,14 @@ export function Providers({ children }: { children: React.ReactNode }) { storageKey="liquidity-party-theme" > -
-
-
- {children} -
-
+ +
+
+
+ {children} +
+
+
diff --git a/src/components/swap-form.tsx b/src/components/swap-form.tsx index 75bdef8..d267549 100644 --- a/src/components/swap-form.tsx +++ b/src/components/swap-form.tsx @@ -5,25 +5,26 @@ import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; -import { ArrowDownUp, ChevronDown } from 'lucide-react'; +import { ArrowDownUp, ChevronDown, Settings } from 'lucide-react'; import { useAccount } from 'wagmi'; import { useTokenDetails, useGetPoolsByToken, type TokenDetails } from '@/hooks/usePartyPlanner'; import { useSwapAmounts, useSwap, selectBestSwapRoute } from '@/hooks/usePartyPool'; import { formatUnits, parseUnits } from 'viem'; import { SwapReviewModal } from './swap-review-modal'; +import { useToast } from '@/components/ui/toast'; export function SwapForm() { const { t } = useTranslation(); const { isConnected, address } = useAccount(); + const { addToast, updateToast } = useToast(); const [fromAmount, setFromAmount] = useState(''); const [toAmount, setToAmount] = useState(''); const [selectedFromToken, setSelectedFromToken] = useState(null); const [selectedToToken, setSelectedToToken] = useState(null); const [isFromDropdownOpen, setIsFromDropdownOpen] = useState(false); const [isToDropdownOpen, setIsToDropdownOpen] = useState(false); - const [slippage, setSlippage] = useState(0.5); // Default 0.5% - const [customSlippage, setCustomSlippage] = useState(''); - const [isCustomSlippage, setIsCustomSlippage] = useState(false); + const [maxSlippage, setMaxSlippage] = useState('5.5'); // Default 5.5% + const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isReviewModalOpen, setIsReviewModalOpen] = useState(false); const fromDropdownRef = useRef(null); const toDropdownRef = useRef(null); @@ -45,8 +46,8 @@ export function SwapForm() { return null; }, [selectedFromToken, selectedToToken, availableTokens]); - // Get the current slippage value (either custom or preset) - const currentSlippage = isCustomSlippage ? parseFloat(customSlippage) || 0.5 : slippage; + // Get the current slippage value + const currentSlippage = parseFloat(maxSlippage) || 5.5; // Calculate swap amounts for the selected token pair only const { swapAmounts } = useSwapAmounts( @@ -94,12 +95,24 @@ export function SwapForm() { return; } + // Show swapping toast + const toastId = addToast({ + title: 'Swapping', + description: `${fromAmount} ${selectedFromToken.symbol} → ${toAmount} ${selectedToToken.symbol}`, + type: 'loading', + }); + try { // Use the shared helper to select the best swap route const bestRoute = selectBestSwapRoute(swapAmounts); if (!bestRoute) { console.error('No valid swap route found'); + updateToast(toastId, { + title: 'Swap Failed', + description: 'No valid swap route found', + type: 'error', + }); return; } @@ -115,8 +128,26 @@ export function SwapForm() { maxAmountIn, currentSlippage ); + + // Update toast to success + updateToast(toastId, { + title: 'Swap Confirmed', + description: `Successfully swapped ${fromAmount} ${selectedFromToken.symbol} to ${toAmount} ${selectedToToken.symbol}`, + type: 'success', + }); + + // Clear the form after successful swap + setFromAmount(''); + setToAmount(''); + setSelectedFromToken(null); + setSelectedToToken(null); } catch (err) { console.error('Swap failed:', err); + updateToast(toastId, { + title: 'Swap Failed', + description: err instanceof Error ? err.message : 'Transaction failed', + type: 'error', + }); } }; @@ -143,7 +174,17 @@ export function SwapForm() { return ( - {t('swap.title')} +
+ {t('swap.title')} + +
{/* From Token */} @@ -281,66 +322,27 @@ export function SwapForm() { - {/* Slippage Tolerance */} -
-
- - - {isCustomSlippage ? customSlippage || '0' : slippage}% - -
-
- {[0.1, 0.2, 0.3, 1, 2, 3].map((percent) => ( - - ))} -
- { - setCustomSlippage(e.target.value); - setIsCustomSlippage(true); - }} - onFocus={() => setIsCustomSlippage(true)} - className={`h-9 pr-6 ${isCustomSlippage ? 'border-primary' : ''}`} - step="0.01" - /> - {isCustomSlippage && ( - - % + {/* Gas Estimate, Slippage, and Fees */} + {isConnected && fromAmount && toAmount && ( +
+
+ Estimated Gas Cost: + + {isEstimatingGas ? 'Calculating...' : gasEstimate ? `$${gasEstimate.estimatedCostUsd}` : '-'} + +
+
+ Max Slippage: + {maxSlippage}% +
+ {swapAmounts && swapAmounts.length > 0 && selectedToToken && ( +
+ Fee: + + {(Number(swapAmounts[0].fee) / Math.pow(10, selectedToToken.decimals)).toFixed(6)} {selectedToToken.symbol} - )} -
-
-
- - {/* Gas Estimate */} - {isConnected && fromAmount && toAmount && gasEstimate && !isEstimatingGas && ( -
-
- Estimated Gas Cost: - ${gasEstimate.estimatedCostUsd} -
-
- )} - {isConnected && fromAmount && toAmount && isEstimatingGas && ( -
-
- Estimated Gas Cost: - Calculating... -
+
+ )}
)} @@ -356,6 +358,54 @@ export function SwapForm() { + {/* Settings Modal */} + {isSettingsOpen && ( + <> +
setIsSettingsOpen(false)} + /> +
+
+
+

Settings

+ +
+ +
+
+ +
+ setMaxSlippage(e.target.value)} + className="pr-8" + step="0.1" + min="0" + max="100" + /> + + % + +
+

+ Your transaction will revert if the price changes unfavorably by more than this percentage. +

+
+
+
+
+ + )} + {/* Review Modal */} 0 ? swapAmounts[0].fee : null} onConfirm={async () => { await handleSwap(); setIsReviewModalOpen(false); diff --git a/src/components/swap-review-modal.tsx b/src/components/swap-review-modal.tsx index 6a4aec9..3746bf4 100644 --- a/src/components/swap-review-modal.tsx +++ b/src/components/swap-review-modal.tsx @@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button'; import { ArrowDown, X } from 'lucide-react'; import type { TokenDetails } from '@/hooks/usePartyPlanner'; import type { GasEstimate } from '@/hooks/usePartyPool'; +import { useToast } from '@/components/ui/toast'; interface SwapReviewModalProps { open: boolean; @@ -14,6 +15,7 @@ interface SwapReviewModalProps { toAmount: string; slippage: number; gasEstimate: GasEstimate | null; + fee: bigint | null; onConfirm: () => void; isSwapping: boolean; } @@ -27,11 +29,22 @@ export function SwapReviewModal({ toAmount, slippage, gasEstimate, + fee, onConfirm, isSwapping, }: SwapReviewModalProps) { if (!open) return null; + // Calculate exchange rate + const exchangeRate = fromAmount && toAmount && parseFloat(fromAmount) > 0 + ? (parseFloat(toAmount) / parseFloat(fromAmount)).toFixed(6) + : '0'; + + // Format fee + const feeFormatted = fee && toToken + ? (Number(fee) / Math.pow(10, toToken.decimals)).toFixed(6) + : '0'; + return ( <> {/* Backdrop */} @@ -80,11 +93,27 @@ export function SwapReviewModal({ {/* Details */}
+ {/* Exchange Rate */} +
+ Rate + + 1 {fromToken?.symbol} = {exchangeRate} {toToken?.symbol} + +
+ + {/* Fee */} +
+ Fee + {feeFormatted} {toToken?.symbol} +
+ + {/* Slippage */}
Slippage Tolerance {slippage}%
+ {/* Network Fee */} {gasEstimate && (
Network Fee @@ -93,13 +122,13 @@ export function SwapReviewModal({ )}
- {/* Confirm Button */} + {/* Approve and Swap Button */}
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..3405acd --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { createContext, useContext, useState, useCallback, ReactNode } from 'react'; +import { X, Loader2, CheckCircle2 } from 'lucide-react'; + +interface Toast { + id: string; + title: string; + description?: string; + type?: 'loading' | 'success' | 'error'; +} + +interface ToastContextType { + toasts: Toast[]; + addToast: (toast: Omit) => string; + removeToast: (id: string) => void; + updateToast: (id: string, toast: Partial>) => void; +} + +const ToastContext = createContext(undefined); + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substr(2, 9); + setToasts((prev) => [...prev, { ...toast, id }]); + + // Auto remove after 5 seconds for success/error toasts + if (toast.type === 'success' || toast.type === 'error') { + setTimeout(() => { + removeToast(id); + }, 5000); + } + + return id; + }, []); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + const updateToast = useCallback((id: string, updates: Partial>) => { + setToasts((prev) => + prev.map((toast) => + toast.id === id ? { ...toast, ...updates } : toast + ) + ); + + // Auto remove if updated to success/error + if (updates.type === 'success' || updates.type === 'error') { + setTimeout(() => { + removeToast(id); + }, 5000); + } + }, [removeToast]); + + return ( + + {children} +
+ {toasts.map((toast) => ( +
+ {toast.type === 'loading' && ( + + )} + {toast.type === 'success' && ( + + )} + +
+
{toast.title}
+ {toast.description && ( +
+ {toast.description} +
+ )} +
+ + +
+ ))} +
+
+ ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within ToastProvider'); + } + return context; +} \ No newline at end of file