Skip to content

Comments

Feat: implement single claim model logic#109

Closed
Bosun-Josh121 wants to merge 2 commits intoboundlessfi:mainfrom
Bosun-Josh121:feat/implement-single-claim-model-logic
Closed

Feat: implement single claim model logic#109
Bosun-Josh121 wants to merge 2 commits intoboundlessfi:mainfrom
Bosun-Josh121:feat/implement-single-claim-model-logic

Conversation

@Bosun-Josh121
Copy link

@Bosun-Josh121 Bosun-Josh121 commented Feb 22, 2026

Description

This PR implements the Single Claim Model logic for the bounty details page, bridging the gap between an open bounty and a claimed state.

Closes #79

Key Changes

  • Data Types & Schemas: Added the ClaimInfo type to the Bounty interface (types/bounty.ts) and updated the Zod schema (lib/api/bounties.ts) to validate and pass the new data structure.
  • Claimant Profile UI: Created a new ClaimantProfile component in the sidebar (bounty-detail-sidebar-cta.tsx) that displays the avatar, username, and timestamp of the user who claimed the bounty.
  • Button State Logic: Updated the main CTA button to automatically disable and display "Already Claimed" when the bounty status is no longer open.
  • Mock Data: Updated mockBounties to include a fully mocked claimInfo object for UI testing.

Acceptance Criteria Met

  • Show claimant profile when claimed
  • Disable claim button if already claimed
  • Display "Already Claimed" state
  • Cannot claim if already claimed

Testing Steps

  1. Navigate to an open bounty (e.g., /bounty/1).
  2. Click Claim Bounty. Verify the success toast appears, the button disables to "Already Claimed", and the Claimant Profile card appears below it.
  3. Navigate to an already claimed bounty (e.g., /bounty/2). Verify the Claimant Profile renders correctly on load.
bounty

Summary by CodeRabbit

Release Notes

  • New Features

    • Added bounty claiming workflow with submission forms and draft persistence
    • Introduced claim tracking that displays who claimed a bounty and when
    • Added user completion history tracking on profiles
    • Enhanced claim status categorization with "Expired" section
  • Improvements

    • Improved null/undefined handling in bounty detail displays
    • Better error handling and loading states on transparency page
    • Upgraded profile pages to display real completion history data

@vercel
Copy link

vercel bot commented Feb 22, 2026

@Bosun-Josh121 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Adds a nested claimInfo structure and integrates single-claim logic across API, types, hooks, and UI; implements submission dialog and submission API; replaces Apollo with graphql-request/react-query client; adds completion-history API/hook and various UI guards and null-safety fixes.

Changes

