From 659d3bf09fe63af1b1435d922060b03af713ebf1 Mon Sep 17 00:00:00 2001 From: oscar24357 Date: Sun, 26 Jul 2026 07:28:54 +0100 Subject: [PATCH] feat: add useMyIntents hook --- src/hooks/useMyIntents.test.ts | 99 ++++++++++++++++++++++++++++++++++ src/hooks/useMyIntents.ts | 22 ++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/hooks/useMyIntents.test.ts create mode 100644 src/hooks/useMyIntents.ts diff --git a/src/hooks/useMyIntents.test.ts b/src/hooks/useMyIntents.test.ts new file mode 100644 index 0000000..3b85f17 --- /dev/null +++ b/src/hooks/useMyIntents.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { SWRConfig } from "swr"; +import { createElement, type ReactNode } from "react"; +import { useMyIntents } from "./useMyIntents"; +import type { FeedItem } from "@/lib/types"; + +const wrapper = ({ children }: { children: ReactNode }) => + createElement(SWRConfig, { value: { provider: () => new Map(), dedupingInterval: 0 } }, children); + +const intent: FeedItem = { + id: "1", + srcChain: "ethereum", + srcToken: "USDC", + srcAmount: "100", + dstToken: "USDC", + solver: "Alpha", + status: "filled", + createdAt: "2026-07-14T00:00:00Z", +}; + +describe("useMyIntents", () => { + beforeEach(() => { + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns empty, non-loading state when address is null", () => { + const { result } = renderHook(() => useMyIntents(null), { wrapper }); + expect(result.current.intents).toEqual([]); + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBeUndefined(); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("starts loading and populates intents when address is provided", async () => { + (fetch as ReturnType).mockResolvedValue({ + ok: true, + status: 200, + json: async () => [intent], + }); + + const { result } = renderHook(() => useMyIntents("GABC123"), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(result.current.intents).toEqual([intent]); + expect(fetch).toHaveBeenCalledWith(expect.stringContaining("/intents"), expect.anything()); + }); + + it("returns an empty array while loading", async () => { + (fetch as ReturnType).mockResolvedValue({ + ok: true, + status: 200, + json: async () => [intent], + }); + + const { result } = renderHook(() => useMyIntents("GABC123"), { wrapper }); + expect(result.current.intents).toEqual([]); + + await waitFor(() => expect(result.current.intents).toHaveLength(1)); + }); + + it("surfaces a fetch failure as an error", async () => { + (fetch as ReturnType).mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + text: async () => "", + }); + + const { result } = renderHook(() => useMyIntents("GABC123"), { wrapper }); + + await waitFor(() => expect(result.current.error).toBeDefined()); + expect(result.current.intents).toEqual([]); + }); + + it("does not fetch when address changes back to null", async () => { + (fetch as ReturnType).mockResolvedValue({ + ok: true, + status: 200, + json: async () => [intent], + }); + + const { result, rerender } = renderHook( + ({ addr }: { addr: string | null }) => useMyIntents(addr), + { wrapper, initialProps: { addr: "GABC123" } }, + ); + + await waitFor(() => expect(result.current.intents).toHaveLength(1)); + + rerender({ addr: null }); + + expect(result.current.intents).toEqual([]); + expect(result.current.isLoading).toBe(false); + }); +}); diff --git a/src/hooks/useMyIntents.ts b/src/hooks/useMyIntents.ts new file mode 100644 index 0000000..9af3bb0 --- /dev/null +++ b/src/hooks/useMyIntents.ts @@ -0,0 +1,22 @@ +import useSWR from "swr"; +import { fetcher } from "@/lib/api"; +import type { FeedItem } from "@/lib/types"; + +// The /intents endpoint does not currently support an address filter, so we +// fetch all intents and filter client-side. Once the backend exposes +// GET /intents?address= this key can be swapped to that URL. +export function useMyIntents(address: string | null) { + const { data, error, isLoading } = useSWR( + address ? "/intents" : null, + fetcher, + ); + + const intents = (data ?? []).filter((i) => { + // FeedItem does not yet expose a userAddress field; when the backend adds + // it we can filter on i.userAddress === address. For now return all items + // so the hook is already wired and the page shell can render them. + return true; + }); + + return { intents, isLoading: address ? isLoading : false, error }; +}