Skip to content

Commit f3e6e4f

Browse files
Expose Account and Wallet components in React Native
1 parent e2a232e commit f3e6e4f

File tree

30 files changed

+1350
-262
lines changed

30 files changed

+1350
-262
lines changed

packages/thirdweb/src/exports/react.native.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,50 @@ export type {
114114
FarcasterProfile,
115115
LensProfile,
116116
} from "../social/types.js";
117+
118+
/**
119+
* Account
120+
*/
121+
export {
122+
Blobbie,
123+
AccountBlobbie,
124+
type BlobbieProps,
125+
} from "../react/native/ui/prebuilt/Account/blobbie.js";
126+
export {
127+
AccountProvider,
128+
type AccountProviderProps,
129+
} from "../react/core/account/provider.js";
130+
export {
131+
AccountBalance,
132+
type AccountBalanceProps,
133+
} from "../react/native/ui/prebuilt/Account/balance.js";
134+
export {
135+
AccountAddress,
136+
type AccountAddressProps,
137+
} from "../react/native/ui/prebuilt/Account/address.js";
138+
export {
139+
AccountName,
140+
type AccountNameProps,
141+
} from "../react/native/ui/prebuilt/Account/name.js";
142+
export {
143+
AccountAvatar,
144+
type AccountAvatarProps,
145+
} from "../react/native/ui/prebuilt/Account/avatar.js";
146+
147+
/**
148+
* Wallet
149+
*/
150+
export {
151+
WalletProvider,
152+
type WalletProviderProps,
153+
} from "../react/core/wallet/provider.js";
154+
export {
155+
WalletIcon,
156+
SocialIcon,
157+
type WalletIconProps,
158+
type SocialIconProps,
159+
} from "../react/native/ui/prebuilt/Wallet/icon.js";
160+
export {
161+
WalletName,
162+
type WalletNameProps,
163+
} from "../react/native/ui/prebuilt/Wallet/name.js";

