diff --git a/.changeset/tough-dogs-enjoy.md b/.changeset/tough-dogs-enjoy.md
new file mode 100644
index 00000000000..3fd85d6332e
--- /dev/null
+++ b/.changeset/tough-dogs-enjoy.md
@@ -0,0 +1,56 @@
+---
+"thirdweb": minor
+---
+
+Support Account and Wallet headless components in react native
+
+You can now use the Account and Wallet headless components in react native, this lets you build your own UI, styling it however you want, but letting the components handle the logic.
+
+Example Account components usage:
+
+```tsx
+
+ /* avatar */
+
+ }
+ fallbackComponent={
+
+ }
+ style={{
+ width: 92,
+ height: 92,
+ borderRadius: 100,
+ }}
+ />
+ /* address */
+
+ /* balance */
+ }
+ fallbackComponent={
+ Failed to load balance
+ }
+ style={{
+ color: "white",
+ fontSize: 48,
+ fontWeight: "bold",
+ }}
+ />
+
+```
+
+Example Wallet components usage:
+
+```tsx
+
+
+
+
+```
diff --git a/packages/thirdweb/src/exports/react.native.ts b/packages/thirdweb/src/exports/react.native.ts
index 89cd7a3e4da..358f29d4d6a 100644
--- a/packages/thirdweb/src/exports/react.native.ts
+++ b/packages/thirdweb/src/exports/react.native.ts
@@ -114,3 +114,50 @@ export type {
FarcasterProfile,
LensProfile,
} from "../social/types.js";
+
+/**
+ * Account
+ */
+export {
+ Blobbie,
+ AccountBlobbie,
+ type BlobbieProps,
+} from "../react/native/ui/prebuilt/Account/blobbie.js";
+export {
+ AccountProvider,
+ type AccountProviderProps,
+} from "../react/core/account/provider.js";
+export {
+ AccountBalance,
+ type AccountBalanceProps,
+} from "../react/native/ui/prebuilt/Account/balance.js";
+export {
+ AccountAddress,
+ type AccountAddressProps,
+} from "../react/native/ui/prebuilt/Account/address.js";
+export {
+ AccountName,
+ type AccountNameProps,
+} from "../react/native/ui/prebuilt/Account/name.js";
+export {
+ AccountAvatar,
+ type AccountAvatarProps,
+} from "../react/native/ui/prebuilt/Account/avatar.js";
+
+/**
+ * Wallet
+ */
+export {
+ WalletProvider,
+ type WalletProviderProps,
+} from "../react/core/wallet/provider.js";
+export {
+ WalletIcon,
+ SocialIcon,
+ type WalletIconProps,
+ type SocialIconProps,
+} from "../react/native/ui/prebuilt/Wallet/icon.js";
+export {
+ WalletName,
+ type WalletNameProps,
+} from "../react/native/ui/prebuilt/Wallet/name.js";
diff --git a/packages/thirdweb/src/exports/react.ts b/packages/thirdweb/src/exports/react.ts
index 8d41ef0d04a..dd58207579a 100644
--- a/packages/thirdweb/src/exports/react.ts
+++ b/packages/thirdweb/src/exports/react.ts
@@ -223,8 +223,8 @@ export {
export {
AccountBalance,
type AccountBalanceProps,
- type AccountBalanceInfo,
} from "../react/web/ui/prebuilt/Account/balance.js";
+export type { AccountBalanceInfo } from "../react/core/utils/account.js";
export {
AccountName,
type AccountNameProps,
@@ -233,7 +233,7 @@ export { AccountBlobbie } from "../react/web/ui/prebuilt/Account/blobbie.js";
export {
AccountProvider,
type AccountProviderProps,
-} from "../react/web/ui/prebuilt/Account/provider.js";
+} from "../react/core/account/provider.js";
export {
AccountAvatar,
type AccountAvatarProps,
@@ -278,9 +278,11 @@ export { getLastAuthProvider } from "../react/web/utils/storage.js";
export {
WalletProvider,
type WalletProviderProps,
-} from "../react/web/ui/prebuilt/Wallet/provider.js";
+} from "../react/core/wallet/provider.js";
export {
WalletIcon,
+ SocialIcon,
+ type SocialIconProps,
type WalletIconProps,
} from "../react/web/ui/prebuilt/Wallet/icon.js";
export {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.test.tsx b/packages/thirdweb/src/react/core/account/provider.test.tsx
similarity index 95%
rename from packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.test.tsx
rename to packages/thirdweb/src/react/core/account/provider.test.tsx
index 7bb5aebc2cd..f97911cc523 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.test.tsx
+++ b/packages/thirdweb/src/react/core/account/provider.test.tsx
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
-import { AccountAddress } from "./address.js";
+import { AccountAddress } from "../../web/ui/prebuilt/Account/address.js";
import { AccountProvider } from "./provider.js";
describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.tsx b/packages/thirdweb/src/react/core/account/provider.tsx
similarity index 84%
rename from packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.tsx
rename to packages/thirdweb/src/react/core/account/provider.tsx
index 2d2714cd022..5209f829240 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.tsx
+++ b/packages/thirdweb/src/react/core/account/provider.tsx
@@ -1,10 +1,9 @@
"use client";
-import type { Address } from "abitype";
import type React from "react";
import { createContext, useContext } from "react";
-import type { ThirdwebClient } from "../../../../../client/client.js";
-
+import type { ThirdwebClient } from "../../../client/client.js";
+import { isAddress } from "../../../utils/address.js";
/**
* Props for the component
* @component
@@ -14,7 +13,7 @@ export type AccountProviderProps = {
/**
* The user's wallet address
*/
- address: Address;
+ address: string;
/**
* thirdweb Client
*/
@@ -35,7 +34,7 @@ const AccountProviderContext = /* @__PURE__ */ createContext<
* ```tsx
* import { AccountProvider, AccountAvatar, AccountName, AccountAddress } from "thirdweb/react";
*
- *
+ *
*
*
*
@@ -54,6 +53,9 @@ export function AccountProvider(
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
);
}
+ if (!isAddress(props.address)) {
+ throw new Error(`AccountProvider: Invalid address: ${props.address}`);
+ }
return (
{props.children}
diff --git a/packages/thirdweb/src/react/core/utils/account.ts b/packages/thirdweb/src/react/core/utils/account.ts
new file mode 100644
index 00000000000..f0ed135ca9c
--- /dev/null
+++ b/packages/thirdweb/src/react/core/utils/account.ts
@@ -0,0 +1,146 @@
+import type { Chain } from "../../../chains/types.js";
+import type { ThirdwebClient } from "../../../client/client.js";
+import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
+import { convertCryptoToFiat } from "../../../pay/convert/cryptoToFiat.js";
+import type { SupportedFiatCurrency } from "../../../pay/convert/type.js";
+import { type Address, isAddress } from "../../../utils/address.js";
+import { formatNumber } from "../../../utils/formatNumber.js";
+import { shortenLargeNumber } from "../../../utils/shortenLargeNumber.js";
+import { getWalletBalance } from "../../../wallets/utils/getWalletBalance.js";
+
+export const COLOR_OPTIONS = [
+ ["#fca5a5", "#b91c1c"],
+ ["#fdba74", "#c2410c"],
+ ["#fcd34d", "#b45309"],
+ ["#fde047", "#a16207"],
+ ["#a3e635", "#4d7c0f"],
+ ["#86efac", "#15803d"],
+ ["#67e8f9", "#0e7490"],
+ ["#7dd3fc", "#0369a1"],
+ ["#93c5fd", "#1d4ed8"],
+ ["#a5b4fc", "#4338ca"],
+ ["#c4b5fd", "#6d28d9"],
+ ["#d8b4fe", "#7e22ce"],
+ ["#f0abfc", "#a21caf"],
+ ["#f9a8d4", "#be185d"],
+ ["#fda4af", "#be123c"],
+];
+
+/**
+ * @component
+ * @wallet
+ */
+export type AccountBalanceInfo = {
+ /**
+ * Represents either token balance or fiat balance.
+ */
+ balance: number;
+ /**
+ * Represents either token symbol or fiat symbol
+ */
+ symbol: string;
+};
+
+/**
+ * @internal Exported for tests
+ */
+export async function loadAccountBalance(props: {
+ chain?: Chain;
+ client: ThirdwebClient;
+ address: Address;
+ tokenAddress?: Address;
+ showBalanceInFiat?: SupportedFiatCurrency;
+}): Promise {
+ const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
+ if (!chain) {
+ throw new Error("chain is required");
+ }
+
+ if (
+ tokenAddress &&
+ tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
+ ) {
+ throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
+ }
+
+ if (!isAddress(address)) {
+ throw new Error("Invalid wallet address. Expected an EVM address");
+ }
+
+ if (tokenAddress && !isAddress(tokenAddress)) {
+ throw new Error("Invalid tokenAddress. Expected an EVM contract address");
+ }
+
+ const tokenBalanceData = await getWalletBalance({
+ chain,
+ client,
+ address,
+ tokenAddress,
+ }).catch(() => undefined);
+
+ if (!tokenBalanceData) {
+ throw new Error(
+ `Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
+ );
+ }
+
+ if (showBalanceInFiat) {
+ const fiatData = await convertCryptoToFiat({
+ fromAmount: Number(tokenBalanceData.displayValue),
+ fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
+ to: showBalanceInFiat,
+ chain,
+ client,
+ }).catch(() => undefined);
+
+ if (fiatData === undefined) {
+ throw new Error(
+ `Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
+ );
+ }
+ const result = {
+ balance: fiatData?.result,
+ symbol: getFiatSymbol(showBalanceInFiat),
+ };
+
+ return result;
+ }
+
+ return {
+ balance: Number(tokenBalanceData.displayValue),
+ symbol: tokenBalanceData.symbol,
+ };
+}
+
+function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) {
+ switch (showBalanceInFiat) {
+ case "USD":
+ return "$";
+ }
+}
+
+/**
+ * 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
+ * Used internally for the Details button and the Details Modal
+ */
+export function formatAccountTokenBalance(
+ props: AccountBalanceInfo & { decimals: number },
+): string {
+ const formattedTokenBalance = formatNumber(props.balance, props.decimals);
+ return `${formattedTokenBalance} ${props.symbol}`;
+}
+
+/**
+ * Used internally for the Details button and Details Modal
+ * @internal
+ */
+export function formatAccountFiatBalance(
+ props: AccountBalanceInfo & { decimals: number },
+) {
+ const num = formatNumber(props.balance, props.decimals);
+ // Need to keep them short to avoid UI overflow issues
+ const formattedFiatBalance = shortenLargeNumber(num);
+ return `${props.symbol}${formattedFiatBalance}`;
+}
diff --git a/packages/thirdweb/src/react/core/utils/walletIcon.ts b/packages/thirdweb/src/react/core/utils/walletIcon.ts
index 23df9ad847f..aecd4a8c1a9 100644
--- a/packages/thirdweb/src/react/core/utils/walletIcon.ts
+++ b/packages/thirdweb/src/react/core/utils/walletIcon.ts
@@ -1,4 +1,8 @@
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
import type { AuthOption } from "../../../wallets/types.js";
+import type { WalletId } from "../../../wallets/wallet-types.js";
+import { useWalletContext } from "../wallet/provider.js";
// TODO make the social icons usable in RN too
const googleIconUri =
@@ -106,3 +110,27 @@ export function getSocialIcon(provider: AuthOption | ({} & string)): string {
return genericWalletIcon;
}
}
+
+/**
+ * @internal
+ */
+export function useWalletIcon(props: {
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}) {
+ const { id } = useWalletContext();
+ const imageQuery = useQuery({
+ queryKey: ["walletIcon", id],
+ queryFn: async () => fetchWalletImage({ id }),
+ ...props.queryOptions,
+ });
+ return imageQuery;
+}
+
+/**
+ * @internal Exported for tests only
+ */
+export async function fetchWalletImage(props: {
+ id: WalletId;
+}) {
+ return getWalletInfo(props.id, true);
+}
diff --git a/packages/thirdweb/src/react/core/utils/walletname.ts b/packages/thirdweb/src/react/core/utils/walletname.ts
new file mode 100644
index 00000000000..dff60d34898
--- /dev/null
+++ b/packages/thirdweb/src/react/core/utils/walletname.ts
@@ -0,0 +1,53 @@
+import { useQuery } from "@tanstack/react-query";
+import type { UseQueryOptions } from "@tanstack/react-query";
+import { getFunctionId } from "../../../utils/function-id.js";
+import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
+import type { WalletId } from "../../../wallets/wallet-types.js";
+import { useWalletContext } from "../wallet/provider.js";
+
+/**
+ * @internal
+ */
+export function useWalletName(props: {
+ formatFn?: (str: string) => string;
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}) {
+ const { id } = useWalletContext();
+ const nameQuery = useQuery({
+ queryKey: getQueryKeys({ id, formatFn: props.formatFn }),
+ queryFn: async () => fetchWalletName({ id, formatFn: props.formatFn }),
+ ...props.queryOptions,
+ });
+ return nameQuery;
+}
+
+/**
+ * @internal Exported for tests only
+ */
+function getQueryKeys(props: {
+ id: WalletId;
+ formatFn?: (str: string) => string;
+}) {
+ if (typeof props.formatFn === "function") {
+ return [
+ "walletName",
+ props.id,
+ { resolver: getFunctionId(props.formatFn) },
+ ] as const;
+ }
+ return ["walletName", props.id] as const;
+}
+
+/**
+ * @internal Exported for tests only
+ */
+async function fetchWalletName(props: {
+ id: WalletId;
+ formatFn?: (str: string) => string;
+}) {
+ const info = await getWalletInfo(props.id);
+ if (typeof props.formatFn === "function") {
+ return props.formatFn(info.name);
+ }
+ return info.name;
+}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx b/packages/thirdweb/src/react/core/wallet/provider.test.tsx
similarity index 100%
rename from packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.test.tsx
rename to packages/thirdweb/src/react/core/wallet/provider.test.tsx
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx b/packages/thirdweb/src/react/core/wallet/provider.tsx
similarity index 95%
rename from packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx
rename to packages/thirdweb/src/react/core/wallet/provider.tsx
index 6a8f2765833..cc1122f47b1 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx
+++ b/packages/thirdweb/src/react/core/wallet/provider.tsx
@@ -2,7 +2,7 @@
import type React from "react";
import { createContext, useContext } from "react";
-import type { WalletId } from "../../../../../wallets/wallet-types.js";
+import type { WalletId } from "../../../wallets/wallet-types.js";
/**
* Props for the WalletProvider component
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Account/address.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Account/address.tsx
new file mode 100644
index 00000000000..915e3a74d89
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Account/address.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import { Text, type TextProps } from "react-native";
+import { useAccountContext } from "../../../../core/account/provider.js";
+
+/**
+ * @component
+ * @wallet
+ */
+export interface AccountAddressProps extends Omit {
+ /**
+ * The function used to transform (format) the wallet address
+ * Specifically useful for shortening the wallet.
+ *
+ * This function should take in a string and output a string
+ */
+ formatFn?: (str: string) => string;
+ className?: string;
+}
+
+/**
+ *
+ * @returns a containing the full wallet address of the account
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { AccountProvider, AccountAddress } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * 0x12345674b599ce99958242b3D3741e7b01841DF3
+ * ```
+ *
+ *
+ * ### Shorten the address
+ * ```tsx
+ * import { AccountProvider, AccountAddress } from "thirdweb/react";
+ * import { shortenAddress } from "thirdweb/utils";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * 0x1234...1DF3
+ * ```
+ *
+ * @component
+ * @wallet
+ * @beta
+ */
+export function AccountAddress({
+ formatFn,
+ ...restProps
+}: AccountAddressProps) {
+ const { address } = useAccountContext();
+ const value = formatFn ? formatFn(address) : address;
+ return {value};
+}
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Account/avatar.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Account/avatar.tsx
new file mode 100644
index 00000000000..51c2f1308c7
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Account/avatar.tsx
@@ -0,0 +1,225 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import { Image, type ImageProps } from "react-native";
+import { resolveAvatar } from "../../../../../extensions/ens/resolve-avatar.js";
+import {
+ type ResolveNameOptions,
+ resolveName,
+} from "../../../../../extensions/ens/resolve-name.js";
+import { getSocialProfiles } from "../../../../../social/profiles.js";
+import type { SocialProfile } from "../../../../../social/types.js";
+import { parseAvatarRecord } from "../../../../../utils/ens/avatar.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
+/**
+ * Props for the AccountAvatar component
+ * @component
+ * @wallet
+ */
+export interface AccountAvatarProps
+ extends Omit,
+ Omit {
+ /**
+ * Use this prop to prioritize the social profile that you want to display
+ * This is useful for a wallet containing multiple social profiles.
+ * This component inherits all attributes of a HTML's
, so you can interact with it just like a normal
+ *
+ * @example
+ * If you have ENS, Lens and Farcaster profiles linked to your wallet
+ * you can prioritize showing the image for Lens by:
+ * ```tsx
+ *
+ * ```
+ */
+ socialType?: SocialProfile["type"];
+
+ /**
+ * This component will be shown while the avatar of the account is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: React.ComponentType;
+ /**
+ * This component will be shown if the request for fetching the avatar is done
+ * but could not retreive any result.
+ * You can pass a dummy avatar/image to this prop.
+ *
+ * If not passed, the component will return `null`
+ *
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ fallbackComponent?: React.ComponentType;
+
+ /**
+ * Optional query options for `useQuery`
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * The component for showing the avatar of the account.
+ * If fetches all the social profiles linked to your wallet, including: Farcaster, ENS, Lens (more to be added)
+ * You can choose which social profile you want to display. Defaults to the first item in the list.
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { AccountProvider, AccountAvatar } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ * Result: An
component, if the avatar is resolved successfully
+ * ```html
+ *
+ * ```
+ *
+ * ### Show a loading sign when the avatar is being resolved
+ * ```tsx
+ * import { AccountProvider, AccountAvatar } from "thirdweb/react";
+ *
+ *
+ * }
+ * />
+ *
+ * ```
+ *
+ * ### Fallback to something when the avatar fails to resolve
+ * ```tsx
+ * import { AccountProvider, AccountAvatar } from "thirdweb/react";
+ *
+ *
+ * }
+ * />
+ *
+ * ```
+ *
+ * ### Select a social profile to display
+ * If you wallet associates with more than one social profiles (Lens, Farcaster, ENS, etc.)
+ * You can specify which service you want to prioritize using the `socialType` props
+ * ```tsx
+ * import { AccountProvider, AccountAvatar } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom ENS resolver chain
+ * This component shares the same props with the ENS extension `resolveAvatar`
+ * ```tsx
+ * import { AccountProvider, AccountAvatar } from "thirdweb/react";
+ * import { base } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ * @returns An
if the avatar is resolved successfully
+ * @component
+ * @wallet
+ * @beta
+ */
+export function AccountAvatar({
+ socialType,
+ resolverAddress,
+ resolverChain,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: AccountAvatarProps) {
+ const { address, client } = useAccountContext();
+ const avatarQuery = useQuery({
+ queryKey: [
+ "account-avatar",
+ address,
+ { socialType },
+ { resolverAddress, resolverChain },
+ ],
+ queryFn: async (): Promise => {
+ const [socialData, ensName] = await Promise.all([
+ getSocialProfiles({ address, client }),
+ resolveName({
+ client,
+ address: address || "",
+ resolverAddress,
+ resolverChain,
+ }),
+ ]);
+
+ const uri = socialData?.filter(
+ (p) => p.avatar && (socialType ? p.type === socialType : true),
+ )[0]?.avatar;
+
+ const [resolvedSocialAvatar, resolvedENSAvatar] = await Promise.all([
+ uri ? parseAvatarRecord({ client, uri }) : undefined,
+ ensName
+ ? resolveAvatar({
+ client,
+ name: ensName,
+ })
+ : undefined,
+ ]);
+
+ // If no social image + ens name found -> exit and show
+ if (!resolvedSocialAvatar && !resolvedENSAvatar) {
+ throw new Error("Failed to resolve social + ens avatar");
+ }
+
+ // else, prioritize the social image first
+ if (resolvedSocialAvatar) {
+ return resolvedSocialAvatar;
+ }
+
+ if (resolvedENSAvatar) {
+ return resolvedENSAvatar;
+ }
+
+ throw new Error("Failed to resolve social + ens avatar");
+ },
+ retry: false,
+ ...queryOptions,
+ });
+
+ if (avatarQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!avatarQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ return ;
+}
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Account/balance.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Account/balance.tsx
new file mode 100644
index 00000000000..7b83440f2bf
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Account/balance.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import { Text, type TextProps } from "react-native";
+import type { Chain } from "../../../../../chains/types.js";
+import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
+import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
+import { getAddress } from "../../../../../utils/address.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
+import {
+ type AccountBalanceInfo,
+ formatAccountFiatBalance,
+ loadAccountBalance,
+} from "../../../../core/utils/account.js";
+import { formatAccountTokenBalance } from "../../../../core/utils/account.js";
+
+/**
+ * Props for the AccountBalance component
+ * @component
+ * @wallet
+ */
+export interface AccountBalanceProps extends Omit {
+ /**
+ * The network to fetch balance on
+ * If not passed, the component will use the current chain that the wallet is connected to (`useActiveWalletChain()`)
+ */
+ chain?: Chain;
+ /**
+ * By default this component will fetch the balance for the native token on a given chain
+ * If you want to fetch balance for an ERC20 token, use the `tokenAddress` props
+ */
+ tokenAddress?: string;
+ /**
+ * A function to format the balance's display value
+ * use this function to transform the balance display value like round up the number
+ * Particularly useful to avoid overflowing-UI issues
+ */
+ formatFn?: (props: AccountBalanceInfo) => string;
+ /**
+ * This component will be shown while the balance of the account is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * }
+ * />
+ * ```
+ */
+ loadingComponent?: React.ComponentType;
+ /**
+ * This component will be shown if the balance fails to be retreived
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * balance was not fetched succesfully
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+ fallbackComponent?: React.ComponentType;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit<
+ UseQueryOptions,
+ "queryFn" | "queryKey"
+ >;
+
+ /**
+ * Show the token balance in a supported fiat currency (e.g "USD")
+ */
+ showBalanceInFiat?: SupportedFiatCurrency;
+}
+
+/**
+ * This component fetches and shows the balance of the wallet address on a given chain.
+ * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal
+ *
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { AccountProvider, AccountBalance } from "thirdweb/react";
+ * import { ethereum } from "thirdweb/chains";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * 1.091435 ETH
+ * ```
+ *
+ *
+ * ### Format the balance (round up, shorten etc.)
+ * The AccountBalance component accepts a `formatFn` which takes in an object of type `AccountBalanceInfo` and outputs a string
+ * The function is used to modify the display value of the wallet balance (either in crypto or fiat)
+ *
+ * ```tsx
+ * import type { AccountBalanceInfo } from "thirdweb/react";
+ * import { formatNumber } from "thirdweb/utils";
+ *
+ * const format = (props: AccountInfoBalance):string => `${formatNumber(props.balance, 1)} ${props.symbol.toLowerCase()}`
+ *
+ *
+ * ```
+ *
+ * Result:
+ * ```html
+ * 1.1 eth // the balance is rounded up to 1 decimal and the symbol is lowercased
+ * ```
+ *
+ * ### Show a loading sign when the balance is being fetched
+ * ```tsx
+ * import { AccountProvider, AccountBalance } from "thirdweb/react";
+ *
+ *
+ * }
+ * />
+ *
+ * ```
+ *
+ * ### Fallback to something when the balance fails to resolve
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @wallet
+ * @beta
+ */
+export function AccountBalance({
+ chain,
+ tokenAddress,
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ formatFn,
+ showBalanceInFiat,
+ ...restProps
+}: AccountBalanceProps) {
+ const { address, client } = useAccountContext();
+ const walletChain = useActiveWalletChain();
+ const chainToLoad = chain || walletChain;
+ const balanceQuery = useQuery({
+ queryKey: [
+ "internal_account_balance",
+ chainToLoad?.id || -1,
+ address,
+ { tokenAddress },
+ showBalanceInFiat,
+ ] as const,
+ queryFn: async (): Promise =>
+ loadAccountBalance({
+ chain: chainToLoad,
+ client,
+ address: getAddress(address),
+ tokenAddress: tokenAddress ? getAddress(tokenAddress) : undefined,
+ showBalanceInFiat,
+ }),
+ retry: false,
+ ...queryOptions,
+ });
+
+ if (balanceQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (balanceQuery.data === undefined) {
+ return fallbackComponent || null;
+ }
+
+ // Prioritize using the formatFn from users
+ if (formatFn) {
+ return {formatFn(balanceQuery.data)};
+ }
+
+ if (showBalanceInFiat) {
+ return (
+
+ {formatAccountFiatBalance({ ...balanceQuery.data, decimals: 2 })}
+
+ );
+ }
+
+ return (
+
+ {formatAccountTokenBalance({
+ ...balanceQuery.data,
+ decimals: balanceQuery.data.balance < 1 ? 3 : 2,
+ })}
+
+ );
+}
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Account/blobbie.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Account/blobbie.tsx
new file mode 100644
index 00000000000..badf07bd951
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Account/blobbie.tsx
@@ -0,0 +1,94 @@
+import { hexToNumber } from "@noble/curves/abstract/utils";
+import { useMemo } from "react";
+import { StyleSheet, View, type ViewStyle } from "react-native";
+import { Defs, LinearGradient, Rect, Stop, Svg } from "react-native-svg";
+import { useAccountContext } from "../../../../core/account/provider.js";
+import { COLOR_OPTIONS } from "../../../../core/utils/account.js";
+/**
+ * Props for the Blobbie component
+ * @component
+ */
+export type BlobbieProps = {
+ address: string;
+ size: number;
+ style?: ViewStyle;
+};
+
+/**
+ * A wrapper for the Blobbie component
+ * @param props BlobbieProps
+ * @beta
+ * @wallet
+ */
+export function AccountBlobbie(props: Omit) {
+ const { address } = useAccountContext();
+ return ;
+}
+
+/**
+ * A unique gradient avatar based on the provided address.
+ * @param props The component props.
+ * @param props.address The address to generate the gradient with.
+ * @param props.style The style for the component
+ * @param props.size The size of each side of the square avatar (in pixels)
+ * @component
+ * @wallet
+ * @example
+ * ```tsx
+ * import { Blobbie } from "thirdweb/react";
+ *
+ *
+ * ```
+ */
+export function Blobbie(props: BlobbieProps) {
+ const colors = useMemo(
+ () =>
+ COLOR_OPTIONS[
+ Number(hexToNumber(props.address.slice(2, 4))) % COLOR_OPTIONS.length
+ ] as [string, string],
+ [props.address],
+ );
+
+ const containerStyle = useMemo(() => {
+ const baseStyle = props.style || {};
+ if (props.size) {
+ return {
+ ...baseStyle,
+ width: props.size,
+ height: props.size,
+ };
+ }
+ return baseStyle;
+ }, [props.style, props.size]);
+
+ const gradientUniqueId = `grad${colors[0]}+${colors[1]}`.replace(
+ /[^a-zA-Z0-9 ]/g,
+ "",
+ );
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ overflow: "hidden",
+ },
+});
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Account/name.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Account/name.tsx
new file mode 100644
index 00000000000..12801369597
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Account/name.tsx
@@ -0,0 +1,180 @@
+"use client";
+
+import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import { Text, type TextProps } from "react-native";
+import {
+ type ResolveNameOptions,
+ resolveName,
+} from "../../../../../extensions/ens/resolve-name.js";
+import { getSocialProfiles } from "../../../../../social/profiles.js";
+import type { SocialProfile } from "../../../../../social/types.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
+
+/**
+ * Props for the AccountName component
+ * @component
+ * @wallet
+ */
+export interface AccountNameProps
+ extends Omit,
+ Omit {
+ /**
+ * A function used to transform (format) the name of the account.
+ * it should take in a string and output a string.
+ *
+ * This function is particularly useful
+ */
+ formatFn?: (str: string) => string;
+ /**
+ * Use this prop to prioritize the social profile that you want to display
+ * This is useful for a wallet containing multiple social profiles
+ */
+ socialType?: SocialProfile["type"];
+ /**
+ * This component will be shown while the name of the account is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: React.ComponentType;
+ /**
+ * This component will be shown if the request for fetching the name is done but could not retreive any result.
+ * You can pass the wallet address as the fallback option if that's the case.
+ *
+ * If not passed, the component will return `null`
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+ fallbackComponent?: React.ComponentType;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component is used to display the name of the account.
+ * A "name" in this context is the username, or account of the social profiles that the wallet may have.
+ * In case a name is not found or failed to resolve, you can always fallback to displaying the wallet address instead by using the `fallbackComponent` prop.
+ *
+ * This component inherits all attribute of a native HTML element, so you can style it just like how you would style a .
+ *
+ * @param props
+ * @returns A `` containing the name of the account
+ * ```html
+ * {name}
+ * ```
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { AccountProvider, AccountName } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ### Show wallet address while social name is being loaded
+ * ```tsx
+ * }
+ * />
+ * ```
+ *
+ *
+ * ### Fallback to showing wallet address if fail to resolve social name
+ * ```tsx
+ * }
+ * />
+ * ```
+ *
+ * ### Transform the account name using `formatFn` prop
+ * ```tsx
+ * import { isAddress, shortenAddress } from "thirdweb/utils";
+ * import { AccountProvider, AccountName } from "thirdweb/react";
+ *
+ * // Let's say we want the name to be capitalized without using CSS
+ * const formatName = (name: string) => name.toUpperCase();
+ *
+ * return
+ * ```
+ *
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @wallet
+ * @beta
+ */
+export function AccountName({
+ resolverAddress,
+ resolverChain,
+ socialType,
+ formatFn,
+ queryOptions,
+ loadingComponent,
+ fallbackComponent,
+ ...restProps
+}: AccountNameProps) {
+ const { address, client } = useAccountContext();
+ const nameQuery = useQuery({
+ queryKey: [
+ "account-name",
+ address,
+ { socialType },
+ { resolverAddress, resolverChain },
+ ],
+ queryFn: async () => {
+ const [socialData, ensName] = await Promise.all([
+ getSocialProfiles({ address, client }),
+ resolveName({
+ client,
+ address,
+ resolverAddress,
+ resolverChain,
+ }),
+ ]);
+
+ const name =
+ socialData?.filter(
+ (p) => p.name && (socialType ? p.type === socialType : true),
+ )[0]?.name || ensName;
+
+ if (!name) {
+ throw new Error("Failed to resolve account name");
+ }
+ return formatFn ? formatFn(name) : name;
+ },
+ retry: false,
+ ...queryOptions,
+ });
+
+ if (nameQuery.isLoading) {
+ return loadingComponent || null;
+ }
+
+ if (!nameQuery.data) {
+ return fallbackComponent || null;
+ }
+
+ return {nameQuery.data};
+}
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/icon.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/icon.tsx
new file mode 100644
index 00000000000..31622c0a731
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/icon.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import type { UseQueryOptions } from "@tanstack/react-query";
+import { Image, type ImageProps } from "react-native";
+import { SvgXml, type XmlProps } from "react-native-svg";
+import type { AuthOption } from "../../../../../wallets/types.js";
+import { useWalletIcon } from "../../../../core/utils/walletIcon.js";
+import { getAuthProviderImage } from "../../components/WalletImage.js";
+
+export interface WalletIconProps extends Omit {
+ /**
+ * This component will be shown while the icon of the wallet is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: React.ComponentType;
+ /**
+ * This component will be shown if the icon fails to be retrieved
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * icon was not fetched successfully
+ * @example
+ * ```tsx
+ * Failed to load}
+ * />
+ * ```
+ */
+ fallbackComponent?: React.ComponentType;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+}
+
+/**
+ * This component tries to resolve the icon of a given wallet, then return an image.
+ * @returns an
with the src of the wallet icon
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { WalletProvider, WalletIcon } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * Result: An
component with the src of the icon
+ * ```html
+ *
+ * ```
+ *
+ * ### Show a loading sign while the icon is being loaded
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Fallback to a dummy image if the wallet icon fails to resolve
+ * ```tsx
+ * } />
+ * ```
+ *
+ * ### Usage with queryOptions
+ * WalletIcon uses useQuery() from tanstack query internally.
+ * It allows you to pass a custom queryOptions of your choice for more control of the internal fetching logic
+ * ```tsx
+ *
+ * ```
+ *
+ * @component
+ * @wallet
+ * @beta
+ */
+export function WalletIcon({
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ ...restProps
+}: WalletIconProps) {
+ const imageQuery = useWalletIcon({ queryOptions });
+ if (imageQuery.isLoading) {
+ return loadingComponent || null;
+ }
+ if (!imageQuery.data) {
+ return fallbackComponent || null;
+ }
+ return ;
+}
+
+export interface SocialIconProps extends Omit {
+ provider: AuthOption | (string & {});
+}
+
+/**
+ * Social auth provider icon
+ * @returns an
component with the src set to the svg
+ *
+ * @example
+ * ```tsx
+ * import { SocialIcon } from "thirdweb/react";
+ *
+ *
+ * ```
+ *
+ * Result: An
component with the src of the icon
+ * ```html
+ *
+ * ```
+ *
+ * @component
+ * @wallet
+ * @beta
+ */
+export function SocialIcon({ provider, ...restProps }: SocialIconProps) {
+ const src = getAuthProviderImage(provider);
+ return ;
+}
diff --git a/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/name.tsx b/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/name.tsx
new file mode 100644
index 00000000000..3c80235175e
--- /dev/null
+++ b/packages/thirdweb/src/react/native/ui/prebuilt/Wallet/name.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import type { UseQueryOptions } from "@tanstack/react-query";
+import { Text, type TextProps } from "react-native";
+import { useWalletName } from "../../../../core/utils/walletname.js";
+/**
+ * Props for the WalletName component
+ * @component
+ * @wallet
+ */
+export interface WalletNameProps extends Omit {
+ /**
+ * This component will be shown while the name of the wallet name is being fetched
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a loading sign or spinner to this prop.
+ * @example
+ * ```tsx
+ * } />
+ * ```
+ */
+ loadingComponent?: React.ComponentType;
+ /**
+ * This component will be shown if the name fails to be retreived
+ * If not passed, the component will return `null`.
+ *
+ * You can/should pass a descriptive text/component to this prop, indicating that the
+ * name was not fetched succesfully
+ * @example
+ * ```tsx
+ * Failed to load}
+ * />
+ * ```
+ */
+ fallbackComponent?: React.ComponentType;
+ /**
+ * Optional `useQuery` params
+ */
+ queryOptions?: Omit, "queryFn" | "queryKey">;
+ /**
+ * A function to format the name's display value
+ * ```tsx
+ * doSomething()} />
+ * ```
+ */
+ formatFn?: (str: string) => string;
+}
+
+/**
+ * This component fetches then shows the name of a wallet.
+ * It inherits all the attributes of a HTML component, hence you can style it just like how you would style a normal
+ *
+ * @example
+ * ### Basic usage
+ * ```tsx
+ * import { WalletProvider, WalletName } from "thirdweb/react";
+ *
+ *
+ *
+ *
+ * ```
+ * Result:
+ * ```html
+ * MetaMask
+ * ```
+ *
+ * ### Show a loading sign when the name is being fetched
+ * ```tsx
+ * import { WalletProvider, WalletName } from "thirdweb/react";
+ *
+ *
+ * } />
+ *
+ * ```
+ *
+ * ### Fallback to something when the name fails to resolve
+ * ```tsx
+ *
+ * Failed to load} />
+ *
+ * ```
+ *
+ * ### Custom query options for useQuery
+ * This component uses `@tanstack-query`'s useQuery internally.
+ * You can use the `queryOptions` prop for more fine-grained control
+ * ```tsx
+ *
+ * @component
+ * @beta
+ * @wallet
+ */
+export function WalletName({
+ loadingComponent,
+ fallbackComponent,
+ queryOptions,
+ formatFn,
+ ...restProps
+}: WalletNameProps) {
+ const nameQuery = useWalletName({ queryOptions, formatFn });
+ if (nameQuery.isLoading) {
+ return loadingComponent || null;
+ }
+ if (!nameQuery.data) {
+ return fallbackComponent || null;
+ }
+ return {nameQuery.data};
+}
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Blobbie.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Blobbie.tsx
index d47a2f1b1b3..d8a3e9d6d99 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Blobbie.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Blobbie.tsx
@@ -1,24 +1,7 @@
"use client";
import { hexToNumber } from "@noble/curves/abstract/utils";
import { useId, useMemo } from "react";
-
-const COLOR_OPTIONS = [
- ["#fca5a5", "#b91c1c"],
- ["#fdba74", "#c2410c"],
- ["#fcd34d", "#b45309"],
- ["#fde047", "#a16207"],
- ["#a3e635", "#4d7c0f"],
- ["#86efac", "#15803d"],
- ["#67e8f9", "#0e7490"],
- ["#7dd3fc", "#0369a1"],
- ["#93c5fd", "#1d4ed8"],
- ["#a5b4fc", "#4338ca"],
- ["#c4b5fd", "#6d28d9"],
- ["#d8b4fe", "#7e22ce"],
- ["#f0abfc", "#a21caf"],
- ["#f9a8d4", "#be185d"],
- ["#fda4af", "#be123c"],
-];
+import { COLOR_OPTIONS } from "../../../core/utils/account.js";
/**
* Props for the Blobbie component
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx
index 881b8dac2e5..23b787bc8e8 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectButton.tsx
@@ -3,6 +3,7 @@
import styled from "@emotion/styled";
import { useEffect, useMemo, useState } from "react";
import { getDefaultWallets } from "../../../../wallets/defaultWallets.js";
+import { AccountProvider } from "../../../core/account/provider.js";
import { iconSize } from "../../../core/design-system/index.js";
import { useSiweAuth } from "../../../core/hooks/auth/useSiweAuth.js";
import type { ConnectButtonProps } from "../../../core/hooks/connection/ConnectButtonProps.js";
@@ -23,7 +24,6 @@ import { Spinner } from "../components/Spinner.js";
import { Container } from "../components/basic.js";
import { Button } from "../components/buttons.js";
import { fadeInAnimation } from "../design-system/animations.js";
-import { AccountProvider } from "../prebuilt/Account/provider.js";
import { ConnectedWalletDetails } from "./Details.js";
import ConnectModal from "./Modal/ConnectModal.js";
import { LockIcon } from "./icons/LockIcon.js";
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.test.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.test.tsx
index 7b4f8c616cb..84fced21fd7 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.test.tsx
@@ -14,8 +14,8 @@ import { base } from "../../../../chains/chain-definitions/base.js";
import { ethereum } from "../../../../chains/chain-definitions/ethereum.js";
import { useActiveAccount } from "../../../../react/core/hooks/wallets/useActiveAccount.js";
import { useActiveWalletChain } from "../../../../react/core/hooks/wallets/useActiveWalletChain.js";
+import { AccountProvider } from "../../../core/account/provider.js";
import { ThirdwebProvider } from "../../providers/thirdweb-provider.js";
-import { AccountProvider } from "../prebuilt/Account/provider.js";
import {
ConnectedToSmartWallet,
ConnectedWalletDetails,
diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
index 4da5b86f345..e55d8309339 100644
--- a/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
+++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/Details.tsx
@@ -40,6 +40,7 @@ import type {
EcosystemWalletId,
WalletId,
} from "../../../../wallets/wallet-types.js";
+import { AccountProvider } from "../../../core/account/provider.js";
import {
CustomThemeProvider,
parseTheme,
@@ -67,6 +68,11 @@ import { useAdminWallet } from "../../../core/hooks/wallets/useAdminWallet.js";
import { useDisconnect } from "../../../core/hooks/wallets/useDisconnect.js";
import { useSwitchActiveWalletChain } from "../../../core/hooks/wallets/useSwitchActiveWalletChain.js";
import { SetRootElementContext } from "../../../core/providers/RootElementContext.js";
+import {
+ type AccountBalanceInfo,
+ formatAccountFiatBalance,
+ formatAccountTokenBalance,
+} from "../../../core/utils/account.js";
import type {
SupportedNFTs,
SupportedTokens,
@@ -91,15 +97,9 @@ import { fadeInAnimation } from "../design-system/animations.js";
import { StyledButton } from "../design-system/elements.js";
import { AccountAddress } from "../prebuilt/Account/address.js";
import { AccountAvatar } from "../prebuilt/Account/avatar.js";
-import {
- AccountBalance,
- type AccountBalanceInfo,
- formatAccountFiatBalance,
- formatAccountTokenBalance,
-} from "../prebuilt/Account/balance.js";
+import { AccountBalance } 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";
import { ChainIcon } from "../prebuilt/Chain/icon.js";
import { ChainName } from "../prebuilt/Chain/name.js";
import { ChainProvider } from "../prebuilt/Chain/provider.js";
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.test.tsx
index da588cb8c09..9f6fb23820d 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.test.tsx
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
import { render, screen, waitFor } from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
import { shortenAddress } from "../../../../../utils/address.js";
+import { AccountProvider } from "../../../../core/account/provider.js";
import { AccountAddress } from "./address.js";
-import { AccountProvider } from "./provider.js";
describe.runIf(process.env.TW_SECRET_KEY)("AccountAddress component", () => {
it("should format the address properly", () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.tsx
index 85b994aa61c..52caee3e260 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/address.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useAccountContext } from "./provider.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
/**
* @component
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.test.tsx
index dc355aeec92..b0acf1c9ccd 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.test.tsx
@@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest";
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 { AccountProvider } from "../../../../core/account/provider.js";
import { AccountAvatar } from "./avatar.js";
-import { AccountProvider } from "./provider.js";
describe.runIf(process.env.TW_SECRET_KEY)("AccountAvatar component", () => {
it("should render an image", () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.tsx
index 7aa319cfb12..74d7283662c 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/avatar.tsx
@@ -11,8 +11,7 @@ import {
import { getSocialProfiles } from "../../../../../social/profiles.js";
import type { SocialProfile } from "../../../../../social/types.js";
import { parseAvatarRecord } from "../../../../../utils/ens/avatar.js";
-import { useAccountContext } from "./provider.js";
-
+import { useAccountContext } from "../../../../core/account/provider.js";
/**
* Props for the AccountAvatar component
* @component
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.test.tsx
index c577c982c2c..7db5228bf28 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.test.tsx
@@ -7,13 +7,13 @@ import { ethereum } from "../../../../../chains/chain-definitions/ethereum.js";
import { sepolia } from "../../../../../chains/chain-definitions/sepolia.js";
import { defineChain } from "../../../../../chains/utils.js";
import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
+import { AccountProvider } from "../../../../core/account/provider.js";
import {
- AccountBalance,
formatAccountFiatBalance,
formatAccountTokenBalance,
loadAccountBalance,
-} from "./balance.js";
-import { AccountProvider } from "./provider.js";
+} from "../../../../core/utils/account.js";
+import { AccountBalance } from "./balance.js";
const queryClient = new QueryClient();
@@ -95,7 +95,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
await expect(() =>
loadAccountBalance({
client: TEST_CLIENT,
- address: "haha",
+ // biome-ignore lint/suspicious/noExplicitAny: for the test
+ address: "haha" as any,
chain: ethereum,
}),
).rejects.toThrowError("Invalid wallet address. Expected an EVM address");
@@ -106,7 +107,8 @@ describe.runIf(process.env.TW_SECRET_KEY)("AccountBalance component", () => {
loadAccountBalance({
client: TEST_CLIENT,
address: VITALIK_WALLET,
- tokenAddress: "haha",
+ // biome-ignore lint/suspicious/noExplicitAny: for the test
+ tokenAddress: "haha" as any,
chain: ethereum,
}),
).rejects.toThrowError(
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx
index 9a7ef10c2ef..608e87f880d 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/balance.tsx
@@ -1,35 +1,19 @@
"use client";
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
-import type { Address } from "abitype";
import type React from "react";
import type { JSX } from "react";
import type { Chain } from "../../../../../chains/types.js";
-import type { ThirdwebClient } from "../../../../../client/client.js";
-import { NATIVE_TOKEN_ADDRESS } from "../../../../../constants/addresses.js";
-import { convertCryptoToFiat } from "../../../../../exports/pay.js";
import type { SupportedFiatCurrency } from "../../../../../pay/convert/type.js";
import { useActiveWalletChain } from "../../../../../react/core/hooks/wallets/useActiveWalletChain.js";
-import { isAddress } from "../../../../../utils/address.js";
-import { formatNumber } from "../../../../../utils/formatNumber.js";
-import { shortenLargeNumber } from "../../../../../utils/shortenLargeNumber.js";
-import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
-import { useAccountContext } from "./provider.js";
-
-/**
- * @component
- * @wallet
- */
-export type AccountBalanceInfo = {
- /**
- * Represents either token balance or fiat balance.
- */
- balance: number;
- /**
- * Represents either token symbol or fiat symbol
- */
- symbol: string;
-};
+import { getAddress } from "../../../../../utils/address.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
+import {
+ type AccountBalanceInfo,
+ formatAccountFiatBalance,
+ loadAccountBalance,
+} from "../../../../core/utils/account.js";
+import { formatAccountTokenBalance } from "../../../../core/utils/account.js";
/**
* Props for the AccountBalance component
@@ -199,8 +183,8 @@ export function AccountBalance({
loadAccountBalance({
chain: chainToLoad,
client,
- address,
- tokenAddress,
+ address: getAddress(address),
+ tokenAddress: tokenAddress ? getAddress(tokenAddress) : undefined,
showBalanceInFiat,
}),
retry: false,
@@ -237,107 +221,3 @@ export function AccountBalance({
);
}
-
-/**
- * @internal Exported for tests
- */
-export async function loadAccountBalance(props: {
- chain?: Chain;
- client: ThirdwebClient;
- address: Address;
- tokenAddress?: Address;
- showBalanceInFiat?: SupportedFiatCurrency;
-}): Promise {
- const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
- if (!chain) {
- throw new Error("chain is required");
- }
-
- if (
- tokenAddress &&
- tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
- ) {
- throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
- }
-
- if (!isAddress(address)) {
- throw new Error("Invalid wallet address. Expected an EVM address");
- }
-
- if (tokenAddress && !isAddress(tokenAddress)) {
- throw new Error("Invalid tokenAddress. Expected an EVM contract address");
- }
-
- const tokenBalanceData = await getWalletBalance({
- chain,
- client,
- address,
- tokenAddress,
- }).catch(() => undefined);
-
- if (!tokenBalanceData) {
- throw new Error(
- `Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
- );
- }
-
- if (showBalanceInFiat) {
- const fiatData = await convertCryptoToFiat({
- fromAmount: Number(tokenBalanceData.displayValue),
- fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
- to: showBalanceInFiat,
- chain,
- client,
- }).catch(() => undefined);
-
- if (fiatData === undefined) {
- throw new Error(
- `Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
- );
- }
- return {
- balance: fiatData?.result,
- symbol:
- new Intl.NumberFormat("en", {
- style: "currency",
- currency: showBalanceInFiat,
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- })
- .formatToParts(0)
- .find((p) => p.type === "currency")?.value ||
- showBalanceInFiat.toUpperCase(),
- };
- }
-
- return {
- balance: Number(tokenBalanceData.displayValue),
- symbol: tokenBalanceData.symbol,
- };
-}
-
-/**
- * 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
- * Used internally for the Details button and the Details Modal
- */
-export function formatAccountTokenBalance(
- props: AccountBalanceInfo & { decimals: number },
-): string {
- const formattedTokenBalance = formatNumber(props.balance, props.decimals);
- return `${formattedTokenBalance} ${props.symbol}`;
-}
-
-/**
- * Used internally for the Details button and Details Modal
- * @internal
- */
-export function formatAccountFiatBalance(
- props: AccountBalanceInfo & { decimals: number },
-) {
- const num = formatNumber(props.balance, props.decimals);
- // Need to keep them short to avoid UI overflow issues
- const formattedFiatBalance = shortenLargeNumber(num);
- return `${props.symbol}${formattedFiatBalance}`;
-}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/blobbie.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/blobbie.tsx
index f36e257b4d8..a6952d7ea62 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/blobbie.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/blobbie.tsx
@@ -1,7 +1,7 @@
"use client";
+import { useAccountContext } from "../../../../core/account/provider.js";
import { Blobbie, type BlobbieProps } from "../../ConnectWallet/Blobbie.js";
-import { useAccountContext } from "./provider.js";
/**
* A wrapper for the Blobbie component
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.test.tsx
index afaf32e1179..88d1f8051cf 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.test.tsx
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import { render, screen, waitFor } from "~test/react-render.js";
import { TEST_CLIENT } from "~test/test-clients.js";
+import { AccountProvider } from "../../../../core/account/provider.js";
import { AccountName } from "./name.js";
-import { AccountProvider } from "./provider.js";
describe.runIf(process.env.TW_SECRET_KEY)("AccountName component", () => {
it("should return the correct social name", () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.tsx
index ff7abd1217c..2156ce31473 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Account/name.tsx
@@ -9,7 +9,7 @@ import {
} from "../../../../../extensions/ens/resolve-name.js";
import { getSocialProfiles } from "../../../../../social/profiles.js";
import type { SocialProfile } from "../../../../../social/types.js";
-import { useAccountContext } from "./provider.js";
+import { useAccountContext } from "../../../../core/account/provider.js";
/**
* Props for the AccountName component
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx
index 5c1e139c533..e219fb927ed 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.test.tsx
@@ -1,7 +1,8 @@
import { describe, expect, it } from "vitest";
import { render, waitFor } from "~test/react-render.js";
-import { SocialIcon, WalletIcon, fetchWalletImage } from "./icon.js";
-import { WalletProvider } from "./provider.js";
+import { fetchWalletImage } from "../../../../core/utils/walletIcon.js";
+import { WalletProvider } from "../../../../core/wallet/provider.js";
+import { SocialIcon, WalletIcon } from "./icon.js";
describe("WalletIcon", () => {
it("should fetch wallet image", async () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx
index 15ed8f5c776..4cdb6256478 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/icon.tsx
@@ -1,12 +1,12 @@
"use client";
-import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type { UseQueryOptions } from "@tanstack/react-query";
import type { JSX } from "react";
-import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js";
import type { AuthOption } from "../../../../../wallets/types.js";
-import type { WalletId } from "../../../../../wallets/wallet-types.js";
-import { getSocialIcon } from "../../../../core/utils/walletIcon.js";
-import { useWalletContext } from "./provider.js";
+import {
+ getSocialIcon,
+ useWalletIcon,
+} from "../../../../core/utils/walletIcon.js";
export interface WalletIconProps
extends Omit, "src"> {
@@ -96,31 +96,7 @@ export function WalletIcon({
return
;
}
-/**
- * @internal
- */
-function useWalletIcon(props: {
- queryOptions?: Omit, "queryFn" | "queryKey">;
-}) {
- const { id } = useWalletContext();
- const imageQuery = useQuery({
- queryKey: ["walletIcon", id],
- queryFn: async () => fetchWalletImage({ id }),
- ...props.queryOptions,
- });
- return imageQuery;
-}
-
-/**
- * @internal Exported for tests only
- */
-export async function fetchWalletImage(props: {
- id: WalletId;
-}) {
- return getWalletInfo(props.id, true);
-}
-
-interface SocialIconProps
+export interface SocialIconProps
extends Omit, "src"> {
provider: AuthOption | string;
}
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx
index c8d50206015..139f23cd5c6 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.test.tsx
@@ -1,8 +1,12 @@
import { describe, expect, it } from "vitest";
import { render, waitFor } from "~test/react-render.js";
import { getFunctionId } from "../../../../../utils/function-id.js";
-import { WalletName, fetchWalletName, getQueryKeys } from "./name.js";
-import { WalletProvider } from "./provider.js";
+import {
+ fetchWalletName,
+ getQueryKeys,
+} from "../../../../core/utils/walletname.js";
+import { WalletProvider } from "../../../../core/wallet/provider.js";
+import { WalletName } from "./name.js";
describe.runIf(process.env.TW_SECRET_KEY)("WalletName", () => {
it("fetchWalletName: should fetch wallet name from id", async () => {
diff --git a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx
index 92a8fac34d6..98ed7d59f64 100644
--- a/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx
+++ b/packages/thirdweb/src/react/web/ui/prebuilt/Wallet/name.tsx
@@ -1,11 +1,8 @@
"use client";
-import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
+import type { UseQueryOptions } from "@tanstack/react-query";
import type { JSX } from "react";
-import { getFunctionId } from "../../../../../utils/function-id.js";
-import { getWalletInfo } from "../../../../../wallets/__generated__/getWalletInfo.js";
-import type { WalletId } from "../../../../../wallets/wallet-types.js";
-import { useWalletContext } from "./provider.js";
+import { useWalletName } from "../../../../core/utils/walletname.js";
/**
* Props for the WalletName component
@@ -115,50 +112,3 @@ export function WalletName({
}
return {nameQuery.data};
}
-
-/**
- * @internal
- */
-function useWalletName(props: {
- formatFn?: (str: string) => string;
- queryOptions?: Omit, "queryFn" | "queryKey">;
-}) {
- const { id } = useWalletContext();
- const nameQuery = useQuery({
- queryKey: getQueryKeys({ id, formatFn: props.formatFn }),
- queryFn: async () => fetchWalletName({ id, formatFn: props.formatFn }),
- ...props.queryOptions,
- });
- return nameQuery;
-}
-
-/**
- * @internal Exported for tests only
- */
-export function getQueryKeys(props: {
- id: WalletId;
- formatFn?: (str: string) => string;
-}) {
- if (typeof props.formatFn === "function") {
- return [
- "walletName",
- props.id,
- { resolver: getFunctionId(props.formatFn) },
- ] as const;
- }
- return ["walletName", props.id] as const;
-}
-
-/**
- * @internal Exported for tests only
- */
-export async function fetchWalletName(props: {
- id: WalletId;
- formatFn?: (str: string) => string;
-}) {
- const info = await getWalletInfo(props.id);
- if (typeof props.formatFn === "function") {
- return props.formatFn(info.name);
- }
- return info.name;
-}
diff --git a/packages/thirdweb/src/utils/shortenLargeNumber.ts b/packages/thirdweb/src/utils/shortenLargeNumber.ts
index 82710a25092..4ae02882e04 100644
--- a/packages/thirdweb/src/utils/shortenLargeNumber.ts
+++ b/packages/thirdweb/src/utils/shortenLargeNumber.ts
@@ -13,6 +13,9 @@
* @utils
*/
export function shortenLargeNumber(value: number) {
+ if (value === 0) {
+ return "0.00";
+ }
if (value < 1000) {
return value.toString();
}