Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 25 additions & 3 deletions components/bounty-detail/bounty-detail-submissions-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useState, useEffect } from "react";
import { Loader2, DollarSign } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -21,6 +21,7 @@
useMarkSubmissionPaid,
} from "@/hooks/use-submission-mutations";
import { authClient } from "@/lib/auth-client";
import { useSubmissionDraft } from "@/hooks/use-submission-draft";

interface ExtendedUser {
id: string;
Expand All @@ -44,6 +45,7 @@
}: BountyDetailSubmissionsCardProps) {
const { data: session } = authClient.useSession();
const submissions = bounty.submissions || [];
const { draft, clearDraft, autoSave } = useSubmissionDraft(bounty.id);
Copy link

@coderabbitai coderabbitai bot Feb 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope draft storage by user identity to prevent cross-account draft exposure.

useSubmissionDraft(bounty.id) scopes persisted data only by bounty. On shared browsers, a different signed-in user visiting the same bounty can restore someone else’s draft content. Please key drafts by both user id and bounty id (likely by extending the hook API so bountyId in the stored payload remains canonical).

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

In `@components/bounty-detail/bounty-detail-submissions-card.tsx` at line 48, The
draft storage is currently only keyed by bounty id, allowing drafts to leak
between accounts; update the draft hook and its usage so drafts are scoped by
both user id and bounty id. Change the call site that currently does
useSubmissionDraft(bounty.id) (and references draft, clearDraft, autoSave) to
pass the current user's stable id as well (e.g., useSubmissionDraft(bounty.id,
userId)), and modify the useSubmissionDraft implementation to incorporate userId
into the persistence key and to include a canonical bountyId and userId in the
stored payload so restore/clear operations validate both identifiers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope draft storage by user identity to prevent cross-account draft exposure.

useSubmissionDraft(bounty.id) scopes persisted data only by bounty. On shared browsers, a different signed-in user visiting the same bounty can restore someone else’s draft content. Please key drafts by both user id and bounty id (likely by extending the hook API so bountyId in the stored payload remains canonical).

🤖 Prompt for AI Agents

skip it

Copy link

Choose a reason for hiding this comment

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

@devJaja, understood – I'll skip this suggestion. Feel free to revisit it later if cross-user draft isolation becomes a concern.

    /)_/)
   (•_• )
   />💭 (okay...)


const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewDialogOpen, setReviewDialogOpen] = useState(false);
Expand All @@ -55,15 +57,29 @@
useState<BountySubmissionType | null>(null);

const [prUrl, setPrUrl] = useState("");
const [submitComments, setSubmitComments] = useState("");
const [reviewComments, setReviewComments] = useState("");
const [submitComments, setSubmitComments] = useState("");
const [reviewComments, setReviewComments] = useState("");
const [reviewStatus, setReviewStatus] = useState("APPROVED");

Check warning on line 62 in components/bounty-detail/bounty-detail-submissions-card.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'setReviewStatus' is assigned a value but never used
const [transactionHash, setTransactionHash] = useState("");

const submitToBounty = useSubmitToBounty();
const reviewSubmission = useReviewSubmission();
const markSubmissionPaid = useMarkSubmissionPaid();

// Load draft on mount
useEffect(() => {
if (draft?.formData) {
setPrUrl(draft.formData.githubPullRequestUrl);
setSubmitComments(draft.formData.comments);
}
}, [draft]);

// Auto-save on form changes
useEffect(() => {
const cleanup = autoSave({ githubPullRequestUrl: prUrl, comments: submitComments });
return cleanup;
}, [prUrl, submitComments, autoSave]);

