diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 96f6767..97a694c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -18,6 +18,7 @@ import { type TimeFormat, type RefreshIntervalSeconds, REFRESH_INTERVAL_OPTIONS, + notifySettingsUpdated, } from "@/hooks/useSettings"; @@ -102,6 +103,9 @@ export default function SettingsPage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + // Wake up mounted useSettings() subscribers in this tab — the + // `storage` event only fires in *other* tabs (#586). + notifySettingsUpdated(); } catch { // Storage can be unavailable in private browsing or embedded webviews. } diff --git a/app/stream/[id]/page.tsx b/app/stream/[id]/page.tsx index 8740708..29f338f 100644 --- a/app/stream/[id]/page.tsx +++ b/app/stream/[id]/page.tsx @@ -27,6 +27,7 @@ import { import { useNetworkStatus } from "@/hooks/useNetworkStatus"; import { fromStroops, formatTimestamp, truncateAddress } from "@/lib/format"; import { tokenByAddress } from "@/lib/tokens"; +import { tryGetFactoryContractId } from "@/lib/env"; import { useSettings, MIN_REFRESH_INTERVAL_S } from "@/hooks/useSettings"; // ── Types ──────────────────────────────────────────────────────────────────── @@ -56,7 +57,7 @@ export default function StreamPage() { // NetworkTroubleBanner — defer to it rather than printing a raw // "circuit breaker open" string here. const { status: networkStatus } = useNetworkStatus(); - const { autoRefreshInterval } = useSettings(); + const { autoRefreshInterval, advancedMode } = useSettings(); const mounted = useRef(true); const loadSeq = useRef(0); @@ -451,7 +452,17 @@ export default function StreamPage() { Rate - {`${fromStroops(info.ratePerSecond)} / sec`} + + {`${fromStroops(info.ratePerSecond)} / sec`} + {advancedMode && ( + + {info.ratePerSecond.toString()} stroops/s + + )} + @@ -459,6 +470,11 @@ export default function StreamPage() { {fromStroops(totalDeposited)} + {advancedMode && ( + + {totalDeposited.toString()} stroops + + )} @@ -467,6 +483,11 @@ export default function StreamPage() { {fromStroops(info.withdrawn)} + {advancedMode && ( + + {info.withdrawn.toString()} stroops + + )} @@ -499,6 +520,87 @@ export default function StreamPage() { + {/* Advanced details — only when advanced mode is enabled (#586). + Surfaces raw stroop values and full contract addresses inline so + power users can verify on-chain state without leaving the page. */} + {advancedMode && ( + +

+ Advanced details +

