Skip to content

Commit 5ea8323

Browse files
committed
update
1 parent b30177c commit 5ea8323

File tree

16 files changed

+1053
-4
lines changed

16 files changed

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

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: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,27 @@ 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
2943
*/
30-
export function Blobbie(props: { address: Address; size: number }) {
44+
export function Blobbie(props: BlobbieProps) {
3145
const id = useId();
3246
const colors = useMemo(
3347
() =>
@@ -41,10 +55,16 @@ export function Blobbie(props: { address: Address; size: number }) {
4155
<div
4256
id={id}
4357
style={{
44-
width: `${props.size}px`,
45-
height: `${props.size}px`,
58+
...props.style,
4659
backgroundImage: `radial-gradient(ellipse at left bottom, ${colors[0]}, ${colors[1]})`,
60+
...(props.size
61+
? {
62+
width: `${props.size}px`,
63+
height: `${props.size}px`,
64+
}
65+
: undefined),
4766
}}
67+
className={props.className}
4868
/>
4969
);
5070
}
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 { Address } from "./address.js";
6+
import { Provider } from "./provider.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("Account.Provider component", () => {
9+
it("should format the address properly", () => {
10+
render(
11+
<Provider
12+
address="0x12345674b599ce99958242b3D3741e7b01841DF3"
13+
client={TEST_CLIENT}
14+
>
15+
<Address formatFn={shortenAddress} />
16+
</Provider>,
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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 { Account } from "thirdweb/react";
28+
*
29+
* <Account address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
30+
* <Account.Address />
31+
* </Account>
32+
* ```
33+
* Result:
34+
* ```html
35+
* <span>0x12345674b599ce99958242b3D3741e7b01841DF3</span>
36+
* ```
37+
*
38+
*
39+
* ### Shorten the address
40+
* ```tsx
41+
* import { Account } from "thirdweb/react";
42+
* import { shortenAddress } from "thirdweb/utils";
43+
*
44+
* <Account address="0x12345674b599ce99958242b3D3741e7b01841DF3" client={TW_CLIENT}>
45+
* <Account.Address formatFn={shortenAddress} />
46+
* </Account>
47+
* ```
48+
* Result:
49+
* ```html
50+
* <span>0x1234...1DF3</span>
51+
* ```
52+
*
53+
* @component
54+
* @account
55+
*/
56+
export const Address = ({ formatFn, ...restProps }: AccountAddressProps) => {
57+
const { address } = useAccountContext();
58+
const value = formatFn ? formatFn(address) : address;
59+
return <span {...restProps}>{value}</span>;
60+
};
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 { Avatar } from "./avatar.js";
6+
import { Provider } from "./provider.js";
7+
8+
describe.runIf(process.env.TW_SECRET_KEY)("Account.Provider component", () => {
9+
it("should render an image", () => {
10+
render(
11+
<Provider
12+
address={"0x12345674b599ce99958242b3D3741e7b01841DF3"}
13+
client={TEST_CLIENT}
14+
>
15+
<Avatar />
16+
</Provider>,
17+
);
18+
19+
waitFor(() => expect(screen.getByRole("img")).toBeInTheDocument());
20+
});
21+
22+
it("should fallback properly if failed to load", () => {
23+
render(
24+
<Provider address={TEST_ACCOUNT_A.address} client={TEST_CLIENT}>
25+
<Avatar fallbackComponent={<span>oops</span>} />
26+
</Provider>,
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+
<Provider address={"invalid-wallet-address"} client={TEST_CLIENT}>
42+
<Avatar />
43+
</Provider>,
44+
);
45+
46+
waitFor(() => expect(screen.getByRole("img")).not.toBeInTheDocument());
47+
});
48+
});

0 commit comments

Comments
 (0)