Cohort / File(s) Summary
Bounty detail UI
components/bounty-detail/bounty-detail-header-card.tsx, components/bounty-detail/bounty-detail-sidebar-cta.tsx, components/bounty-detail/submission-dialog.tsx, components/bounty-detail/submission-dialog.tsx
Null-safety for project/tags, ClaimantProfile component, claim mutation & pending UI, submission dialog component with form, draft persistence, and submit flow.
Forms & Validation
components/bounty/forms/schemas.ts
Added submissionFormSchema and SubmissionFormValue types for submission validation.
Types & Domain
types/bounty.ts, types/participation.ts
Added ClaimInfo and claimInfo? on Bounty; expanded Submission shape (explanation, attachments, urls, wallet).
API surface (rest/schema)
lib/api/bounties.ts, app/api/bounties/[id]/submit/route.ts
Added claimInfoSchema and claim/submit changes (claim sends empty body; new submit endpoint with validation and store).
Mock & Generated types
lib/mock-bounty.ts, lib/graphql/generated.ts
Mock bounty updated to nested claimInfo; large GraphQL types added.
React Query hooks & mutations
hooks/use-bounty-mutations.ts, hooks/use-reputation.ts, hooks/use-transparency.ts
useClaimBounty onSuccess now applies API data or patches local detail with claimInfo; added useCompletionHistory and keys; stylistic updates.
GraphQL & Query client
lib/graphql/client.ts, providers/query-provider.tsx, codegen.ts, package.json
Replaced Apollo with graphql-request client and fetcher; updated codegen to typescript-react-query; package deps updated.
Reputation / Profile
app/api/reputation/[userId]/completion-history/route.ts, lib/api/reputation.ts, app/profile/[userId]/page.tsx, components/reputation/my-claims.tsx
Added completion-history API + client, useCompletionHistory hook usage in profile, new "Expired" claims section, and mapping of completion records.
Transparency & Misc UI
app/transparency/page.tsx, app/api/transparency/*, components/global-navbar.tsx, components/login/sign-in.tsx
UI/formatting adjustments, improved error/loading handling on transparency page, and minor JSX formatting changes.
Other
lib/graphql/generated.ts, tests and minor files...
GraphQL-generated types added; various formatting/refactors across tests and hooks.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Bounty UI
    participant Hook as useClaimBounty
    participant API as Bounties API
    participant Cache as Query Cache

    User->>UI: Click "Claim"
    activate UI
    UI->>Hook: mutate(claim)
    deactivate UI

    activate Hook
    Hook->>API: POST /bounties/{id}/claim (empty body)
    deactivate Hook

    activate API
    API-->>Hook: returns either full bounty data or claimInfo
    deactivate API

    activate Hook
    alt Full bounty returned
        Hook->>Cache: replace bounty detail with API data
    else
        Hook->>Cache: patch bounty detail.status = "claimed", add claimInfo
    end
    Hook->>Cache: invalidate bounty lists
    Hook-->>UI: success
    deactivate Hook

    UI->>UI: render ClaimantProfile, disable Claim button
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 I hopped to guard the single claim,
Nestled claimant info in the frame,
A submission box, a tidy store,
One victor crowned — none ask for more,
Puff, spinner, toast — the rabbit cheers!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Beyond the core single claim model implementation, the PR includes additional features: submission/claim dialog workflow [submission-dialog.tsx, app/api/bounties/[id]/submit/route.ts], GraphQL client refactoring [lib/graphql/client.ts, codegen.ts], completion history tracking [hooks/use-reputation.ts, app/api/reputation/[userId]/completion-history/route.ts], and profile page enhancements [app/profile/[userId]/page.tsx]. Consider clarifying whether the GraphQL migration, completion history feature, and profile page enhancements are intentional scope expansions or should be separated into distinct pull requests for cleaner change history.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: implement single claim model logic' accurately and clearly describes the main objective of the pull request, matching the implementation of single claim model functionality outlined in issue #79.
Linked Issues check ✅ Passed All coding requirements from issue #79 are met: ClaimInfo type added to Bounty interface [lib/api/bounties.ts, types/bounty.ts], ClaimantProfile component displays claimant details [bounty-detail-sidebar-cta.tsx], claim button disables with 'Already Claimed' state [bounty-detail-sidebar-cta.tsx], claim mutation logic integrated [hooks/use-bounty-mutations.ts], mock data updated with claimInfo structure [lib/mock-bounty.ts], and submission/claim workflows implemented [app/api/bounties/[id]/submit/route.ts].

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/mock-bounty.ts (1)

186-193: ⚠️ Potential issue | 🟡 Minor

Bounty id: "6" has status: "claimed" but no claimInfo — the sidebar will show "Already Claimed" with an empty claimant section.

The SidebarCTA renders <ClaimantProfile> when status === "claimed", but ClaimantProfile returns null when claimInfo is missing. This means the "Claimed By" card silently disappears, leaving the user wondering who claimed it.

Add mock claimInfo to keep the test data consistent, or note this as an intentional edge-case test.

Proposed fix
     status: "claimed",
+    claimInfo: {
+      claimedBy: {
+        userId: "user-2",
+        username: "refactor_pro",
+        avatarUrl: "https://github.com/shadcn.png",
+      },
+      claimedAt: "2025-01-21T16:45:00Z",
+    },
     createdAt: "2025-01-18T08:00:00Z",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mock-bounty.ts` around lines 186 - 193, Bounty with id "6" has status
"claimed" but lacks claimInfo, causing SidebarCTA -> ClaimantProfile to render
null and hide the "Claimed By" card; update the mock in lib/mock-bounty.ts for
the object with id "6" to include a realistic claimInfo object (e.g., claimant
name/handle/avatar, claimantAddress, claimedAt timestamp, and claimedAmount) so
ClaimantProfile receives the data it expects, or explicitly mark this entry as
an intentional edge-case in a comment if you want the missing-claimInfo behavior
tested; reference SidebarCTA and ClaimantProfile when adding the fields to
ensure shape matches what ClaimantProfile reads.
lib/api/bounties.ts (1)

63-67: ⚠️ Potential issue | 🔴 Critical

Consolidate bounty type definitions to eliminate dual/triple source confusion.

Three separate type definition sources now exist for Bounty, BountyType, BountyStatus, ClaimingModel, and DifficultyLevel:

  • types/bounty.ts (manually defined)
  • lib/api/bounties.ts (Zod-inferred)
  • lib/types.ts (alternative manual definitions)

Divergences confirmed:

  • ClaimingModel: types/bounty.ts includes "milestone" (absent from lib/api/bounties.ts Zod schema)
  • BountyStatus: types/bounty.ts and lib/api/bounties.ts use "open" | "claimed" | "closed", while lib/types.ts defines completely different values ("open" | "in-progress" | "completed")

Codebase usage shows active three-way split: hooks and query layers import from @/lib/api, some UI components import from @/types/bounty, and legacy code imports from @/lib/types. This creates genuine import confusion risk.

Choose a single authoritative source and derive all other type exports from it, or align all three definitions and designate one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/bounties.ts` around lines 63 - 67, There are three conflicting
definitions for Bounty-related types; pick one authoritative source (prefer the
Zod schemas: bountySchema, bountyTypeSchema, bountyStatusSchema,
difficultySchema, claimingModelSchema) and update all other exports to be
derived from that source (replace manual typings Bounty, BountyType,
BountyStatus, DifficultyLevel, ClaimingModel in other modules with
z.infer<typeof ...> references), ensuring the claimingModelSchema is extended to
include "milestone" if needed and reconciling BountyStatus values to match the
chosen canonical set; update exports/imports so all code imports these unified
z.infer types (or export a single re-exporting barrel) to eliminate the
three-way split.
🧹 Nitpick comments (2)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)

33-44: Duplicated claim/action logic between SidebarCTA and MobileCTA.

handleAction, the CTA label function, and the claimMutation setup are nearly identical in both components. Consider extracting into a shared hook (e.g., useClaimAction(bounty)) that returns { handleAction, ctaLabel, isPending } to keep them in sync and reduce maintenance burden.

Also applies to: 204-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 33 - 44,
Duplicate claim/action logic in SidebarCTA and MobileCTA should be extracted
into a shared hook: create useClaimAction(bounty) that encapsulates the
claimMutation setup and computes ctaLabel and isPending, and exposes
handleAction; move the existing claimMutation.mutate logic and the branch on
bounty.claimingModel ("single-claim" vs opening bounty.githubIssueUrl) into this
hook, have SidebarCTA and MobileCTA call useClaimAction(bounty) and use the
returned { handleAction, ctaLabel, isPending } to render the button and toast
behavior so both components stay in sync.
lib/api/bounties.ts (1)

52-54: Flat claimedAt/claimedBy coexist with nested claimInfo — clarify intent.

The schema keeps the legacy flat fields (claimedAt, claimedBy as a plain string) alongside the new nested claimInfo. If these are kept for backward compatibility, that's fine, but UI code now reads from claimInfo exclusively. Document or add a TODO indicating when the flat fields can be removed to avoid confusion about which is the canonical source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/bounties.ts` around lines 52 - 54, The schema currently defines
legacy flat fields claimedAt and claimedBy alongside the new nested claimInfo
(claimInfoSchema), which causes ambiguity since UI code reads exclusively from
claimInfo; add a clear in-code comment/TODO above this schema explaining that
claimInfo is the canonical source, that claimedAt/claimedBy are kept only for
backward compatibility, and include when or under what condition they can be
removed (e.g., after migrating clients or a specific release/milestone); also
update any related function/comment references to claimInfoSchema to state the
preferred field so future maintainers know which to use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 33-43: handleAction currently calls claimMutation for
"single-claim" bounties without checking auth; use authClient.useSession() (same
hook used in use-socket-sync.ts) to verify the session at the start of
handleAction and in the MobileCTA equivalent, and if there is no authenticated
session either redirect to the login flow or show a specific toast/notification
and return early; only call claimMutation.mutate(bounty.id, ...) when the
session is present, and ensure the error path still handles API errors as a
fallback.

In `@hooks/use-bounty-mutations.ts`:
- Around line 104-126: onSuccess's fallback in queryClient.setQueryData (used in
bountyKeys.detail) writes hardcoded mock claimant data; replace that by
obtaining the real current user from the auth/session context (e.g., useAuth or
getCurrentUser) and use that user's id/username/avatar when patching claimInfo,
and validate the API response with the parseBounty Zod schema (instead of only
checking data.projectName) before deciding to return data vs patching old; if
you need a short-term marker, add a clear TODO in the onSuccess fallback noting
"replace with real user context" so it isn't left as mock data.

In `@lib/api/bounties.ts`:
- Around line 15-20: The Zod enum claimingModelSchema in lib/api/bounties.ts is
missing the "milestone" variant defined in types/bounty.ts; update
claimingModelSchema (the const named claimingModelSchema) to include "milestone"
so parseBounty and other Zod validation accepts that variant, or alternatively
remove "milestone" from the ClaimingModel union in types/bounty.ts if that
variant is not intended—make the two definitions consistent.

In `@types/bounty.ts`:
- Around line 8-13: The union type ClaimingModel declares a "milestone" variant
but claimingModelSchema in lib/api/bounties.ts does not include "milestone",
causing runtime validation to reject valid types; update claimingModelSchema to
include "milestone" as an allowed literal (or remove "milestone" from the
ClaimingModel type) so both the TypeScript type ClaimingModel and the Zod schema
claimingModelSchema match exactly; locate ClaimingModel in types/bounty.ts and
claimingModelSchema in lib/api/bounties.ts to apply the change.
- Line 46: There are two incompatible Bounty status unions causing type
mismatches; unify them by defining a single source-of-truth BountyStatus (e.g.,
export type BountyStatus = "open" | "claimed" | "closed" | "in-progress" |
"completed" or pick the canonical set) and update all Bounty/Bounty type
definitions to reference that single type (ensure types/bounty.ts, lib/types.ts
and lib/api/bounties.ts import/use the same BountyStatus) and then adjust any
usage in components like Bounty (and components/cards/bounty-card.tsx) or
functions that expect the previous statuses so they accept the unified
enum/union; update or add a central types module exporting BountyStatus and have
all modules import from it.

---

Outside diff comments:
In `@lib/api/bounties.ts`:
- Around line 63-67: There are three conflicting definitions for Bounty-related
types; pick one authoritative source (prefer the Zod schemas: bountySchema,
bountyTypeSchema, bountyStatusSchema, difficultySchema, claimingModelSchema) and
update all other exports to be derived from that source (replace manual typings
Bounty, BountyType, BountyStatus, DifficultyLevel, ClaimingModel in other
modules with z.infer<typeof ...> references), ensuring the claimingModelSchema
is extended to include "milestone" if needed and reconciling BountyStatus values
to match the chosen canonical set; update exports/imports so all code imports
these unified z.infer types (or export a single re-exporting barrel) to
eliminate the three-way split.

In `@lib/mock-bounty.ts`:
- Around line 186-193: Bounty with id "6" has status "claimed" but lacks
claimInfo, causing SidebarCTA -> ClaimantProfile to render null and hide the
"Claimed By" card; update the mock in lib/mock-bounty.ts for the object with id
"6" to include a realistic claimInfo object (e.g., claimant name/handle/avatar,
claimantAddress, claimedAt timestamp, and claimedAmount) so ClaimantProfile
receives the data it expects, or explicitly mark this entry as an intentional
edge-case in a comment if you want the missing-claimInfo behavior tested;
reference SidebarCTA and ClaimantProfile when adding the fields to ensure shape
matches what ClaimantProfile reads.

---

Nitpick comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 33-44: Duplicate claim/action logic in SidebarCTA and MobileCTA
should be extracted into a shared hook: create useClaimAction(bounty) that
encapsulates the claimMutation setup and computes ctaLabel and isPending, and
exposes handleAction; move the existing claimMutation.mutate logic and the
branch on bounty.claimingModel ("single-claim" vs opening bounty.githubIssueUrl)
into this hook, have SidebarCTA and MobileCTA call useClaimAction(bounty) and
use the returned { handleAction, ctaLabel, isPending } to render the button and
toast behavior so both components stay in sync.

In `@lib/api/bounties.ts`:
- Around line 52-54: The schema currently defines legacy flat fields claimedAt
and claimedBy alongside the new nested claimInfo (claimInfoSchema), which causes
ambiguity since UI code reads exclusively from claimInfo; add a clear in-code
comment/TODO above this schema explaining that claimInfo is the canonical
source, that claimedAt/claimedBy are kept only for backward compatibility, and
include when or under what condition they can be removed (e.g., after migrating
clients or a specific release/milestone); also update any related
function/comment references to claimInfoSchema to state the preferred field so
future maintainers know which to use.

Comment on lines 33 to 43
const handleAction = () => {
if (!canAct) return;

if (bounty.claimingModel === "single-claim") {
claimMutation.mutate(bounty.id, {
onSuccess: () => toast.success("Bounty claimed successfully!"),
onError: (err) => toast.error(err.message || "Failed to claim bounty"),
});
} else {
window.open(bounty.githubIssueUrl, "_blank", "noopener,noreferrer");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's an auth context/hook used elsewhere that could be leveraged here
rg -n "useAuth\|useSession\|useUser\|isAuthenticated" --type=ts -g '!node_modules' -C2

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# First, locate and read the component file
find . -name "bounty-detail-sidebar-cta.tsx" -type f 2>/dev/null | head -5

Repository: boundlessfi/bounties

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Read the component file to understand the mutation setup
cat -n "./components/bounty-detail/bounty-detail-sidebar-cta.tsx"

Repository: boundlessfi/bounties

Length of output: 11817


🏁 Script executed:

#!/bin/bash
# Find and read the bounty mutations hook
find . -name "use-bounty-mutations*" -type f 2>/dev/null

Repository: boundlessfi/bounties

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Read the bounty mutations hook
cat -n "./hooks/use-bounty-mutations.ts"

Repository: boundlessfi/bounties

Length of output: 4725


🏁 Script executed:

#!/bin/bash
# Find the API implementation
find . -path "*/lib/api*" -type f 2>/dev/null | head -10

