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");