Skip to content

Commit 901c3a1

Browse files
committed
[SDK] Feature: Account components (#5388)
cnct-2137 <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a new `Account` component suite to the `thirdweb` library, enhancing user account management with features like account name, address, avatar, and balance display. It also marks existing `NFT` components as `@beta`. ### Detailed summary - Added `Account` components: `AccountName`, `AccountAddress`, `AccountAvatar`, `AccountBalance`, and `AccountBlobbie`. - Updated existing `NFT` components to `@beta`. - Introduced tests for new `Account` components. - Enhanced `AccountProvider` for context management. - Updated documentation for new components and props. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 3253a53 commit 901c3a1

File tree

21 files changed

+1059
-8
lines changed

21 files changed

+1059
-8
lines changed

.changeset/metal-cows-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Add headless UI component: Account (Name, Image, Address, Balance)

apps/portal/src/app/react/v5/sidebar.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ export const sidebar: SideBar = {
109109
href: `${slug}/${name}`,
110110
icon: <CodeIcon />,
111111
})),
112+
{
113+
name: "Account",
114+
isCollapsible: true,
115+
links: [
116+
"AccountProvider",
117+
"AccountAddress",
118+
"AccountAvatar",
119+
"AccountName",
120+
"AccountBlobbie",
121+
"AccountBalance",
122+
].map((name) => ({
123+
name,
124+
href: `${slug}/${name}`,
125+
icon: <CodeIcon />,
126+
})),
127+
},
112128
],
113129
},
114130
{

apps/portal/src/app/references/components/TDoc/utils/getSidebarLinkgroups.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const tagsToGroup = {
4141
"@social": "Social API",
4242
"@modules": "Modules",
4343
"@client": "Client",
44+
"@account": "Account Components",
4445
} as const;
4546

4647
type TagKey = keyof typeof tagsToGroup;
@@ -79,6 +80,7 @@ const sidebarGroupOrder: TagKey[] = [
7980
"@theme",
8081
"@utils",
8182
"@others",
83+
"@account",
8284
];
8385

8486
function findTag(

packages/thirdweb/src/exports/react.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,26 @@ export type {
212212
// Site Embed and Linking
213213
export { SiteEmbed } from "../react/web/ui/SiteEmbed.js";
214214
export { SiteLink } from "../react/web/ui/SiteLink.js";
215+
216+
// Account
217+
export {
218+
AccountAddress,
219+
type AccountAddressProps,
220+
} from "../react/web/ui/prebuilt/Account/address.js";
221+
export {
222+
AccountBalance,
223+
type AccountBalanceProps,
224+
} from "../react/web/ui/prebuilt/Account/balance.js";
225+
export {
226+
AccountName,
227+
type AccountNameProps,
228+
} from "../react/web/ui/prebuilt/Account/name.js";
229+
export { AccountBlobbie } from "../react/web/ui/prebuilt/Account/blobbie.js";
230+
export {
231+
AccountProvider,
232+
type AccountProviderProps,
233+
} from "../react/web/ui/prebuilt/Account/provider.js";
234+
export {
235+
AccountAvatar,
236+
type AccountAvatarProps,
237+
} from "../react/web/ui/prebuilt/Account/avatar.js";

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,34 @@ const COLOR_OPTIONS = [
2121
["#fda4af", "#be123c"],
2222
];
2323

24+
/**
25+
* Props for the Blobbie component
26+
* @component
27+
*/
28+
export type BlobbieProps = {
29+
address: Address;
30+
style?: Omit<React.CSSProperties, "backgroundImage">;
31+
className?: string;
32+
size?: number;
33+
};
34+
2435
/**
2536
* A unique gradient avatar based on the provided address.
2637
* @param props The component props.
2738
* @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)
39+
* @param props.style The CSS style for the component - excluding `backgroundImage`
40+
* @param props.className The className for the component
41+
* @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.
42+
* @component
43+
* @wallet
2944
* @example
3045
* ```tsx
31-
* <Blobbie address="0x...." size={24} />
46+
* import { Blobbie } from "thirdweb/react";
47+
*
48+
* <Blobbie address="0x...." className="w-10 h-10" />
3249
* ```
33-
* @wallet
3450
*/
35-
export function Blobbie(props: { address: Address; size: number }) {
51+
export function Blobbie(props: BlobbieProps) {
3652
const id = useId();
3753
const colors = useMemo(
3854
() =>
@@ -46,10 +62,16 @@ export function Blobbie(props: { address: Address; size: number }) {
4662
<div
4763
id={id}
4864
style={{
49-
width: `${props.size}px`,
50-
height: `${props.size}px`,
65+
...props.style,
5166
backgroundImage: `radial-gradient(ellipse at left bottom, ${colors[0]}, ${colors[1]})`,
67+
...(props.size
68+
? {
69+
width: `${props.size}px`,
70+
height: `${props.size}px`,
71+
}
72+
: undefined),
5273
}}
74+
className={props.className}
5375
/>
5476
);
5577
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
import { render, screen, waitFor } from "~test/react-render.js";
3+
import { TEST_CLIENT } from "~test/test-clients.js";
4+
import { shortenAddress } from "../../../../../utils/address.js";
5+
import { AccountAddress } from "./address.js";
6+
import { AccountProvider } from "./provider.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("AccountAddress component", () => {
9+
it("should format the address properly", () => {
10+
render(
11+
<AccountProvider
12+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
13+
client={TEST_CLIENT}
14+
>
15+
<AccountAddress formatFn={shortenAddress} />
16+
</AccountProvider>,
17+
);
18+
19+
waitFor(() =>
20+
expect(
21+
screen.getByText("0x1234...1DF3", {
22+
exact: true,
23+
selector: "span",
24+
}),
25+
).toBeInTheDocument(),
26+
);
27+
});
28+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use client";
2+
3+
import { useAccountContext } from "./provider.js";
4+
5+
/**
6+
* @component
7+
* @account
8+
*/
9+
export interface AccountAddressProps
10+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
11+
/**
12+
* The function used to transform (format) the wallet address
13+
* Specifically useful for shortening the wallet.
14+
*
15+
* This function should take in a string and output a string
16+
*/
17+
formatFn?: (str: string) => string;
18+
}
19+
20+
/**
21+
*
22+
* @returns a <span> containing the full wallet address of the account
23+
*
24+
* @example
25+
* ### Basic usage
26+
* ```tsx
27+
* import { AccountProvider, AccountAddress } from "thirdweb/react";
28+
*
29+
* <AccountProvider address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
30+
* <AccountAddress />
31+
* </AccountProvider>
32+
* ```
33+
* Result:
34+
* ```html
35+
* <span>0x12345674b599ce99958242b3D3741e7b01841DF3</span>
36+
* ```
37+
*
38+
*
39+
* ### Shorten the address
40+
* ```tsx
41+
* import { AccountProvider, AccountAddress } from "thirdweb/react";
42+
* import { shortenAddress } from "thirdweb/utils";
43+
*
44+
* <AccountProvider address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
45+
* <AccountAddress formatFn={shortenAddress} />
46+
* </AccountProvider>
47+
* ```
48+
* Result:
49+
* ```html
50+
* <span>0x1234...1DF3</span>
51+
* ```
52+
*
53+
* @component
54+
* @account
55+
* @beta
56+
*/
57+
export function AccountAddress({
58+
formatFn,
59+
...restProps
60+
}: AccountAddressProps) {
61+
const { address } = useAccountContext();
62+
const value = formatFn ? formatFn(address) : address;
63+
return <span {...restProps}>{value}</span>;
64+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, expect, it } from "vitest";
2+
import { render, screen, waitFor } from "~test/react-render.js";
3+
import { TEST_CLIENT } from "~test/test-clients.js";
4+
import { TEST_ACCOUNT_A } from "~test/test-wallets.js";
5+
import { AccountAvatar } from "./avatar.js";
6+
import { AccountProvider } from "./provider.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("AccountAvatar component", () => {
9+
it("should render an image", () => {
10+
render(
11+
<AccountProvider
12+
address={"0x12345674b599ce99958242b3D3741e7b01841DF3"}
13+
client={TEST_CLIENT}
14+
>
15+
<AccountAvatar />
16+
</AccountProvider>,
17+
);
18+
19+
waitFor(() => expect(screen.getByRole("img")).toBeInTheDocument());
20+
});
21+
22+
it("should fallback properly if failed to load", () => {
23+
render(
24+
<AccountProvider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
25+
<AccountAvatar fallbackComponent={<span>oops</span>} />
26+
</AccountProvider>,
27+
);
28+
29+
waitFor(() =>
30+
expect(
31+
screen.getByText("oops", {
32+
exact: true,
33+
selector: "span",
34+
}),
35+
).toBeInTheDocument(),
36+
);
37+
});
38+
39+
it("should NOT render anything if fail to resolve avatar", () => {
40+
render(
41+
<AccountProvider address={"invalid-wallet-address"} client={TEST_CLIENT}>
42+
<AccountAvatar />
43+
</AccountProvider>,
44+
);
45+
46+
waitFor(() => expect(screen.getByRole("img")).not.toBeInTheDocument());
47+
});
48+
});

0 commit comments

Comments
 (0)