From bd26edbad01438d3f5111e6233f92f504409c431 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Fri, 26 Jun 2026 21:25:39 +0530 Subject: [PATCH 1/3] refactor: extract useWalletAddress() hook to deduplicate session casts Replace duplicated (session?.user as { walletAddress?: string; address?: string }) wallet address extraction across 5 files with a single shared hook. New hook at hooks/use-wallet-address.ts: - Returns walletAddress, falls back to address, then null - Marked 'use client' for client component usage - Includes 5 unit tests Files updated: - hooks/use-competition-join-state.ts - components/bounty/competition-submission.tsx - components/bounty/competition-judging.tsx - components/bounty/fcfs-approval-panel.tsx - components/bounty/fcfs-claim-button.tsx Closes #275 --- components/bounty/competition-judging.tsx | 11 +---- components/bounty/competition-submission.tsx | 11 +---- components/bounty/fcfs-approval-panel.tsx | 8 +--- components/bounty/fcfs-claim-button.tsx | 8 +--- hooks/__tests__/use-wallet-address.test.ts | 48 ++++++++++++++++++++ hooks/use-competition-join-state.ts | 11 +---- hooks/use-wallet-address.ts | 15 ++++++ 7 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 hooks/__tests__/use-wallet-address.test.ts create mode 100644 hooks/use-wallet-address.ts diff --git a/components/bounty/competition-judging.tsx b/components/bounty/competition-judging.tsx index 36e45bbb..30e9a8ba 100644 --- a/components/bounty/competition-judging.tsx +++ b/components/bounty/competition-judging.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { Loader2, Trophy, Award, CheckCircle2, Lock } from "lucide-react"; import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -45,7 +45,7 @@ export function CompetitionJudging({ totalReward, currency, }: CompetitionJudgingProps) { - const { data: session } = authClient.useSession(); + const walletAddress = useWalletAddress(); const approveMutation = useApproveContestWinner(); const finalizeMutation = useFinalizeContest(); @@ -55,13 +55,6 @@ export function CompetitionJudging({ // On next query invalidation the backend status takes over. const [localApproved, setLocalApproved] = useState>(new Set()); - const walletAddress = - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.walletAddress || - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.address || - null; - const handleApprove = async (sub: Submission) => { if (!walletAddress) { toast.error("Connect your wallet to approve."); diff --git a/components/bounty/competition-submission.tsx b/components/bounty/competition-submission.tsx index 669c07b7..ef59f712 100644 --- a/components/bounty/competition-submission.tsx +++ b/components/bounty/competition-submission.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from "react"; import { Loader2, Lock, Send, Clock } from "lucide-react"; import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; @@ -40,7 +40,7 @@ export function CompetitionSubmission({ deadline, hasJoined, }: CompetitionSubmissionProps) { - const { data: session } = authClient.useSession(); + const walletAddress = useWalletAddress(); const [workCid, setWorkCid] = useState(""); const submitMutation = useSubmitContestWork(); const isPastDeadline = useDeadlinePassed(deadline); @@ -55,13 +55,6 @@ export function CompetitionSubmission({ return () => clearInterval(id); }, [deadline]); - const walletAddress = - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.walletAddress || - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.address || - null; - if (!hasJoined) return null; const trimmed = workCid.trim(); diff --git a/components/bounty/fcfs-approval-panel.tsx b/components/bounty/fcfs-approval-panel.tsx index df066bc8..9574bcbe 100644 --- a/components/bounty/fcfs-approval-panel.tsx +++ b/components/bounty/fcfs-approval-panel.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -29,12 +30,7 @@ export function FcfsApprovalPanel({ bounty }: { bounty: FcfsApprovalBounty }) { const [points, setPoints] = useState(10); const currentUserId = (session?.user as { id?: string } | undefined)?.id; - const walletAddress = - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.walletAddress || - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.address || - null; + const walletAddress = useWalletAddress(); const isCreator = Boolean( currentUserId && currentUserId === bounty.createdBy, diff --git a/components/bounty/fcfs-claim-button.tsx b/components/bounty/fcfs-claim-button.tsx index ba7e2232..7578e680 100644 --- a/components/bounty/fcfs-claim-button.tsx +++ b/components/bounty/fcfs-claim-button.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, Clock3, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -73,12 +74,7 @@ export function FcfsClaimButton({ bounty }: { bounty: FcfsBounty }) { }, []); const currentUserId = (session?.user as { id?: string } | undefined)?.id; - const walletAddress = - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.walletAddress || - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.address || - null; + const walletAddress = useWalletAddress(); const isFcfs = bounty.type === "FIXED_PRICE"; const isOpen = bounty.status === "OPEN"; diff --git a/hooks/__tests__/use-wallet-address.test.ts b/hooks/__tests__/use-wallet-address.test.ts new file mode 100644 index 00000000..7a25d1c6 --- /dev/null +++ b/hooks/__tests__/use-wallet-address.test.ts @@ -0,0 +1,48 @@ +import { renderHook } from "@testing-library/react"; +import { useWalletAddress } from "../use-wallet-address"; + +jest.mock("@/lib/auth-client", () => ({ + authClient: { + useSession: jest.fn(), + }, +})); + +const { authClient } = require("@/lib/auth-client"); + +describe("useWalletAddress", () => { + it("returns null when there is no session", () => { + authClient.useSession.mockReturnValue({ data: null }); + const { result } = renderHook(() => useWalletAddress()); + expect(result.current).toBeNull(); + }); + + it("returns null when session has no user", () => { + authClient.useSession.mockReturnValue({ data: { user: undefined } }); + const { result } = renderHook(() => useWalletAddress()); + expect(result.current).toBeNull(); + }); + + it("returns walletAddress when available", () => { + authClient.useSession.mockReturnValue({ + data: { user: { walletAddress: "0xABC", address: "0xDEF" } }, + }); + const { result } = renderHook(() => useWalletAddress()); + expect(result.current).toBe("0xABC"); + }); + + it("falls back to address when walletAddress is missing", () => { + authClient.useSession.mockReturnValue({ + data: { user: { address: "0xDEF" } }, + }); + const { result } = renderHook(() => useWalletAddress()); + expect(result.current).toBe("0xDEF"); + }); + + it("returns null when neither walletAddress nor address exists", () => { + authClient.useSession.mockReturnValue({ + data: { user: { name: "test" } }, + }); + const { result } = renderHook(() => useWalletAddress()); + expect(result.current).toBeNull(); + }); +}); diff --git a/hooks/use-competition-join-state.ts b/hooks/use-competition-join-state.ts index f3c5ee21..07787ed9 100644 --- a/hooks/use-competition-join-state.ts +++ b/hooks/use-competition-join-state.ts @@ -2,7 +2,7 @@ import { useState } from "react"; import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { useJoinCompetition, ContestError, @@ -22,17 +22,10 @@ interface CompetitionJoinState { export function useCompetitionJoinState( bounty: BountyFieldsFragment & Partial, ): CompetitionJoinState { - const { data: session } = authClient.useSession(); + const walletAddress = useWalletAddress(); const joinMutation = useJoinCompetition(); const [localJoined, setLocalJoined] = useState(false); - const walletAddress = - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.walletAddress || - (session?.user as { walletAddress?: string; address?: string } | undefined) - ?.address || - null; - const deadline = bounty.bountyWindow?.endDate ?? null; const isPastDeadline = useDeadlinePassed(deadline); diff --git a/hooks/use-wallet-address.ts b/hooks/use-wallet-address.ts new file mode 100644 index 00000000..676b21b4 --- /dev/null +++ b/hooks/use-wallet-address.ts @@ -0,0 +1,15 @@ +"use client"; + +import { authClient } from "@/lib/auth-client"; + +/** + * Returns the connected wallet address from the current session, or null + * if the user is not signed in or has no wallet attached. + */ +export function useWalletAddress(): string | null { + const { data: session } = authClient.useSession(); + const user = session?.user as + | { walletAddress?: string; address?: string } + | undefined; + return user?.walletAddress || user?.address || null; +} From 13a970e64d101919761b2ad9d2be1b6fde298c87 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Fri, 26 Jun 2026 21:32:09 +0530 Subject: [PATCH 2/3] fix: replace require() with ES import in use-wallet-address test --- hooks/__tests__/use-wallet-address.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hooks/__tests__/use-wallet-address.test.ts b/hooks/__tests__/use-wallet-address.test.ts index 7a25d1c6..3c29912c 100644 --- a/hooks/__tests__/use-wallet-address.test.ts +++ b/hooks/__tests__/use-wallet-address.test.ts @@ -1,5 +1,6 @@ import { renderHook } from "@testing-library/react"; import { useWalletAddress } from "../use-wallet-address"; +import { authClient } from "@/lib/auth-client"; jest.mock("@/lib/auth-client", () => ({ authClient: { @@ -7,8 +8,6 @@ jest.mock("@/lib/auth-client", () => ({ }, })); -const { authClient } = require("@/lib/auth-client"); - describe("useWalletAddress", () => { it("returns null when there is no session", () => { authClient.useSession.mockReturnValue({ data: null }); From 2ff3b2905de97a34acd9033ce8ae367a97a2a622 Mon Sep 17 00:00:00 2001 From: shogun444 Date: Sat, 27 Jun 2026 12:33:54 +0530 Subject: [PATCH 3/3] fix: cast authClient.useSession as jest.Mock in test; migrate bounty-detail-client to useWalletAddress() --- components/bounty-detail/bounty-detail-client.tsx | 6 +++--- hooks/__tests__/use-wallet-address.test.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index 3e93b7fa..cda5bb15 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -18,6 +18,7 @@ import { RefundStatusTracker } from "../bounty/refund-status"; import { FeeCalculator } from "../bounty/fee-calculator"; import { useEscrowPool } from "@/hooks/use-escrow"; import { authClient } from "@/lib/auth-client"; +import { useWalletAddress } from "@/hooks/use-wallet-address"; import { useDeadlinePassed } from "@/hooks/use-deadline-passed"; import type { CancellationRecord } from "@/types/escrow"; import { MilestoneFunnel } from "@/components/bounty/milestone-funnel"; @@ -87,6 +88,8 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { }, []); const pastDeadline = useDeadlinePassed(bounty?.bountyWindow?.endDate); + // walletAddress is required for contract actions. Do NOT fallback to user.id. + const walletAddress = useWalletAddress() ?? ""; if (isPending) return ; @@ -145,9 +148,6 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { const isCreator = (session?.user as { id?: string } | undefined)?.id === bounty.createdBy; const isFinalized = bounty.status === "COMPLETED"; - // walletAddress is required for contract actions. Do NOT fallback to user.id. - const walletAddress = - (session?.user as { walletAddress?: string })?.walletAddress || ""; // Identify if the current user is the assigned contributor // using a fallback check on submissions or assumed backend field. diff --git a/hooks/__tests__/use-wallet-address.test.ts b/hooks/__tests__/use-wallet-address.test.ts index 3c29912c..3124d61a 100644 --- a/hooks/__tests__/use-wallet-address.test.ts +++ b/hooks/__tests__/use-wallet-address.test.ts @@ -10,19 +10,21 @@ jest.mock("@/lib/auth-client", () => ({ describe("useWalletAddress", () => { it("returns null when there is no session", () => { - authClient.useSession.mockReturnValue({ data: null }); + (authClient.useSession as jest.Mock).mockReturnValue({ data: null }); const { result } = renderHook(() => useWalletAddress()); expect(result.current).toBeNull(); }); it("returns null when session has no user", () => { - authClient.useSession.mockReturnValue({ data: { user: undefined } }); + (authClient.useSession as jest.Mock).mockReturnValue({ + data: { user: undefined }, + }); const { result } = renderHook(() => useWalletAddress()); expect(result.current).toBeNull(); }); it("returns walletAddress when available", () => { - authClient.useSession.mockReturnValue({ + (authClient.useSession as jest.Mock).mockReturnValue({ data: { user: { walletAddress: "0xABC", address: "0xDEF" } }, }); const { result } = renderHook(() => useWalletAddress()); @@ -30,7 +32,7 @@ describe("useWalletAddress", () => { }); it("falls back to address when walletAddress is missing", () => { - authClient.useSession.mockReturnValue({ + (authClient.useSession as jest.Mock).mockReturnValue({ data: { user: { address: "0xDEF" } }, }); const { result } = renderHook(() => useWalletAddress()); @@ -38,7 +40,7 @@ describe("useWalletAddress", () => { }); it("returns null when neither walletAddress nor address exists", () => { - authClient.useSession.mockReturnValue({ + (authClient.useSession as jest.Mock).mockReturnValue({ data: { user: { name: "test" } }, }); const { result } = renderHook(() => useWalletAddress());