diff --git a/apps/explorer/src/app/(explorer)/[network]/NetworkSyncWrapper.tsx b/apps/explorer/src/app/(explorer)/[network]/NetworkSyncWrapper.tsx new file mode 100644 index 00000000..217c9cb1 --- /dev/null +++ b/apps/explorer/src/app/(explorer)/[network]/NetworkSyncWrapper.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import type { ReactNode } from "react"; +import { useEffect } from "react"; +import useNetwork from "@/hooks/useNetwork"; +import type { Network } from "@/types"; + +interface NetworkSyncWrapperProps { + urlNetwork: Network; + children: ReactNode; +} + +// URL <-> context reconciliation for explorer routes. A fresh load or deep link +// adopts the URL's network. After an explicit selection (header selector, +// console switcher, wallet sync) the selection wins: a stale URL — typically +// reached via the browser back button after switching networks in the console — +// is rewritten instead of clobbering the selection. +export function NetworkSyncWrapper({ urlNetwork, children }: NetworkSyncWrapperProps) { + const { network, adoptNetwork, hasUserSelection } = useNetwork(); + const pathname = usePathname(); + const router = useRouter(); + + useEffect(() => { + if (network === urlNetwork) return; + if (hasUserSelection) { + router.replace(pathname.replace(`/${urlNetwork}`, `/${network}`)); + } else { + adoptNetwork(urlNetwork); + } + }, [urlNetwork, network, adoptNetwork, hasUserSelection, pathname, router]); + + return <>{children}; +} diff --git a/apps/explorer/src/app/[network]/accounts/[address]/page.tsx b/apps/explorer/src/app/(explorer)/[network]/accounts/[address]/page.tsx similarity index 100% rename from apps/explorer/src/app/[network]/accounts/[address]/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/accounts/[address]/page.tsx diff --git a/apps/explorer/src/app/[network]/accounts/page.tsx b/apps/explorer/src/app/(explorer)/[network]/accounts/page.tsx similarity index 100% rename from apps/explorer/src/app/[network]/accounts/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/accounts/page.tsx diff --git a/apps/explorer/src/app/[network]/layout.tsx b/apps/explorer/src/app/(explorer)/[network]/layout.tsx similarity index 100% rename from apps/explorer/src/app/[network]/layout.tsx rename to apps/explorer/src/app/(explorer)/[network]/layout.tsx diff --git a/apps/explorer/src/app/[network]/operators/page.tsx b/apps/explorer/src/app/(explorer)/[network]/operators/page.tsx similarity index 100% rename from apps/explorer/src/app/[network]/operators/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/operators/page.tsx diff --git a/apps/explorer/src/app/[network]/page.tsx b/apps/explorer/src/app/(explorer)/[network]/page.tsx similarity index 56% rename from apps/explorer/src/app/[network]/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/page.tsx index 40adcc62..28cea55c 100644 --- a/apps/explorer/src/app/[network]/page.tsx +++ b/apps/explorer/src/app/(explorer)/[network]/page.tsx @@ -1,8 +1,9 @@ -import { GlobalSearchBar, Stats, TopAccounts, TopOperators } from "@/components/Home"; +import { ConsoleHero, GlobalSearchBar, Stats, TopAccounts, TopOperators } from "@/components/Home"; function Page() { return ( <> + diff --git a/apps/explorer/src/app/[network]/rails/[id]/page.tsx b/apps/explorer/src/app/(explorer)/[network]/rails/[id]/page.tsx similarity index 100% rename from apps/explorer/src/app/[network]/rails/[id]/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/rails/[id]/page.tsx diff --git a/apps/explorer/src/app/[network]/rails/page.tsx b/apps/explorer/src/app/(explorer)/[network]/rails/page.tsx similarity index 100% rename from apps/explorer/src/app/[network]/rails/page.tsx rename to apps/explorer/src/app/(explorer)/[network]/rails/page.tsx diff --git a/apps/explorer/src/app/(explorer)/layout.tsx b/apps/explorer/src/app/(explorer)/layout.tsx new file mode 100644 index 00000000..93eb2259 --- /dev/null +++ b/apps/explorer/src/app/(explorer)/layout.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from "react"; +import { Navigation } from "@/components/shared"; + +type ExplorerLayoutProps = Readonly<{ children: ReactNode }>; + +export default function ExplorerLayout({ children }: ExplorerLayoutProps) { + return ( + <> + + {children} + + ); +} diff --git a/apps/explorer/src/app/page.tsx b/apps/explorer/src/app/(explorer)/page.tsx similarity index 100% rename from apps/explorer/src/app/page.tsx rename to apps/explorer/src/app/(explorer)/page.tsx diff --git a/apps/explorer/src/app/[network]/NetworkSyncWrapper.tsx b/apps/explorer/src/app/[network]/NetworkSyncWrapper.tsx deleted file mode 100644 index b3d99667..00000000 --- a/apps/explorer/src/app/[network]/NetworkSyncWrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { useEffect } from "react"; -import useNetwork from "@/hooks/useNetwork"; -import type { Network } from "@/types"; - -interface NetworkSyncWrapperProps { - urlNetwork: Network; - children: ReactNode; -} - -export function NetworkSyncWrapper({ urlNetwork, children }: NetworkSyncWrapperProps) { - const { network, setNetwork } = useNetwork(); - - useEffect(() => { - if (network !== urlNetwork) { - setNetwork(urlNetwork); - } - }, [urlNetwork, network, setNetwork]); - - return <>{children}; -} diff --git a/apps/explorer/src/app/console/authorizations/page.tsx b/apps/explorer/src/app/console/authorizations/page.tsx new file mode 100644 index 00000000..40a88718 --- /dev/null +++ b/apps/explorer/src/app/console/authorizations/page.tsx @@ -0,0 +1,9 @@ +"use client"; +import { OperatorApprovalsSection } from "@/components/UserConsole"; +import ConsoleAccountGate from "@/components/UserConsole/ConsoleAccountGate"; + +const AuthorizationsPage = () => ( + {({ account }) => } +); + +export default AuthorizationsPage; diff --git a/apps/explorer/src/app/console/layout.tsx b/apps/explorer/src/app/console/layout.tsx new file mode 100644 index 00000000..0e7da981 --- /dev/null +++ b/apps/explorer/src/app/console/layout.tsx @@ -0,0 +1,13 @@ +"use client"; + +import type { ReactNode } from "react"; +import ConsoleProviders from "@/components/UserConsole/ConsoleProviders"; +import ConsoleShell from "@/components/UserConsole/ConsoleShell"; + +export default function ConsoleLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/explorer/src/app/console/notifications/page.tsx b/apps/explorer/src/app/console/notifications/page.tsx index 8197fe3d..3c91af39 100644 --- a/apps/explorer/src/app/console/notifications/page.tsx +++ b/apps/explorer/src/app/console/notifications/page.tsx @@ -1,17 +1,14 @@ "use client"; import { Button } from "@filecoin-foundation/ui-filecoin/Button"; import { EmptyStateCard } from "@filecoin-foundation/ui-filecoin/EmptyStateCard"; -import { PageSection } from "@filecoin-foundation/ui-filecoin/PageSection"; import { WarningCircleIcon } from "@phosphor-icons/react"; import { useMutation } from "@tanstack/react-query"; -import { ArrowLeft, ChevronRight, WifiOff } from "lucide-react"; -import Link from "next/link"; +import { ChevronRight, WifiOff } from "lucide-react"; import type React from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { BaseError, UserRejectedRequestError } from "viem"; import { createSiweMessage, generateSiweNonce } from "viem/siwe"; import { useConnection, useSignMessage } from "wagmi"; -import ConsoleProviders from "@/components/UserConsole/ConsoleProviders"; import { AlertsActiveCard, AlertsOffCard, @@ -440,47 +437,32 @@ const NotificationsMain = () => { } return ( - -
-
- - - Back to console - -
-

