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