Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] USD balance for AccountBalance #5533

Open
wants to merge 1 commit into
base: kien/pay-convert-endpoints
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export {
export {
AccountBalance,
type AccountBalanceProps,
type AccountBalanceFormatParams,
} from "../react/web/ui/prebuilt/Account/balance.js";
export {
AccountName,
Expand Down
77 changes: 72 additions & 5 deletions packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@
import { StyledButton } from "../design-system/elements.js";
import { AccountAddress } from "../prebuilt/Account/address.js";
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
import { AccountBalance } from "../prebuilt/Account/balance.js";
import {
AccountBalance,
type AccountBalanceFormatParams,
} from "../prebuilt/Account/balance.js";
import { AccountBlobbie } from "../prebuilt/Account/blobbie.js";
import { AccountName } from "../prebuilt/Account/name.js";
import { AccountProvider } from "../prebuilt/Account/provider.js";
Expand Down Expand Up @@ -278,12 +281,13 @@
chain={walletChain}
loadingComponent={<Skeleton height={fontSize.xs} width="70px" />}
fallbackComponent={<Skeleton height={fontSize.xs} width="70px" />}
formatFn={formatBalanceOnButton}
formatFn={formatAccountBalanceForButton}

Check warning on line 284 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L284 was not covered by tests
tokenAddress={
props.detailsButton?.displayBalanceToken?.[
Number(walletChain?.id)
]
}
showFiatValue="USD"

Check warning on line 290 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L290 was not covered by tests
/>
</Text>
</Container>
Expand Down Expand Up @@ -380,11 +384,12 @@
<AccountBalance
fallbackComponent={<Skeleton height="1em" width="100px" />}
loadingComponent={<Skeleton height="1em" width="100px" />}
formatFn={(num: number) => formatNumber(num, 9)}
chain={walletChain}
tokenAddress={
props.displayBalanceToken?.[Number(walletChain?.id)]
}
formatFn={formatAccountBalanceForModal}
showFiatValue="USD"

Check warning on line 392 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L391-L392

Added lines #L391 - L392 were not covered by tests
/>
</Text>
</Text>
Expand Down Expand Up @@ -1006,8 +1011,70 @@
);
}

