From 63aaf0887f2fd30d446adcd84e36de25804512b8 Mon Sep 17 00:00:00 2001 From: samad13 Date: Thu, 18 Jun 2026 19:33:14 +0100 Subject: [PATCH] fixes --- src/components/WalletConnectButton.tsx | 138 ++++++++++++++++ .../__tests__/WalletConnectButton.test.tsx | 99 +++++++++++ src/components/landing-page/Navigation.tsx | 154 ++++++++++-------- .../__tests__/Navigation.test.tsx | 21 +++ src/hooks/useWallet.ts | 13 +- 5 files changed, 357 insertions(+), 68 deletions(-) create mode 100644 src/components/WalletConnectButton.tsx create mode 100644 src/components/__tests__/WalletConnectButton.test.tsx create mode 100644 src/components/landing-page/__tests__/Navigation.test.tsx diff --git a/src/components/WalletConnectButton.tsx b/src/components/WalletConnectButton.tsx new file mode 100644 index 0000000..397a92e --- /dev/null +++ b/src/components/WalletConnectButton.tsx @@ -0,0 +1,138 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { useWallet } from "@/hooks/useWallet"; + +const truncateAddress = (address: string) => + address ? `${address.slice(0, 4)}…${address.slice(-4)}` : ""; + +const walletErrorMessage = (error: string | null) => { + if (!error) return ""; + + const normalized = error.toLowerCase(); + + if ( + normalized.includes("freighter") || + normalized.includes("not installed") || + normalized.includes("not found") + ) { + return "Freighter is not available. Install it from freighter.app and refresh to continue."; + } + + if ( + normalized.includes("reject") || + normalized.includes("denied") || + normalized.includes("cancel") + ) { + return "Connection canceled in Freighter. Try again when you are ready."; + } + + return "Unable to connect your wallet. Try again or check Freighter in your browser."; +}; + +export const WalletConnectButton: React.FC = () => { + const { connected, address, connect, disconnect, error, connecting } = + useWallet(); + const [menuOpen, setMenuOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setMenuOpen(false); + } + }; + + const handleEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEsc); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEsc); + }; + }, []); + + const errorMessage = walletErrorMessage(error); + + return ( +
+ {connected ? ( +
+ + + {menuOpen && ( +
+
+ +
+
+ )} +
+ ) : ( + + )} + + {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} +
+ ); +}; diff --git a/src/components/__tests__/WalletConnectButton.test.tsx b/src/components/__tests__/WalletConnectButton.test.tsx new file mode 100644 index 0000000..9254658 --- /dev/null +++ b/src/components/__tests__/WalletConnectButton.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, beforeEach, vi } from "vitest"; + +import { WalletConnectButton } from "@/components/WalletConnectButton"; +import { getAddress } from "@stellar/freighter-api"; + +vi.mock("@stellar/freighter-api", () => ({ + getAddress: vi.fn(), +})); + +const mockedGetAddress = vi.mocked(getAddress); + +describe("WalletConnectButton", () => { + beforeEach(() => { + mockedGetAddress.mockReset(); + }); + + it("shows a connect button and Freighter error when Freighter is not installed", async () => { + mockedGetAddress.mockResolvedValueOnce({ + error: "Freighter not installed", + }); + + render(); + + const connectButton = await screen.findByRole("button", { + name: /connect wallet/i, + }); + expect(connectButton).toBeEnabled(); + + expect(await screen.findByRole("alert")).toHaveTextContent( + /Freighter is not available/i, + ); + }); + + it("shows connecting state while the connection request is pending", async () => { + let resolvePromise: (value: unknown) => void = () => undefined; + mockedGetAddress + .mockResolvedValueOnce({ error: "Freighter not installed" }) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolvePromise = resolve; + }), + ); + + render(); + + const connectButton = await screen.findByRole("button", { + name: /connect wallet/i, + }); + fireEvent.click(connectButton); + + expect(connectButton).toBeDisabled(); + expect(connectButton).toHaveTextContent(/connecting/i); + + resolvePromise({ address: "GABCD1234EFGH5678" }); + await waitFor(() => + expect(screen.getByText(/GABC…5678/)).toBeInTheDocument(), + ); + }); + + it("renders connected address and allows disconnecting", async () => { + mockedGetAddress.mockResolvedValueOnce({ address: "GABCD1234EFGH5678" }); + + render(); + + await waitFor(() => + expect(screen.getByText(/GABC…5678/)).toBeInTheDocument(), + ); + + const accountButton = screen.getByRole("button", { + name: /connected wallet/i, + }); + fireEvent.click(accountButton); + + const disconnectButton = await screen.findByRole("menuitem", { + name: /disconnect/i, + }); + fireEvent.click(disconnectButton); + + await waitFor(() => + expect( + screen.getByRole("button", { name: /connect wallet/i }), + ).toBeInTheDocument(), + ); + }); + + it("shows a recovery message when the user rejects the connection in Freighter", async () => { + mockedGetAddress.mockResolvedValueOnce({ error: "User rejected request" }); + + render(); + + await waitFor(() => + expect(screen.getByRole("alert")).toHaveTextContent( + /Connection canceled in Freighter/i, + ), + ); + }); +}); diff --git a/src/components/landing-page/Navigation.tsx b/src/components/landing-page/Navigation.tsx index 2366263..a32853e 100644 --- a/src/components/landing-page/Navigation.tsx +++ b/src/components/landing-page/Navigation.tsx @@ -1,91 +1,115 @@ // Navigation component with wallet integration "use client"; -import React, { useState, useRef, useEffect } from "react"; +import React, { useState } from "react"; import Link from "next/link"; -// Real wallet hook import -import { useWallet } from "../../hooks/useWallet"; - -// Use the real wallet hook -const { connected, address, connect, disconnect } = useWallet(); +import { WalletConnectButton } from "@/components/WalletConnectButton"; export const Navigation: React.FC = () => { const [menuOpen, setMenuOpen] = useState(false); - const [accountMenuOpen, setAccountMenuOpen] = useState(false); - const accountBtnRef = useRef(null); - const handleToggle = () => setMenuOpen((open) => !open); const handleNavClick = () => setMenuOpen(false); - // Close account menu on outside click - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (accountBtnRef.current && !accountBtnRef.current.contains(e.target as Node)) { - setAccountMenuOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - const truncate = (addr: string) => (addr ? `${addr.slice(0, 4)}…${addr.slice(-4)}` : ""); - return (
- -
diff --git a/src/components/landing-page/__tests__/Navigation.test.tsx b/src/components/landing-page/__tests__/Navigation.test.tsx new file mode 100644 index 0000000..4a83b28 --- /dev/null +++ b/src/components/landing-page/__tests__/Navigation.test.tsx @@ -0,0 +1,21 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { Navigation } from "@/components/landing-page/Navigation"; +import { getAddress } from "@stellar/freighter-api"; + +vi.mock("@stellar/freighter-api", () => ({ + getAddress: vi.fn().mockResolvedValue({ error: "Freighter not installed" }), +})); + +describe("Navigation", () => { + it("renders the wallet connect control in the header", async () => { + render(); + + await waitFor(() => + expect( + screen.getByRole("button", { name: /connect wallet/i }), + ).toBeInTheDocument(), + ); + }); +}); diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts index 0749917..f9d0b20 100644 --- a/src/hooks/useWallet.ts +++ b/src/hooks/useWallet.ts @@ -9,10 +9,14 @@ export const useWallet = () => { const [connected, setConnected] = useState(false); const [address, setAddress] = useState(""); const [error, setError] = useState(null); + const [connecting, setConnecting] = useState(false); const fetchAddress = useCallback(async () => { + setConnecting(true); + try { const result = await getAddress(); + if (result.error) { setError(result.error); setConnected(false); @@ -23,14 +27,16 @@ export const useWallet = () => { setError(null); } } catch (e) { - setError((e as Error).message); + setError((e as Error).message || "Unable to connect to Freighter."); setConnected(false); setAddress(""); + } finally { + setConnecting(false); } }, []); const connect = useCallback(() => { - // Freighter prompts the user when getAddress is called. + setError(null); fetchAddress(); }, [fetchAddress]); @@ -38,6 +44,7 @@ export const useWallet = () => { setConnected(false); setAddress(""); setError(null); + setConnecting(false); }, []); // Auto-detect on mount (e.g., if already connected) @@ -45,5 +52,5 @@ export const useWallet = () => { fetchAddress(); }, [fetchAddress]); - return { connected, address, connect, disconnect, error }; + return { connected, address, connect, disconnect, error, connecting }; };