Repository: boundlessfi/bounties

Length of output: 315


🏁 Script executed:

#!/bin/bash
# Read the bounties API implementation
cat -n "./lib/api/bounties.ts"

Repository: boundlessfi/bounties

Length of output: 4852


🏁 Script executed:

#!/bin/bash
# Read the API client implementation to check for auth handling
cat -n "./lib/api/client.ts"

Repository: boundlessfi/bounties

Length of output: 4964


🏁 Script executed:

#!/bin/bash
# Search for auth-related context or hooks
find . -path "*/hooks/*auth*" -o -path "*/context/*auth*" -o -path "*/lib/*auth*" 2>/dev/null | grep -v node_modules | head -20

Repository: boundlessfi/bounties

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Read the auth client to see if there's a hook to check auth state
cat -n "./lib/auth-client.ts"

Repository: boundlessfi/bounties

Length of output: 714


🏁 Script executed:

#!/bin/bash
# Search for how authClient is used and if there's a useSession hook available
rg "authClient\.\$" --type=ts -A 2 | head -30

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for uses of better-auth hooks like useSession
rg "useSession\|authClient\.\\\$" --type=ts -C 2 | head -50

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for better-auth usage patterns in the codebase
rg "better-auth" --type=ts | head -20

Repository: boundlessfi/bounties

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Search for useAuth or session-related patterns in the codebase
rg "authClient\.\\\$|use.*[Aa]uth|use.*[Ss]ession" --type=tsx --type=ts | head -30

Repository: boundlessfi/bounties

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for auth patterns more broadly
rg "authClient\.\\\$|use.*Auth|useSession" --type=ts -C 2 | head -50

Repository: boundlessfi/bounties

Length of output: 999


🏁 Script executed:

#!/bin/bash
# Check how useSession is used and if it's available for the component
rg "authClient\.useSession" --type=ts -C 3

Repository: boundlessfi/bounties

Length of output: 484


Add authentication check before claiming bounty.

Unauthenticated users can trigger the claim mutation, which will fail at the API with a 401/403 error. Since authClient.useSession() is available (as used in use-socket-sync.ts), check auth state before mutating and either redirect to login or display an appropriate message instead of relying on generic API errors.

The same issue exists in the MobileCTA component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 33 - 43,
handleAction currently calls claimMutation for "single-claim" bounties without
checking auth; use authClient.useSession() (same hook used in
use-socket-sync.ts) to verify the session at the start of handleAction and in
the MobileCTA equivalent, and if there is no authenticated session either
redirect to the login flow or show a specific toast/notification and return
early; only call claimMutation.mutate(bounty.id, ...) when the session is
present, and ensure the error path still handles API errors as a fallback.

