From ddb0aac0f3a927fd88cb8e6eafde0b13a72aaf4a Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Wed, 18 Mar 2026 16:29:48 +0000 Subject: [PATCH 1/2] [#328] Improve royalty claim UX: post-claim state + claimed history - Post-claim: button disabled with "Claimed" text, balance reflects 0 - Success message shows claimed amount with clickable BaseScan tx link - Read cumulative claimed from getRoyaltyInfo; show "claimed so far" suffix when > 0 - Auto-reset to idle state on next data refetch after claim Fixes #328 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/ClaimRoyalties.tsx | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/ClaimRoyalties.tsx b/src/components/ClaimRoyalties.tsx index f1ba75b7..241d4436 100644 --- a/src/components/ClaimRoyalties.tsx +++ b/src/components/ClaimRoyalties.tsx @@ -35,17 +35,22 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo const reserveLabel = IS_TESTNET ? "WETH" : "$PLOT"; - // Fetch unclaimed royalty balance + // Fetch unclaimed royalty balance + cumulative claimed const { data: royaltyInfo, refetch } = useQuery({ queryKey: ["royalty-info", tokenAddress, beneficiary], queryFn: async () => { - const [balance] = await publicClient.readContract({ + const [balance, claimed] = await publicClient.readContract({ address: MCV2_BOND, abi: mcv2BondAbi, functionName: "getRoyaltyInfo", args: [beneficiary, PLOT_TOKEN], }); - return { unclaimed: balance }; + // Reset to idle when refetch brings fresh data after a claim + if (txState === "done") { + setTxState("idle"); + setError(null); + } + return { unclaimed: balance, claimed }; }, refetchInterval: 30000, }); @@ -58,6 +63,7 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo const decimals = tvlData?.decimals ?? 18; const unclaimed = royaltyInfo?.unclaimed ?? BigInt(0); + const totalClaimed = royaltyInfo?.claimed ?? BigInt(0); const eligible = plotCount >= 2; const canClaim = eligible && unclaimed > BigInt(0); @@ -131,18 +137,24 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo BigInt(0) ? "text-accent" : "text-foreground"}`}> {formatTruncated(unclaimed, decimals)} {reserveLabel} + {totalClaimed > BigInt(0) && ( + + (claimed: {formatTruncated(totalClaimed, decimals)} {reserveLabel} so far) + + )} @@ -152,14 +164,15 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo Chain at least 2 plots to enable royalty claims ({plotCount}/2)

)} - {eligible && unclaimed === BigInt(0) && txState === "idle" && ( + {eligible && unclaimed === BigInt(0) && txState === "idle" && totalClaimed === BigInt(0) && (

No royalties yet — royalties accrue when readers trade your token

)} {txHash && txState === "done" && (

- Tx:{" "} + Claimed {formatTruncated(claimedAmount, decimals)} {reserveLabel} —{" "} + tx:{" "} Date: Wed, 18 Mar 2026 16:33:12 +0000 Subject: [PATCH 2/2] [#328] Fix post-claim reset: defer to next refetch interval Remove inline txState reset from queryFn and immediate refetch() call from executeClaim. Instead, use a ref+effect to detect the next automatic royaltyInfo refetch (30s interval) and reset to idle then. This preserves the "Claimed" success state with tx link until fresh data arrives. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/ClaimRoyalties.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/components/ClaimRoyalties.tsx b/src/components/ClaimRoyalties.tsx index 241d4436..8e7c4ddc 100644 --- a/src/components/ClaimRoyalties.tsx +++ b/src/components/ClaimRoyalties.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { useWriteContract } from "wagmi"; import { useQuery } from "@tanstack/react-query"; import { formatUnits, type Address } from "viem"; @@ -36,7 +36,7 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo const reserveLabel = IS_TESTNET ? "WETH" : "$PLOT"; // Fetch unclaimed royalty balance + cumulative claimed - const { data: royaltyInfo, refetch } = useQuery({ + const { data: royaltyInfo } = useQuery({ queryKey: ["royalty-info", tokenAddress, beneficiary], queryFn: async () => { const [balance, claimed] = await publicClient.readContract({ @@ -45,11 +45,6 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo functionName: "getRoyaltyInfo", args: [beneficiary, PLOT_TOKEN], }); - // Reset to idle when refetch brings fresh data after a claim - if (txState === "done") { - setTxState("idle"); - setError(null); - } return { unclaimed: balance, claimed }; }, refetchInterval: 30000, @@ -67,6 +62,23 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo const eligible = plotCount >= 2; const canClaim = eligible && unclaimed > BigInt(0); + // Track dataUpdatedAt to detect refetch after claim + const claimDoneRef = useRef(false); + useEffect(() => { + if (txState === "done") { + claimDoneRef.current = true; + } + }, [txState]); + // Reset to idle when royaltyInfo updates after a successful claim + useEffect(() => { + if (claimDoneRef.current && txState === "done") { + claimDoneRef.current = false; + setTxState("idle"); + setError(null); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [royaltyInfo]); + const executeClaim = useCallback(async () => { try { setError(null); @@ -85,12 +97,11 @@ export function ClaimRoyalties({ tokenAddress, plotCount, beneficiary }: ClaimRo await publicClient.waitForTransactionReceipt({ hash }); setTxState("done"); - refetch(); } catch (err) { setError(err instanceof Error ? err.message : "Claim failed"); setTxState("error"); } - }, [unclaimed, writeContractAsync, refetch]); + }, [unclaimed, writeContractAsync]); const reset = useCallback(() => { setTxState("idle");