+
+
+
+ Stream contract +
+
+ +
+
+
+
+ Token contract +
+
+ +
+
+ {tryGetFactoryContractId() && ( +
+
+ Factory contract +
+
+ +
+
+ )} +
+
+ Rate (stroops/s) +
+
+ {info.ratePerSecond.toString()} +
+
+
+
+ Withdrawable (stroops) +
+
+ {withdrawable.toString()} +
+
+
+
+ Withdrawn (stroops) +
+
+ {info.withdrawn.toString()} +
+
+
+
+ Start (unix) +
+
+ {info.startTime} +
+
+
+
+ End (unix) +
+
+ {info.endTime} +
+
+
+
+ )} + {/* Actions */} {(isSender || isRecipient) && (
diff --git a/app/streams/compare/page.tsx b/app/streams/compare/page.tsx index f44af68..8487779 100644 --- a/app/streams/compare/page.tsx +++ b/app/streams/compare/page.tsx @@ -8,6 +8,7 @@ import { ArrowLeft } from 'lucide-react'; import { Badge } from '@/components/ui/Badge'; import { StreamProgressBar } from '@/components/stream/StreamProgressBar'; import { useWallet } from '@/contexts/WalletContext'; +import { useSettings } from '@/hooks/useSettings'; import { getStreamAddress, getStreamInfo, type StreamInfo } from '@/lib/stream'; import { fromStroops, formatDuration, formatTimestamp, truncateAddress } from '@/lib/format'; import { tokenByAddress } from '@/lib/tokens'; @@ -124,6 +125,7 @@ function CompareView() { const searchParams = useSearchParams(); const idsParam = searchParams.get('ids'); const { publicKey, connected } = useWallet(); + const { advancedMode } = useSettings(); const [columns, setColumns] = useState([]); const [loading, setLoading] = useState(false); @@ -223,6 +225,39 @@ function CompareView() { ))} ))} + {/* Raw stroop rates + full token contract addresses — advanced mode only (#586) */} + {advancedMode && ( + <> + + + Rate (stroops/s) + + {columns.map((col) => ( + + {col.kind === 'error' ? ( + — + ) : ( + col.info.ratePerSecond.toString() + )} + + ))} + + + + Token contract + + {columns.map((col) => ( + + {col.kind === 'error' ? ( + — + ) : ( + col.info.token + )} + + ))} + + + )}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 94b92e3..e9a581b 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -9,8 +9,9 @@ import { ThemeToggle } from '@/components/ThemeToggle'; import { ErrorBoundary } from '@/components/ErrorBoundary'; import { NetworkBadge } from '@/components/NetworkBadge'; import { useSelectedNetwork } from '@/hooks/useSelectedNetwork'; +import { useSettings } from '@/hooks/useSettings'; -const NAV = [ +const BASE_NAV = [ { href: '/streams', label: 'Streams' }, { href: '/transactions', label: 'History' }, { href: '/create', label: 'Create' }, @@ -24,6 +25,15 @@ export function Navbar() { const mobileMenuRef = useRef(null); const previousFocusRef = useRef(null); const network = useSelectedNetwork(); + const { advancedMode } = useSettings(); + + // The side-by-side compare view is an advanced/power-user tool, so it is + // only linked in the nav when advanced mode is enabled (#586). The route + // itself stays directly reachable via /streams/compare?ids=… so shared + // links keep working. + const NAV = advancedMode + ? [...BASE_NAV, { href: '/streams/compare', label: 'Compare' }] + : BASE_NAV; // Close the mobile menu whenever the route changes (e.g. after a nav tap). useEffect(() => { diff --git a/components/stream/StreamCard.tsx b/components/stream/StreamCard.tsx index 3eb9f32..34f2395 100644 --- a/components/stream/StreamCard.tsx +++ b/components/stream/StreamCard.tsx @@ -7,6 +7,7 @@ import { StreamProgressBar } from "@/components/stream/StreamProgressBar"; import { fromStroops } from "@/lib/format"; import { CopyAddress } from "@/components/ui/CopyAddress"; import { getStreamNote } from "@/lib/stream-notes-storage"; +import { useSettings } from "@/hooks/useSettings"; interface StreamCardProps { id: string; @@ -38,6 +39,7 @@ export function StreamCard({ streamAddress, }: StreamCardProps) { const rateFormatted = fromStroops(ratePerSecond); + const { advancedMode } = useSettings(); const [note, setNote] = useState(null); // Load note from storage @@ -85,11 +87,22 @@ export function StreamCard({ {/* Rate number centered, green text */} -
- {rateFormatted}/s +
+
+ {rateFormatted}/s +
+ {/* Raw stroop rate — only in advanced mode (#586) */} + {advancedMode && ( + + {ratePerSecond.toString()} stroops/s + + )}
diff --git a/hooks/useSettings.ts b/hooks/useSettings.ts index 725aa87..f23a5e0 100644 --- a/hooks/useSettings.ts +++ b/hooks/useSettings.ts @@ -33,6 +33,13 @@ export interface PublicSettings { timeFormat: TimeFormat; /** 0 = disabled */ autoRefreshInterval: RefreshIntervalSeconds; + /** + * When true, advanced/debugging details are surfaced across the app: + * raw stroop amounts, full contract addresses, and the stream compare + * view link. Mirrors the `advancedMode` toggle on the settings page + * (issue #586). + */ + advancedMode: boolean; } const STORAGE_KEY = "conduit:settings"; @@ -40,6 +47,7 @@ const STORAGE_KEY = "conduit:settings"; const DEFAULTS: PublicSettings = { timeFormat: "absolute", autoRefreshInterval: 0, + advancedMode: false, }; function readFromStorage(): PublicSettings { @@ -55,6 +63,7 @@ function readFromStorage(): PublicSettings { ) ? (parsed.autoRefreshInterval as RefreshIntervalSeconds) : 0, + advancedMode: parsed.advancedMode === true, }; } catch { return DEFAULTS; @@ -62,18 +71,38 @@ function readFromStorage(): PublicSettings { } /** - * Returns current public settings, and re-reads them whenever another tab or - * the settings page writes to localStorage (via the `storage` event). + * Custom event fired on `window` after a same-tab settings write so mounted + * components re-read without waiting for a cross-tab `storage` event (which + * by spec never fires in the tab that performed the write). See #586. + */ +export const SETTINGS_UPDATED_EVENT = "conduit:settings-updated"; + +/** Notify mounted `useSettings` subscribers to re-read from localStorage. */ +export function notifySettingsUpdated(): void { + if (typeof window !== "undefined") { + window.dispatchEvent(new Event(SETTINGS_UPDATED_EVENT)); + } +} + +/** + * Returns current public settings, and re-reads them whenever any tab (via + * the `storage` event) or the settings page in this tab (via + * {@link notifySettingsUpdated}) writes to localStorage. */ export function useSettings(): PublicSettings { const [settings, setSettings] = useState(readFromStorage); useEffect(() => { + const refresh = () => setSettings(readFromStorage()); const onStorage = (e: StorageEvent) => { - if (e.key === STORAGE_KEY) setSettings(readFromStorage()); + if (e.key === STORAGE_KEY) refresh(); }; window.addEventListener("storage", onStorage); - return () => window.removeEventListener("storage", onStorage); + window.addEventListener(SETTINGS_UPDATED_EVENT, refresh); + return () => { + window.removeEventListener("storage", onStorage); + window.removeEventListener(SETTINGS_UPDATED_EVENT, refresh); + }; }, []); return settings;