11"use client" ;
22
3- import { useEffect , useState } from "react" ;
3+ import { useCallback , useEffect , useState } from "react" ;
44import Link from "next/link" ;
5- import { useQuery } from "@tanstack/react-query" ;
5+ import { useQuery , useQueryClient } from "@tanstack/react-query" ;
66import {
77 AlertCircle ,
88 ArrowLeft ,
@@ -47,7 +47,7 @@ import { POIDH_ABI } from "@/lib/poidh/abi";
4747import { CHAIN_NAMES , getExplorerUrl , getTxUrl , POIDH_CONTRACTS } from "@/lib/poidh/config" ;
4848import { getThirdwebClient } from "@/lib/thirdweb" ;
4949import { THIRDWEB_AA_CONFIG , THIRDWEB_WALLETS } from "@/lib/thirdweb-wallets" ;
50- import type { PoidhBounty } from "@/types/poidh" ;
50+ import type { PoidhBounty , PoidhClaim } from "@/types/poidh" ;
5151
5252const VIDEO_EXTENSIONS = / \. ( m o v | m p 4 | w e b m | o g g | m 4 v ) ( \? .* ) ? $ / i;
5353const IPFS_GATEWAYS = [
@@ -183,6 +183,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
183183 } ) ;
184184
185185 const bounty = data ?. bounty ;
186+ const queryClient = useQueryClient ( ) ;
186187
187188 const { ethPrice } = useEthPrice ( ) ;
188189 const [ joinAmount , setJoinAmount ] = useState ( "0.001" ) ;
@@ -198,6 +199,89 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
198199 const resolveVoteHook = usePoidhResolveVote ( chainId ) ;
199200 const resetVotingHook = usePoidhResetVotingPeriod ( chainId ) ;
200201
202+ // Refresh bounty state after actions that change on-chain status
203+ const bountyQueryKey = [ "poidh-bounty" , chainId , bountyId ] ;
204+
205+ // Refresh bounty state after actions that change on-chain status
206+ useEffect ( ( ) => {
207+ if ( submitForVoteHook . isSuccess ) queryClient . invalidateQueries ( { queryKey : bountyQueryKey } ) ;
208+ // eslint-disable-next-line react-hooks/exhaustive-deps
209+ } , [ submitForVoteHook . isSuccess ] ) ;
210+ useEffect ( ( ) => {
211+ if ( resolveVoteHook . isSuccess ) queryClient . invalidateQueries ( { queryKey : bountyQueryKey } ) ;
212+ // eslint-disable-next-line react-hooks/exhaustive-deps
213+ } , [ resolveVoteHook . isSuccess ] ) ;
214+ useEffect ( ( ) => {
215+ if ( cancelHook . isSuccess ) queryClient . invalidateQueries ( { queryKey : bountyQueryKey } ) ;
216+ // eslint-disable-next-line react-hooks/exhaustive-deps
217+ } , [ cancelHook . isSuccess ] ) ;
218+
219+ // Optimistic claim helpers — persist across page refresh until indexer catches up
220+ const pendingClaimKey = `poidh:pending-claim:${ chainId } :${ bountyId } ` ;
221+
222+ const injectOptimisticClaim = useCallback (
223+ ( pending : { name : string ; description : string ; url : string ; issuer : string ; savedAt : number } ) => {
224+ queryClient . setQueryData < { bounty : PoidhBounty } > ( [ "poidh-bounty" , chainId , bountyId ] , ( old ) => {
225+ if ( ! old ) return old ;
226+ const already = old . bounty . claims ?. some (
227+ ( c ) => c . issuer . toLowerCase ( ) === pending . issuer . toLowerCase ( ) && c . name === pending . name && c . id < 2_000_000_000 ,
228+ ) ;
229+ if ( already ) return old ;
230+ const tmpId = Date . now ( ) ;
231+ const optimistic : PoidhClaim = {
232+ id : tmpId ,
233+ onChainId : tmpId ,
234+ bountyId : old . bounty . id ,
235+ name : pending . name ,
236+ description : pending . description ,
237+ url : pending . url || null ,
238+ issuer : pending . issuer ,
239+ createdAt : Math . floor ( pending . savedAt / 1000 ) ,
240+ accepted : false ,
241+ } ;
242+ return { bounty : { ...old . bounty , claims : [ ...( old . bounty . claims ?? [ ] ) , optimistic ] , hasClaims : true } } ;
243+ } ) ;
244+ } ,
245+ [ queryClient , chainId , bountyId ] ,
246+ ) ;
247+
248+ // On mount: restore pending claim from localStorage if indexer hasn't caught up yet
249+ useEffect ( ( ) => {
250+ try {
251+ const raw = localStorage . getItem ( pendingClaimKey ) ;
252+ if ( ! raw ) return ;
253+ injectOptimisticClaim ( JSON . parse ( raw ) as { name : string ; description : string ; url : string ; issuer : string ; savedAt : number } ) ;
254+ } catch { /* ignore parse errors */ }
255+ // eslint-disable-next-line react-hooks/exhaustive-deps
256+ } , [ ] ) ;
257+
258+ // Clear localStorage once the real claim arrives from the API
259+ useEffect ( ( ) => {
260+ if ( ! bounty ?. claims ) return ;
261+ try {
262+ const raw = localStorage . getItem ( pendingClaimKey ) ;
263+ if ( ! raw ) return ;
264+ const pending = JSON . parse ( raw ) as { name : string ; issuer : string } ;
265+ const arrived = bounty . claims . some (
266+ ( c ) => c . issuer . toLowerCase ( ) === pending . issuer . toLowerCase ( ) && c . name === pending . name && c . id < 2_000_000_000 ,
267+ ) ;
268+ if ( arrived ) localStorage . removeItem ( pendingClaimKey ) ;
269+ } catch { /* ignore */ }
270+ // eslint-disable-next-line react-hooks/exhaustive-deps
271+ } , [ bounty ?. claims ] ) ;
272+
273+ const handleClaimSuccess = useCallback (
274+ ( { name, description, url } : { name : string ; description : string ; url : string } ) => {
275+ if ( ! address ) return ;
276+ const pending = { name, description, url, issuer : address , savedAt : Date . now ( ) } ;
277+ try { localStorage . setItem ( pendingClaimKey , JSON . stringify ( pending ) ) ; } catch { /* quota */ }
278+ injectOptimisticClaim ( pending ) ;
279+ setTimeout ( ( ) => queryClient . invalidateQueries ( { queryKey : bountyQueryKey } ) , 15_000 ) ;
280+ } ,
281+ // eslint-disable-next-line react-hooks/exhaustive-deps
282+ [ address , injectOptimisticClaim , queryClient ] ,
283+ ) ;
284+
201285 const deadlineTimestamp = bounty ?. deadline ?? null ;
202286 const countdown = useCountdown ( deadlineTimestamp ) ;
203287
@@ -392,7 +476,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
392476 Be the first to complete this challenge. Film your proof and submit it on-chain.
393477 </ p >
394478 </ div >
395- < ClaimBountyModal bounty = { bounty } >
479+ < ClaimBountyModal bounty = { bounty } onSuccess = { handleClaimSuccess } >
396480 < Button size = "lg" className = "mt-2" >
397481 Join
398482 </ Button >
@@ -794,7 +878,7 @@ export function BountyDetailView({ initialBounty, chainId, bountyId }: BountyDet
794878 </ CardDescription >
795879 </ CardHeader >
796880 < CardContent >
797- < ClaimBountyModal bounty = { bounty } >
881+ < ClaimBountyModal bounty = { bounty } onSuccess = { handleClaimSuccess } >
798882 < Button size = "lg" className = "w-full" >
799883 Join
800884 </ Button >
0 commit comments