Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { MilestoneSubmissionCard } from "./milestone-submission-card";
import { Model4MaintainerDashboard } from "./model4-maintainer-dashboard";
import type { Milestone, ContributorProgress } from "@/types/bounty";
import {
ApplicationReviewDashboard,
type Application,
} from "@/components/bounty/application-review-dashboard";
import { SubmissionApprovalPanel } from "@/components/bounty/submission-approval-panel";
import { ApplicationSubmitWorkPanel } from "@/components/bounty/application-submit-work-panel";

type BountyData = ReturnType<typeof useBountyDetail>["data"];

Expand Down Expand Up @@ -62,6 +68,15 @@ function getFullMilestoneData(bounty: BountyData): {
};
}

// Backend does not currently provide applications in the response.
// Fall back to empty array until the schema supports it.
const getApplications = (bounty: BountyData): Application[] => {
return (
(bounty as BountyData & { applications?: Application[] })?.applications ??
[]
);
};

export function BountyDetailClient({ bountyId }: { bountyId: string }) {
const router = useRouter();
const { data: bounty, isPending, isError, error } = useBountyDetail(bountyId);
Expand Down Expand Up @@ -133,6 +148,18 @@ 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.
const isAssignedApplicant =
(bounty as BountyData & { assignedContributorId?: string })
?.assignedContributorId === session?.user?.id ||
bounty.submissions?.some((s) => s.submittedBy === session?.user?.id) ||
(!isCreator && bounty.status === "IN_PROGRESS");

// submissions is present on BountyQuery (single-bounty query) but not on
// BountyFieldsFragment (list query). The cast is safe here because
// useBountyDetail returns BountyFieldsFragment & Partial<BountyQuery["bounty"]>.
Expand Down Expand Up @@ -200,9 +227,43 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {

{!isCancelled && pool && <EscrowDetailPanel poolId={bountyId} />}
<RefundStatusTracker bountyId={bountyId} isCancelled={isCancelled} />
{bounty.type !== "FIXED_PRICE" && !isCompetition && (
<BountyDetailSubmissionsCard bounty={bounty} />
)}

{/* Model 2 Application Flow integration */}
{bounty.type === "MILESTONE_BASED" &&
isCreator &&
bounty.status === "OPEN" && (
<ApplicationReviewDashboard
bountyId={bountyId}
creatorAddress={walletAddress}
applications={getApplications(bounty)}
/>
)}
Comment on lines +231 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Gate creator panels on non-empty walletAddress to prevent invalid contract calls.

ApplicationReviewDashboard (line 237) and SubmissionApprovalPanel (line 257) receive walletAddress as creatorAddress without verifying it's non-empty. If the user's wallet isn't connected, these panels will render but contract mutations will fail or behave unexpectedly.

🔧 Proposed fix
 {bounty.type === "MILESTONE_BASED" &&
   isCreator &&
+  walletAddress &&
   bounty.status === "OPEN" && (
     <ApplicationReviewDashboard
       ...
     />
   )}

 {bounty.type === "MILESTONE_BASED" &&
   isCreator &&
+  walletAddress &&
   bounty.status === "UNDER_REVIEW" && (
     <SubmissionApprovalPanel
       ...
     />
   )}

Also applies to: 252-262

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

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 231 - 240,
The creator-only panels render even when walletAddress is empty; update the
gating conditions for ApplicationReviewDashboard and SubmissionApprovalPanel to
require a non-empty walletAddress (e.g., add walletAddress && to the existing
conditional that checks bounty.type, isCreator, and bounty.status) so they only
render when a connected wallet exists, and continue passing walletAddress as
creatorAddress into those components (ApplicationReviewDashboard,
SubmissionApprovalPanel).


{bounty.type === "MILESTONE_BASED" &&
isAssignedApplicant &&
walletAddress &&
bounty.status === "IN_PROGRESS" && (
<ApplicationSubmitWorkPanel
bountyId={bountyId}
contributorAddress={walletAddress}
/>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{bounty.type === "MILESTONE_BASED" &&
isCreator &&
bounty.status === "UNDER_REVIEW" && (
<SubmissionApprovalPanel
bounty={bounty}
creatorAddress={walletAddress}
submittedWorkCid={
bounty.submissions?.[0]?.githubPullRequestUrl || undefined
}
Comment on lines +258 to +260

Copy link
Copy Markdown

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 BountySubmission type has a dedicated CID field
rg -n 'workCid|ipfsCid|deliverableCid' --type=ts types/

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Find and examine the BountySubmission type definition
fd -t f 'bounty.ts' types/

Repository: boundlessfi/bounties

Length of output: 80


🏁 Script executed:

# Check the context around the submittedWorkCid prop in the component
rg -B5 -A5 'submittedWorkCid=' components/bounty-detail/bounty-detail-client.tsx

Repository: boundlessfi/bounties

Length of output: 400


🏁 Script executed:

# Search for BountySubmission type definition and related fields
rg -n 'type BountySubmission|interface BountySubmission' --type=ts

Repository: boundlessfi/bounties

Length of output: 310


🏁 Script executed:

# Search for githubPullRequestUrl usage to understand the field's purpose
rg -n 'githubPullRequestUrl' --type=ts -B2 -A2 | head -50

Repository: boundlessfi/bounties

Length of output: 2507


🏁 Script executed:

# Read the full BountySubmission interface
sed -n '51,65p' types/bounty.ts

Repository: boundlessfi/bounties

Length of output: 509


🏁 Script executed:

# Find and examine the SubmissionApprovalPanel component
fd -t f 'SubmissionApprovalPanel' components/

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

# Search for IPFS or CID references in the codebase
rg -n 'ipfs|cid|CID' --type=ts components/ -i | head -30

Repository: boundlessfi/bounties

Length of output: 2654


🏁 Script executed:

# Search for SubmissionApprovalPanel component definition
rg -n 'SubmissionApprovalPanel' --type=ts -A 20 | head -50

Repository: boundlessfi/bounties

Length of output: 4534


🏁 Script executed:

# Search for submittedWorkCid prop usage throughout codebase
rg -n 'submittedWorkCid' --type=ts -B 2 -A 2

Repository: boundlessfi/bounties

Length of output: 1841


🏁 Script executed:

# Look for where githubPullRequestUrl is actually used/set to understand field purpose
rg -n 'githubPullRequestUrl' --type=ts -B 3 -A 3 | head -100

Repository: boundlessfi/bounties

Length of output: 5494


submittedWorkCid receives githubPullRequestUrl — incompatible types.

The SubmissionApprovalPanel expects submittedWorkCid to be an IPFS CID (it constructs https://ipfs.io/ipfs/${submittedWorkCid}), but the code passes bounty.submissions?.[0]?.githubPullRequestUrl, which is a GitHub PR URL. This mismatch breaks the IPFS link rendering.

Add a dedicated workCid field to BountySubmission in types/bounty.ts to store the actual IPFS CID separately from githubPullRequestUrl.

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

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 258 - 260,
The prop submittedWorkCid is receiving githubPullRequestUrl (a URL) causing an
IPFS CID/type mismatch; add a new field workCid to the BountySubmission type in
types/bounty.ts, populate it wherever BountySubmission instances are
created/returned, and update the component usage in bounty-detail-client.tsx to
pass bounty.submissions?.[0]?.workCid (not githubPullRequestUrl) into
SubmissionApprovalPanel's submittedWorkCid prop; also audit any consumers (e.g.,
SubmissionApprovalPanel prop types/usage) and submission creation code to ensure
workCid is typed/filled and githubPullRequestUrl remains unchanged.

/>
)}

{bounty.type !== "FIXED_PRICE" &&
bounty.type !== "MILESTONE_BASED" &&
!isCompetition && <BountyDetailSubmissionsCard bounty={bounty} />}
{bounty.type === "FIXED_PRICE" && <FcfsApprovalPanel bounty={bounty} />}
{isCompetition && isCreator && (pastDeadline || isFinalized) && (
<CompetitionJudging
Expand Down
67 changes: 67 additions & 0 deletions components/bounty-detail/bounty-detail-sidebar-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import { useCompetitionJoinState } from "@/hooks/use-competition-join-state";
import type { CancellationRecord } from "@/types/escrow";
import { useCancelBountyDialog } from "@/hooks/use-cancel-bounty-dialog";
import type { Bounty } from "@/types/bounty";
import {
ApplicationDialog,
type ApplicationFormValues,
} from "@/components/bounty/application-dialog";
import { useApplyToBounty } from "@/hooks/use-bounty-application";

/** Props accept the wider intersection returned by useBountyDetail so
* callers don't need a cast. Optional Bounty fields (maxSlots, etc.)
Expand Down Expand Up @@ -87,6 +92,17 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) {
}
};

const { mutateAsync: applyToBounty } = useApplyToBounty();

const handleApply = async (values: ApplicationFormValues) => {
if (!walletAddress) return;
await applyToBounty({
bountyId: bounty.id,
applicantAddress: walletAddress,
proposal: JSON.stringify(values),
});
};

const ctaLabel = () => {
if (!canAct) {
switch (bounty.status) {
Expand Down Expand Up @@ -203,6 +219,20 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) {
{canAct && !isPastDeadline ? "Join Competition" : ctaLabel()}
</Button>
)
) : bounty.type === "MILESTONE_BASED" && canAct && !isCreator ? (
<ApplicationDialog
bountyTitle={bounty.title}
onApply={handleApply}
trigger={
<Button
className="w-full h-11 font-bold tracking-wide"
size="lg"
disabled={!walletAddress}
>
Apply for Bounty
</Button>
}
/>
) : (
<Button
className="w-full h-11 font-bold tracking-wide"
Expand Down Expand Up @@ -393,6 +423,17 @@ export function MobileCTA({ bounty, onCancelled }: MobileCTAProps) {
const { walletAddress, hasJoined, isPastDeadline, joinMutation, handleJoin } =
useCompetitionJoinState(bounty);

const { mutateAsync: applyToBounty } = useApplyToBounty();

const handleApply = async (values: ApplicationFormValues) => {
if (!walletAddress) return;
await applyToBounty({
bountyId: bounty.id,
applicantAddress: walletAddress,
proposal: JSON.stringify(values),
});
};

const label = () => {
if (!canAct) {
switch (bounty.status) {
Expand Down Expand Up @@ -436,6 +477,32 @@ export function MobileCTA({ bounty, onCancelled }: MobileCTAProps) {
? "Join Competition"
: label()}
</Button>
) : bounty.type === "MILESTONE_BASED" && canAct && !isCreator ? (
<div className="flex gap-2">
<ApplicationDialog
bountyTitle={bounty.title}
onApply={handleApply}
trigger={
<Button
className="flex-1 h-11 font-bold tracking-wide"
size="lg"
disabled={!walletAddress}
>
Apply for Bounty
</Button>
}
/>
{canCancel && (
<Button
variant="outline"
size="lg"
className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0"
onClick={() => setCancelDialogOpen(true)}
>
<XCircle className="size-4" />
</Button>
)}
</div>
) : (
<div className="flex gap-2">
<Button
Expand Down
Loading
Loading