@@ -32,7 +32,17 @@ import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
3232import { ClaimBountyModal } from '@/components/bounties/ClaimBountyModal' ;
3333import { MediaEmbed } from '@/components/bounties/MediaEmbed' ;
3434import { AddressDisplay } from '@/components/ui/address-display' ;
35- import { usePoidhCancelBounty , usePoidhJoinBounty , usePoidhWithdrawFromBounty , usePoidhAcceptClaim , usePoidhSubmitClaimForVote , usePoidhVoteClaim , usePoidhResolveVote } from '@/hooks/usePoidhContract' ;
35+ import {
36+ usePoidhCancelBounty ,
37+ usePoidhJoinBounty ,
38+ usePoidhClaimRefundFromCancelledBounty ,
39+ usePoidhAcceptClaim ,
40+ usePoidhSubmitClaimForVote ,
41+ usePoidhVoteClaim ,
42+ usePoidhResolveVote ,
43+ usePoidhResetVotingPeriod ,
44+ } from '@/hooks/usePoidhContract' ;
45+ import { VoteDashboard } from '@/components/bounties/VoteDashboard' ;
3646import { POIDH_ABI } from '@/lib/poidh/abi' ;
3747import { useEthPrice , formatEthToUsd } from '@/hooks/use-eth-price' ;
3848import { useUserAddress } from '@/hooks/use-user-address' ;
@@ -166,25 +176,17 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
166176
167177 const cancelHook = usePoidhCancelBounty ( chainId ) ;
168178 const joinHook = usePoidhJoinBounty ( chainId ) ;
169- const withdrawHook = usePoidhWithdrawFromBounty ( chainId ) ;
179+ const claimRefundHook = usePoidhClaimRefundFromCancelledBounty ( chainId ) ;
170180 const acceptClaimHook = usePoidhAcceptClaim ( chainId ) ;
171181 const submitForVoteHook = usePoidhSubmitClaimForVote ( chainId ) ;
172182 const voteClaimHook = usePoidhVoteClaim ( chainId ) ;
173183 const resolveVoteHook = usePoidhResolveVote ( chainId ) ;
184+ const resetVotingHook = usePoidhResetVotingPeriod ( chainId ) ;
174185
175186 const deadlineTimestamp = bounty ?. deadline ?? null ;
176187 const countdown = useCountdown ( deadlineTimestamp ) ;
177188
178- // Read the authoritative on-chain isOpenBounty flag (overrides API field which can be null on V2)
179- const { data : onChainBountyData } = useReadContract ( {
180- address : POIDH_CONTRACTS [ chainId ] ,
181- abi : POIDH_ABI ,
182- functionName : 'getBounty' ,
183- args : [ BigInt ( bounty ?. onChainId ?? 0 ) ] ,
184- chainId,
185- query : { enabled : ! ! ( bounty ?. onChainId ) } ,
186- } ) ;
187- const isJoinable = onChainBountyData ? onChainBountyData . isOpenBounty : ( bounty ?. isOpenBounty || bounty ?. isMultiplayer ) ;
189+ const isJoinable = bounty ?. isOpenBounty || bounty ?. isMultiplayer ;
188190
189191
190192 const { data : participantsData } = useReadContract ( {
@@ -198,6 +200,15 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
198200 const participants = participantsData ?. [ 0 ] as `0x${string } `[ ] | undefined ;
199201 const participantAmounts = participantsData ?. [ 1 ] as bigint [ ] | undefined ;
200202
203+ const { data : hadExternalContributor } = useReadContract ( {
204+ address : POIDH_CONTRACTS [ chainId ] ,
205+ abi : POIDH_ABI ,
206+ functionName : 'everHadExternalContributor' ,
207+ args : [ BigInt ( bounty ?. onChainId ?? 0 ) ] ,
208+ chainId,
209+ query : { enabled : ! ! ( bounty ?. onChainId ) } ,
210+ } ) ;
211+
201212 if ( ! bounty ) return null ;
202213
203214 const chainName = CHAIN_NAMES [ chainId as keyof typeof CHAIN_NAMES ] || 'Unknown' ;
@@ -447,8 +458,8 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
447458 </ >
448459 ) }
449460 </ div >
450- { /* Accept button (creator only, if not already accepted ) */ }
451- { isCreator && ! claim . accepted && ! bounty . isCanceled && (
461+ { /* Accept button (creator only, solo bounties or open bounties with no contributors ) */ }
462+ { isCreator && ! claim . accepted && ! bounty . isCanceled && ! hadExternalContributor && (
452463 < Button
453464 size = "sm"
454465 variant = "default"
@@ -465,6 +476,12 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
465476 ) }
466477 </ Button >
467478 ) }
479+ { /* Guide issuer to use vote flow when open bounty had contributors */ }
480+ { isCreator && ! claim . accepted && ! bounty . isCanceled && hadExternalContributor && ! bounty . isVoting && (
481+ < p className = "text-xs text-muted-foreground" >
482+ Use < strong > Submit for Vote</ strong > — contributors must vote to accept.
483+ </ p >
484+ ) }
468485 </ div >
469486 { /* Accept success message */ }
470487 { acceptClaimHook . isSuccess && acceptClaimHook . hash && (
@@ -519,7 +536,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
519536 variant = "outline"
520537 className = "flex-1 border-emerald-500/30 text-emerald-500 hover:bg-emerald-500/10"
521538 disabled = { voteClaimHook . isPending }
522- onClick = { ( ) => voteClaimHook . vote ( bounty . onChainId , claim . id , true ) }
539+ onClick = { ( ) => voteClaimHook . vote ( bounty . onChainId , true ) }
523540 >
524541 { voteClaimHook . isPending ? (
525542 < Loader2 className = "w-3 h-3 animate-spin" />
@@ -532,7 +549,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
532549 variant = "outline"
533550 className = "flex-1 border-red-500/30 text-red-500 hover:bg-red-500/10"
534551 disabled = { voteClaimHook . isPending }
535- onClick = { ( ) => voteClaimHook . vote ( bounty . onChainId , claim . id , false ) }
552+ onClick = { ( ) => voteClaimHook . vote ( bounty . onChainId , false ) }
536553 >
537554 { voteClaimHook . isPending ? (
538555 < Loader2 className = "w-3 h-3 animate-spin" />
@@ -562,7 +579,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
562579 variant = "outline"
563580 className = "w-full"
564581 disabled = { resolveVoteHook . isPending }
565- onClick = { ( ) => resolveVoteHook . resolve ( bounty . onChainId , claim . id ) }
582+ onClick = { ( ) => resolveVoteHook . resolve ( bounty . onChainId ) }
566583 >
567584 { resolveVoteHook . isPending ? (
568585 < > < Loader2 className = "w-3 h-3 mr-1 animate-spin" /> { resolveVoteHook . hash ? 'Confirming…' : 'Confirm in wallet…' } </ >
@@ -587,6 +604,34 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
587604 </ a >
588605 </ div >
589606 ) }
607+ { /* Reset voting period — recovery if vote failed (contract reverts if vote would have passed) */ }
608+ < Button
609+ size = "sm"
610+ variant = "ghost"
611+ className = "w-full text-xs text-muted-foreground"
612+ disabled = { resetVotingHook . isPending }
613+ onClick = { ( ) => resetVotingHook . resetVoting ( bounty . onChainId ) }
614+ >
615+ { resetVotingHook . isPending
616+ ? < > < Loader2 className = "w-3 h-3 mr-1 animate-spin" /> Resetting…</ >
617+ : 'Reset voting period (if vote failed)'
618+ }
619+ </ Button >
620+ { resetVotingHook . error && (
621+ < div className = "flex items-start gap-2 rounded-md bg-destructive/10 border border-destructive/20 px-2 py-1.5 text-xs text-destructive" >
622+ < AlertCircle className = "w-3 h-3 shrink-0 mt-0.5" />
623+ < span > { resetVotingHook . error . message . split ( '\n' ) [ 0 ] } </ span >
624+ </ div >
625+ ) }
626+ { resetVotingHook . isSuccess && resetVotingHook . hash && (
627+ < div className = "flex items-center gap-2 py-1.5 px-2 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-xs" >
628+ < CheckCircle2 className = "w-3 h-3 shrink-0" />
629+ < span > Voting period reset.</ span >
630+ < a href = { getTxUrl ( chainId , resetVotingHook . hash ) } target = "_blank" rel = "noopener noreferrer" className = "ml-auto flex items-center gap-1 hover:underline" >
631+ View tx < ExternalLink className = "w-3 h-3" />
632+ </ a >
633+ </ div >
634+ ) }
590635 </ div >
591636 ) }
592637 </ div >
@@ -696,34 +741,38 @@ joinHook.join(bounty.onChainId, joinAmount);
696741 </ Card >
697742 ) }
698743
699- { /* Withdraw from canceled bounty (participant ) */ }
744+ { /* Withdraw from canceled bounty (contributor pull-payment ) */ }
700745 { bounty . isCanceled && isJoinable && ! isCreator && (
701746 < Card className = "border-border" >
702747 < CardHeader className = "pb-3" >
703748 < CardTitle className = "text-base" > Withdraw Your Contribution</ CardTitle >
704749 < CardDescription > This bounty was canceled. Recover your contribution.</ CardDescription >
705750 </ CardHeader >
706751 < CardContent className = "space-y-3" >
707- { withdrawHook . isSuccess ? (
752+ { claimRefundHook . isSuccess ? (
708753 < div className = "flex flex-col items-center gap-2 py-2 text-center" >
709754 < CheckCircle2 className = "w-8 h-8 text-emerald-500" />
710755 < p className = "text-sm font-medium" > Withdrawal confirmed!</ p >
711- { withdrawHook . hash && (
712- < a href = { getTxUrl ( chainId , withdrawHook . hash ) } target = "_blank" rel = "noopener noreferrer" className = "flex items-center gap-1 text-xs text-primary hover:underline" >
756+ { claimRefundHook . hash && (
757+ < a href = { getTxUrl ( chainId , claimRefundHook . hash ) } target = "_blank" rel = "noopener noreferrer" className = "flex items-center gap-1 text-xs text-primary hover:underline" >
713758 View tx < ExternalLink className = "w-3 h-3" />
714759 </ a >
715760 ) }
716761 </ div >
717762 ) : (
718763 < >
719- { withdrawHook . error && (
764+ { claimRefundHook . error && (
720765 < div className = "flex items-start gap-2 rounded-md bg-destructive/10 border border-destructive/20 px-3 py-2 text-sm text-destructive" >
721766 < AlertCircle className = "w-4 h-4 shrink-0 mt-0.5" />
722- < span > { withdrawHook . error . message . split ( '\n' ) [ 0 ] } </ span >
767+ < span > { claimRefundHook . error . message . split ( '\n' ) [ 0 ] } </ span >
723768 </ div >
724769 ) }
725- < Button variant = "outline" className = "w-full" disabled = { withdrawHook . isPending } onClick = { ( ) => withdrawHook . withdraw ( bounty . onChainId ) } >
726- { withdrawHook . isPending ? < > < Loader2 className = "w-4 h-4 mr-2 animate-spin" /> { withdrawHook . hash ? 'Confirming…' : 'Confirm in wallet…' } </ > : 'Withdraw Funds' }
770+ < Button variant = "outline" className = "w-full" disabled = { claimRefundHook . isPending }
771+ onClick = { ( ) => claimRefundHook . claimRefund ( bounty . onChainId ) } >
772+ { claimRefundHook . isPending
773+ ? < > < Loader2 className = "w-4 h-4 mr-2 animate-spin" /> { claimRefundHook . hash ? 'Confirming…' : 'Confirm in wallet…' } </ >
774+ : 'Withdraw Funds'
775+ }
727776 </ Button >
728777 </ >
729778 ) }
@@ -776,6 +825,11 @@ joinHook.join(bounty.onChainId, joinAmount);
776825 ) }
777826
778827
828+ { /* Vote Dashboard — live yes/no tallies and deadline */ }
829+ { bounty . isVoting && bounty . onChainId > 0 && (
830+ < VoteDashboard chainId = { chainId } onChainBountyId = { bounty . onChainId } />
831+ ) }
832+
779833 { /* Bounty Details */ }
780834 < Card >
781835 < CardHeader >
0 commit comments