diff --git a/src/components/CrossChainPayment.tsx b/src/components/CrossChainPayment.tsx index a83fac0..eba1866 100644 --- a/src/components/CrossChainPayment.tsx +++ b/src/components/CrossChainPayment.tsx @@ -53,11 +53,38 @@ function ChainSelector({ function FeeBreakdown({ amount, estimate, + loading, + error, }: { amount: string; estimate: FeeEstimate | null; + loading: boolean; + error: boolean; }) { - if (!amount || parseFloat(amount) <= 0 || !estimate) return null; + if (!amount || parseFloat(amount) <= 0) return null; + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + if (error || !estimate) { + return ( +
+ Estimate unavailable +
+ ); + } return (
@@ -243,6 +270,8 @@ export default function CrossChainPayment({ invoiceId, stellarDestination }: Pro const [chain, setChain] = useState("ethereum"); const [amount, setAmount] = useState(""); const [estimate, setEstimate] = useState(null); + const [estimateLoading, setEstimateLoading] = useState(false); + const [estimateError, setEstimateError] = useState(false); // Wallet const [walletAddress, setWalletAddress] = useState(null); @@ -260,11 +289,39 @@ export default function CrossChainPayment({ invoiceId, stellarDestination }: Pro // Recalculate fee estimate whenever chain or amount changes useEffect(() => { - if (amount && parseFloat(amount) > 0) { - setEstimate(estimateBridgeFee(chain, amount)); - } else { + if (!amount || parseFloat(amount) <= 0) { setEstimate(null); + setEstimateLoading(false); + setEstimateError(false); + return; } + + let cancelled = false; + setEstimateLoading(true); + setEstimateError(false); + + // Debounce briefly and simulate fetching a fresh estimate for the + // selected chain/amount. + const timer = setTimeout(() => { + try { + const result = estimateBridgeFee(chain, amount); + if (!cancelled) { + setEstimate(result); + setEstimateLoading(false); + } + } catch { + if (!cancelled) { + setEstimate(null); + setEstimateError(true); + setEstimateLoading(false); + } + } + }, 300); + + return () => { + cancelled = true; + clearTimeout(timer); + }; }, [chain, amount]); // Reset wallet when chain changes @@ -408,7 +465,12 @@ export default function CrossChainPayment({ invoiceId, stellarDestination }: Pro
{/* Fee estimate */} - + )} diff --git a/src/components/NetworkStatus.tsx b/src/components/NetworkStatus.tsx index 3008c04..4d5d2aa 100644 --- a/src/components/NetworkStatus.tsx +++ b/src/components/NetworkStatus.tsx @@ -63,11 +63,19 @@ export default function NetworkStatus() { const [checkedAt, setCheckedAt] = useState(null); const [dismissed, setDismissed] = useState(false); const [secondsAgo, setSecondsAgo] = useState(0); + const [justReconnected, setJustReconnected] = useState(false); + const [browserOffline, setBrowserOffline] = useState(false); const timerRef = useRef | null>(null); const poll = useCallback(async () => { const s = await checkHealth(); - setStatus(s); + setStatus((prev) => { + if (prev === "offline" && s !== "offline") { + setJustReconnected(true); + setTimeout(() => setJustReconnected(false), 4000); + } + return s; + }); setCheckedAt(new Date()); setSecondsAgo(0); if (s !== "offline") setDismissed(false); @@ -88,12 +96,26 @@ export default function NetworkStatus() { const onVisibility = () => { document.hidden ? stop() : (poll(), start()); }; + const onOnline = () => { + setBrowserOffline(false); + poll(); + }; + const onOffline = () => { + setBrowserOffline(true); + setStatus("offline"); + setCheckedAt(new Date()); + setDismissed(false); + }; start(); document.addEventListener("visibilitychange", onVisibility); + window.addEventListener("online", onOnline); + window.addEventListener("offline", onOffline); return () => { stop(); document.removeEventListener("visibilitychange", onVisibility); + window.removeEventListener("online", onOnline); + window.removeEventListener("offline", onOffline); }; }, [poll]); @@ -159,7 +181,9 @@ export default function NetworkStatus() { className="fixed top-0 left-0 right-0 z-[9999] flex items-center justify-between gap-3 bg-red-900/95 border-b border-red-700 px-4 py-2 text-sm text-red-100" > - Network issues detected — transactions may fail + {browserOffline + ? "You are offline — transactions may fail" + : "Network issues detected — transactions may fail"} {NETWORK !== "Unknown" && ( ({NETWORK}) )} @@ -175,6 +199,16 @@ export default function NetworkStatus() {
)} + {/* ── "Back online" banner ── */} + {status !== "offline" && justReconnected && ( +
+ Back online +
+ )} + {/* ── Network badge + RPC health ── */}
{/* Permanent Mainnet / Testnet badge */} diff --git a/src/components/ReputationBadge.tsx b/src/components/ReputationBadge.tsx index 5be4f40..2f015af 100644 --- a/src/components/ReputationBadge.tsx +++ b/src/components/ReputationBadge.tsx @@ -37,16 +37,34 @@ export default function ReputationBadge({ address }: Props) { const isVerified = reputation !== null && reputation > 0; + const tooltipText = + "Reputation is calculated from on-time payments, dispute win rate, " + + "and total transaction volume. Consistent, timely activity raises your score."; + return ( - - {isVerified ? "✓" : "○"} + + + {isVerified ? "✓" : "○"} + + {/* Tooltip: shown on hover and keyboard focus */} + + + Reputation: {reputation ?? 0} + + {tooltipText} + ); } diff --git a/src/components/SimulationModeToggle.tsx b/src/components/SimulationModeToggle.tsx index b215862..ae13422 100644 --- a/src/components/SimulationModeToggle.tsx +++ b/src/components/SimulationModeToggle.tsx @@ -6,6 +6,7 @@ import { getSimulationMode, setSimulationMode } from "@/lib/simulationMode"; export default function SimulationModeToggle() { const [enabled, setEnabled] = useState(false); const [mounted, setMounted] = useState(false); + const [confirming, setConfirming] = useState(false); useEffect(() => { setEnabled(getSimulationMode()); @@ -22,18 +23,77 @@ export default function SimulationModeToggle() { if (!mounted) return null; + const handleToggleClick = () => { + if (enabled) { + // Turning simulation mode OFF (switching to live mode) — require confirmation. + setConfirming(true); + } else { + // Turning simulation mode ON — safe, no confirmation needed. + setSimulationMode(true); + } + }; + + const handleConfirmLiveMode = () => { + setSimulationMode(false); + setConfirming(false); + }; + + const handleCancel = () => { + setConfirming(false); + }; + return ( - + <> + + + {confirming && ( +
+
+

+ Switch to live mode? +

+

+ You are switching to live mode — all actions will use real + funds. +

+
+ + +
+
+
+ )} + ); }