Skip to content

Commit 4b66fbd

Browse files
committed
update
1 parent 07afcee commit 4b66fbd

File tree

4 files changed

+118
-44
lines changed

4 files changed

+118
-44
lines changed

packages/thirdweb/src/exports/react.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export {
221221
export {
222222
AccountBalance,
223223
type AccountBalanceProps,
224+
type AccountBalanceFormatParams
224225
} from "../react/web/ui/prebuilt/Account/balance.js";
225226
export {
226227
AccountName,

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ import { fadeInAnimation } from "../design-system/animations.js";
8686
import { StyledButton } from "../design-system/elements.js";
8787
import { AccountAddress } from "../prebuilt/Account/address.js";
8888
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
89-
import { AccountBalance } from "../prebuilt/Account/balance.js";
89+
import {
90+
AccountBalance,
91+
type AccountBalanceFormatParams,
92+
} from "../prebuilt/Account/balance.js";
9093
import { AccountBlobbie } from "../prebuilt/Account/blobbie.js";
9194
import { AccountName } from "../prebuilt/Account/name.js";
9295
import { AccountProvider } from "../prebuilt/Account/provider.js";
@@ -278,12 +281,13 @@ export const ConnectedWalletDetails: React.FC<{
278281
chain={walletChain}
279282
loadingComponent={<Skeleton height={fontSize.xs} width="70px" />}
280283
fallbackComponent={<Skeleton height={fontSize.xs} width="70px" />}
281-
formatFn={formatBalanceOnButton}
284+
formatFn={formatAccountBalanceForButton}
282285
tokenAddress={
283286
props.detailsButton?.displayBalanceToken?.[
284287
Number(walletChain?.id)
285288
]
286289
}
290+
showFiatValue="USD"
287291
/>
288292
</Text>
289293
</Container>
@@ -380,11 +384,12 @@ function DetailsModal(props: {
380384
<AccountBalance
381385
fallbackComponent={<Skeleton height="1em" width="100px" />}
382386
loadingComponent={<Skeleton height="1em" width="100px" />}
383-
formatFn={(num: number) => formatNumber(num, 9)}
384387
chain={walletChain}
385388
tokenAddress={
386389
props.displayBalanceToken?.[Number(walletChain?.id)]
387390
}
391+
formatFn={formatAccountBalanceForModal}
392+
showFiatValue="USD"
388393
/>
389394
</Text>
390395
</Text>
@@ -1006,8 +1011,38 @@ function DetailsModal(props: {
10061011
);
10071012
}
10081013

1009-
function formatBalanceOnButton(num: number) {
1010-
return formatNumber(num, num < 1 ? 5 : 4);
1014+
/**
1015+
* Format the display balance for both crypto and fiat, in the Details button and Modal
1016+
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
1017+
* @internal
1018+
*/
1019+
function formatAccountBalanceForButton(
1020+
props: AccountBalanceFormatParams,
1021+
): string {
1022+
if (props.fiatBalance && props.fiatSymbol) {
1023+
// Need to keep them short to avoid UI overflow issues
1024+
const formattedTokenBalance = formatNumber(props.tokenBalance, 1);
1025+
const formattedFiatBalance = formatNumber(props.fiatBalance, 0);
1026+
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
1027+
}
1028+
const formattedTokenBalance = formatNumber(
1029+
props.tokenBalance,
1030+
props.tokenBalance < 1 ? 5 : 4,
1031+
);
1032+
return `${formattedTokenBalance} ${props.tokenSymbol}`;
1033+
}
1034+
1035+
function formatAccountBalanceForModal(
1036+
props: AccountBalanceFormatParams,
1037+
): string {
1038+
if (props.fiatBalance && props.fiatSymbol) {
1039+
// Need to keep them short to avoid UI overflow issues
1040+
const formattedTokenBalance = formatNumber(props.tokenBalance, 5);
1041+
const formattedFiatBalance = formatNumber(props.fiatBalance, 4);
1042+
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
1043+
}
1044+
const formattedTokenBalance = formatNumber(props.tokenBalance, 9);
1045+
return `${formattedTokenBalance} ${props.tokenSymbol}`;
10111046
}
10121047

10131048
const WalletInfoButton = /* @__PURE__ */ StyledButton((_) => {

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.test.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,11 @@
11
import { describe, expect, it } from "vitest";
2-
import { ANVIL_CHAIN } from "~test/chains.js";
32
import { render, screen, waitFor } from "~test/react-render.js";
43
import { TEST_CLIENT } from "~test/test-clients.js";
54
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
6-
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
75
import { AccountBalance } from "./balance.js";
86
import { AccountProvider } from "./provider.js";
97

108
describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
11-
it("format the balance properly", async () => {
12-
const roundTo1Decimal = (num: number): number => Math.round(num * 10) / 10;
13-
const balance = await getWalletBalance({
14-
chain: ANVIL_CHAIN,
15-
client: TEST_CLIENT,
16-
address: TEST_ACCOUNT_A.address,
17-
});
18-
19-
render(
20-
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
21-
<AccountBalance chain={ANVIL_CHAIN} formatFn={roundTo1Decimal} />
22-
</AccountProvider>,
23-
);
24-
25-
waitFor(() =>
26-
expect(
27-
screen.getByText(roundTo1Decimal(Number(balance.displayValue)), {
28-
exact: true,
29-
selector: "span",
30-
}),
31-
).toBeInTheDocument(),
32-
);
33-
});
34-
359
it("should fallback properly if failed to load", () => {
3610
render(
3711
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@ import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
44
import type React from "react";
55
import type { JSX } from "react";
66
import type { Chain } from "../../../../../chains/types.js";
7+
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
8+
import { convertCryptoToFiat } from "../../../../../exports/pay.js";
79
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
8-
import {
9-
type GetWalletBalanceResult,
10-
getWalletBalance,
11-
} from "../../../../../wallets/utils/getWalletBalance.js";
10+
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
1211
import { useAccountContext } from "./provider.js";
1312

13+
/**
14+
* @internal
15+
*/
16+
export type AccountBalanceFormatParams = {
17+
tokenBalance: number;
18+
tokenSymbol: string;
19+
fiatBalance?: number;
20+
fiatSymbol?: string;
21+
};
22+
1423
/**
1524
* Props for the AccountBalance component
1625
* @component
@@ -33,7 +42,7 @@ export interface AccountBalanceProps
3342
* use this function to transform the balance display value like round up the number
3443
* Particularly useful to avoid overflowing-UI issues
3544
*/
36-
formatFn?: (num: number) => number;
45+
formatFn?: (props: AccountBalanceFormatParams) => string;
3746
/**
3847
* This component will be shown while the balance of the account is being fetched
3948
* If not passed, the component will return `null`.
@@ -67,9 +76,11 @@ export interface AccountBalanceProps
6776
* Optional `useQuery` params
6877
*/
6978
queryOptions?: Omit<
70-
UseQueryOptions<GetWalletBalanceResult>,
79+
UseQueryOptions<AccountBalanceFormatParams>,
7180
"queryFn" | "queryKey"
7281
>;
82+
83+
showFiatValue?: "USD";
7384
}
7485

7586
/**
@@ -149,10 +160,11 @@ export interface AccountBalanceProps
149160
export function AccountBalance({
150161
chain,
151162
tokenAddress,
152-
formatFn,
153163
loadingComponent,
154164
fallbackComponent,
155165
queryOptions,
166+
formatFn,
167+
showFiatValue,
156168
...restProps
157169
}: AccountBalanceProps) {
158170
const { address, client } = useAccountContext();
@@ -164,20 +176,61 @@ export function AccountBalance({
164176
chainToLoad?.id || -1,
165177
address || "0x0",
166178
{ tokenAddress },
179+
showFiatValue,
167180
] as const,
168-
queryFn: async () => {
181+
queryFn: async (): Promise<AccountBalanceFormatParams> => {
169182
if (!chainToLoad) {
170183
throw new Error("chain is required");
171184
}
172185
if (!client) {
173186
throw new Error("client is required");
174187
}
175-
return getWalletBalance({
188+
const tokenBalanceData = await getWalletBalance({
176189
chain: chainToLoad,
177190
client,
178191
address,
179192
tokenAddress,
180193
});
194+
195+
if (!tokenBalanceData) {
196+
throw new Error(
197+
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chainToLoad.id}`,
198+
);
199+
}
200+
201+
if (showFiatValue) {
202+
const fiatData = await convertCryptoToFiat({
203+
fromAmount: Number(tokenBalanceData.displayValue),
204+
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
205+
to: showFiatValue,
206+
chain: chainToLoad,
207+
client,
208+
}).catch(() => undefined);
209+
210+
// We can never support 100% of token out there, so if something fails to resolve, it's expected
211+
// in that case just return the tokenBalance and symbol
212+
return {
213+
tokenBalance: Number(tokenBalanceData.displayValue),
214+
tokenSymbol: tokenBalanceData.symbol,
215+
fiatBalance: fiatData?.result,
216+
fiatSymbol: fiatData?.result
217+
? new Intl.NumberFormat("en", {
218+
style: "currency",
219+
currency: showFiatValue,
220+
minimumFractionDigits: 0,
221+
maximumFractionDigits: 0,
222+
})
223+
.formatToParts(0)
224+
.find((p) => p.type === "currency")?.value ||
225+
showFiatValue.toUpperCase()
226+
: undefined,
227+
};
228+
}
229+
230+
return {
231+
tokenBalance: Number(tokenBalanceData.displayValue),
232+
tokenSymbol: tokenBalanceData.symbol,
233+
};
181234
},
182235
...queryOptions,
183236
});
@@ -190,13 +243,24 @@ export function AccountBalance({
190243
return fallbackComponent || null;
191244
}
192245

193-
const displayValue = formatFn
194-
? formatFn(Number(balanceQuery.data.displayValue))
195-
: balanceQuery.data.displayValue;
246+
if (formatFn) {
247+
return <span {...restProps}>{formatFn(balanceQuery.data)}</span>;
248+
}
249+
250+
const { tokenBalance, tokenSymbol, fiatBalance, fiatSymbol } =
251+
balanceQuery.data;
252+
253+
if (fiatBalance && fiatSymbol) {
254+
return (
255+
<span {...restProps}>
256+
{`${tokenBalance} ${tokenSymbol} (${fiatSymbol}${fiatBalance})`}
257+
</span>
258+
);
259+
}
196260

197261
return (
198262
<span {...restProps}>
199-
{displayValue} {balanceQuery.data.symbol}
263+
{tokenBalance} {tokenSymbol}
200264
</span>
201265
);
202266
}

0 commit comments

Comments
 (0)