Skip to content

Commit 74d4074

Browse files
committed
Dashboard: Add fallback to native token balance on contract/split page when insight is not supported (#7847)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on improving the handling of token balances and recipient percentages in the `ContractSplitPage` and related components, enhancing user experience and error handling. ### Detailed summary - Updated `transactionCount` logic in `DistributeButton` to handle undefined cases. - Added error handling for fetching token balances using `tryCatch`. - Improved UI structure and styling in `ContractSplitPage`. - Changed variable naming for clarity in `useReadContract`. - Enhanced loading states for recipient percentages and balances. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved reliability of token balances by falling back to native balance if token data fails to load. * **Style** * Refreshed Split page layout: simplified headers, refined spacing, and updated table containers. * Adjusted column widths for Tokens and Addresses for better readability. * Enhanced loading skeletons and maintained clear empty/error states. * **Refactor** * Streamlined data flow for recipients and balances to use a unified query object. * **Chores** * Button tweaks: small size on error; hide transaction count when only one transaction. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a36c850 commit 74d4074

File tree

3 files changed

+169
-132
lines changed

3 files changed

+169
-132
lines changed

apps/dashboard/src/@/hooks/useSplit.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import {
77
import { toast } from "sonner";
88
import {
99
type Chain,
10+
NATIVE_TOKEN_ADDRESS,
1011
sendAndConfirmTransaction,
1112
type ThirdwebClient,
1213
type ThirdwebContract,
1314
} from "thirdweb";
15+
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
1416
import { distribute, distributeByToken } from "thirdweb/extensions/split";
1517
import { getOwnedTokens } from "thirdweb/insight";
1618
import { useActiveAccount } from "thirdweb/react";
19+
import { getWalletBalance } from "thirdweb/wallets";
1720
import invariant from "tiny-invariant";
1821
import { parseError } from "../utils/errorParser";
22+
import { tryCatch } from "../utils/try-catch";
1923

2024
function getTokenBalancesQuery(params: {
2125
ownerAddress: string;
@@ -24,14 +28,39 @@ function getTokenBalancesQuery(params: {
2428
}) {
2529
return queryOptions({
2630
queryFn: async () => {
27-
return getOwnedTokens({
31+
const ownedTokenBalancePromise = getOwnedTokens({
2832
client: params.client,
2933
chains: [params.chain],
3034
ownerAddress: params.ownerAddress,
3135
queryOptions: {
3236
include_native: "true",
3337
},
3438
});
39+
40+
const result = await tryCatch(ownedTokenBalancePromise);
41+
42+
// fallback to fetch native token balance with rpc
43+
if (result.error) {
44+
const walletBalance = await getWalletBalance({
45+
address: params.ownerAddress,
46+
client: params.client,
47+
chain: params.chain,
48+
});
49+
50+
const nativeTokenBalance: GetBalanceResult = {
51+
name: walletBalance.name,
52+
value: walletBalance.value,
53+
decimals: walletBalance.decimals,
54+
displayValue: walletBalance.displayValue,
55+
symbol: walletBalance.symbol,
56+
chainId: params.chain.id,
57+
tokenAddress: NATIVE_TOKEN_ADDRESS,
58+
};
59+
60+
return [nativeTokenBalance];
61+
}
62+
63+
return result.data;
3564
},
3665
queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress],
3766
retry: false,

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/ContractSplitPage.tsx

Lines changed: 137 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function ContractSplitPage({
2929
}) {
3030
const address = useActiveAccount()?.address;
3131

32-
const { data: allRecipientsPercentages } = useReadContract(
32+
const allRecipientsPercentagesQuery = useReadContract(
3333
getAllRecipientsPercentages,
3434
{ contract },
3535
);
@@ -39,156 +39,163 @@ export function ContractSplitPage({
3939
chain: contract.chain,
4040
});
4141

42-
const activeRecipient = (allRecipientsPercentages || []).find(
42+
const activeRecipient = (allRecipientsPercentagesQuery.data || []).find(
4343
(r) => r.address.toLowerCase() === address?.toLowerCase(),
4444
);
4545

4646
return (
4747
<div>
48-
{/* header */}
49-
<div className="flex flex-col gap-3 lg:flex-row lg:items-center justify-between mb-6 lg:mb-4">
50-
<div>
51-
<h2 className="text-2xl font-semibold tracking-tight mb-1">
52-
Balances
53-
</h2>
54-
<p className="text-muted-foreground">
55-
The Split can receive funds in the native token or in any ERC20
56-
</p>
57-
</div>
48+
<div className="mb-4">
49+
<h2 className="text-2xl font-semibold tracking-tight">Balances</h2>
50+
<p className="text-muted-foreground">
51+
The Split can receive funds in the native token or in any ERC20
52+
</p>
5853
</div>
5954

6055
{/* balances table */}
61-
<div className="mb-10">
62-
<div className="border rounded-lg overflow-hidden">
63-
<TableContainer className="border-none">
64-
<Table>
65-
<TableHeader>
66-
<TableRow>
67-
<TableHead>Token</TableHead>
68-
<TableHead>Balance</TableHead>
69-
{activeRecipient && <TableHead>Your Share</TableHead>}
70-
</TableRow>
71-
</TableHeader>
72-
<TableBody>
73-
{balanceQuery.isPending
74-
? new Array(3).fill(null).map((_, index) => (
75-
// biome-ignore lint/suspicious/noArrayIndexKey: ok
76-
<TableRow key={index}>
77-
<TableCell>
78-
<Skeleton className="h-5 w-52" />
79-
</TableCell>
56+
<div className="bg-card border rounded-lg overflow-hidden">
57+
<TableContainer className="border-none rounded-none">
58+
<Table>
59+
<TableHeader>
60+
<TableRow>
61+
<TableHead className="w-[240px] lg:w-[320px]">Token</TableHead>
62+
<TableHead>Balance</TableHead>
63+
{activeRecipient && <TableHead>Your Share</TableHead>}
64+
</TableRow>
65+
</TableHeader>
66+
<TableBody>
67+
{balanceQuery.isPending
68+
? new Array(3).fill(null).map((_, index) => (
69+
// biome-ignore lint/suspicious/noArrayIndexKey: ok
70+
<TableRow key={index}>
71+
<TableCell>
72+
<Skeleton className="h-5 w-52" />
73+
</TableCell>
74+
<TableCell>
75+
<Skeleton className="h-5 w-16" />
76+
</TableCell>
77+
{activeRecipient && (
8078
<TableCell>
8179
<Skeleton className="h-5 w-16" />
8280
</TableCell>
83-
{activeRecipient && (
84-
<TableCell>
85-
<Skeleton className="h-5 w-16" />
86-
</TableCell>
87-
)}
88-
</TableRow>
89-
))
90-
: balanceQuery.data?.map((tokenBalance) => (
91-
<TableRow key={tokenBalance.tokenAddress}>
92-
{/* token */}
93-
<TableCell className="font-medium">
94-
<Button
95-
asChild
96-
variant="ghost"
97-
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
98-
>
99-
{tokenBalance.tokenAddress ===
100-
NATIVE_TOKEN_ADDRESS ? (
101-
<span>{tokenBalance.symbol}</span>
102-
) : (
103-
<Link
104-
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
105-
target="_blank"
106-
>
107-
{tokenBalance.name}
108-
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
109-
</Link>
110-
)}
111-
</Button>
112-
</TableCell>
81+
)}
82+
</TableRow>
83+
))
84+
: balanceQuery.data?.map((tokenBalance) => (
85+
<TableRow key={tokenBalance.tokenAddress}>
86+
{/* token */}
87+
<TableCell className="font-medium">
88+
<Button
89+
asChild
90+
variant="ghost"
91+
className="flex items-center gap-2 w-fit h-auto py-1 px-2 -translate-x-2"
92+
>
93+
{tokenBalance.tokenAddress ===
94+
NATIVE_TOKEN_ADDRESS ? (
95+
<span>{tokenBalance.symbol}</span>
96+
) : (
97+
<Link
98+
href={`https://thirdweb.com/${tokenBalance.chainId}/${tokenBalance.tokenAddress}`}
99+
target="_blank"
100+
>
101+
{tokenBalance.name}
102+
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
103+
</Link>
104+
)}
105+
</Button>
106+
</TableCell>
107+
108+
{/* balance */}
109+
<TableCell>
110+
{tokenBalance.displayValue} {tokenBalance.symbol}
111+
</TableCell>
113112

114-
{/* balance */}
113+
{/* your share percent */}
114+
{activeRecipient && (
115115
<TableCell>
116-
{tokenBalance.displayValue} {tokenBalance.symbol}
116+
{activeRecipient.splitPercentage}%
117117
</TableCell>
118+
)}
119+
</TableRow>
120+
))}
121+
</TableBody>
122+
</Table>
118123

119-
{/* your share percent */}
120-
{activeRecipient && (
121-
<TableCell>
122-
{activeRecipient.splitPercentage}%
123-
</TableCell>
124-
)}
125-
</TableRow>
126-
))}
127-
</TableBody>
128-
</Table>
129-
130-
{balanceQuery.isError && (
131-
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
132-
{balanceQuery.error.message}
133-
</div>
134-
)}
135-
136-
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
137-
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
138-
No funds received yet
139-
</div>
140-
)}
141-
</TableContainer>
124+
{balanceQuery.isError && (
125+
<div className="text-red-500 px-4 flex justify-center items-center py-20 text-muted-foreground">
126+
{balanceQuery.error.message}
127+
</div>
128+
)}
142129

143-
{balanceQuery.data && balanceQuery.data.length > 0 && (
144-
<div className="border-t p-4 lg:px-6 flex justify-end bg-card">
145-
<DistributeButton
146-
balances={balanceQuery.data || []}
147-
balancesIsError={balanceQuery.isError}
148-
balancesIsPending={balanceQuery.isPending}
149-
contract={contract}
150-
isLoggedIn={isLoggedIn}
151-
/>
130+
{!balanceQuery.isPending && balanceQuery.data?.length === 0 && (
131+
<div className="px-4 flex justify-center items-center py-20 text-muted-foreground">
132+
No funds received yet
152133
</div>
153134
)}
154-
</div>
135+
</TableContainer>
136+
137+
{balanceQuery.data && balanceQuery.data.length > 0 && (
138+
<div className="border-t p-4 lg:py-5 lg:px-6 flex justify-end bg-card">
139+
<DistributeButton
140+
balances={balanceQuery.data || []}
141+
balancesIsError={balanceQuery.isError}
142+
balancesIsPending={balanceQuery.isPending}
143+
contract={contract}
144+
isLoggedIn={isLoggedIn}
145+
/>
146+
</div>
147+
)}
155148
</div>
156149

157-
{/* recipients table */}
158-
<div>
159-
<div className="mb-4">
160-
<h3 className="text-2xl font-semibold tracking-tight mb-1">
161-
Split Recipients
162-
</h3>
163-
<p className="text-muted-foreground">
164-
List of addresses that can receive funds from the Split and their
165-
percentage share.
166-
</p>
167-
</div>
168-
<TableContainer>
169-
<Table>
170-
<TableHeader>
171-
<TableRow>
172-
<TableHead>Address</TableHead>
173-
<TableHead>Percentage</TableHead>
174-
</TableRow>
175-
</TableHeader>
176-
<TableBody>
177-
{(allRecipientsPercentages || []).map((split) => (
178-
<TableRow key={split.address}>
179-
<TableCell>
180-
<WalletAddress
181-
address={split.address}
182-
client={contract.client}
183-
/>
184-
</TableCell>
185-
<TableCell>{split.splitPercentage}%</TableCell>
186-
</TableRow>
187-
))}
188-
</TableBody>
189-
</Table>
190-
</TableContainer>
150+
<div className="h-10" />
151+
152+
<div className="mb-4">
153+
<h3 className="text-2xl font-semibold tracking-tight">
154+
Split Recipients
155+
</h3>
156+
<p className="text-muted-foreground">
157+
List of addresses that can receive funds from the Split and their
158+
percentage share.
159+
</p>
191160
</div>
161+
162+
{/* recipients table */}
163+
<TableContainer>
164+
<Table>
165+
<TableHeader>
166+
<TableRow>
167+
<TableHead className="w-[240px] lg:w-[320px]">Address</TableHead>
168+
<TableHead>Percentage</TableHead>
169+
</TableRow>
170+
</TableHeader>
171+
<TableBody>
172+
{allRecipientsPercentagesQuery.isPending
173+
? new Array(3).fill(null).map((_, index) => (
174+
// biome-ignore lint/suspicious/noArrayIndexKey: ok
175+
<TableRow key={index}>
176+
<TableCell>
177+
<Skeleton className="h-5 w-52" />
178+
</TableCell>
179+
<TableCell>
180+
<Skeleton className="h-5 w-16" />
181+
</TableCell>
182+
</TableRow>
183+
))
184+
: (allRecipientsPercentagesQuery.data || []).map((split) => (
185+
<TableRow key={split.address}>
186+
<TableCell>
187+
<WalletAddress
188+
address={split.address}
189+
client={contract.client}
190+
className="h-auto py-1"
191+
/>
192+
</TableCell>
193+
<TableCell>{split.splitPercentage}%</TableCell>
194+
</TableRow>
195+
))}
196+
</TableBody>
197+
</Table>
198+
</TableContainer>
192199
</div>
193200
);
194201
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/split/components/distribute-button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const DistributeButton = ({
6161
isLoggedIn={isLoggedIn}
6262
isPending={mutation.isPending}
6363
onClick={distributeFunds}
64+
size="sm"
6465
transactionCount={undefined}
6566
// if we fail to get the balances, we can't know how many transactions there are going to be
6667
txChainID={contract.chain.id}
@@ -87,7 +88,7 @@ export const DistributeButton = ({
8788
isLoggedIn={isLoggedIn}
8889
isPending={mutation.isPending}
8990
onClick={distributeFunds}
90-
transactionCount={numTransactions}
91+
transactionCount={numTransactions === 1 ? undefined : numTransactions}
9192
txChainID={contract.chain.id}
9293
variant="default"
9394
size="sm"

0 commit comments

Comments
 (0)