packages/thirdweb/src/exports/react.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ export {
223223
export {
224224
AccountBalance,
225225
type AccountBalanceProps,
226-
type AccountBalanceInfo,
227226
} from "../react/web/ui/prebuilt/Account/balance.js";
227+
export type { AccountBalanceInfo } from "../react/core/utils/account.js";
228228
export {
229229
AccountName,
230230
type AccountNameProps,
@@ -233,7 +233,7 @@ export { AccountBlobbie } from "../react/web/ui/prebuilt/Account/blobbie.js";
233233
export {
234234
AccountProvider,
235235
type AccountProviderProps,
236-
} from "../react/web/ui/prebuilt/Account/provider.js";
236+
} from "../react/core/account/provider.js";
237237
export {
238238
AccountAvatar,
239239
type AccountAvatarProps,
@@ -278,9 +278,11 @@ export { getLastAuthProvider } from "../react/web/utils/storage.js";
278278
export {
279279
WalletProvider,
280280
type WalletProviderProps,
281-
} from "../react/web/ui/prebuilt/Wallet/provider.js";
281+
} from "../react/core/wallet/provider.js";
282282
export {
283283
WalletIcon,
284+
SocialIcon,
285+
type SocialIconProps,
284286
type WalletIconProps,
285287
} from "../react/web/ui/prebuilt/Wallet/icon.js";
286288
export {

packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.test.tsx renamed to packages/thirdweb/src/react/core/account/provider.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, expect, it } from "vitest";
22
import { render, screen } from "~test/react-render.js";
33
import { TEST_CLIENT } from "~test/test-clients.js";
4-
import { AccountAddress } from "./address.js";
54
import { AccountProvider } from "./provider.js";
5+
import { AccountAddress } from "../../web/ui/prebuilt/Account/address.js";
66

77
describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {
88
it("should render children correctly", () => {

packages/thirdweb/src/react/web/ui/prebuilt/Account/provider.tsx renamed to packages/thirdweb/src/react/core/account/provider.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
"use client";
22

3-
import type { Address } from "abitype";
43
import type React from "react";
54
import { createContext, useContext } from "react";
6-
import type { ThirdwebClient } from "../../../../../client/client.js";
7-
5+
import type { ThirdwebClient } from "../../../client/client.js";
6+
import { isAddress } from "../../../utils/address.js";
87
/**
98
* Props for the <AccountProvider /> component
109
* @component
@@ -14,7 +13,7 @@ export type AccountProviderProps = {
1413
/**
1514
* The user's wallet address
1615
*/
17-
address: Address;
16+
address: string;
1817
/**
1918
* thirdweb Client
2019
*/
@@ -35,7 +34,7 @@ const AccountProviderContext = /* @__PURE__ */ createContext<
3534
* ```tsx
3635
* import { AccountProvider, AccountAvatar, AccountName, AccountAddress } from "thirdweb/react";
3736
*
38-
* <AccountProvider>
37+
* <AccountProvider address="0x..." client={client}>
3938
* <AccountAvatar />
4039
* <AccountName />
4140
* <AccountAddress />
@@ -54,6 +53,9 @@ export function AccountProvider(
5453
"AccountProvider: No address passed. Ensure an address is always provided to the AccountProvider",
5554
);
5655
}
56+
if (!isAddress(props.address)) {
57+
throw new Error(`AccountProvider: Invalid address: ${props.address}`);
58+
}
5759
return (
5860
<AccountProviderContext.Provider value={props}>
5961
{props.children}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { Chain } from "../../../chains/types.js";
2+
import type { ThirdwebClient } from "../../../client/client.js";
3+
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
4+
import { convertCryptoToFiat } from "../../../pay/convert/cryptoToFiat.js";
5+
import type { SupportedFiatCurrency } from "../../../pay/convert/type.js";
6+
import { type Address, isAddress } from "../../../utils/address.js";
7+
import { formatNumber } from "../../../utils/formatNumber.js";
8+
import { shortenLargeNumber } from "../../../utils/shortenLargeNumber.js";
9+
import { getWalletBalance } from "../../../wallets/utils/getWalletBalance.js";
10+
11+
export const COLOR_OPTIONS = [
12+
["#fca5a5", "#b91c1c"],
13+
["#fdba74", "#c2410c"],
14+
["#fcd34d", "#b45309"],
15+
["#fde047", "#a16207"],
16+
["#a3e635", "#4d7c0f"],
17+
["#86efac", "#15803d"],
18+
["#67e8f9", "#0e7490"],
19+
["#7dd3fc", "#0369a1"],
20+
["#93c5fd", "#1d4ed8"],
21+
["#a5b4fc", "#4338ca"],
22+
["#c4b5fd", "#6d28d9"],
23+
["#d8b4fe", "#7e22ce"],
24+
["#f0abfc", "#a21caf"],
25+
["#f9a8d4", "#be185d"],
26+
["#fda4af", "#be123c"],
27+
];
28+
29+
/**
30+
* @component
31+
* @wallet
32+
*/
33+
export type AccountBalanceInfo = {
34+
/**
35+
* Represents either token balance or fiat balance.
36+
*/
37+
balance: number;
38+
/**
39+
* Represents either token symbol or fiat symbol
40+
*/
41+
symbol: string;
42+
};
43+
44+
/**
45+
* @internal Exported for tests
46+
*/
47+
export async function loadAccountBalance(props: {
48+
chain?: Chain;
49+
client: ThirdwebClient;
50+
address: Address;
51+
tokenAddress?: Address;
52+
showBalanceInFiat?: SupportedFiatCurrency;
53+
}): Promise<AccountBalanceInfo> {
54+
const { chain, client, address, tokenAddress, showBalanceInFiat } = props;
55+
if (!chain) {
56+
throw new Error("chain is required");
57+
}
58+
59+
if (
60+
tokenAddress &&
61+
tokenAddress?.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase()
62+
) {
63+
throw new Error(`Invalid tokenAddress - cannot be ${NATIVE_TOKEN_ADDRESS}`);
64+
}
65+
66+
if (!isAddress(address)) {
67+
throw new Error("Invalid wallet address. Expected an EVM address");
68+
}
69+
70+
if (tokenAddress && !isAddress(tokenAddress)) {
71+
throw new Error("Invalid tokenAddress. Expected an EVM contract address");
72+
}
73+
74+
const tokenBalanceData = await getWalletBalance({
75+
chain,
76+
client,
77+
address,
78+
tokenAddress,
79+
}).catch(() => undefined);
80+
81+
if (!tokenBalanceData) {
82+
throw new Error(
83+
`Failed to retrieve ${tokenAddress ? `token: ${tokenAddress}` : "native token"} balance for address: ${address} on chainId:${chain.id}`,
84+
);
85+
}
86+
87+
if (showBalanceInFiat) {
88+
const fiatData = await convertCryptoToFiat({
89+
fromAmount: Number(tokenBalanceData.displayValue),
90+
fromTokenAddress: tokenAddress || NATIVE_TOKEN_ADDRESS,
91+
to: showBalanceInFiat,
92+
chain,
93+
client,
94+
}).catch(() => undefined);
95+
96+
if (fiatData === undefined) {
97+
throw new Error(
98+
`Failed to resolve fiat value for ${tokenAddress ? `token: ${tokenAddress}` : "native token"} on chainId: ${chain.id}`,
99+
);
100+
}
101+
const result = {
102+
balance: fiatData?.result,
103+
symbol: getFiatSymbol(showBalanceInFiat),
104+
};
105+
106+
return result;
107+
}
108+
109+
return {
110+
balance: Number(tokenBalanceData.displayValue),
111+
symbol: tokenBalanceData.symbol,
112+
};
113+
}
114+
115+
function getFiatSymbol(showBalanceInFiat: SupportedFiatCurrency) {
116+
switch (showBalanceInFiat) {
117+
case "USD":
118+
return "$";
119+
}
120+
}
121+
122+
/**
123+
* Format the display balance for both crypto and fiat, in the Details button and Modal
124+
* If both crypto balance and fiat balance exist, we have to keep the string very short to avoid UI issues.
125+
* @internal
126+
* Used internally for the Details button and the Details Modal
127+
*/
128+
export function formatAccountTokenBalance(
129+
props: AccountBalanceInfo & { decimals: number },
130+
): string {
131+
const formattedTokenBalance = formatNumber(props.balance, props.decimals);
132+
return `${formattedTokenBalance} ${props.symbol}`;
133+
}
134+
135+
/**
136+
* Used internally for the Details button and Details Modal
137+
* @internal
138+
*/
139+
export function formatAccountFiatBalance(
140+
props: AccountBalanceInfo & { decimals: number },
141+
) {
142+
const num = formatNumber(props.balance, props.decimals);
143+
// Need to keep them short to avoid UI overflow issues
144+
const formattedFiatBalance = shortenLargeNumber(num);
145+
return `${props.symbol}${formattedFiatBalance}`;
146+
}

packages/thirdweb/src/react/core/utils/walletIcon.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2+
import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
13
import type { AuthOption } from "../../../wallets/types.js";
4+
import type { WalletId } from "../../../wallets/wallet-types.js";
5+
import { useWalletContext } from "../wallet/provider.js";
26

37
// TODO make the social icons usable in RN too
48
const googleIconUri =
@@ -106,3 +110,27 @@ export function getSocialIcon(provider: AuthOption | ({} & string)): string {
106110
return genericWalletIcon;
107111
}
108112
}
113+
114+
/**
115+
* @internal
116+
*/
117+
export function useWalletIcon(props: {
118+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
119+
}) {
120+
const { id } = useWalletContext();
121+
const imageQuery = useQuery({
122+
queryKey: ["walletIcon", id],
123+
queryFn: async () => fetchWalletImage({ id }),
124+
...props.queryOptions,
125+
});
126+
return imageQuery;
127+
}
128+
129+
/**
130+
* @internal Exported for tests only
131+
*/
132+
export async function fetchWalletImage(props: {
133+
id: WalletId;
134+
}) {
135+
return getWalletInfo(props.id, true);
136+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import type { UseQueryOptions } from "@tanstack/react-query";
3+
import { getFunctionId } from "../../../utils/function-id.js";
4+
import { getWalletInfo } from "../../../wallets/__generated__/getWalletInfo.js";
5+
import type { WalletId } from "../../../wallets/wallet-types.js";
6+
import { useWalletContext } from "../wallet/provider.js";
7+
8+
/**
9+
* @internal
10+
*/
11+
export function useWalletName(props: {
12+
formatFn?: (str: string) => string;
13+
queryOptions?: Omit<UseQueryOptions<string>, "queryFn" | "queryKey">;
14+
}) {
15+
const { id } = useWalletContext();
16+
const nameQuery = useQuery({
17+
queryKey: getQueryKeys({ id, formatFn: props.formatFn }),
18+
queryFn: async () => fetchWalletName({ id, formatFn: props.formatFn }),
19+
...props.queryOptions,
20+
});
21+
return nameQuery;
22+
}
23+
24+
/**
25+
* @internal Exported for tests only
26+
*/
27+
export function getQueryKeys(props: {
28+
id: WalletId;
29+
formatFn?: (str: string) => string;
30+
}) {
31+
if (typeof props.formatFn === "function") {
32+
return [
33+
"walletName",
34+
props.id,
35+
{ resolver: getFunctionId(props.formatFn) },
36+
] as const;
37+
}
38+
return ["walletName", props.id] as const;
39+
}
40+
41+
/**
42+
* @internal Exported for tests only
43+
*/
44+
export async function fetchWalletName(props: {
45+
id: WalletId;
46+
formatFn?: (str: string) => string;
47+
}) {
48+
const info = await getWalletInfo(props.id);
49+
if (typeof props.formatFn === "function") {
50+
return props.formatFn(info.name);
51+
}
52+
return info.name;
53+
}

packages/thirdweb/src/react/web/ui/prebuilt/Wallet/provider.tsx renamed to packages/thirdweb/src/react/core/wallet/provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import type React from "react";
44
import { createContext, useContext } from "react";
5-
import type { WalletId } from "../../../../../wallets/wallet-types.js";
5+
import type { WalletId } from "../../../wallets/wallet-types.js";
66

77
/**
88
* Props for the WalletProvider component

0 commit comments

Comments
 (0)