Skip to content

Commit 4550bb2

Browse files
[SDK] feat: Expose Account and Wallet components in React Native (#6082)
1 parent f5f8a40 commit 4550bb2

35 files changed

+1419
-266
lines changed

.changeset/tough-dogs-enjoy.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Support Account and Wallet headless components in react native
6+
7+
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.
8+
9+
Example Account components usage:
10+
11+
```tsx
12+
<AccountProvider address={account.address} client={client}>
13+
/* avatar */
14+
<AccountAvatar
15+
loadingComponent={
16+
<AccountBlobbie size={92} style={{ borderRadius: 100 }} />
17+
}
18+
fallbackComponent={
19+
<AccountBlobbie size={92} style={{ borderRadius: 100 }} />
20+
}
21+
style={{
22+
width: 92,
23+
height: 92,
24+
borderRadius: 100,
25+
}}
26+
/>
27+
/* address */
28+
<AccountAddress
29+
style={{ fontSize: 16, color: Colors.secondary }}
30+
formatFn={shortenAddress}
31+
/>
32+
/* balance */
33+
<AccountBalance
34+
showBalanceInFiat={"USD"}
35+
chain={chain}
36+
loadingComponent={<ActivityIndicator size="large" color={Colors.accent} />}
37+
fallbackComponent={
38+
<Text className="text-primary">Failed to load balance</Text>
39+
}
40+
style={{
41+
color: "white",
42+
fontSize: 48,
43+
fontWeight: "bold",
44+
}}
45+
/>
46+
</AccountProvider>
47+
```
48+
49+
Example Wallet components usage:
50+
51+
```tsx
52+
<WalletProvider id={"io.metamask"}>
53+
<WalletIcon width={32} height={32} />
54+
<WalletName style={{ fontSize: 16, color: Colors.primary }} />
55+
</WalletProvider>
56+
```

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,7 +1,7 @@
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";
4+
import { AccountAddress } from "../../web/ui/prebuilt/Account/address.js";
55
import { AccountProvider } from "./provider.js";
66

77
describe.runIf(process.env.TW_SECRET_KEY)("AccountProvider component", () => {

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

0 commit comments

Comments
 (0)