Skip to content

Commit 5408bc5

Browse files
committed
update
1 parent f1e9de2 commit 5408bc5

File tree

9 files changed

+211
-45
lines changed

9 files changed

+211
-45
lines changed

.changeset/fair-plants-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Export util functions: formatNumber and shortenLargeNumber

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/exports/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,6 @@ export type {
204204
AbiConstructor,
205205
AbiFallback,
206206
} from "abitype";
207+
208+
export { shortenLargeNumber } from "../utils/shortenLargeNumber.js";
209+
export { formatNumber } from "../utils/formatNumber.js";

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

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@radix-ui/react-icons";
1111
import { useQuery } from "@tanstack/react-query";
1212
import { type JSX, useContext, useEffect, useState } from "react";
13+
import { shortenLargeNumber } from "src/utils/shortenLargeNumber.js";
1314
import { trackPayEvent } from "../../../../analytics/track/pay.js";
1415
import type { Chain } from "../../../../chains/types.js";
1516
import type { ThirdwebClient } from "../../../../client/client.js";
@@ -86,7 +87,10 @@ import { fadeInAnimation } from "../design-system/animations.js";
8687
import { StyledButton } from "../design-system/elements.js";
8788
import { AccountAddress } from "../prebuilt/Account/address.js";
8889
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
89-
import { AccountBalance } from "../prebuilt/Account/balance.js";
90+
import {
91+
AccountBalance,
92+
type AccountBalanceFormatParams,
93+
} from "../prebuilt/Account/balance.js";
9094
import { AccountBlobbie } from "../prebuilt/Account/blobbie.js";
9195
import { AccountName } from "../prebuilt/Account/name.js";
9296
import { AccountProvider } from "../prebuilt/Account/provider.js";
@@ -278,12 +282,13 @@ export const ConnectedWalletDetails: React.FC<{
278282
chain={walletChain}
279283
loadingComponent={<Skeleton height={fontSize.xs} width="70px" />}
280284
fallbackComponent={<Skeleton height={fontSize.xs} width="70px" />}
281-
formatFn={formatBalanceOnButton}
285+
formatFn={formatAccountBalanceForButton}
282286
tokenAddress={
283287
props.detailsButton?.displayBalanceToken?.[
284288
Number(walletChain?.id)
285289
]
286290
}
291+
showFiatValue="USD"
287292
/>
288293
</Text>
289294
</Container>
@@ -380,11 +385,12 @@ function DetailsModal(props: {
380385
<AccountBalance
381386
fallbackComponent={<Skeleton height="1em" width="100px" />}
382387
loadingComponent={<Skeleton height="1em" width="100px" />}
383-
formatFn={(num: number) => formatNumber(num, 9)}
384388
chain={walletChain}
385389
tokenAddress={
386390
props.displayBalanceToken?.[Number(walletChain?.id)]
387391
}
392+
formatFn={formatAccountBalanceForModal}
393+
showFiatValue="USD"
388394
/>
389395
</Text>
390396
</Text>
@@ -1006,8 +1012,40 @@ function DetailsModal(props: {
10061012
);
10071013
}
10081014

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

10131051
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
}

packages/thirdweb/src/utils/formatNumber.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
/**
2-
* @internal
2+
* Round up a number to a certain decimal place
3+
* @example
4+
* ```ts
5+
* import { formatNumber } from "thirdweb/utils";
6+
* const value = formatNumber(12.1214141, 1); // 12.1
7+
* ```
8+
* @utils
39
*/
410
export function formatNumber(value: number, decimalPlaces: number) {
511
if (value === 0) return 0;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from "vitest";
2+
import { shortenLargeNumber } from "./shortenLargeNumber.js";
3+
4+
describe("shortenLargeNumber", () => {
5+
it("should not affect number below 10000", () => {
6+
expect(shortenLargeNumber(1000)).toBe("1000");
7+
});
8+
it("should shorten the number to `k`", () => {
9+
expect(shortenLargeNumber(10000)).toBe("10k");
10+
});
11+
it("should shorten the number to `M`", () => {
12+
expect(shortenLargeNumber(1_000_000)).toBe("1M");
13+
});
14+
it("should shorten the number to `B`", () => {
15+
expect(shortenLargeNumber(1_000_000_000)).toBe("1B");
16+
});
17+
18+
it("should not affect number below 10000", () => {
19+
expect(shortenLargeNumber(1001)).toBe("1001");
20+
});
21+
it("should shorten the number to `k`", () => {
22+
expect(shortenLargeNumber(11100)).toBe("11.1k");
23+
});
24+
it("should shorten the number to `M`", () => {
25+
expect(shortenLargeNumber(1_100_000)).toBe("1.1M");
26+
});
27+
it("should shorten the number to `B`", () => {
28+
expect(shortenLargeNumber(1_100_000_001)).toBe("1.1B");
29+
});
30+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Shorten the string for large value
3+
* Mainly used for
4+
* Examples:
5+
* 10_000 -> 10k
6+
* 1_000_000 -> 1M
7+
* 1_000_000_000 -> 1B
8+
* @example
9+
* ```ts
10+
* import { shortenLargeNumber } from "thirdweb/utils";
11+
* const numStr = shortenLargeNumber(1_000_000_000, )
12+
* ```
13+
* @utils
14+
*/
15+
export function shortenLargeNumber(value: number) {
16+
if (value < 10_000) {
17+
return value.toString();
18+
}
19+
if (value < 1_000_000) {
20+
return formatLargeNumber(value, 1_000, "k");
21+
}
22+
if (value < 1_000_000_000) {
23+
return formatLargeNumber(value, 1_000_000, "M");
24+
}
25+
return formatLargeNumber(value, 1_000_000_000, "B");
26+
}
27+
28+
/**
29+
* Shorten the string for large value (over 4 digits)
30+
* 1000 -> 1000
31+
* 10_000 -> 10k
32+
* 1_000_000 -> 1M
33+
* 1_000_000_000 -> 1B
34+
*/
35+
function formatLargeNumber(
36+
value: number,
37+
divisor: number,
38+
suffix: "k" | "M" | "B",
39+
) {
40+
const quotient = value / divisor;
41+
if (Number.isInteger(quotient)) {
42+
return Math.floor(quotient) + suffix;
43+
}
44+
return quotient.toFixed(1).replace(/\.0$/, "") + suffix;
45+
}

0 commit comments

Comments
 (0)