Skip to content

Commit 7beaa25

Browse files
committed
update
1 parent 57fa96b commit 7beaa25

File tree

9 files changed

+201
-45
lines changed

9 files changed

+201
-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: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getLastAuthProvider } from "../../../../react/core/utils/storage.js";
1818
import { shortenAddress } from "../../../../utils/address.js";
1919
import { isContractDeployed } from "../../../../utils/bytecode/is-contract-deployed.js";
2020
import { formatNumber } from "../../../../utils/formatNumber.js";
21+
import { shortenLargeNumber } from "../../../../utils/shortenLargeNumber.js";
2122
import { webLocalStorage } from "../../../../utils/storage/webStorage.js";
2223
import { isEcosystemWallet } from "../../../../wallets/ecosystem/is-ecosystem-wallet.js";
2324
import type { Ecosystem } from "../../../../wallets/in-app/core/wallet/types.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,12 @@ 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}
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,18 @@ function DetailsModal(props: {
10061011
);
10071012
}
10081013

1009-
function formatBalanceOnButton(num: number) {
1010-
return formatNumber(num, num < 1 ? 5 : 4);
1014+
function formatAccountBalanceForModal(
1015+
props: AccountBalanceFormatParams,
1016+
): string {
1017+
if (props.fiatBalance && props.fiatSymbol) {
1018+
// Need to keep them short to avoid UI overflow issues
1019+
const formattedTokenBalance = formatNumber(props.tokenBalance, 5);
1020+
const num = formatNumber(props.fiatBalance, 4);
1021+
const formattedFiatBalance = shortenLargeNumber(num);
1022+
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
1023+
}
1024+
const formattedTokenBalance = formatNumber(props.tokenBalance, 9);
1025+
return `${formattedTokenBalance} ${props.tokenSymbol}`;
10111026
}
10121027

10131028
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: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,24 @@ 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 { formatNumber } from "../../../../../utils/formatNumber.js";
11+
import { shortenLargeNumber } from "../../../../../utils/shortenLargeNumber.js";
12+
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
1213
import { useAccountContext } from "./provider.js";
1314

15+
/**
16+
* @internal
17+
*/
18+
export type AccountBalanceFormatParams = {
19+
tokenBalance: number;
20+
tokenSymbol: string;
21+
fiatBalance?: number;
22+
fiatSymbol?: string;
23+
};
24+
1425
/**
1526
* Props for the AccountBalance component
1627
* @component
@@ -33,7 +44,7 @@ export interface AccountBalanceProps
3344
* use this function to transform the balance display value like round up the number
3445
* Particularly useful to avoid overflowing-UI issues
3546
*/
36-
formatFn?: (num: number) => number;
47+
formatFn?: (props: AccountBalanceFormatParams) => string;
3748
/**
3849
* This component will be shown while the balance of the account is being fetched
3950
* If not passed, the component will return `null`.
@@ -67,9 +78,11 @@ export interface AccountBalanceProps
6778
* Optional `useQuery` params
6879
*/
6980
queryOptions?: Omit<
70-
UseQueryOptions<GetWalletBalanceResult>,
81+
UseQueryOptions<AccountBalanceFormatParams>,
7182
"queryFn" | "queryKey"
7283
>;
84+
85+
showFiatValue?: "USD";
7386
}
7487

7588
/**
@@ -149,10 +162,11 @@ export interface AccountBalanceProps
149162
export function AccountBalance({
150163
chain,
151164
tokenAddress,
152-
formatFn,
153165
loadingComponent,
154166
fallbackComponent,
155167
queryOptions,
168+
formatFn,
169+
showFiatValue,
156170
...restProps
157171
}: AccountBalanceProps) {
158172
const { address, client } = useAccountContext();
@@ -164,20 +178,61 @@ export function AccountBalance({
164178
chainToLoad?.id || -1,
165179
address || "0x0",
166180
{ tokenAddress },
181+
showFiatValue,
167182
] as const,
168-
queryFn: async () => {
183+
queryFn: async (): Promise<AccountBalanceFormatParams> => {
169184
if (!chainToLoad) {
170185
throw new Error("chain is required");
171186
}
172187
if (!client) {
173188
throw new Error("client is required");
174189
}
175-
return getWalletBalance({
190+
const tokenBalanceData = await getWalletBalance({
176191
chain: chainToLoad,
177192
client,
178193
address,
179194
tokenAddress,
180195
});
196+
197+
if (!tokenBalanceData) {
198+
throw new Error(
199+
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chainToLoad.id}`,
200+
);
201+
}
202+
203+
if (showFiatValue) {
204+
const fiatData = await convertCryptoToFiat({
205+
fromAmount: Number(tokenBalanceData.displayValue),
206+
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
207+
to: showFiatValue,
208+
chain: chainToLoad,
209+
client,
210+
}).catch(() => undefined);
211+
212+
// We can never support 100% of token out there, so if something fails to resolve, it's expected
213+
// in that case just return the tokenBalance and symbol
214+
return {
215+
tokenBalance: Number(tokenBalanceData.displayValue),
216+
tokenSymbol: tokenBalanceData.symbol,
217+
fiatBalance: fiatData?.result,
218+
fiatSymbol: fiatData?.result
219+
? new Intl.NumberFormat("en", {
220+
style: "currency",
221+
currency: showFiatValue,
222+
minimumFractionDigits: 0,
223+
maximumFractionDigits: 0,
224+
})
225+
.formatToParts(0)
226+
.find((p) => p.type === "currency")?.value ||
227+
showFiatValue.toUpperCase()
228+
: undefined,
229+
};
230+
}
231+
232+
return {
233+
tokenBalance: Number(tokenBalanceData.displayValue),
234+
tokenSymbol: tokenBalanceData.symbol,
235+
};
181236
},
182237
...queryOptions,
183238
});
@@ -190,13 +245,35 @@ export function AccountBalance({
190245
return fallbackComponent || null;
191246
}
192247

193-
const displayValue = formatFn
194-
? formatFn(Number(balanceQuery.data.displayValue))
195-
: balanceQuery.data.displayValue;
248+
if (formatFn) {
249+
return <span {...restProps}>{formatFn(balanceQuery.data)}</span>;
250+
}
196251

197252
return (
198253
<span {...restProps}>
199-
{displayValue} {balanceQuery.data.symbol}
254+
{formatAccountBalanceForButton(balanceQuery.data)}
200255
</span>
201256
);
202257
}
258+
259+
/**
260+
* Format the display balance for both crypto and fiat, in the Details button and Modal
261+
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
262+
* @internal
263+
*/
264+
function formatAccountBalanceForButton(
265+
props: AccountBalanceFormatParams,
266+
): string {
267+
if (props.fiatBalance && props.fiatSymbol) {
268+
// Need to keep them short to avoid UI overflow issues
269+
const formattedTokenBalance = formatNumber(props.tokenBalance, 1);
270+
const num = formatNumber(props.fiatBalance, 0);
271+
const formattedFiatBalance = shortenLargeNumber(num);
272+
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
273+
}
274+
const formattedTokenBalance = formatNumber(
275+
props.tokenBalance,
276+
props.tokenBalance < 1 ? 5 : 4,
277+
);
278+
return `${formattedTokenBalance} ${props.tokenSymbol}`;
279+
}

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)