From 9e9367d5c27f561328108b6704501b5fadf26308 Mon Sep 17 00:00:00 2001 From: egwujiohaifesinachiperpetual-max Date: Sat, 27 Jun 2026 10:00:30 +0100 Subject: [PATCH 1/2] fix(recipient): clean up and configure withdrawal reset timer --- src/lib/transactionConfig.ts | 4 ++ src/pages/Recipient.tsx | 25 ++++++- src/pages/__tests__/Recipient.test.tsx | 96 +++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/src/lib/transactionConfig.ts b/src/lib/transactionConfig.ts index f29c4e0..c03dc22 100644 --- a/src/lib/transactionConfig.ts +++ b/src/lib/transactionConfig.ts @@ -49,3 +49,7 @@ export const transactionPollingConfig = { ), ), }; + +// Configurable delay for withdrawal confirmation reset (ms) +export const TRANSACTION_RESET_DELAY_MS = 5000; // Align with previous hard‑coded value + diff --git a/src/pages/Recipient.tsx b/src/pages/Recipient.tsx index 134682d..81bb416 100644 --- a/src/pages/Recipient.tsx +++ b/src/pages/Recipient.tsx @@ -7,16 +7,28 @@ import { useWallet } from "../components/wallet-connect/Walletcontext"; import { useToast } from "../components/toast/ToastProvider"; import { withdraw } from "../lib/stellar/tx"; import "./Streams.css"; -import "./Recipient.css"; +import { TRANSACTION_RESET_DELAY_MS } from "../lib/transactionConfig"; +import { useRef } from "react"; + +// (Removed top-level timeoutRef and useEffect; will be added inside component) export default function Recipient() { + const timeoutRef = useRef(null); const wallet = useWallet(); const { addToast } = useToast(); const [loading, setLoading] = useState(true); const [txState, setTxState] = useState<"idle" | "signing" | "submitting" | "confirmed" | "error">("idle"); - const [errorMsg, setErrorMsg] = useState(null); + useEffect(() => { + return () => { + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, []); + const [errorMsg, setErrorMsg] = useState(null); useEffect(() => { const t = setTimeout(() => setLoading(false), 2000); return () => clearTimeout(t); @@ -52,7 +64,14 @@ export default function Recipient() { await withdraw(recipientAddr, streamId, amountStr); setTxState("confirmed"); addToast("Withdrawal completed successfully on-chain!", "success"); - setTimeout(() => setTxState("idle"), 5000); + // Clear any existing timeout to avoid overlapping timers + if (timeoutRef.current !== null) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = window.setTimeout(() => { + setTxState("idle"); + timeoutRef.current = null; + }, TRANSACTION_RESET_DELAY_MS); } catch (err: any) { setTxState("error"); setErrorMsg(err.message || "Withdrawal failed."); diff --git a/src/pages/__tests__/Recipient.test.tsx b/src/pages/__tests__/Recipient.test.tsx index d5b9c08..7414e1c 100644 --- a/src/pages/__tests__/Recipient.test.tsx +++ b/src/pages/__tests__/Recipient.test.tsx @@ -1,4 +1,98 @@ -import { act, render, screen, within } from "@testing-library/react"; +import { act, fireEvent, render, screen, within } from "@testing-library/react"; +import { axe } from "vitest-axe"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock wallet context +vi.mock("../../components/wallet-connect/Walletcontext", () => ({ + useWallet: () => ({ + address: "GATDOSCZNJ5YZHNOX7IOD4QDCQSTMR2YNF5IXHFNX3H6B4ICCMSDLOWN", + network: { label: "TESTNET" }, + connected: true, + loading: false, + error: null, + expectedNetwork: "TESTNET", + expectedNetworkLabel: "Testnet", + isNetworkMismatch: false, + connect: vi.fn(), + disconnect: vi.fn(), + }), + WalletProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +// Mock toast provider +vi.mock("../../components/toast/ToastProvider", () => ({ + useToast: () => ({ addToast: vi.fn() }), +})); + +// Mock withdraw function +vi.mock("../../lib/stellar/tx", () => ({ + withdraw: vi.fn(), +})); + +import Recipient from "../Recipient"; + +describe("Recipient page", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + const renderLoadedRecipient = async () => { + const view = render(); + await act(async () => { + vi.advanceTimersByTime(2000); + }); + return view; + }; + + it("renders loaded recipient portal without axe violations", async () => { + const { container } = await renderLoadedRecipient(); + const results = await axe(container); + expect(results.violations).toEqual([]); + }); + + it("exposes an enabled withdraw action when a connected recipient has balance", async () => { + await renderLoadedRecipient(); + const withdrawButton = screen.getByRole("button", { + name: /withdraw 22,600 usdc/i, + }); + expect(withdrawButton).toBeEnabled(); + }); + + it("handles successful withdrawal and resets state after delay", async () => { + const { addToast } = require("../../components/toast/ToastProvider").useToast(); + const { withdraw } = require("../../lib/stellar/tx"); + withdraw.mockResolvedValue(undefined); + await renderLoadedRecipient(); + const button = screen.getByRole("button", { name: /withdraw/i }); + fireEvent.click(button); + await act(async () => {}); // wait for async withdraw + expect(addToast).toHaveBeenCalledWith( + "Withdrawal completed successfully on-chain!", + "success", + ); + await act(async () => { + vi.advanceTimersByTime(5000); + }); + expect(button).toHaveTextContent(/withdraw/i); + }); + + it("clears timeout on unmount", async () => { + const { unmount } = await renderLoadedRecipient(); + const button = screen.getByRole("button", { name: /withdraw/i }); + fireEvent.click(button); + unmount(); + await act(async () => { + vi.advanceTimersByTime(6000); + }); + expect(true).toBe(true); + }); +}); + import { axe } from "vitest-axe"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; From 84e14d8e0b022af7599efa030536247bea8e979e Mon Sep 17 00:00:00 2001 From: egwujiohaifesinachiperpetual-max Date: Sat, 27 Jun 2026 10:15:27 +0100 Subject: [PATCH 2/2] Revert "fix(recipient): clean up and configure withdrawal reset timer" --- src/lib/transactionConfig.ts | 4 -- src/pages/Recipient.tsx | 25 +------ src/pages/__tests__/Recipient.test.tsx | 96 +------------------------- 3 files changed, 4 insertions(+), 121 deletions(-) diff --git a/src/lib/transactionConfig.ts b/src/lib/transactionConfig.ts index c03dc22..f29c4e0 100644 --- a/src/lib/transactionConfig.ts +++ b/src/lib/transactionConfig.ts @@ -49,7 +49,3 @@ export const transactionPollingConfig = { ), ), }; - -// Configurable delay for withdrawal confirmation reset (ms) -export const TRANSACTION_RESET_DELAY_MS = 5000; // Align with previous hard‑coded value - diff --git a/src/pages/Recipient.tsx b/src/pages/Recipient.tsx index 81bb416..134682d 100644 --- a/src/pages/Recipient.tsx +++ b/src/pages/Recipient.tsx @@ -7,28 +7,16 @@ import { useWallet } from "../components/wallet-connect/Walletcontext"; import { useToast } from "../components/toast/ToastProvider"; import { withdraw } from "../lib/stellar/tx"; import "./Streams.css"; -import { TRANSACTION_RESET_DELAY_MS } from "../lib/transactionConfig"; -import { useRef } from "react"; - -// (Removed top-level timeoutRef and useEffect; will be added inside component) +import "./Recipient.css"; export default function Recipient() { - const timeoutRef = useRef(null); const wallet = useWallet(); const { addToast } = useToast(); const [loading, setLoading] = useState(true); const [txState, setTxState] = useState<"idle" | "signing" | "submitting" | "confirmed" | "error">("idle"); - useEffect(() => { - return () => { - if (timeoutRef.current !== null) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - }; - }, []); - const [errorMsg, setErrorMsg] = useState(null); + useEffect(() => { const t = setTimeout(() => setLoading(false), 2000); return () => clearTimeout(t); @@ -64,14 +52,7 @@ export default function Recipient() { await withdraw(recipientAddr, streamId, amountStr); setTxState("confirmed"); addToast("Withdrawal completed successfully on-chain!", "success"); - // Clear any existing timeout to avoid overlapping timers - if (timeoutRef.current !== null) { - clearTimeout(timeoutRef.current); - } - timeoutRef.current = window.setTimeout(() => { - setTxState("idle"); - timeoutRef.current = null; - }, TRANSACTION_RESET_DELAY_MS); + setTimeout(() => setTxState("idle"), 5000); } catch (err: any) { setTxState("error"); setErrorMsg(err.message || "Withdrawal failed."); diff --git a/src/pages/__tests__/Recipient.test.tsx b/src/pages/__tests__/Recipient.test.tsx index 7414e1c..d5b9c08 100644 --- a/src/pages/__tests__/Recipient.test.tsx +++ b/src/pages/__tests__/Recipient.test.tsx @@ -1,98 +1,4 @@ -import { act, fireEvent, render, screen, within } from "@testing-library/react"; -import { axe } from "vitest-axe"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -// Mock wallet context -vi.mock("../../components/wallet-connect/Walletcontext", () => ({ - useWallet: () => ({ - address: "GATDOSCZNJ5YZHNOX7IOD4QDCQSTMR2YNF5IXHFNX3H6B4ICCMSDLOWN", - network: { label: "TESTNET" }, - connected: true, - loading: false, - error: null, - expectedNetwork: "TESTNET", - expectedNetworkLabel: "Testnet", - isNetworkMismatch: false, - connect: vi.fn(), - disconnect: vi.fn(), - }), - WalletProvider: ({ children }: { children: React.ReactNode }) => children, -})); - -// Mock toast provider -vi.mock("../../components/toast/ToastProvider", () => ({ - useToast: () => ({ addToast: vi.fn() }), -})); - -// Mock withdraw function -vi.mock("../../lib/stellar/tx", () => ({ - withdraw: vi.fn(), -})); - -import Recipient from "../Recipient"; - -describe("Recipient page", () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - vi.clearAllMocks(); - }); - - const renderLoadedRecipient = async () => { - const view = render(); - await act(async () => { - vi.advanceTimersByTime(2000); - }); - return view; - }; - - it("renders loaded recipient portal without axe violations", async () => { - const { container } = await renderLoadedRecipient(); - const results = await axe(container); - expect(results.violations).toEqual([]); - }); - - it("exposes an enabled withdraw action when a connected recipient has balance", async () => { - await renderLoadedRecipient(); - const withdrawButton = screen.getByRole("button", { - name: /withdraw 22,600 usdc/i, - }); - expect(withdrawButton).toBeEnabled(); - }); - - it("handles successful withdrawal and resets state after delay", async () => { - const { addToast } = require("../../components/toast/ToastProvider").useToast(); - const { withdraw } = require("../../lib/stellar/tx"); - withdraw.mockResolvedValue(undefined); - await renderLoadedRecipient(); - const button = screen.getByRole("button", { name: /withdraw/i }); - fireEvent.click(button); - await act(async () => {}); // wait for async withdraw - expect(addToast).toHaveBeenCalledWith( - "Withdrawal completed successfully on-chain!", - "success", - ); - await act(async () => { - vi.advanceTimersByTime(5000); - }); - expect(button).toHaveTextContent(/withdraw/i); - }); - - it("clears timeout on unmount", async () => { - const { unmount } = await renderLoadedRecipient(); - const button = screen.getByRole("button", { name: /withdraw/i }); - fireEvent.click(button); - unmount(); - await act(async () => { - vi.advanceTimersByTime(6000); - }); - expect(true).toBe(true); - }); -}); - +import { act, render, screen, within } from "@testing-library/react"; import { axe } from "vitest-axe"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";