From db8dfaa52432285740cc1d18af11c4b0cb72c95c Mon Sep 17 00:00:00 2001 From: BWM0223 Date: Wed, 24 Jun 2026 23:15:59 -0700 Subject: [PATCH] feat: add WinnerSelectionPayout component with escrow signing flow --- components/bounty/WinnerSelectionPayout.tsx | 127 ++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 components/bounty/WinnerSelectionPayout.tsx diff --git a/components/bounty/WinnerSelectionPayout.tsx b/components/bounty/WinnerSelectionPayout.tsx new file mode 100644 index 000000000..9e0fc454a --- /dev/null +++ b/components/bounty/WinnerSelectionPayout.tsx @@ -0,0 +1,127 @@ + +"use client"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Trophy, Loader2, CheckCircle, Hash } from "lucide-react"; + +interface WinnerSlot { + position: number; + label: string; + amount: number; + submissionId?: string; + applicantAddress?: string; + applicantName?: string; +} + +interface WinnerSelectionPayoutProps { + bountyId: string; + slots: WinnerSlot[]; + escrowMode: "managed" | "external"; + onSelectWinnersAndPay: (winners: { applicantAddress: string; position: number }[]) => Promise<{ txHash: string }>; +} + +type Step = "review" | "confirm" | "paying" | "complete"; + +export function WinnerSelectionPayout({ bountyId, slots, escrowMode, onSelectWinnersAndPay }: WinnerSelectionPayoutProps) { + const [step, setStep] = useState("review"); + const [txHash, setTxHash] = useState(""); + const [error, setError] = useState(""); + + const filled = slots.filter(s => s.applicantAddress); + const allFilled = filled.length === slots.length; + const totalPayout = slots.reduce((sum, s) => sum + s.amount, 0); + + const handlePay = async () => { + setStep("paying"); + setError(""); + try { + const winners = filled.map(s => ({ applicantAddress: s.applicantAddress!, position: s.position })); + const result = await onSelectWinnersAndPay(winners); + setTxHash(result.txHash); + setStep("complete"); + } catch (e: any) { + setError(e.message ?? "Payout failed"); + setStep("confirm"); + } + }; + + if (step === "complete") { + return ( + + + +

Bounty Completed!

+

Winners paid. Bounty is now COMPLETED.

+ {txHash && ( + + {txHash.slice(0,16)}... + + )} +
+
+ ); + } + + return ( +
+ + + + Winner Selection + + + {escrowMode === "managed" ? "Payout signs server-side" : "Payout requires your wallet"} + + + + {slots.map(slot => ( +
+
+ + {slot.label} + + + {slot.applicantName ?? Not selected} + +
+ {slot.amount} USDC +
+ ))} +
+ Total payout + {totalPayout} USDC +
+
+
+ + {error &&

{error}

} + + {step === "review" && ( + + )} + {step === "confirm" && ( +
+

+ This will pay {totalPayout} USDC on-chain and mark the bounty COMPLETED. +

+
+ + +
+
+ )} + {step === "paying" && ( + + )} +
+ ); +} +export default WinnerSelectionPayout;