Skip to content

Commit 998f133

Browse files
committed
update
1 parent b30177c commit 998f133

File tree

10 files changed

+464
-4
lines changed

10 files changed

+464
-4
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { THIRDWEB_CLIENT } from "@/lib/client";
5+
import { Suspense } from "react";
6+
import { sepolia } from "thirdweb/chains";
7+
import { Account, Blobbie, ThirdwebProvider } from "thirdweb/react";
8+
import { shortenAddress } from "thirdweb/utils";
9+
10+
export default function Page() {
11+
return (
12+
<ThirdwebProvider>
13+
<div className="flex h-full w-full flex-col items-center gap-3">
14+
<div>
15+
<div>Let&apos;s rebuild a basic ConnectButton-details component</div>
16+
<BasicConnectedButton />
17+
</div>
18+
</div>
19+
</ThirdwebProvider>
20+
);
21+
}
22+
23+
const BasicConnectedButton = () => {
24+
const roundUpBalance = (num: number) => Math.round(num * 10) / 10;
25+
return (
26+
<Account.Provider
27+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
28+
client={THIRDWEB_CLIENT}
29+
>
30+
<Button className="flex h-12 w-40 flex-row justify-start gap-3 px-2">
31+
<Suspense
32+
fallback={
33+
<Blobbie
34+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
35+
className="h-10 w-10 rounded-full"
36+
/>
37+
}
38+
>
39+
<Account.Avatar className="h-10 w-10 rounded-full" />
40+
</Suspense>
41+
<div className="flex flex-col items-start">
42+
<Suspense fallback={<Account.Address formatFn={shortenAddress} />}>
43+
<Account.Name />
44+
</Suspense>
45+
<Suspense fallback={"skeleton"}>
46+
<Account.Balance chain={sepolia} formatFn={roundUpBalance} />
47+
</Suspense>
48+
</div>
49+
</Button>
50+
</Account.Provider>
51+
);
52+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import { THIRDWEB_CLIENT } from "@/lib/client";
5+
import { Suspense } from "react";
6+
import { defineChain } from "thirdweb/chains";
7+
import { Account, Blobbie, ThirdwebProvider } from "thirdweb/react";
8+
import { shortenAddress } from "thirdweb/utils";
9+
10+
export default function Page() {
11+
return (
12+
<ThirdwebProvider>
13+
<div className="flex h-full w-full flex-col items-center gap-3">
14+
<div>
15+
<div>Let&apos;s rebuild a basic ConnectButton-details component</div>
16+
<BasicConnectedButton />
17+
</div>
18+
</div>
19+
</ThirdwebProvider>
20+
);
21+
}
22+
23+
const BasicConnectedButton = () => {
24+
const roundUpBalance = (num: number) => Math.round(num * 10) / 10;
25+
return (
26+
<Account.Provider
27+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
28+
client={THIRDWEB_CLIENT}
29+
>
30+
<Button className="flex h-12 w-40 flex-row justify-start gap-3 px-2">
31+
<Suspense
32+
fallback={
33+
<Blobbie
34+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
35+
className="h-10 w-10 rounded-full"
36+
/>
37+
}
38+
>
39+
<Account.Avatar className="h-10 w-10 rounded-full" />
40+
</Suspense>
41+
<div className="flex flex-col items-start">
42+
<Suspense fallback={<Account.Address formatFn={shortenAddress} />}>
43+
<Account.Name />
44+
</Suspense>
45+
<Suspense fallback={"skeleton"}>
46+
<Account.Balance
47+
chain={defineChain(-1)}
48+
formatFn={roundUpBalance}
49+
/>
50+
</Suspense>
51+
</div>
52+
</Button>
53+
</Account.Provider>
54+
);
55+
};

packages/thirdweb/src/exports/react.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,6 @@ export type {
199199
// Site Embed and Linking
200200
export { SiteEmbed } from "../react/web/ui/SiteEmbed.js";
201201
export { SiteLink } from "../react/web/ui/SiteLink.js";
202+
203+
// Account
204+
export * as Account from "../react/web/ui/prebuilt/Account/index.js";

packages/thirdweb/src/react/web/ui/ConnectWallet/Blobbie.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,16 @@ const COLOR_OPTIONS = [
2525
* A unique gradient avatar based on the provided address.
2626
* @param props The component props.
2727
* @param props.address The address to generate the gradient with.
28-
* @param props.size The size of each side of the square avatar (in pixels)
28+
* @param props.style The CSS style for the component - excluding `backgroundImage`
29+
* @param props.className The className for the component
30+
* @param props.size The size of each side of the square avatar (in pixels). This prop will override the `width` and `height` attributes from the `style` prop.
2931
*/
30-
export function Blobbie(props: { address: Address; size: number }) {
32+
export function Blobbie(props: {
33+
address: Address;
34+
style?: Omit<React.CSSProperties, "backgroundImage">;
35+
className?: string;
36+
size?: number;
37+
}) {
3138
const id = useId();
3239
const colors = useMemo(
3340
() =>
@@ -41,10 +48,16 @@ export function Blobbie(props: { address: Address; size: number }) {
4148
<div
4249
id={id}
4350
style={{
44-
width: `${props.size}px`,
45-
height: `${props.size}px`,
51+
...props.style,
4652
backgroundImage: `radial-gradient(ellipse at left bottom, ${colors[0]}, ${colors[1]})`,
53+
...(props.size
54+
? {
55+
width: `${props.size}px`,
56+
height: `${props.size}px`,
57+
}
58+
: undefined),
4759
}}
60+
className={props.className}
4861
/>
4962
);
5063
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client";
2+
3+
import { useAccountContext } from "./provider.js";
4+
5+
export interface AccountAddressProps
6+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
7+
formatFn?: (str: string) => string;
8+
}
9+
10+
/**
11+
*
12+
* @returns a <span> containing the full wallet address of the account
13+
*
14+
* @example
15+
* ### Basic usage
16+
* ```tsx
17+
* import { Account } from "thirdweb/react";
18+
*
19+
* <Account address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
20+
* <Account.Address />
21+
* </Account>
22+
* ```
23+
* Result:
24+
* ```html
25+
* <span>0x12345674b599ce99958242b3D3741e7b01841DF3</span>
26+
* ```
27+
*
28+
*
29+
* ### Shorten the address
30+
* ```tsx
31+
* import { Account } from "thirdweb/react";
32+
* import { shortenAddress } from "thirdweb/utils";
33+
*
34+
* <Account address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
35+
* <Account.Address formatFn={shortenAddress} />
36+
* </Account>
37+
* ```
38+
* Result:
39+
* ```html
40+
* <span>0x1234...1DF3</span>
41+
* ```
42+
*
43+
*/
44+
export const Address = ({ formatFn, ...restProps }: AccountAddressProps) => {
45+
const { address } = useAccountContext();
46+
const value = formatFn ? formatFn(address) : address;
47+
return <span {...restProps}>{value}</span>;
48+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use client";
2+
3+
import { useSuspenseQuery } from "@tanstack/react-query";
4+
import type React from "react";
5+
import { resolveAvatar } from "../../../../../extensions/ens/resolve-avatar.js";
6+
import {
7+
type ResolveNameOptions,
8+
resolveName,
9+
} from "../../../../../extensions/ens/resolve-name.js";
10+
import { getSocialProfiles } from "../../../../../social/profiles.js";
11+
import type { SocialProfile } from "../../../../../social/types.js";
12+
import { parseAvatarRecord } from "../../../../../utils/ens/avatar.js";
13+
import { useAccountContext } from "./provider.js";
14+
15+
export interface AccountAvatar
16+
extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src">,
17+
Omit<ResolveNameOptions, "client" | "address"> {
18+
/**
19+
* Use this prop to prioritize the social profile that you want to display
20+
* This is useful for a wallet containing multiple social profiles
21+
*/
22+
socialType?: SocialProfile["type"];
23+
}
24+
25+
export const Avatar = ({
26+
socialType,
27+
resolverAddress,
28+
resolverChain,
29+
...restProps
30+
}: AccountAvatar) => {
31+
const { address, client } = useAccountContext();
32+
const avatarQuery = useSuspenseQuery({
33+
queryKey: ["account-avatar", address],
34+
queryFn: async () => {
35+
const [socialData, ensName] = await Promise.all([
36+
getSocialProfiles({ address, client }),
37+
resolveName({
38+
client,
39+
address: address || "",
40+
resolverAddress,
41+
resolverChain,
42+
}),
43+
]);
44+
45+
const uri = socialData?.filter(
46+
(p) => p.avatar && (socialType ? p.type === socialType : true),
47+
)[0]?.avatar;
48+
49+
const [resolvedSocialAvatar, resolvedENSAvatar] = await Promise.all([
50+
uri ? parseAvatarRecord({ client, uri }) : undefined,
51+
ensName
52+
? resolveAvatar({
53+
client,
54+
name: ensName,
55+
})
56+
: undefined,
57+
]);
58+
59+
// If no social image + ens name found -> exit and show <Blobbie />
60+
if (!resolvedSocialAvatar && !resolvedENSAvatar) {
61+
throw new Error("Failed to resolve social + ens avatar");
62+
}
63+
64+
// else, prioritize the social image first
65+
if (resolvedSocialAvatar) {
66+
return resolvedSocialAvatar;
67+
}
68+
69+
if (resolvedENSAvatar) {
70+
return resolvedENSAvatar;
71+
}
72+
73+
throw new Error("Failed to resolve social + ens avatar");
74+
},
75+
});
76+
return <img src={avatarQuery.data} {...restProps} alt="" />;
77+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"use client";
2+
3+
import { useSuspenseQuery } from "@tanstack/react-query";
4+
import type React from "react";
5+
import type { Chain } from "../../../../../chains/types.js";
6+
import { getWalletBalance } from "../../../../../wallets/utils/getWalletBalance.js";
7+
import { useAccountContext } from "./provider.js";
8+
9+
export interface AccountBalanceProps
10+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
11+
chain?: Chain;
12+
tokenAddress?: string;
13+
/**
14+
* A function to format the balance's display value
15+
*/
16+
formatFn?: (num: number) => number;
17+
}
18+
19+
/**
20+
*
21+
* ### Basic usage
22+
* ```tsx
23+
* import { Account } from "thirdweb/react";
24+
*
25+
* <Account address="0x...">
26+
* <Account.Balance />
27+
* </Account>
28+
* ```
29+
* Result:
30+
* ```html
31+
* <span>1.091435 ETH</span>
32+
* ```
33+
*
34+
*
35+
* ### Format the balance (round up, shorten etc.)
36+
* The Account.Balance component accepts a `formatFn` which takes in a number and output a number
37+
* The function is used to modify the display value of the wallet balance
38+
*
39+
* ```tsx
40+
* const roundTo1Decimal(num: number):number => Math.round(num * 10) / 10;
41+
*
42+
* <Account.Balance formatFn={roundTo1Decimal} />
43+
* ```
44+
*
45+
* Result:
46+
* ```html
47+
* <span>1.1 ETH</span>
48+
* ```
49+
*/
50+
export const Balance = ({
51+
chain,
52+
tokenAddress,
53+
formatFn,
54+
...restProps
55+
}: AccountBalanceProps) => {
56+
const { address, client } = useAccountContext();
57+
const balanceQuery = useSuspenseQuery({
58+
queryKey: [
59+
"walletBalance",
60+
chain?.id || -1,
61+
address || "0x0",
62+
{ tokenAddress },
63+
] as const,
64+
queryFn: async () => {
65+
if (!chain) {
66+
throw new Error("chain is required");
67+
}
68+
if (!client) {
69+
throw new Error("client is required");
70+
}
71+
return getWalletBalance({
72+
chain,
73+
client,
74+
address,
75+
tokenAddress,
76+
});
77+
},
78+
});
79+
80+
const displayValue = balanceQuery.data
81+
? formatFn
82+
? formatFn(Number(balanceQuery.data.displayValue))
83+
: balanceQuery.data.displayValue
84+
: "";
85+
86+
return (
87+
<span {...restProps}>
88+
{displayValue} {balanceQuery.data?.symbol}
89+
</span>
90+
);
91+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { Address, type AccountAddressProps } from "./address.js";
2+
export { Avatar, type AccountAvatar } from "./avatar.js";
3+
export { Balance, type AccountBalanceProps } from "./balance.js";
4+
export { Name, type AccountName } from "./name.js";
5+
export { Provider, type AccountProps } from "./provider.js";

0 commit comments

Comments
 (0)