function formatBalanceOnButton(num: number) {
return formatNumber(num, num < 1 ? 5 : 4);
/**
* Format the display balance for both crypto and fiat, in the Details button and Modal
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
* @internal
*/
function formatAccountBalanceForButton(
props: AccountBalanceFormatParams,
): string {
if (props.fiatBalance && props.fiatSymbol) {

Check warning on line 1022 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1019-L1022

Added lines #L1019 - L1022 were not covered by tests
// Need to keep them short to avoid UI overflow issues
const formattedTokenBalance = formatNumber(props.tokenBalance, 1);
const formattedFiatBalance = formatFiatValue(props.fiatBalance, 0);
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
}
const formattedTokenBalance = formatNumber(
props.tokenBalance,
props.tokenBalance < 1 ? 5 : 4,
);
return `${formattedTokenBalance} ${props.tokenSymbol}`;
}

Check warning on line 1033 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1024-L1033

Added lines #L1024 - L1033 were not covered by tests

function formatAccountBalanceForModal(
props: AccountBalanceFormatParams,
): string {
if (props.fiatBalance && props.fiatSymbol) {

Check warning on line 1038 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1035-L1038

Added lines #L1035 - L1038 were not covered by tests
// Need to keep them short to avoid UI overflow issues
const formattedTokenBalance = formatNumber(props.tokenBalance, 5);
const formattedFiatBalance = formatFiatValue(props.fiatBalance, 4);
return `${formattedTokenBalance} ${props.tokenSymbol} (${props.fiatSymbol}${formattedFiatBalance})`;
}
const formattedTokenBalance = formatNumber(props.tokenBalance, 9);
return `${formattedTokenBalance} ${props.tokenSymbol}`;
}

Check warning on line 1046 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1040-L1046

Added lines #L1040 - L1046 were not covered by tests

function formatFiatValue(value: number, decimals: number): string {
const num = formatNumber(value, decimals);
if (num < 1000) {
return num.toString();
}
if (num < 1_000_000) {
return formatLargeNumber(num, 1000000, "k");
}
if (num < 1000000000) {
return formatLargeNumber(num, 1000000, "M");
}
return formatLargeNumber(num, 1000000000, "B");
}

Check warning on line 1060 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1048-L1060

Added lines #L1048 - L1060 were not covered by tests

/**
* Shorten the string for large value
* 10_000 -> 10k
* 1_000_000 -> 1M
* 1_000_000_000 -> 1B
*/
function formatLargeNumber(
value: number,
divisor: number,
suffix: "k" | "M" | "B",
) {
const quotient = value / divisor;
if (Number.isInteger(quotient)) {
return Math.floor(quotient) + suffix;
}
return quotient.toFixed(1).replace(/\.0$/, "") + suffix;

Check warning on line 1077 in packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx#L1068-L1077

Added lines #L1068 - L1077 were not covered by tests
}

const WalletInfoButton = /* @__PURE__ */ StyledButton((_) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,11 @@
import { describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "~test/chains.js";
import { render, screen, waitFor } from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
import { AccountBalance } from "./balance.js";
import { AccountProvider } from "./provider.js";

describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
it("format the balance properly", async () => {
const roundTo1Decimal = (num: number): number => Math.round(num * 10) / 10;
const balance = await getWalletBalance({
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
address: TEST_ACCOUNT_A.address,
});

render(
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
<AccountBalance chain={ANVIL_CHAIN} formatFn={roundTo1Decimal} />
</AccountProvider>,
);

waitFor(() =>
expect(
screen.getByText(roundTo1Decimal(Number(balance.displayValue)), {
exact: true,
selector: "span",
}),
).toBeInTheDocument(),
);
});

it("should fallback properly if failed to load", () => {
render(
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
Expand Down
90 changes: 77 additions & 13 deletions packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
import type React from "react";
import type { JSX } from "react";
import type { Chain } from "../../../../../chains/types.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
import { convertCryptoToFiat } from "../../../../../exports/pay.js";
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
import {
type GetWalletBalanceResult,
getWalletBalance,
} from "../../../../../wallets/utils/getWalletBalance.js";
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
import { useAccountContext } from "./provider.js";

/**
* @internal
*/
export type AccountBalanceFormatParams = {
tokenBalance: number;
tokenSymbol: string;
fiatBalance?: number;
fiatSymbol?: string;
};

/**
* Props for the AccountBalance component
* @component
Expand All @@ -33,7 +42,7 @@
* use this function to transform the balance display value like round up the number
* Particularly useful to avoid overflowing-UI issues
*/
formatFn?: (num: number) => number;
formatFn?: (props: AccountBalanceFormatParams) => string;
/**
* This component will be shown while the balance of the account is being fetched
* If not passed, the component will return `null`.
Expand Down Expand Up @@ -67,9 +76,11 @@
* Optional `useQuery` params
*/
queryOptions?: Omit<
UseQueryOptions<GetWalletBalanceResult>,
UseQueryOptions<AccountBalanceFormatParams>,
"queryFn" | "queryKey"
>;

showFiatValue?: "USD";
}

/**
Expand Down Expand Up @@ -149,10 +160,11 @@
export function AccountBalance({
chain,
tokenAddress,
formatFn,
loadingComponent,
fallbackComponent,
queryOptions,
formatFn,
showFiatValue,
...restProps
}: AccountBalanceProps) {
const { address, client } = useAccountContext();
Expand All @@ -164,20 +176,61 @@
chainToLoad?.id || -1,
address || "0x0",
{ tokenAddress },
showFiatValue,
] as const,
queryFn: async () => {
queryFn: async (): Promise<AccountBalanceFormatParams> => {
if (!chainToLoad) {
throw new Error("chain is required");
}
if (!client) {
throw new Error("client is required");
}
return getWalletBalance({
const tokenBalanceData = await getWalletBalance({

Check warning on line 188 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L188 was not covered by tests
chain: chainToLoad,
client,
address,
tokenAddress,
});

if (!tokenBalanceData) {
throw new Error(
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chainToLoad.id}`,
);
}

Check warning on line 199 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L195-L199

Added lines #L195 - L199 were not covered by tests

if (showFiatValue) {
const fiatData = await convertCryptoToFiat({
fromAmount: Number(tokenBalanceData.displayValue),
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
to: showFiatValue,
chain: chainToLoad,
client,
}).catch(() => undefined);

Check warning on line 208 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L201-L208

Added lines #L201 - L208 were not covered by tests

// We can never support 100% of token out there, so if something fails to resolve, it's expected
// in that case just return the tokenBalance and symbol
return {
tokenBalance: Number(tokenBalanceData.displayValue),
tokenSymbol: tokenBalanceData.symbol,
fiatBalance: fiatData?.result,
fiatSymbol: fiatData?.result
? new Intl.NumberFormat("en", {
style: "currency",
currency: showFiatValue,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
})
.formatToParts(0)
.find((p) => p.type === "currency")?.value ||
showFiatValue.toUpperCase()
Comment on lines +215 to +225
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify currency symbol formatting by using a simple mapping object instead of Intl.NumberFormat. Could replace with: const currencySymbols = { USD: '$' }; const fiatSymbol = currencySymbols[showFiatValue] || showFiatValue.toUpperCase();

Spotted by Graphite Reviewer (based on CI logs)

Is this helpful? React 👍 or 👎 to let us know.

: undefined,
};
}

Check warning on line 228 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L212-L228

Added lines #L212 - L228 were not covered by tests

return {
tokenBalance: Number(tokenBalanceData.displayValue),
tokenSymbol: tokenBalanceData.symbol,
};

Check warning on line 233 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L230-L233

Added lines #L230 - L233 were not covered by tests
},
...queryOptions,
});
Expand All @@ -190,13 +243,24 @@
return fallbackComponent || null;
}

const displayValue = formatFn
? formatFn(Number(balanceQuery.data.displayValue))
: balanceQuery.data.displayValue;
if (formatFn) {
return <span {...restProps}>{formatFn(balanceQuery.data)}</span>;
}

Check warning on line 248 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L246-L248

Added lines #L246 - L248 were not covered by tests

const { tokenBalance, tokenSymbol, fiatBalance, fiatSymbol } =
balanceQuery.data;

Check warning on line 251 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L250-L251

Added lines #L250 - L251 were not covered by tests

if (fiatBalance && fiatSymbol) {
return (
<span {...restProps}>
{`${tokenBalance} ${tokenSymbol} (${fiatSymbol}${fiatBalance})`}
</span>

Check warning on line 257 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx#L254-L257

Added lines #L254 - L257 were not covered by tests
);
}

Check warning on line 259 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L259 was not covered by tests

return (
<span {...restProps}>
{displayValue} {balanceQuery.data.symbol}
{tokenBalance} {tokenSymbol}

Check warning on line 263 in packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L263 was not covered by tests
</span>
);
}
Loading