Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/register-dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { GlobalRegistrator } from '@happy-dom/global-registrator';
GlobalRegistrator.register();
8 changes: 3 additions & 5 deletions apps/web/setup-tests.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import "./register-dom"
import "@testing-library/jest-dom/vitest"
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./test/msw/server";
import { cleanup } from "@testing-library/react";

GlobalRegistrator.register();

afterEach(async () => {
const { cleanup } = await import("@testing-library/react");
afterEach(() => {
cleanup();
});

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/features/wallet/components/AccountBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function AccountBadge({ address, className, ...props }: AccountBadgeProps
const balanceData = useBalance()
const balance = balanceData?.xlm
const isLoading = balanceData?.isLoading ?? false
const { isMainnet } = useNetwork()
const { displayLabel } = useNetwork()

useEffect(() => {
if (!open) return
Expand Down Expand Up @@ -103,7 +103,7 @@ export function AccountBadge({ address, className, ...props }: AccountBadgeProps
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-muted p-3">
<span className="text-muted-foreground">Network</span>
<span>{isMainnet ? "Mainnet" : "Testnet"}</span>
<span>{displayLabel}</span>
</div>

<div className="flex items-center justify-between rounded-xl border border-border/70 bg-muted p-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("ConnectButton - Disconnected State", () => {
const dialog = screen.getByRole("dialog")
expect(dialog).toBeInTheDocument()

const dialogTitle = screen.getByText("Connect Wallet")
const dialogTitle = screen.getByRole("heading", { name: "Connect Wallet" })
expect(dialogTitle).toBeInTheDocument()
})

Expand Down Expand Up @@ -259,7 +259,7 @@ describe("ConnectButton - Disconnected State", () => {
render(<ConnectButton />)

// Account badge should be rendered instead
expect(screen.getByText(/GAAAAAA/)).toBeInTheDocument()
expect(screen.getByText(/GAAAAA/)).toBeInTheDocument()
})
})

Expand Down Expand Up @@ -356,7 +356,7 @@ describe("ConnectButton - Disconnected State", () => {
expect(
screen.queryByRole("button", { name: /Connect wallet/i }),
).not.toBeInTheDocument()
expect(screen.getByText(/GAAAAAA/)).toBeInTheDocument()
expect(screen.getByText(/GAAAAA/)).toBeInTheDocument()
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const SESSION_KEY = "so4-network-mismatch-dismissed"

export function NetworkMismatchBanner() {
const { pathname } = useLocation()
const { mismatch, network } = useNetwork()
const { mismatch, displayLabel } = useNetwork()
const { status } = useWalletStore()
const [dismissed, setDismissed] = useState(
() => sessionStorage.getItem(SESSION_KEY) === "1"
Expand All @@ -18,7 +18,7 @@ export function NetworkMismatchBanner() {
if (pathname === "/") return null
if (!mismatch || status !== "connected" || dismissed) return null

const walletLabel = network === "mainnet" ? "Mainnet" : "Testnet"
const walletLabel = displayLabel
const appLabel = NETWORK.name === "mainnet" ? "Mainnet" : "Testnet"

function dismiss() {
Expand Down
132 changes: 132 additions & 0 deletions apps/web/src/features/wallet/hooks/useNetwork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, expect, it, beforeEach } from "vitest"
import { renderHook } from "@testing-library/react"
import { useNetwork, normalizeNetwork } from "./useNetwork"
import { useWalletStore } from "../store/wallet-store"
import { NETWORK } from "@/app/config/network"

describe("normalizeNetwork", () => {
it("should normalize Stellar Testnet strings", () => {
expect(normalizeNetwork("testnet")).toBe("testnet")
expect(normalizeNetwork("TESTNET")).toBe("testnet")
expect(normalizeNetwork("Test SDF Network ; September 2015")).toBe("testnet")
expect(normalizeNetwork(" testnet ")).toBe("testnet")
})

it("should normalize Stellar Mainnet/Public strings", () => {
expect(normalizeNetwork("public")).toBe("mainnet")
expect(normalizeNetwork("mainnet")).toBe("mainnet")
expect(normalizeNetwork("PUBLIC")).toBe("mainnet")
expect(normalizeNetwork("Public Global Stellar Network ; September 2015")).toBe("mainnet")
expect(normalizeNetwork(" public ")).toBe("mainnet")
})

it("should return unknown for other strings", () => {
expect(normalizeNetwork("unknown")).toBe("unknown")
expect(normalizeNetwork("custom")).toBe("unknown")
expect(normalizeNetwork("arbitrary")).toBe("unknown")
})

it("should return unknown for empty or missing inputs", () => {
expect(normalizeNetwork(null)).toBe("unknown")
expect(normalizeNetwork(undefined)).toBe("unknown")
expect(normalizeNetwork("")).toBe("unknown")
expect(normalizeNetwork(" ")).toBe("unknown")
})
})

describe("useNetwork Hook", () => {
beforeEach(() => {
useWalletStore.setState({
address: null,
walletId: null,
status: "disconnected",
pendingTransactionXdr: null,
network: "testnet",
})
})

describe("when status is disconnected", () => {
it("should always return mismatch as false", () => {
// Set network to a mismatching network but status disconnected
const testNetwork = NETWORK.name === "mainnet" ? "testnet" : "mainnet"
useWalletStore.setState({ status: "disconnected", network: testNetwork })

const { result } = renderHook(() => useNetwork())

expect(result.current.mismatch).toBe(false)
})
})

describe("when status is connected", () => {
it("should return mismatch as false if wallet network matches app network", () => {
useWalletStore.setState({ status: "connected", network: NETWORK.name })

const { result } = renderHook(() => useNetwork())

expect(result.current.mismatch).toBe(false)
})

it("should return mismatch as true if wallet network does not match app network", () => {
const opposingNetwork = NETWORK.name === "mainnet" ? "testnet" : "mainnet"
useWalletStore.setState({ status: "connected", network: opposingNetwork })

const { result } = renderHook(() => useNetwork())

expect(result.current.mismatch).toBe(true)
})

it("should return mismatch as true for unknown wallet networks", () => {
useWalletStore.setState({ status: "connected", network: "custom-network" })

const { result } = renderHook(() => useNetwork())

expect(result.current.mismatch).toBe(true)
})
})

describe("network classification and labels", () => {
it("should expose correct details for testnet value", () => {
useWalletStore.setState({ network: "Test SDF Network ; September 2015" })

const { result } = renderHook(() => useNetwork())

expect(result.current.normalizedNetwork).toBe("testnet")
expect(result.current.isTestnet).toBe(true)
expect(result.current.isMainnet).toBe(false)
expect(result.current.displayLabel).toBe("Testnet")
})

it("should expose correct details for mainnet/public value", () => {
useWalletStore.setState({ network: "Public Global Stellar Network ; September 2015" })

const { result } = renderHook(() => useNetwork())

expect(result.current.normalizedNetwork).toBe("mainnet")
expect(result.current.isTestnet).toBe(false)
expect(result.current.isMainnet).toBe(true)
expect(result.current.displayLabel).toBe("Mainnet")
})

it("should expose correct details for unknown value", () => {
useWalletStore.setState({ network: "unknown" })

const { result } = renderHook(() => useNetwork())

expect(result.current.normalizedNetwork).toBe("unknown")
expect(result.current.isTestnet).toBe(false)
expect(result.current.isMainnet).toBe(false)
expect(result.current.displayLabel).toBe("Unknown")
})

it("should expose correct details for missing/null value", () => {
useWalletStore.setState({ network: null })

const { result } = renderHook(() => useNetwork())

expect(result.current.normalizedNetwork).toBe("unknown")
expect(result.current.isTestnet).toBe(false)
expect(result.current.isMainnet).toBe(false)
expect(result.current.displayLabel).toBe("Unknown")
})
})
})
47 changes: 43 additions & 4 deletions apps/web/src/features/wallet/hooks/useNetwork.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
import { useWalletStore } from "../store/wallet-store"
import { NETWORK } from "@/app/config/network"

export function normalizeNetwork(network: string | null | undefined): "testnet" | "mainnet" | "unknown" {
if (!network) return "unknown"

const normalized = network.trim().toLowerCase()

if (
normalized === "testnet" ||
normalized === "test sdf network ; september 2015"
) {
return "testnet"
}

if (
normalized === "public" ||
normalized === "mainnet" ||
normalized === "public global stellar network ; september 2015"
) {
return "mainnet"
}

return "unknown"
}

export function useNetwork() {
const { network, status } = useWalletStore()

const isTestnet = network === "testnet"
const isMainnet = network === "mainnet"
const normalizedNetwork = normalizeNetwork(network)
const isTestnet = normalizedNetwork === "testnet"
const isMainnet = normalizedNetwork === "mainnet"

// Mismatch only meaningful when a wallet is connected
const mismatch = status === "connected" && network !== NETWORK.name
const mismatch = status === "connected" && normalizedNetwork !== NETWORK.name

const displayLabel =
normalizedNetwork === "testnet"
? "Testnet"
: normalizedNetwork === "mainnet"
? "Mainnet"
: "Unknown"

return { network, isTestnet, isMainnet, mismatch }
return {
network,
normalizedNetwork,
isTestnet,
isMainnet,
mismatch,
displayLabel,
}
}
4 changes: 2 additions & 2 deletions apps/web/src/features/wallet/store/wallet-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { create } from "zustand"
import { persist } from "zustand/middleware"

type WalletStatus = "disconnected" | "connecting" | "connected" | "error"
type Network = "testnet" | "mainnet"
type Network = string | null

type WalletStore = {
address: string | null
Expand All @@ -17,7 +17,7 @@ type WalletStore = {
}

const DEFAULT_NETWORK: Network =
(import.meta.env.VITE_NETWORK as Network) === "mainnet" ? "mainnet" : "testnet"
(import.meta.env.VITE_NETWORK as string) === "mainnet" ? "mainnet" : "testnet"

export const useWalletStore = create<WalletStore>()(
persist(
Expand Down