const isOrgMember =
(session?.user as ExtendedUser)?.organizations?.includes(
bounty.organizationId,
Expand All @@ -77,6 +93,7 @@
githubPullRequestUrl: prUrl,
comments: submitComments.trim() || undefined,
});
clearDraft();
} catch (err) {
// Replace with toast or error UI as needed
console.error("Submit PR failed:", err);
Expand Down Expand Up @@ -161,6 +178,11 @@
<DialogTitle>Submit Pull Request</DialogTitle>
<DialogDescription>
Submit your GitHub pull request URL.
{draft && (
<span className="block mt-1 text-xs text-blue-400">
Draft restored from {new Date(draft.updatedAt).toLocaleString()}
</span>
)}
</DialogDescription>
</DialogHeader>

Expand Down
76 changes: 76 additions & 0 deletions docs/SUBMISSION_DRAFTS_EXAMPLE.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Submission Draft System - Quick Start Example
*
* This example shows how to integrate the submission draft system
* into any form component.
*/

import { useState, useEffect } from "react";
import { useSubmissionDraft } from "@/hooks/use-submission-draft";

export function SubmissionFormExample({ bountyId }: { bountyId: string }) {
const { draft, clearDraft, autoSave } = useSubmissionDraft(bountyId);

const [prUrl, setPrUrl] = useState("");
const [comments, setComments] = useState("");

// 1. Load draft when component mounts
useEffect(() => {
if (draft?.formData) {
setPrUrl(draft.formData.githubPullRequestUrl);
setComments(draft.formData.comments);
}
}, [draft]);

// 2. Auto-save when form changes
useEffect(() => {
const cleanup = autoSave({
githubPullRequestUrl: prUrl,
comments
});
return cleanup;
}, [prUrl, comments, autoSave]);

// 3. Clear draft on successful submit
const handleSubmit = async () => {
// Your submit logic here
await submitToAPI({ prUrl, comments });

// Clear draft after success
clearDraft();

// Reset form
setPrUrl("");
setComments("");
};

return (
<form onSubmit={handleSubmit}>
{/* Show draft indicator */}
Comment on lines +27 to +41
Copy link

@coderabbitai coderabbitai bot Feb 25, 2026

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:

fd -t f "SUBMISSION_DRAFTS_EXAMPLE.tsx"

Repository: boundlessfi/bounties

Length of output: 98


🏁 Script executed:

cat -n docs/SUBMISSION_DRAFTS_EXAMPLE.tsx

Repository: boundlessfi/bounties

Length of output: 2523


Add preventDefault() to the async form submit handler to prevent page reload.

The form handler is missing event.preventDefault(), which allows the browser's default form submission to interrupt the async submitToAPI call and draft system flow.

Required changes
-import { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
+import type { FormEvent } from "react";

-  const handleSubmit = async () => {
+  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
+    event.preventDefault();
     // Your submit logic here
     await submitToAPI({ prUrl, comments });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/SUBMISSION_DRAFTS_EXAMPLE.tsx` around lines 35 - 49, The handleSubmit
async function is missing event.preventDefault(), so update handleSubmit to
accept the form event (e.g., event: React.FormEvent) and immediately call
event.preventDefault() before awaiting submitToAPI({ prUrl, comments }); to
prevent the browser from doing a full page submit and interrupting the async
flow; keep the existing clearDraft(), setPrUrl(""), and setComments("") logic
after the await so the draft reset still occurs on success.

Copy link
Contributor

Choose a reason for hiding this comment

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

@devJaja you skipped this

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

{draft && (
<div className="text-xs text-blue-400">
Draft saved at {new Date(draft.updatedAt).toLocaleString()}
</div>
)}

<input
value={prUrl}
onChange={(e) => setPrUrl(e.target.value)}
placeholder="Pull Request URL"
/>

<textarea
value={comments}
onChange={(e) => setComments(e.target.value)}
placeholder="Comments"
/>

<button type="submit">Submit</button>
</form>
);
}

// Mock submit function
async function submitToAPI(data: { prUrl: string; comments: string }) {
console.log("Submitting:", data);
}
85 changes: 85 additions & 0 deletions hooks/__tests__/use-submission-draft.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { renderHook, act, waitFor } from "@testing-library/react";
import { useSubmissionDraft } from "../use-submission-draft";

describe("useSubmissionDraft", () => {
const bountyId = "test-bounty-123";

beforeEach(() => {
localStorage.clear();
});

it("should initialize with no draft", () => {
const { result } = renderHook(() => useSubmissionDraft(bountyId));
expect(result.current.draft).toBeNull();
});

it("should save draft", () => {
const { result } = renderHook(() => useSubmissionDraft(bountyId));
const formData = {
githubPullRequestUrl: "https://github.com/test/pr/1",
comments: "Test comment",
};

act(() => {
result.current.saveDraft(formData);
});

expect(result.current.draft).not.toBeNull();
expect(result.current.draft?.formData).toEqual(formData);
expect(result.current.draft?.bountyId).toBe(bountyId);
});

it("should clear draft", () => {
const { result } = renderHook(() => useSubmissionDraft(bountyId));
const formData = {
githubPullRequestUrl: "https://github.com/test/pr/1",
comments: "Test comment",
};

act(() => {
result.current.saveDraft(formData);
});

expect(result.current.draft).not.toBeNull();

act(() => {
result.current.clearDraft();
});

expect(result.current.draft).toBeNull();
});

it("should auto-save after delay", async () => {
const { result } = renderHook(() => useSubmissionDraft(bountyId));
const formData = {
githubPullRequestUrl: "https://github.com/test/pr/1",
comments: "Auto-saved comment",
};

act(() => {
result.current.autoSave(formData);
});

await waitFor(
() => {
expect(result.current.draft?.formData).toEqual(formData);
},
{ timeout: 1500 }
);
});

it("should persist draft across hook instances", () => {
const { result: result1 } = renderHook(() => useSubmissionDraft(bountyId));
const formData = {
githubPullRequestUrl: "https://github.com/test/pr/1",
comments: "Persisted comment",
};

act(() => {
result1.current.saveDraft(formData);
});

const { result: result2 } = renderHook(() => useSubmissionDraft(bountyId));
expect(result2.current.draft?.formData).toEqual(formData);
});
});
47 changes: 47 additions & 0 deletions hooks/use-submission-draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useEffect, useCallback } from "react";

Check warning on line 1 in hooks/use-submission-draft.ts

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'useEffect' is defined but never used
import { useLocalStorage } from "./use-local-storage";
import type { SubmissionDraft, SubmissionForm } from "@/types/submission-draft";

const DRAFT_KEY_PREFIX = "submission_draft_";
const AUTO_SAVE_DELAY = 1000;

export function useSubmissionDraft(bountyId: string) {
const draftKey = `${DRAFT_KEY_PREFIX}${bountyId}`;
const [draft, setDraft] = useLocalStorage<SubmissionDraft | null>(draftKey, null);

const saveDraft = useCallback(
(formData: SubmissionForm) => {
const newDraft: SubmissionDraft = {
id: `draft_${bountyId}_${Date.now()}`,
bountyId,
formData,
updatedAt: new Date().toISOString(),
};
setDraft(newDraft);
},
[bountyId, setDraft]
);

const clearDraft = useCallback(() => {
setDraft(null);
}, [setDraft]);

const autoSave = useCallback(
(formData: SubmissionForm) => {
const timer = setTimeout(() => {
if (formData.githubPullRequestUrl || formData.comments) {
saveDraft(formData);
}
}, AUTO_SAVE_DELAY);
return () => clearTimeout(timer);
},
[saveDraft]
Comment on lines +29 to +38
Copy link

@coderabbitai coderabbitai bot Feb 25, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear persisted draft when form content becomes empty.

At Line 32, empty form state is ignored rather than persisted/cleared, so stale draft data can be restored later.

💡 Proposed fix
   const autoSave = useCallback(
     (formData: SubmissionForm) => {
       const timer = setTimeout(() => {
-        if (formData.githubPullRequestUrl || formData.comments) {
+        const hasContent =
+          formData.githubPullRequestUrl.trim().length > 0 ||
+          formData.comments.trim().length > 0;
+
+        if (hasContent) {
           saveDraft(formData);
+        } else {
+          clearDraft();
         }
       }, AUTO_SAVE_DELAY);
       return () => clearTimeout(timer);
     },
-    [saveDraft]
+    [saveDraft, clearDraft]
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-submission-draft.ts` around lines 29 - 38, The autoSave callback
currently skips saving when both formData.githubPullRequestUrl and
formData.comments are empty, leaving stale drafts; update the autoSave logic (in
use-submission-draft.ts -> autoSave) to always call saveDraft(formData) after
the debounce (or call a dedicated clearDraft when available) instead of only
when those fields are present so an empty form clears the persisted draft; keep
the same AUTO_SAVE_DELAY and return the clearTimeout cleanup as before.

Copy link
Contributor

Choose a reason for hiding this comment

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

@devJaja you skipped this

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

);

return {
draft,
saveDraft,
clearDraft,
autoSave,
};
}
11 changes: 11 additions & 0 deletions types/submission-draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface SubmissionForm {
githubPullRequestUrl: string;
comments: string;
}

export interface SubmissionDraft {
id: string;
bountyId: string;
formData: SubmissionForm;
updatedAt: string;
}
Loading