Comment on lines +104 to +126
onSuccess: (data, id) => {
queryClient.setQueryData<Bounty>(bountyKeys.detail(id), (old) => {
if (!old) return old;

// If the API returned a perfectly valid object with a projectName, use it
if (data && data.projectName) {
return data;
}

// Otherwise, safely patch the existing data with the claimed state
return {
...old,
status: "claimed",
claimInfo: {
claimedBy: {
userId: "current-user",
username: "You (Mock User)",
avatarUrl: "https://github.com/shadcn.png",
},
claimedAt: new Date().toISOString(),
},
};
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded mock user data in onSuccess fallback will produce incorrect claim info in production.

When the API response lacks projectName, the fallback path writes a fabricated user ("current-user", "You (Mock User)", shadcn avatar) into the query cache. In production, if the claim endpoint returns a partial response (e.g., missing projectName but valid otherwise), every claim will show this mock identity instead of the actual claimant.

The actual current user's identity should come from auth state (e.g., a session/user context), and the validity check on data.projectName is a fragile proxy for "is this a complete response."

Consider:

  1. Fetching the current user from an auth context/hook.
  2. Validating the response with the Zod schema (parseBounty) instead of checking a single field.
  3. At minimum, marking this as a // TODO: replace with real user context so it's not forgotten.
Sketch of a safer approach
-    onSuccess: (data, id) => {
-      queryClient.setQueryData<Bounty>(bountyKeys.detail(id), (old) => {
-        if (!old) return old;
-
-        // If the API returned a perfectly valid object with a projectName, use it
-        if (data && data.projectName) {
-          return data;
-        }
-
-        // Otherwise, safely patch the existing data with the claimed state
-        return {
-          ...old,
-          status: "claimed",
-          claimInfo: {
-            claimedBy: {
-              userId: "current-user",
-              username: "You (Mock User)",
-              avatarUrl: "https://github.com/shadcn.png",
-            },
-            claimedAt: new Date().toISOString(),
-          },
-        };
-      });
+    onSuccess: (data, id) => {
+      // Always use the API response as the source of truth
+      queryClient.setQueryData<Bounty>(bountyKeys.detail(id), (old) => {
+        if (!old) return old;
+        return { ...old, ...data };
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-bounty-mutations.ts` around lines 104 - 126, onSuccess's fallback
in queryClient.setQueryData (used in bountyKeys.detail) writes hardcoded mock
claimant data; replace that by obtaining the real current user from the
auth/session context (e.g., useAuth or getCurrentUser) and use that user's
id/username/avatar when patching claimInfo, and validate the API response with
the parseBounty Zod schema (instead of only checking data.projectName) before
deciding to return data vs patching old; if you need a short-term marker, add a
clear TODO in the onSuccess fallback noting "replace with real user context" so
it isn't left as mock data.

Comment on lines +15 to +20
const claimingModelSchema = z.enum([
"single-claim",
"application",
"competition",
"multi-winner",
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ClaimingModel mismatch: "milestone" is missing from the Zod schema.

types/bounty.ts (line 13) defines ClaimingModel with a "milestone" variant, but claimingModelSchema here only has four values. If API data ever contains "milestone", Zod validation via parseBounty will reject it at runtime.

Either add "milestone" here or remove it from types/bounty.ts to keep both in sync.

Proposed fix
 const claimingModelSchema = z.enum([
   "single-claim",
   "application",
   "competition",
   "multi-winner",
+  "milestone",
 ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const claimingModelSchema = z.enum([
"single-claim",
"application",
"competition",
"multi-winner",
]);
const claimingModelSchema = z.enum([
"single-claim",
"application",
"competition",
"multi-winner",
"milestone",
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/bounties.ts` around lines 15 - 20, The Zod enum claimingModelSchema
in lib/api/bounties.ts is missing the "milestone" variant defined in
types/bounty.ts; update claimingModelSchema (the const named
claimingModelSchema) to include "milestone" so parseBounty and other Zod
validation accepts that variant, or alternatively remove "milestone" from the
ClaimingModel union in types/bounty.ts if that variant is not intended—make the
two definitions consistent.

Comment on lines +8 to +13
export type ClaimingModel =
| "single-claim"
| "application"
| "competition"
| "multi-winner"
| "milestone";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

"milestone" variant exists here but not in the Zod schema — runtime validation mismatch.

claimingModelSchema in lib/api/bounties.ts only has four values and will reject any bounty with claimingModel: "milestone". Either add it to the Zod schema or remove it here. This is the root cause of the mismatch flagged in the schema file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/bounty.ts` around lines 8 - 13, The union type ClaimingModel declares a
"milestone" variant but claimingModelSchema in lib/api/bounties.ts does not
include "milestone", causing runtime validation to reject valid types; update
claimingModelSchema to include "milestone" as an allowed literal (or remove
"milestone" from the ClaimingModel type) so both the TypeScript type
ClaimingModel and the Zod schema claimingModelSchema match exactly; locate
ClaimingModel in types/bounty.ts and claimingModelSchema in lib/api/bounties.ts
to apply the change.

tags: string[];

status: 'open' | 'claimed' | 'closed'
status: "open" | "claimed" | "closed";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all BountyStatus definitions and usages
echo "=== BountyStatus type/interface definitions ==="
rg -n "type\s+BountyStatus|interface\s+BountyStatus" --type=ts

echo ""
echo "=== Files containing 'BountyStatus' references ==="
rg -n "BountyStatus" --type=ts -l

echo ""
echo "=== Bounty type/interface definitions ==="
rg -n "type\s+Bounty\b|interface\s+Bounty\b" --type=ts -A5

Repository: boundlessfi/bounties

Length of output: 4161


🏁 Script executed:

#!/bin/bash
# Read types/bounty.ts to see the full Bounty interface
echo "=== types/bounty.ts (full file) ==="
cat -n types/bounty.ts

echo ""
echo "=== lib/api/bounties.ts (Zod schemas) ==="
rg -n "bountyStatusSchema|bountySchema" --type=ts -A10 lib/api/bounties.ts

echo ""
echo "=== lib/types.ts (BountyStatus and Bounty definitions) ==="
rg -n "export type BountyStatus|export interface Bounty" --type=ts -A5 lib/types.ts

Repository: boundlessfi/bounties

Length of output: 4678


🏁 Script executed:

#!/bin/bash
# Check lib/api/index.ts to see which BountyStatus is re-exported
echo "=== lib/api/index.ts (full file) ==="
cat -n lib/api/index.ts

echo ""
echo "=== Check imports of BountyStatus in consuming files ==="
rg -n "import.*BountyStatus|from.*lib/types|from.*lib/api" --type=ts -B1 -A1 | head -100

echo ""
echo "=== lib/types.ts context - is it part of public API? ==="
head -n 50 lib/types.ts

Repository: boundlessfi/bounties

Length of output: 8539


Consolidate conflicting BountyStatus definitions across modules.

Two incompatible BountyStatus definitions exist:

  • types/bounty.ts and lib/api/bounties.ts: "open" | "claimed" | "closed"
  • lib/types.ts: "open" | "in-progress" | "completed"

At least one component (components/cards/bounty-card.tsx) imports Bounty from lib/types, potentially causing type mismatches when passed to functions expecting the lib/api Bounty type with different status values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/bounty.ts` at line 46, There are two incompatible Bounty status unions
causing type mismatches; unify them by defining a single source-of-truth
BountyStatus (e.g., export type BountyStatus = "open" | "claimed" | "closed" |
"in-progress" | "completed" or pick the canonical set) and update all
Bounty/Bounty type definitions to reference that single type (ensure
types/bounty.ts, lib/types.ts and lib/api/bounties.ts import/use the same
BountyStatus) and then adjust any usage in components like Bounty (and
components/cards/bounty-card.tsx) or functions that expect the previous statuses
so they accept the unified enum/union; update or add a central types module
exporting BountyStatus and have all modules import from it.

@Bosun-Josh121 Bosun-Josh121 force-pushed the feat/implement-single-claim-model-logic branch from 6869305 to 8806d35 Compare February 23, 2026 01:01
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
providers/query-provider.tsx (1)

18-21: ⚠️ Potential issue | 🟠 Major

Remove global retry from mutations — risky for non-idempotent operations.

TanStack Query v5 intentionally defaults mutations.retry to 0. Setting it to 3 globally means every mutation (claim submissions, deletes, updates) will be automatically retried up to 3 times on failure. For the single-claim model introduced in this PR, a transient network error during the "claim bounty" mutation could trigger two or three additional POST requests, potentially creating duplicate claim records or corrupting the claimed state server-side.

Non-idempotent mutations must opt in to retry individually when safe to do so, not inherit a global default.

🔧 Proposed fix
     mutations: {
-      retry: 3,
-      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
     },

If retry is needed for specific mutations (e.g., idempotent updates), configure it per-mutation at the call site:

useMutation({ mutationFn: ..., retry: 3, retryDelay: ... })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@providers/query-provider.tsx` around lines 18 - 21, The global mutations
config currently sets mutations.retry and mutations.retryDelay in the query
client/provider; remove mutations.retry (and related global retryDelay) so
mutations inherit the v5 default of 0 and do not automatically retry
non-idempotent operations, then document/encourage using per-mutation retry
options at call sites (e.g., useMutation({ mutationFn: ..., retry: 3,
retryDelay: ... })) for any idempotent-safe mutations; update the code that
defines the mutations config (the mutations object used when creating the
QueryClient/QueryProvider) to drop the global retry settings.
lib/api/bounties.ts (1)

35-69: ⚠️ Potential issue | 🟠 Major

Consolidate competing Bounty type definitions — API schema is missing required fields

The Zod-inferred Bounty type from bountySchema (line 65) is missing fields that the Bounty interface in types/bounty.ts declares: milestones, applicants, competitors, and members. Additionally, the schema's claimingModel enum is missing the "milestone" option present in the types.

This causes real inconsistency: files like hooks/use-bounty-mutations.ts and components/bounty-detail components import Bounty from @/lib/api and lack these fields, while lib/store.ts and other components import from @/types/bounty and have them. Even components/bounty/bounty-list.tsx imports from both sources (lines 9-10), creating ambiguity.

The narrower API schema should either be expanded to include all fields the interface declares, or the interface should be derived from the schema to maintain a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/bounties.ts` around lines 35 - 69, The bountySchema-derived Bounty
type is missing fields and an enum option used elsewhere; update bountySchema
and claimingModelSchema so the schema includes the missing array/object fields
(milestones, applicants, competitors, members) with appropriate types (e.g.,
z.array(...) or z.record/.../nullable as per types/bounty.ts) and add the
"milestone" option to claimingModelSchema, or alternatively remove the separate
interface and export Bounty by inferring z.infer<typeof bountySchema> everywhere
to enforce a single source of truth; ensure you modify the symbols bountySchema,
claimingModelSchema and any exported Bounty type usages so imports across
hooks/components use the same schema-derived type.
🧹 Nitpick comments (13)
app/transparency/page.tsx (1)

101-138: Dead statsError branch in statCards values.

The statsError ? "—" : … ternary in each card's value (lines 104, 113, 122, 131) is unreachable because the entire stats grid is conditionally excluded when statsError is true (line 178). The "—" values are computed but never rendered.

This isn't harmful, but it's slightly misleading for future readers. Consider simplifying the value expressions to only handle the stats / fallback case, since the error state is already gated at the rendering level.

♻️ Simplified statCards values (example for one card)
     {
       title: "Total Funds Distributed",
-      value: statsError
-        ? "—"
-        : stats
-          ? `$${stats.totalFundsDistributed.toLocaleString()}`
-          : "$0",
+      value: stats
+        ? `$${stats.totalFundsDistributed.toLocaleString()}`
+        : "$0",
       icon: DollarSign,
     },

Apply the same pattern to the other three cards.

Also applies to: 177-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/transparency/page.tsx` around lines 101 - 138, The statCards array
currently uses a dead branch conditional (statsError ? "—" : ...) even though
the component already gates rendering when statsError is true; update the
statCards entries in statCards so each card's value only checks stats (e.g.,
value: stats ? `<formatted>` : `<fallback>`) removing the statsError ternary,
and apply the same simplification wherever the same pattern appears in this file
(the stats grid rendering logic that already hides the grid on statsError).
Ensure you update all four cards (titles: "Total Funds Distributed",
"Contributors Paid", "Projects Funded", "Avg. Payout Time") to use only
stats-based conditionals.
providers/query-provider.tsx (1)

50-55: Devtools import is bundled in production despite the render guard.

The static import at line 4 means @tanstack/react-query-devtools is included in the production JS bundle even though it is never rendered when NODE_ENV !== "development". Modern bundlers (including Next.js/Turbopack) cannot tree-shake this because the import is unconditional.

For stricter production bundle hygiene, consider a dynamic import:

♻️ Proposed refactor (optional)
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import dynamic from "next/dynamic";
+const ReactQueryDevtools =
+  process.env.NODE_ENV === "development"
+    ? dynamic(() =>
+        import("@tanstack/react-query-devtools").then(
+          (mod) => ({ default: mod.ReactQueryDevtools })
+        )
+      )
+    : () => null;

Then simplify the JSX:

-      {process.env.NODE_ENV === "development" && (
-        <ReactQueryDevtools
-          initialIsOpen={false}
-          buttonPosition="bottom-right"
-        />
-      )}
+      <ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-right" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@providers/query-provider.tsx` around lines 50 - 55, The unconditional static
import of ReactQueryDevtools causes it to be bundled in production; change to a
dynamic import and lazy render only in development: remove the top-level static
import of ReactQueryDevtools and instead dynamically import it inside the
providers/query-provider.tsx component (e.g., using React.lazy/Suspense or a
framework dynamic import) and render the lazily-loaded ReactQueryDevtools only
when process.env.NODE_ENV === "development" (the guard around the JSX should
remain); update any references to ReactQueryDevtools in the file to use the
dynamically imported component so the devtools module is excluded from
production bundles.
app/api/reputation/[userId]/completion-history/route.ts (1)

54-57: No authorization check on user-specific endpoint.

Any caller can fetch completion history for any userId. If this route is intended to persist beyond mock/scaffold usage, consider adding authentication/authorization to ensure users can only access their own data (or admins can access others').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/reputation/`[userId]/completion-history/route.ts around lines 54 -
57, This route reads BountyStore.getBounties() and filters by the path param
userId without any auth checks; enforce authorization by retrieving the
authenticated caller (session/token) and verifying that its user id matches the
requested userId or that the caller has an admin role before returning completed
bounties; update the handler in route.ts to reject (401/403) when the caller is
unauthenticated or not allowed, and only then call BountyStore.getBounties() and
filter by claimedBy === userId.
components/bounty/forms/schemas.ts (1)

78-78: Consider stricter wallet address validation.

z.string().min(1) accepts any non-empty string. If this is for Stellar addresses (as suggested by "XLM" in the reward currency), consider adding a regex or length check (e.g., Stellar public keys are 56 characters starting with G). This would prevent form submission with clearly invalid addresses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/forms/schemas.ts` at line 78, Replace the permissive
walletAddress zod rule (walletAddress: z.string().min(1, ...)) with a stricter
validator: require exact Stellar public key shape by enforcing 56 characters and
that it starts with 'G' and only contains the Stellar Base32 charset (A–Z and
2–7) — either via z.string().length(56).refine(...) or z.string().regex(...),
and provide a clear error message like "Invalid Stellar address" to block
clearly invalid inputs; update the walletAddress schema in
components/bounty/forms/schemas.ts accordingly.
codegen.ts (1)

4-4: Schema path relies on a sibling directory checkout.

"../boundless-nestjs/src/schema.gql" requires the NestJS repo to be co-located. This will break codegen in CI or for contributors who don't have that repo checked out alongside. Consider publishing the schema as an artifact or fetching it via introspection URL to decouple the two repos.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codegen.ts` at line 4, The codegen config currently points schema:
"../boundless-nestjs/src/schema.gql" which requires a sibling repo checkout;
change the schema source in codegen.ts to a decoupled option (prefer an
introspection URL or a published schema artifact) by replacing the hardcoded
relative path in the schema property with either an env-driven introspection
endpoint (e.g., process.env.GRAPHQL_SCHEMA_URL) used by graphql-codegen's url
loader or a packaged/local schema file shipped via your package/artifact; ensure
the new logic in codegen.ts falls back gracefully (env URL -> packaged schema ->
error) so CI and external contributors can run codegen without the sibling repo.
lib/graphql/client.ts (1)

44-75: Address ESLint-flagged any types and empty object type.

Static analysis flags four issues here. The {} default on TVariables and the any casts reduce type safety and will fail CI if ESLint is enforced.

Proposed fix
-export const fetcher = <TData, TVariables extends object = {}>(
+export const fetcher = <TData, TVariables extends Record<string, unknown> = Record<string, never>>(
   query: string,
   variables?: TVariables,
 ) => {
   return async (): Promise<TData> => {
     const token = getAccessToken();
     const headers: Record<string, string> = {};
     if (token) {
       headers.authorization = `Bearer ${token}`;
     }
 
     try {
-      return await (graphQLClient.request as any)(query, variables, headers);
+      return await graphQLClient.request<TData>({
+        document: query,
+        variables,
+        requestHeaders: headers,
+      });
-    } catch (error: any) {
+    } catch (error: unknown) {
+      const err = error as { response?: { errors?: Array<{ extensions?: { status?: number } }> } };
       // Global error handling for auth failures (like Apollo ErrorLink)
-      if (error?.response?.errors) {
-        error.response.errors.forEach((err: any) => {
-          const status = (err?.extensions?.status as number) || 500;
+      if (err?.response?.errors) {
+        err.response.errors.forEach((gqlErr) => {
+          const status = gqlErr?.extensions?.status ?? 500;
           if (isAuthStatus(status)) {
             clearAccessToken();
             if (typeof window !== "undefined") {
               window.dispatchEvent(
                 new CustomEvent("auth:unauthorized", { detail: { status } }),
               );
             }
           }
         });
       }
       throw error;
     }
   };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/graphql/client.ts` around lines 44 - 75, The fetcher currently uses
unsafe any and {} types; update TVariables to extend Record<string, unknown>
(e.g., TVariables extends Record<string, unknown> = Record<string, unknown>)
instead of {}, change the runtime cast (graphQLClient.request as any) to a
properly typed function cast such as (graphQLClient.request as unknown as
(query: string, variables?: TVariables, headers?: Record<string,string>) =>
Promise<TData>)(...), and change the catch parameter to error: unknown and
narrow it before use (e.g., const err = error as { response?: { errors?:
Array<Record<string, unknown>> } } ) so you can safely access
err.response.errors without using any; keep headers typed as
Record<string,string> as-is.
components/bounty-detail/submission-dialog.tsx (2)

89-99: Stale draft reference due to excluded dependency — acceptable but worth noting.

The draft and baseDefaults values are intentionally excluded from the dependency array (with the ESLint disable). This means if draft changes while the dialog is already open, the form won't re-sync. This is likely the desired behavior (only restore draft on dialog open), but documenting the intent in the comment would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/submission-dialog.tsx` around lines 89 - 99, Add an
explanatory comment above the useEffect that documents the intentional exclusion
of draft and baseDefaults from the dependency array: state that the effect only
re-syncs the form when the dialog opens (open changes) so updates to draft while
the dialog is already open are ignored, and note the ESLint disable is
intentional; reference the useEffect that calls form.reset({...draft,
walletAddress: baseDefaults.walletAddress}) and the branch that falls back to
form.reset(baseDefaults) so future maintainers understand the intended behavior.

84-87: as never type cast is a workaround for useFieldArray with primitive arrays.

This is a known limitation with react-hook-form's useFieldArray when working with arrays of primitives (strings) rather than arrays of objects. The as never cast suppresses the type error but reduces type safety.

Consider using an array of objects (e.g., { url: string }) for attachments to avoid the cast and get proper typing from useFieldArray, or extract a small wrapper that encapsulates the cast.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/submission-dialog.tsx` around lines 84 - 87, The
current use of useFieldArray with a cast "as never" for name "attachments"
(const { fields, append, remove } = useFieldArray({...})) hides type issues for
primitive string arrays; change the attachments state/type to an array of
objects (e.g., { url: string }) and update the form schema/type so useFieldArray
can be typed properly, then update references to fields/append/remove to use the
object shape, or alternatively create a small wrapper function around
useFieldArray that performs the cast in one place and documents it so callers
(fields, append, remove, form.control, name "attachments") keep type safety
without sprinkling "as never" across the codebase.
app/profile/[userId]/page.tsx (1)

27-27: Fetching all bounties just to filter for user's claims is inefficient.

useBounties() with no params fetches the entire bounty list. As the dataset grows, this will degrade performance. Consider adding a server-side filter parameter (e.g., claimedBy or userId) to the bounties API, or using a dedicated endpoint for user claims.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx at line 27, Calling useBounties() without
parameters fetches the entire bounty list then filters locally (bountyResponse),
which will not scale; update the call in app/profile/[userId]/page.tsx to
request only that user's claims (e.g., useBounties({ claimedBy: userId }) or
useBounties({ userId })) and ensure the backend API supports a claimedBy/userId
query parameter or add a dedicated endpoint (or implement a new hook like
useUserClaims) so the filtering happens server-side and only the user's bounty
data is returned.
types/bounty.ts (1)

52-54: Complete the Single Claim Model migration: claimInfo is not being populated while top-level claimedBy/claimedAt remain critical for business logic.

The interface coexists claimedAt?: string + claimedBy?: string (lines 52-53) with claimInfo?: ClaimInfo (line 54), but the structures don't fully overlap—claimInfo.claimedBy is an object containing userId, username, and avatarUrl, whereas the top-level claimedBy is a simple string. More critically, claimInfo is never populated in the logic layer (only read in UI), while the top-level fields are actively used for filtering (app/profile/[userId]/page.tsx, completion-history/route.ts) and inactivity checks (lib/logic/bounty-logic.ts).

To unify around ClaimInfo: either migrate the logic layer to populate and consume claimInfo consistently, or if claimInfo serves a different purpose (e.g., richer metadata for display), clarify the intent and add comments distinguishing which field is authoritative for filtering/logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/bounty.ts` around lines 52 - 54, The types declare both
claimedAt/claimedBy and claimInfo (ClaimInfo) but claimInfo is never populated
while business logic and filters use the top-level claimedBy/claimedAt; either
migrate logic to populate and consume claimInfo or explicitly mark top-level
fields as authoritative. Fix by updating the bounty creation/update flows and
readers that set/read claim data (places that call/modify claimedAt, claimedBy
and any functions in bounty-logic that check inactivity) to populate
claimInfo.claimedBy.userId, claimInfo.claimedBy.username,
claimInfo.claimedBy.avatarUrl and claimInfo.claimedAt from the same source, and
update consumers currently reading claimedBy/claimedAt (e.g., profile filters
and completion-history readers) to read from claimInfo where appropriate;
alternatively add clear comments in the types/bounty.ts next to
claimedAt/claimedBy and claimInfo explaining which is authoritative for
filtering/logic and keep population consistent. Ensure the symbol names
referenced: claimedAt, claimedBy, claimInfo, and ClaimInfo are updated together
across creation, update, and read paths.
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)

205-259: MobileCTA duplicates nearly all action/label logic from SidebarCTA.

handleAction, the label function, and the useClaimBounty + dialogOpen state are copy-pasted between the two components. Extract a shared custom hook (e.g., useBountyCTA(bounty)) that encapsulates the mutation, dialog state, pending flag, and label derivation — then consume it in both components.

This prevents logic drift and ensures both CTAs share the same mutation instance (so pending state is consistent if both ever render simultaneously).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 205 -
259, Extract the duplicated CTA logic into a shared hook named
useBountyCTA(bounty) that returns { dialogOpen, setDialogOpen, claimMutation,
canAct, handleAction, label } and move the label derivation, canAct calculation,
useClaimBounty() instantiation and the handleAction branching (single-claim vs
dialog) into that hook; then replace the duplicated logic in MobileCTA and
SidebarCTA to call useBountyCTA(bounty) and use the returned values (use
claimMutation.isPending for UI disabled/loader and label() for button text) so
both components share the same mutation instance and dialog state management.
types/participation.ts (1)

23-29: content is required yet deprecated — consider making it optional.

Since content is deprecated in favor of explanation, keeping it as a required (string) field forces every producer of a Submission to supply it. Making it optional (content?: string) would allow new code paths to omit it entirely while old consumers degrade gracefully, which better reflects the deprecation intent.

♻️ Suggested change
- /** `@deprecated` Use explanation. Kept for backward compatibility with consumers that expect a generic content field. */
- content: string;
+ /** `@deprecated` Use explanation. Kept for backward compatibility with consumers that expect a generic content field. */
+ content?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/participation.ts` around lines 23 - 29, The deprecated field content is
currently required which forces producers to supply it; change its declaration
to optional (content?: string) in the Participation/Submission type so new code
can omit it while preserving backward compatibility with explanation, and ensure
any code paths constructing or validating Submission (references: content,
explanation, attachments, walletAddress) handle undefined content gracefully
(e.g., optional checks or fallback to explanation).
app/api/bounties/[id]/submit/route.ts (1)

4-4: Server route imports validation schema from components/ — move schema to a shared location.

Importing submissionFormSchema from @/components/bounty/forms/schemas couples this API route to the UI layer. If the component directory is reorganised, this route silently breaks. Consider co-locating shared schemas under lib/ (e.g., lib/schemas/submission.ts) and importing from there in both the route and the form component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/submit/route.ts at line 4, The API route imports
submissionFormSchema from the UI components folder; move the schema to a shared
location (e.g., create lib/schemas/submission.ts exporting submissionFormSchema)
and update imports in both app/api/bounties/[id]/submit/route.ts and the form
component to import from the new shared module (ensure the exported symbol name
submissionFormSchema is preserved and update any relative import paths
accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 90-95: The catch block in app/api/bounties/[id]/submit/route.ts
currently swallows errors and returns a 500 without logging; update the catch to
capture the thrown error (e.g., catch (err) or catch (error)), log it
server-side using the existing logger or console.error with context (include
request/bounty id if available), then return the same NextResponse.json({ error:
"Internal Server Error" }, { status: 500 }); ensure you reference the catch
block around NextResponse.json and the handler that performs the submission.
- Around line 46-57: Remove "single-claim" from the allowedModels array in the
submit route so submissions cannot be created for single-claim bounties; locate
the array named allowedModels and the check that uses
allowedModels.includes(bounty.claimingModel) in the submit route handler
(route.ts) and update it to only allow "competition", "multi-winner", and
"application" so single-claim bounties continue to go through the dedicated
claim endpoint which handles status: 'claimed', claimedBy, and claimExpiresAt.

In `@app/profile/`[userId]/page.tsx:
- Around line 20-21: The current unsafe cast of params.userId to string in
page.tsx can panic if useParams() returns a string[]; update the logic around
useParams()/params and the userId variable to defensively handle both string and
string[] values (e.g., check Array.isArray(params.userId) and normalize to a
single string by taking the first element or joining, or handle the
invalid-multiple case explicitly), ensuring downstream code that uses userId
always receives a validated string or returns/redirects on invalid input.
- Around line 33-60: The myClaims computation currently filters bounties using
the legacy bounty.claimedBy field which will miss claims when data is migrated
to the nested claimInfo structure; update the filter inside myClaims to check
claimInfo?.claimedBy?.userId === userId first (with a fallback to the old
bounty.claimedBy === userId) so both shapes are supported, and ensure you
reference bountyResponse?.data and the existing map logic (no other changes
required).

In `@components/bounty-detail/submission-dialog.tsx`:
- Around line 45-51: getBaseDefaults currently seeds the
SubmissionFormValue.walletAddress with a hardcoded mockWalletInfo.address;
replace this before production by wiring the default to the real connected
wallet (e.g., read from your wallet context/provider or currentUser wallet
state) or at minimum add a clear TODO comment. Update the getBaseDefaults
function to pull the wallet address from the authoritative source used elsewhere
in the app (instead of mockWalletInfo.address), and ensure the form
initialization handles the case when no wallet is connected (empty string or
null) to avoid using test data in production.

In `@lib/graphql/client.ts`:
- Around line 39-41: The current graphQLClient uses a possibly relative url
(NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql") which fails in SSR because Node's
fetch requires an absolute URL; update the construction so that if the chosen
url starts with "/" and code is running server-side (typeof window ===
"undefined"), build an absolute URL using a server host env (e.g.,
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.VERCEL_URL ||
process.env.NEXTAUTH_URL) prefixed with "https://" (fall back to
"http://localhost:3000" if none present), then pass that absolute string to new
GraphQLClient; change the symbols url and graphQLClient in lib/graphql/client.ts
accordingly so GraphQLClient always receives an absolute URL on the server.

---

Outside diff comments:
In `@lib/api/bounties.ts`:
- Around line 35-69: The bountySchema-derived Bounty type is missing fields and
an enum option used elsewhere; update bountySchema and claimingModelSchema so
the schema includes the missing array/object fields (milestones, applicants,
competitors, members) with appropriate types (e.g., z.array(...) or
z.record/.../nullable as per types/bounty.ts) and add the "milestone" option to
claimingModelSchema, or alternatively remove the separate interface and export
Bounty by inferring z.infer<typeof bountySchema> everywhere to enforce a single
source of truth; ensure you modify the symbols bountySchema, claimingModelSchema
and any exported Bounty type usages so imports across hooks/components use the
same schema-derived type.

In `@providers/query-provider.tsx`:
- Around line 18-21: The global mutations config currently sets mutations.retry
and mutations.retryDelay in the query client/provider; remove mutations.retry
(and related global retryDelay) so mutations inherit the v5 default of 0 and do
not automatically retry non-idempotent operations, then document/encourage using
per-mutation retry options at call sites (e.g., useMutation({ mutationFn: ...,
retry: 3, retryDelay: ... })) for any idempotent-safe mutations; update the code
that defines the mutations config (the mutations object used when creating the
QueryClient/QueryProvider) to drop the global retry settings.

---

Duplicate comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 35-46: handleAction currently calls claimMutation.mutate without
checking authentication; before calling claimMutation.mutate in the handleAction
function (and before setDialogOpen for multi-claim flow) use
authClient.useSession() to verify a logged-in session (or equivalent auth
helper) and if no session is present, open the login prompt/modal or show a
friendly toast asking the user to sign in instead of calling the API; only call
claimMutation.mutate(bounty.id, ...) or setDialogOpen(true) when a valid session
exists.

In `@hooks/use-bounty-mutations.ts`:
- Around line 104-126: The onSuccess handler for the mutation is writing
hardcoded mock user info into the cache via
queryClient.setQueryData(bountyKeys.detail(id)), which must be removed; instead,
detect whether the API response contains a complete claimInfo (e.g. check
data?.claimInfo?.claimedBy?.userId or data?.status) and if so return data,
otherwise preserve the existing claimInfo from the old object (or set claimInfo
to null) without injecting fabricated values; update the logic inside the
setQueryData callback (referencing onSuccess, queryClient.setQueryData,
bountyKeys.detail) to avoid hardcoded "current-user"/"You (Mock User)" and use
the existing old.claimInfo or authenticated user info from your auth context if
available.

In `@lib/api/bounties.ts`:
- Around line 17-22: The z.enum claimingModelSchema is missing the "milestone"
variant so valid data typed in types/bounty.ts will be rejected; update the
claimingModelSchema declaration (symbol: claimingModelSchema) to include
"milestone" in its enum list so the Zod schema matches the TypeScript type and
validates the same set of claimingModel values.

In `@types/bounty.ts`:
- Around line 8-13: The Zod enum claimingModelSchema in lib/api/bounties.ts is
missing the "milestone" variant present on the ClaimingModel type; update the
claimingModelSchema (the Zod definition referenced where claimingModelSchema is
declared) to include "milestone" alongside "single-claim", "application",
"competition", and "multi-winner" so runtime validation matches the
ClaimingModel type.

---

Nitpick comments:
In `@app/api/bounties/`[id]/submit/route.ts:
- Line 4: The API route imports submissionFormSchema from the UI components
folder; move the schema to a shared location (e.g., create
lib/schemas/submission.ts exporting submissionFormSchema) and update imports in
both app/api/bounties/[id]/submit/route.ts and the form component to import from
the new shared module (ensure the exported symbol name submissionFormSchema is
preserved and update any relative import paths accordingly).

In `@app/api/reputation/`[userId]/completion-history/route.ts:
- Around line 54-57: This route reads BountyStore.getBounties() and filters by
the path param userId without any auth checks; enforce authorization by
retrieving the authenticated caller (session/token) and verifying that its user
id matches the requested userId or that the caller has an admin role before
returning completed bounties; update the handler in route.ts to reject (401/403)
when the caller is unauthenticated or not allowed, and only then call
BountyStore.getBounties() and filter by claimedBy === userId.

In `@app/profile/`[userId]/page.tsx:
- Line 27: Calling useBounties() without parameters fetches the entire bounty
list then filters locally (bountyResponse), which will not scale; update the
call in app/profile/[userId]/page.tsx to request only that user's claims (e.g.,
useBounties({ claimedBy: userId }) or useBounties({ userId })) and ensure the
backend API supports a claimedBy/userId query parameter or add a dedicated
endpoint (or implement a new hook like useUserClaims) so the filtering happens
server-side and only the user's bounty data is returned.

In `@app/transparency/page.tsx`:
- Around line 101-138: The statCards array currently uses a dead branch
conditional (statsError ? "—" : ...) even though the component already gates
rendering when statsError is true; update the statCards entries in statCards so
each card's value only checks stats (e.g., value: stats ? `<formatted>` :
`<fallback>`) removing the statsError ternary, and apply the same simplification
wherever the same pattern appears in this file (the stats grid rendering logic
that already hides the grid on statsError). Ensure you update all four cards
(titles: "Total Funds Distributed", "Contributors Paid", "Projects Funded",
"Avg. Payout Time") to use only stats-based conditionals.

In `@codegen.ts`:
- Line 4: The codegen config currently points schema:
"../boundless-nestjs/src/schema.gql" which requires a sibling repo checkout;
change the schema source in codegen.ts to a decoupled option (prefer an
introspection URL or a published schema artifact) by replacing the hardcoded
relative path in the schema property with either an env-driven introspection
endpoint (e.g., process.env.GRAPHQL_SCHEMA_URL) used by graphql-codegen's url
loader or a packaged/local schema file shipped via your package/artifact; ensure
the new logic in codegen.ts falls back gracefully (env URL -> packaged schema ->
error) so CI and external contributors can run codegen without the sibling repo.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 205-259: Extract the duplicated CTA logic into a shared hook named
useBountyCTA(bounty) that returns { dialogOpen, setDialogOpen, claimMutation,
canAct, handleAction, label } and move the label derivation, canAct calculation,
useClaimBounty() instantiation and the handleAction branching (single-claim vs
dialog) into that hook; then replace the duplicated logic in MobileCTA and
SidebarCTA to call useBountyCTA(bounty) and use the returned values (use
claimMutation.isPending for UI disabled/loader and label() for button text) so
both components share the same mutation instance and dialog state management.

In `@components/bounty-detail/submission-dialog.tsx`:
- Around line 89-99: Add an explanatory comment above the useEffect that
documents the intentional exclusion of draft and baseDefaults from the
dependency array: state that the effect only re-syncs the form when the dialog
opens (open changes) so updates to draft while the dialog is already open are
ignored, and note the ESLint disable is intentional; reference the useEffect
that calls form.reset({...draft, walletAddress: baseDefaults.walletAddress}) and
the branch that falls back to form.reset(baseDefaults) so future maintainers
understand the intended behavior.
- Around line 84-87: The current use of useFieldArray with a cast "as never" for
name "attachments" (const { fields, append, remove } = useFieldArray({...}))
hides type issues for primitive string arrays; change the attachments state/type
to an array of objects (e.g., { url: string }) and update the form schema/type
so useFieldArray can be typed properly, then update references to
fields/append/remove to use the object shape, or alternatively create a small
wrapper function around useFieldArray that performs the cast in one place and
documents it so callers (fields, append, remove, form.control, name
"attachments") keep type safety without sprinkling "as never" across the
codebase.

In `@components/bounty/forms/schemas.ts`:
- Line 78: Replace the permissive walletAddress zod rule (walletAddress:
z.string().min(1, ...)) with a stricter validator: require exact Stellar public
key shape by enforcing 56 characters and that it starts with 'G' and only
contains the Stellar Base32 charset (A–Z and 2–7) — either via
z.string().length(56).refine(...) or z.string().regex(...), and provide a clear
error message like "Invalid Stellar address" to block clearly invalid inputs;
update the walletAddress schema in components/bounty/forms/schemas.ts
accordingly.

In `@lib/graphql/client.ts`:
- Around line 44-75: The fetcher currently uses unsafe any and {} types; update
TVariables to extend Record<string, unknown> (e.g., TVariables extends
Record<string, unknown> = Record<string, unknown>) instead of {}, change the
runtime cast (graphQLClient.request as any) to a properly typed function cast
such as (graphQLClient.request as unknown as (query: string, variables?:
TVariables, headers?: Record<string,string>) => Promise<TData>)(...), and change
the catch parameter to error: unknown and narrow it before use (e.g., const err
= error as { response?: { errors?: Array<Record<string, unknown>> } } ) so you
can safely access err.response.errors without using any; keep headers typed as
Record<string,string> as-is.

In `@providers/query-provider.tsx`:
- Around line 50-55: The unconditional static import of ReactQueryDevtools
causes it to be bundled in production; change to a dynamic import and lazy
render only in development: remove the top-level static import of
ReactQueryDevtools and instead dynamically import it inside the
providers/query-provider.tsx component (e.g., using React.lazy/Suspense or a
framework dynamic import) and render the lazily-loaded ReactQueryDevtools only
when process.env.NODE_ENV === "development" (the guard around the JSX should
remain); update any references to ReactQueryDevtools in the file to use the
dynamically imported component so the devtools module is excluded from
production bundles.

In `@types/bounty.ts`:
- Around line 52-54: The types declare both claimedAt/claimedBy and claimInfo
(ClaimInfo) but claimInfo is never populated while business logic and filters
use the top-level claimedBy/claimedAt; either migrate logic to populate and
consume claimInfo or explicitly mark top-level fields as authoritative. Fix by
updating the bounty creation/update flows and readers that set/read claim data
(places that call/modify claimedAt, claimedBy and any functions in bounty-logic
that check inactivity) to populate claimInfo.claimedBy.userId,
claimInfo.claimedBy.username, claimInfo.claimedBy.avatarUrl and
claimInfo.claimedAt from the same source, and update consumers currently reading
claimedBy/claimedAt (e.g., profile filters and completion-history readers) to
read from claimInfo where appropriate; alternatively add clear comments in the
types/bounty.ts next to claimedAt/claimedBy and claimInfo explaining which is
authoritative for filtering/logic and keep population consistent. Ensure the
symbol names referenced: claimedAt, claimedBy, claimInfo, and ClaimInfo are
updated together across creation, update, and read paths.

In `@types/participation.ts`:
- Around line 23-29: The deprecated field content is currently required which
forces producers to supply it; change its declaration to optional (content?:
string) in the Participation/Submission type so new code can omit it while
preserving backward compatibility with explanation, and ensure any code paths
constructing or validating Submission (references: content, explanation,
attachments, walletAddress) handle undefined content gracefully (e.g., optional
checks or fallback to explanation).

Comment on lines +46 to +57
const allowedModels = [
"single-claim",
"competition",
"multi-winner",
"application",
];
if (!allowedModels.includes(bounty.claimingModel)) {
return NextResponse.json(
{ error: "Submission not allowed for this bounty type" },
{ status: 400 },
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find files related to "claim"
echo "=== Files with 'claim' in name ==="
fd "claim" --type f --extension ts --extension tsx -E node_modules | head -20

echo ""
echo "=== useClaimBounty hook references ==="
rg "useClaimBounty|claimBounty|/claim" --type=ts --type=tsx -C 3 -g '!node_modules' | head -100

Repository: boundlessfi/bounties

Length of output: 269


🏁 Script executed:

#!/bin/bash
# Search for useClaimBounty without file type issues
echo "=== Search for useClaimBounty ==="
rg "useClaimBounty" -C 5 -g '!node_modules' | head -80

echo ""
echo "=== Search for claim endpoint/route ==="
rg "bounties.*claim|/claim" -C 2 -g '!node_modules' | head -100

echo ""
echo "=== Check the submit route file ==="
cat -n app/api/bounties/[id]/submit/route.ts | head -80

echo ""
echo "=== Look for claim route file ==="
fd "route.ts" -path "*/api/bounties/*" -g '!node_modules'

Repository: boundlessfi/bounties

Length of output: 7349


🏁 Script executed:

#!/bin/bash
# Look for claim route file
echo "=== Looking for claim route files ==="
find app/api -name "*.ts" -type f | xargs grep -l "claim" 2>/dev/null

echo ""
echo "=== Check if claim route exists ==="
ls -la app/api/bounties/*/claim 2>/dev/null || echo "No claim directory found"
ls -la app/api/bounties/\[id\]/claim 2>/dev/null || echo "No [id]/claim directory found"

echo ""
echo "=== Look at bounty detail sidebar CTA to see single-claim handling ==="
rg "single-claim" -B 5 -A 5 components/bounty-detail/bounty-detail-sidebar-cta.tsx

echo ""
echo "=== Check if submit endpoint handles single-claim differently ==="
cat -n app/api/bounties/[id]/submit/route.ts | tail -30

Repository: boundlessfi/bounties

Length of output: 3249


🏁 Script executed:

#!/bin/bash
echo "=== Claim route implementation ==="
cat -n app/api/bounties/[id]/claim/route.ts

echo ""
echo "=== Check what happens after submission is created ==="
cat -n app/api/bounties/[id]/submit/route.ts | tail -15

Repository: boundlessfi/bounties

Length of output: 3163


"single-claim" in allowedModels should be removed—it conflicts with the exclusive claim flow.

The claim endpoint (/api/bounties/[id]/claim) explicitly restricts itself to claimingModel === 'single-claim' (line 31) and manages the bounty state transition (status: 'claimed', claimedBy, claimExpiresAt). The CTA routes single-claim bounties exclusively through useClaimBounty (claim endpoint), never submit.

Including "single-claim" in the submit endpoint's allowedModels creates an unintended alternate path: a POST to /submit for a single-claim bounty would create a submission without transitioning the bounty to claimed status, potentially bypassing the claim expiry and status management logic.

Suggested fix
    const allowedModels = [
-     "single-claim",
      "competition",
      "multi-winner",
      "application",
    ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const allowedModels = [
"single-claim",
"competition",
"multi-winner",
"application",
];
if (!allowedModels.includes(bounty.claimingModel)) {
return NextResponse.json(
{ error: "Submission not allowed for this bounty type" },
{ status: 400 },
);
}
const allowedModels = [
"competition",
"multi-winner",
"application",
];
if (!allowedModels.includes(bounty.claimingModel)) {
return NextResponse.json(
{ error: "Submission not allowed for this bounty type" },
{ status: 400 },
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/submit/route.ts around lines 46 - 57, Remove
"single-claim" from the allowedModels array in the submit route so submissions
cannot be created for single-claim bounties; locate the array named
allowedModels and the check that uses
allowedModels.includes(bounty.claimingModel) in the submit route handler
(route.ts) and update it to only allow "competition", "multi-winner", and
"application" so single-claim bounties continue to go through the dedicated
claim endpoint which handles status: 'claimed', claimedBy, and claimExpiresAt.

Comment on lines +90 to +95
} catch {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Log the caught error before returning 500.

The catch block swallows the error entirely. In production, unlogged 500s are nearly impossible to diagnose. Capture and log the error server-side.

♻️ Suggested fix
-  } catch {
+  } catch (error) {
+    console.error("[POST /api/bounties/[id]/submit]", error);
     return NextResponse.json(
       { error: "Internal Server Error" },
       { status: 500 },
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/submit/route.ts around lines 90 - 95, The catch block
in app/api/bounties/[id]/submit/route.ts currently swallows errors and returns a
500 without logging; update the catch to capture the thrown error (e.g., catch
(err) or catch (error)), log it server-side using the existing logger or
console.error with context (include request/bounty id if available), then return
the same NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
ensure you reference the catch block around NextResponse.json and the handler
that performs the submission.

Comment on lines +20 to +21
const params = useParams();
const userId = params.userId as string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unsafe cast of params.userId — could be string[] in edge cases.

useParams() returns Record<string, string | string[]>. While [userId] dynamic segments typically yield a string, defensively handling the array case prevents runtime issues.

Proposed fix
   const params = useParams();
-  const userId = params.userId as string;
+  const rawUserId = params.userId;
+  const userId = Array.isArray(rawUserId) ? rawUserId[0] : rawUserId ?? "";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = useParams();
const userId = params.userId as string;
const params = useParams();
const rawUserId = params.userId;
const userId = Array.isArray(rawUserId) ? rawUserId[0] : rawUserId ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 20 - 21, The current unsafe cast
of params.userId to string in page.tsx can panic if useParams() returns a
string[]; update the logic around useParams()/params and the userId variable to
defensively handle both string and string[] values (e.g., check
Array.isArray(params.userId) and normalize to a single string by taking the
first element or joining, or handle the invalid-multiple case explicitly),
ensuring downstream code that uses userId always receives a validated string or
returns/redirects on invalid input.

Comment on lines +33 to +60
const myClaims = useMemo<MyClaim[]>(() => {
const bounties = bountyResponse?.data ?? [];

return bounties
.filter((bounty) => bounty.claimedBy === userId)
.map((bounty) => {
let status = "active";

if (bounty.status === "closed") {
status = "completed";
} else if (bounty.status === "claimed" && bounty.claimExpiresAt) {
const claimExpiry = new Date(bounty.claimExpiresAt);
if (
!Number.isNaN(claimExpiry.getTime()) &&
claimExpiry < new Date()
) {
status = "expired";
}
}

// Generic Error
return (
<div className="container mx-auto py-16 text-center">
<AlertCircle className="w-12 h-12 mx-auto text-destructive mb-4" />
<h1 className="text-2xl font-bold mb-2">Something went wrong</h1>
<p className="text-muted-foreground mb-6">
We encountered an error while loading the profile.
</p>
<Button variant="outline" onClick={() => window.location.reload()}>
Try Again
</Button>
</div>
);
}
return {
bountyId: bounty.id,
title: bounty.issueTitle,
status,
rewardAmount: bounty.rewardAmount ?? undefined,
};
});
}, [bountyResponse?.data, userId]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

bounty.claimedBy references the legacy top-level field instead of claimInfo.

Line 37 filters bounties using bounty.claimedBy === userId, but the PR is migrating claim data into the nested claimInfo structure. If the API (or mock data) only populates claimInfo.claimedBy.userId and not the top-level claimedBy, this filter will silently produce an empty list.

Proposed fix
     return bounties
-      .filter((bounty) => bounty.claimedBy === userId)
+      .filter(
+        (bounty) =>
+          bounty.claimInfo?.claimedBy?.userId === userId ||
+          bounty.claimedBy === userId,
+      )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const myClaims = useMemo<MyClaim[]>(() => {
const bounties = bountyResponse?.data ?? [];
return bounties
.filter((bounty) => bounty.claimedBy === userId)
.map((bounty) => {
let status = "active";
if (bounty.status === "closed") {
status = "completed";
} else if (bounty.status === "claimed" && bounty.claimExpiresAt) {
const claimExpiry = new Date(bounty.claimExpiresAt);
if (
!Number.isNaN(claimExpiry.getTime()) &&
claimExpiry < new Date()
) {
status = "expired";
}
}
// Generic Error
return (
<div className="container mx-auto py-16 text-center">
<AlertCircle className="w-12 h-12 mx-auto text-destructive mb-4" />
<h1 className="text-2xl font-bold mb-2">Something went wrong</h1>
<p className="text-muted-foreground mb-6">
We encountered an error while loading the profile.
</p>
<Button variant="outline" onClick={() => window.location.reload()}>
Try Again
</Button>
</div>
);
}
return {
bountyId: bounty.id,
title: bounty.issueTitle,
status,
rewardAmount: bounty.rewardAmount ?? undefined,
};
});
}, [bountyResponse?.data, userId]);
const myClaims = useMemo<MyClaim[]>(() => {
const bounties = bountyResponse?.data ?? [];
return bounties
.filter(
(bounty) =>
bounty.claimInfo?.claimedBy?.userId === userId ||
bounty.claimedBy === userId,
)
.map((bounty) => {
let status = "active";
if (bounty.status === "closed") {
status = "completed";
} else if (bounty.status === "claimed" && bounty.claimExpiresAt) {
const claimExpiry = new Date(bounty.claimExpiresAt);
if (
!Number.isNaN(claimExpiry.getTime()) &&
claimExpiry < new Date()
) {
status = "expired";
}
}
return {
bountyId: bounty.id,
title: bounty.issueTitle,
status,
rewardAmount: bounty.rewardAmount ?? undefined,
};
});
}, [bountyResponse?.data, userId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 33 - 60, The myClaims computation
currently filters bounties using the legacy bounty.claimedBy field which will
miss claims when data is migrated to the nested claimInfo structure; update the
filter inside myClaims to check claimInfo?.claimedBy?.userId === userId first
(with a fallback to the old bounty.claimedBy === userId) so both shapes are
supported, and ensure you reference bountyResponse?.data and the existing map
logic (no other changes required).

Comment on lines +45 to +51
const getBaseDefaults = (): SubmissionFormValue => ({
githubUrl: "",
demoUrl: "",
explanation: "",
attachments: [],
walletAddress: mockWalletInfo.address,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded mock wallet address will need replacement before production.

mockWalletInfo.address is used as the default wallet address. This should be sourced from the user's actual connected wallet in production. Consider adding a TODO or wiring this to a real wallet context.

Suggested marker
 const getBaseDefaults = (): SubmissionFormValue => ({
   githubUrl: "",
   demoUrl: "",
   explanation: "",
   attachments: [],
-  walletAddress: mockWalletInfo.address,
+  walletAddress: mockWalletInfo.address, // TODO: Replace with real connected wallet address from wallet context
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getBaseDefaults = (): SubmissionFormValue => ({
githubUrl: "",
demoUrl: "",
explanation: "",
attachments: [],
walletAddress: mockWalletInfo.address,
});
const getBaseDefaults = (): SubmissionFormValue => ({
githubUrl: "",
demoUrl: "",
explanation: "",
attachments: [],
walletAddress: mockWalletInfo.address, // TODO: Replace with real connected wallet address from wallet context
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/submission-dialog.tsx` around lines 45 - 51,
getBaseDefaults currently seeds the SubmissionFormValue.walletAddress with a
hardcoded mockWalletInfo.address; replace this before production by wiring the
default to the real connected wallet (e.g., read from your wallet
context/provider or currentUser wallet state) or at minimum add a clear TODO
comment. Update the getBaseDefaults function to pull the wallet address from the
authoritative source used elsewhere in the app (instead of
mockWalletInfo.address), and ensure the form initialization handles the case
when no wallet is connected (empty string or null) to avoid using test data in
production.

Comment on lines +39 to +41
// Create the generic GraphQLClient instance
const url = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql";
export const graphQLClient = new GraphQLClient(url);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Relative URL fallback may fail during SSR.

When NEXT_PUBLIC_GRAPHQL_URL is unset, the fallback "/api/graphql" is a relative URL. graphql-request's GraphQLClient will pass this to fetch, which requires an absolute URL in Node.js (SSR/server components). This will cause a runtime error like TypeError: Only absolute URLs are supported.

Consider constructing an absolute URL for the server-side case:

Proposed fix
-const url = process.env.NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql";
+const url =
+  process.env.NEXT_PUBLIC_GRAPHQL_URL ||
+  (typeof window === "undefined"
+    ? `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/api/graphql`
+    : "/api/graphql");
graphql-request v7 relative URL server side fetch
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/graphql/client.ts` around lines 39 - 41, The current graphQLClient uses a
possibly relative url (NEXT_PUBLIC_GRAPHQL_URL || "/api/graphql") which fails in
SSR because Node's fetch requires an absolute URL; update the construction so
that if the chosen url starts with "/" and code is running server-side (typeof
window === "undefined"), build an absolute URL using a server host env (e.g.,
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.VERCEL_URL ||
process.env.NEXTAUTH_URL) prefixed with "https://" (fall back to
"http://localhost:3000" if none present), then pass that absolute string to new
GraphQLClient; change the symbols url and graphQLClient in lib/graphql/client.ts
accordingly so GraphQLClient always receives an absolute URL on the server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Single Claim Model Logic

1 participant