- Email alerts -

- {showUpdateEmail && ( - - )} -
-

- Receive alerts when your account has less than 30 days of service runway remaining, so you can top up before - services are affected. -

+
+
+
+

+ Email alerts +

+ {showUpdateEmail && ( + + )}
- -
{renderContent()}
+

+ Receive alerts when your account has less than 30 days of service runway remaining, so you can top up before + services are affected. +

- + +
{renderContent()}
+
); }; -const NotificationsPage = () => ( - - - -); - -export default NotificationsPage; +export default NotificationsMain; diff --git a/apps/explorer/src/app/console/notifications/verify/page.tsx b/apps/explorer/src/app/console/notifications/verify/page.tsx index e110ea80..2246f6a0 100644 --- a/apps/explorer/src/app/console/notifications/verify/page.tsx +++ b/apps/explorer/src/app/console/notifications/verify/page.tsx @@ -1,5 +1,4 @@ "use client"; -import { PageSection } from "@filecoin-foundation/ui-filecoin/PageSection"; import { Card } from "@filecoin-pay/ui/components/card"; import { useQueryClient } from "@tanstack/react-query"; import { AlertCircle, CheckCircle2, Clock, Link2Off, Loader2 } from "lucide-react"; @@ -158,102 +157,98 @@ const VerifyContent = () => { }, [verifyState.type]); return ( - -
- - {verifyState.type === "loading" && } +
+ + {verifyState.type === "loading" && } - {verifyState.type === "success" && ( - } - color='green' - title='Alerts are now on' - description="Your email has been verified. We'll notify you when this account has less than 30 days of service runway remaining." - actions={ -
- - Back to Console - - - Manage alerts - -
- } - /> - )} + {verifyState.type === "success" && ( + } + color='green' + title='Alerts are now on' + description="Your email has been verified. We'll notify you when this account has less than 30 days of service runway remaining." + actions={ +
+ + Back to Console + + + Manage alerts + +
+ } + /> + )} - {verifyState.type === "not-found" && ( - } - color='amber' - title='This verification link is no longer available' - description='It may have expired or already been used. Return to alert settings to request a new verification email.' - actions={} - /> - )} + {verifyState.type === "not-found" && ( + } + color='amber' + title='This verification link is no longer available' + description='It may have expired or already been used. Return to alert settings to request a new verification email.' + actions={} + /> + )} - {verifyState.type === "rate-limited" && ( - } - color='amber' - title='Too many attempts' - description='Please wait a few minutes before trying again, then return to alert settings to request a new verification email.' - actions={} - /> - )} + {verifyState.type === "rate-limited" && ( + } + color='amber' + title='Too many attempts' + description='Please wait a few minutes before trying again, then return to alert settings to request a new verification email.' + actions={} + /> + )} - {verifyState.type === "error" && ( - } - color='red' - title="We couldn't verify your email" - description={ - verifyState.kind === "network" - ? "Check your internet connection and try again." - : "Something went wrong on our end. Try again in a moment, or return to alert settings to request a new verification email." - } - detail={verifyState.detail} - actions={ -
- - -
- } - /> - )} + {verifyState.type === "error" && ( + } + color='red' + title="We couldn't verify your email" + description={ + verifyState.kind === "network" + ? "Check your internet connection and try again." + : "Something went wrong on our end. Try again in a moment, or return to alert settings to request a new verification email." + } + detail={verifyState.detail} + actions={ +
+ + +
+ } + /> + )} - {verifyState.type === "missing-params" && ( - } - color='amber' - title='This verification link is incomplete' - description="We couldn't find the information needed to verify your email. Return to alert settings to request a new verification email." - actions={} - /> - )} -
-
- + {verifyState.type === "missing-params" && ( + } + color='amber' + title='This verification link is incomplete' + description="We couldn't find the information needed to verify your email. Return to alert settings to request a new verification email." + actions={} + /> + )} +
+
); }; const VerifyPage = () => ( - - - -
+ + + } > diff --git a/apps/explorer/src/app/console/page.tsx b/apps/explorer/src/app/console/page.tsx index 9e0096db..1a253592 100644 --- a/apps/explorer/src/app/console/page.tsx +++ b/apps/explorer/src/app/console/page.tsx @@ -1,112 +1,37 @@ "use client"; -import { LoadingStateCard } from "@filecoin-foundation/ui-filecoin/LoadingStateCard"; -import { PageSection } from "@filecoin-foundation/ui-filecoin/PageSection"; -import { AlertTriangle } from "lucide-react"; import { useMemo } from "react"; import { useConnection } from "wagmi"; -import { Balance, ChainSwitcher } from "@/components/shared"; -import { - AlertsBanner, - BetaWarning, - FundsSection, - OperatorApprovalsSection, - RailsSection, -} from "@/components/UserConsole"; -import ConsoleProviders from "@/components/UserConsole/ConsoleProviders"; -import { AccountNotFound, ErrorState, NotConnected, UnsupportedChain } from "@/components/UserConsole/States"; -import { useAccountDetails } from "@/hooks/useAccountDetails"; +import { AlertsBanner, FundsSection, RailsSection } from "@/components/UserConsole"; +import ConsoleAccountGate from "@/components/UserConsole/ConsoleAccountGate"; import { useNotificationStatus } from "@/hooks/useNotificationStatus"; import { getNetworkFromChainId, isNotificationsEligibleNetwork, isSupportedChainId } from "@/utils/network"; -const UserConsoleContent = () => { +const DashboardPage = () => { const { address, isConnected, chainId } = useConnection(); const walletNetwork = useMemo(() => getNetworkFromChainId(chainId), [chainId]); - const isUnsupportedChain = isConnected && chainId && !isSupportedChainId(chainId); - + const isUnsupportedChain = isConnected && chainId !== undefined && !isSupportedChainId(chainId); const isNotificationsEligible = isNotificationsEligibleNetwork(walletNetwork); const { data: notificationStatus, isError: notificationStatusError } = useNotificationStatus(address); const subscribed = notificationStatus?.subscribed ?? false; - const { - data: account, - isLoading, - isError, - error, - } = useAccountDetails(address || "", { networkOverride: walletNetwork }); - return ( - -
-
-

- Filecoin Pay Console -

- {isConnected && ( -
- {isUnsupportedChain ? ( - - - Unsupported Network - - ) : ( - <> - - {chainId && } - - )} -
- )} -
- - {/* Beta Warning */} - - - {/* Alerts Banner — hidden once subscribed */} - {isConnected && !isUnsupportedChain && isNotificationsEligible && !subscribed && !notificationStatusError && ( -
- -
- )} - - {/* Not Connected */} - {(!isConnected || !address) && } - - {/* Unsupported Chain */} - {isUnsupportedChain && } - - {/* Only show content if connected to supported chain */} - {isConnected && !isUnsupportedChain && ( +
+ {/* Alerts Banner — hidden once subscribed */} + {isConnected && !isUnsupportedChain && isNotificationsEligible && !subscribed && !notificationStatusError && ( + + )} + + + {({ account, address: connectedAddress }) => ( <> - {/* Loading */} - {isLoading && } - - {/* Account Not Found */} - {!isError && !isLoading && !account && } - - {!isLoading && address && account && ( - <> - - - - - )} - - {/* Error */} - {isError && } + + )} -
- - ); -}; - -const UserConsole = () => { - return ( - - - + +
); }; -export default UserConsole; +export default DashboardPage; diff --git a/apps/explorer/src/app/error.tsx b/apps/explorer/src/app/error.tsx index 2df41cfc..e2020b00 100644 --- a/apps/explorer/src/app/error.tsx +++ b/apps/explorer/src/app/error.tsx @@ -3,6 +3,7 @@ import { Button } from "@filecoin-foundation/ui-filecoin/Button"; import { ErrorStateCard } from "@filecoin-foundation/ui-filecoin/ErrorStateCard"; import { WarningCircleIcon } from "@phosphor-icons/react"; +import { Navigation } from "@/components/shared"; type ErrorProps = { error: Error & { digest?: string }; @@ -20,15 +21,19 @@ export default function ErrorBoundary({ error, reset }: ErrorProps) { : "An unexpected error occurred. Please try again."; return ( - - - + <> + {/* This boundary replaces segment layouts, so it must supply the header itself. */} + + + + + ); } diff --git a/apps/explorer/src/components/Home/ConsoleHero.tsx b/apps/explorer/src/components/Home/ConsoleHero.tsx new file mode 100644 index 00000000..89763cf8 --- /dev/null +++ b/apps/explorer/src/components/Home/ConsoleHero.tsx @@ -0,0 +1,37 @@ +import { PageSection } from "@filecoin-foundation/ui-filecoin/PageSection"; +import { ArrowRight } from "lucide-react"; +import Link from "next/link"; +import { PATHS } from "@/constants/paths"; + +// Console entry point rendered above search and stats on the network home page; +// complements the account button in the explorer header. +const ConsoleHero = () => ( + +
+
+ +
+ +); + +export default ConsoleHero; diff --git a/apps/explorer/src/components/Home/index.ts b/apps/explorer/src/components/Home/index.ts index e7ddf503..8cc978de 100644 --- a/apps/explorer/src/components/Home/index.ts +++ b/apps/explorer/src/components/Home/index.ts @@ -1,3 +1,4 @@ +export { default as ConsoleHero } from "./ConsoleHero"; export { default as GlobalSearchBar } from "./GlobalSearchBar"; export { default as Stats } from "./Stats"; export { default as TopAccounts } from "./TopAccounts"; diff --git a/apps/explorer/src/components/UserConsole/ConsoleAccountGate.tsx b/apps/explorer/src/components/UserConsole/ConsoleAccountGate.tsx new file mode 100644 index 00000000..fc76880a --- /dev/null +++ b/apps/explorer/src/components/UserConsole/ConsoleAccountGate.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { LoadingStateCard } from "@filecoin-foundation/ui-filecoin/LoadingStateCard"; +import type { Account } from "@filecoin-pay/types"; +import { useMemo } from "react"; +import { useConnection } from "wagmi"; +import { useAccountDetails } from "@/hooks/useAccountDetails"; +import { getNetworkFromChainId, isSupportedChainId } from "@/utils/network"; +import { AccountNotFound, ErrorState, NotConnected, UnsupportedChain } from "./States"; + +export type ConsoleAccountRenderProps = { + account: Account; + address: string; +}; + +type ConsoleAccountGateProps = { + children: (ctx: ConsoleAccountRenderProps) => React.ReactNode; +}; + +// Shared connection/account gating for console pages: resolves wallet state and +// account lookup, rendering the matching empty/error state. Children only render +// with a connected wallet on a supported chain and an indexed account. +const ConsoleAccountGate = ({ children }: ConsoleAccountGateProps) => { + const { address, isConnected, chainId } = useConnection(); + const walletNetwork = useMemo(() => getNetworkFromChainId(chainId), [chainId]); + const isUnsupportedChain = isConnected && chainId !== undefined && !isSupportedChainId(chainId); + + const { + data: account, + isLoading, + isError, + error, + } = useAccountDetails(address || "", { networkOverride: walletNetwork }); + + if (!isConnected || !address) return ; + if (isUnsupportedChain) return ; + if (isLoading) return ; + if (isError) return ; + if (!account) return ; + + return <>{children({ account, address })}; +}; + +export default ConsoleAccountGate; diff --git a/apps/explorer/src/components/UserConsole/ConsoleProviders.tsx b/apps/explorer/src/components/UserConsole/ConsoleProviders.tsx index 4ba5b970..8bcbbde0 100644 --- a/apps/explorer/src/components/UserConsole/ConsoleProviders.tsx +++ b/apps/explorer/src/components/UserConsole/ConsoleProviders.tsx @@ -1,11 +1,10 @@ import { midnightTheme, RainbowKitProvider } from "@rainbow-me/rainbowkit"; -import { WagmiProvider } from "wagmi"; +import WalletProviders from "@/components/shared/WalletProviders"; import { SynapseProvider } from "@/context/Synapse"; -import { config } from "@/services/wagmi/config"; const ConsoleProviders = ({ children }: { children: React.ReactNode }) => { return ( - + { > {children} - + ); }; diff --git a/apps/explorer/src/components/UserConsole/ConsoleShell.tsx b/apps/explorer/src/components/UserConsole/ConsoleShell.tsx new file mode 100644 index 00000000..85fbac27 --- /dev/null +++ b/apps/explorer/src/components/UserConsole/ConsoleShell.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Container } from "@filecoin-foundation/ui-filecoin/Container"; +import { PageSection } from "@filecoin-foundation/ui-filecoin/PageSection"; +import { Section } from "@filecoin-foundation/ui-filecoin/Section/Section"; +import { AlertTriangle } from "lucide-react"; +import { useEffect, useRef } from "react"; +import { useConnection, useSwitchChain } from "wagmi"; +import { Balance, ChainSwitcher, CustomConnectButton } from "@/components/shared"; +import { HomeLogoIconLink } from "@/components/shared/Navigation/components/HomeLogoIconLink"; +import { BetaWarning } from "@/components/UserConsole"; +import useNetwork from "@/hooks/useNetwork"; +import { supportedChains } from "@/services/wagmi/config"; +import { getNetworkFromChainId, isSupportedChainId } from "@/utils/network"; +import ConsoleSidebar from "./ConsoleSidebar"; + +// Account-area chrome: own top bar (logo + chain switcher + wallet chip in the +// same top-right position as the explorer's network selector), beta banner, and +// section sidebar. Replaces the explorer navigation on /console routes. +const ConsoleShell = ({ children }: { children: React.ReactNode }) => { + const { isConnected, chainId } = useConnection(); + const { network, setNetwork } = useNetwork(); + const { switchChain, isPending: isSwitchPending } = useSwitchChain(); + const isUnsupportedChain = isConnected && chainId !== undefined && !isSupportedChainId(chainId); + // Whether we already asked the wallet to follow the explorer's selection. + const switchRequestedRef = useRef(false); + + // Keeps the console selector in sync with the explorer's. On entry the + // explorer selection wins: the wallet is asked once to switch to it. After + // that (or on rejection) the wallet chain is the source of truth and the + // shared network context follows it, so leaving the console returns to the + // same network the wallet is on. + useEffect(() => { + if (!isConnected) { + switchRequestedRef.current = false; + return; + } + if (chainId === undefined || !isSupportedChainId(chainId)) return; + const walletNetwork = getNetworkFromChainId(chainId); + if (walletNetwork === network) { + switchRequestedRef.current = true; + return; + } + if (!switchRequestedRef.current) { + switchRequestedRef.current = true; + const target = supportedChains.find((chain) => chain.slug === network); + if (target) { + switchChain({ chainId: target.id }, { onError: () => setNetwork(walletNetwork) }); + } + return; + } + if (!isSwitchPending) { + setNetwork(walletNetwork); + } + }, [isConnected, chainId, network, setNetwork, switchChain, isSwitchPending]); + + return ( + <> +
+ +
+ +
+ {!isConnected && ( + <> + + + + )} + {isConnected && isUnsupportedChain && ( + + + Unsupported Network + + )} + {isConnected && !isUnsupportedChain && ( + <> + {chainId !== undefined && } + + + )} +
+
+
+
+ + +
+ +
+ +
{children}
+
+
+
+ + ); +}; + +export default ConsoleShell; diff --git a/apps/explorer/src/components/UserConsole/ConsoleSidebar.tsx b/apps/explorer/src/components/UserConsole/ConsoleSidebar.tsx new file mode 100644 index 00000000..efd5d516 --- /dev/null +++ b/apps/explorer/src/components/UserConsole/ConsoleSidebar.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { clsx } from "clsx"; +import { Bell, Compass, LayoutGrid, type LucideIcon, ShieldCheck } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useConnection } from "wagmi"; +import { PATHS } from "@/constants/paths"; +import useNetwork from "@/hooks/useNetwork"; +import { useNotificationStatus } from "@/hooks/useNotificationStatus"; + +type SidebarItem = { + label: string; + href: string; + icon: LucideIcon; + badge?: string; +}; + +const ConsoleSidebar = () => { + const pathname = usePathname(); + const { network } = useNetwork(); + const { address, isConnected } = useConnection(); + const { data: notificationStatus } = useNotificationStatus(address); + + // Only badge a definitive "not subscribed"; while status is unknown the row + // stays unbadged instead of flashing OFF. + const alertsBadge = isConnected && notificationStatus && !notificationStatus.subscribed ? "OFF" : undefined; + + const items: SidebarItem[] = [ + { label: "Dashboard", href: PATHS.CONSOLE.path, icon: LayoutGrid }, + { label: PATHS.CONSOLE_AUTHORIZATIONS.label, href: PATHS.CONSOLE_AUTHORIZATIONS.path, icon: ShieldCheck }, + { label: PATHS.CONSOLE_ALERTS.label, href: PATHS.CONSOLE_ALERTS.path, icon: Bell, badge: alertsBadge }, + ]; + + return ( + + ); +}; + +export default ConsoleSidebar; diff --git a/apps/explorer/src/components/shared/AccountButton/index.tsx b/apps/explorer/src/components/shared/AccountButton/index.tsx new file mode 100644 index 00000000..ffbdc194 --- /dev/null +++ b/apps/explorer/src/components/shared/AccountButton/index.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { Button } from "@filecoin-foundation/ui-filecoin/Button"; +import { Wallet } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useAccount } from "wagmi"; +import { formatAddress } from "@/utils/formatter"; +import WalletProviders from "../WalletProviders"; + +// Header entry point to the console. Disconnected users see a "Connect Wallet" +// call to action; connected users see their address as a chip. Both navigate to +// /console — the console owns the actual wallet-connection flow, so there is a +// single connect surface across the app. +const AccountButtonInner = () => { + const router = useRouter(); + const { address, isConnected } = useAccount(); + + if (isConnected && address) { + return ( + + + {formatAddress(address)} + + ); + } + + return ( + + ); +}; + +const AccountButton = () => ( + + + +); + +export default AccountButton; diff --git a/apps/explorer/src/components/shared/ChainSwitcher.tsx b/apps/explorer/src/components/shared/ChainSwitcher.tsx index 25411772..a2b416e6 100644 --- a/apps/explorer/src/components/shared/ChainSwitcher.tsx +++ b/apps/explorer/src/components/shared/ChainSwitcher.tsx @@ -8,16 +8,21 @@ import { } from "@filecoin-pay/ui/components/dropdown-menu"; import { ChevronDown, Globe } from "lucide-react"; import { useSwitchChain } from "wagmi"; +import useNetwork from "@/hooks/useNetwork"; import { supportedChains } from "@/services/wagmi/config"; import { getNetworkFromChainId } from "@/utils/network"; interface ChainSwitcherProps { - chainId: number; + /** Connected wallet chain; omit when no wallet is connected. */ + chainId?: number; } +// With a connected wallet the selector switches the wallet chain; without one it +// drives the explorer-wide network context instead. const ChainSwitcher = ({ chainId }: ChainSwitcherProps) => { const { switchChain } = useSwitchChain(); - const currentNetwork = getNetworkFromChainId(chainId); + const { network, setNetwork } = useNetwork(); + const currentNetwork = chainId !== undefined ? getNetworkFromChainId(chainId) : network; const currentChain = supportedChains.find((c) => c.slug === currentNetwork) ?? supportedChains[0]; return ( @@ -35,8 +40,8 @@ const ChainSwitcher = ({ chainId }: ChainSwitcherProps) => { {supportedChains.map((chain) => ( switchChain({ chainId: chain.id })} - className={chain.id === chainId ? "font-semibold" : ""} + onClick={() => (chainId !== undefined ? switchChain({ chainId: chain.id }) : setNetwork(chain.slug))} + className={chain.slug === currentNetwork ? "font-semibold" : ""} > {chain.label} diff --git a/apps/explorer/src/components/shared/Navigation/DesktopNavigation.tsx b/apps/explorer/src/components/shared/Navigation/DesktopNavigation.tsx index e16f076c..899a9e02 100644 --- a/apps/explorer/src/components/shared/Navigation/DesktopNavigation.tsx +++ b/apps/explorer/src/components/shared/Navigation/DesktopNavigation.tsx @@ -1,13 +1,12 @@ "use client"; import { NavigationMainLink } from "@filecoin-foundation/ui-filecoin/Navigation/NavigationMainLink"; +import AccountButton from "@/components/shared/AccountButton"; import { useNavigationItems } from "@/hooks/useNavigationItems"; -import useShowNetworkOptions from "@/hooks/useShowNetworkOptions"; import NetworkOptions from "./components/NetworkOptions"; export function DesktopNavigation() { const { headerNavigationItems } = useNavigationItems(); - const showNetworkOptions = useShowNetworkOptions(); return (
@@ -17,11 +16,12 @@ export function DesktopNavigation() { ))} - {showNetworkOptions && ( -
- -
- )} +
  • + +
  • +
  • + +
  • ); diff --git a/apps/explorer/src/components/shared/Navigation/Navigation.tsx b/apps/explorer/src/components/shared/Navigation/Navigation.tsx index 9efdf6c7..e91d6773 100644 --- a/apps/explorer/src/components/shared/Navigation/Navigation.tsx +++ b/apps/explorer/src/components/shared/Navigation/Navigation.tsx @@ -4,7 +4,6 @@ import { Container } from "@filecoin-foundation/ui-filecoin/Container"; import { MobileNavigation } from "@filecoin-foundation/ui-filecoin/Navigation/MobileNavigation"; import { Section, type SectionProps } from "@filecoin-foundation/ui-filecoin/Section/Section"; import { useNavigationItems } from "@/hooks/useNavigationItems"; -import useShowNetworkOptions from "@/hooks/useShowNetworkOptions"; import { HomeLogoIconLink } from "./components/HomeLogoIconLink"; import NetworkOptions from "./components/NetworkOptions"; import { DesktopNavigation } from "./DesktopNavigation"; @@ -15,7 +14,6 @@ type NavigationProps = { function Navigation({ backgroundVariant }: NavigationProps) { const { mobileNavigationItems } = useNavigationItems(); - const showNetworkOptions = useShowNetworkOptions(); return (
    @@ -29,11 +27,9 @@ function Navigation({ backgroundVariant }: NavigationProps) {
    - {showNetworkOptions && ( -
    - -
    - )} +
    + +
    diff --git a/apps/explorer/src/components/shared/Navigation/components/NetworkOptions.tsx b/apps/explorer/src/components/shared/Navigation/components/NetworkOptions.tsx index fc5e7cc3..b459088d 100644 --- a/apps/explorer/src/components/shared/Navigation/components/NetworkOptions.tsx +++ b/apps/explorer/src/components/shared/Navigation/components/NetworkOptions.tsx @@ -4,15 +4,20 @@ import { usePathname, useRouter } from "next/navigation"; import { getChain } from "@/constants/chains"; import useNetwork from "@/hooks/useNetwork"; import { supportedChains } from "@/services/wagmi/config"; +import type { Network } from "@/types"; const NetworkOptions = () => { const pathname = usePathname(); const router = useRouter(); - const { network } = useNetwork(); + const { network, setNetwork } = useNetwork(); - const handleNetworkChange = (newNetwork: string) => { + const handleNetworkChange = (newNetwork: Network) => { if (network === newNetwork) return; + // Record the selection before navigating so the route-level sync sees a + // settled context instead of rewriting the URL back to the old network. + setNetwork(newNetwork); + const pathParts = pathname.split("/").filter(Boolean); const firstSegment = pathParts[0]; diff --git a/apps/explorer/src/components/shared/SiteLayout.tsx b/apps/explorer/src/components/shared/SiteLayout.tsx index 4382b5de..93ff49e9 100644 --- a/apps/explorer/src/components/shared/SiteLayout.tsx +++ b/apps/explorer/src/components/shared/SiteLayout.tsx @@ -3,7 +3,7 @@ import localFont from "next/font/local"; import Script from "next/script"; import type { ReactNode } from "react"; -import { Footer, Navigation, Providers } from "@/components/shared"; +import { Footer, Providers } from "@/components/shared"; const funnelSans = localFont({ src: "../../fonts/Funnel_Sans/FunnelSans[wght].woff2", @@ -42,7 +42,6 @@ function SiteLayout({ children }: SiteLayoutProps) { )} > -
    {children}