Skip to content

Commit 1e7df5a

Browse files
committed
[MNY-252] Dashboard: Update WalletAddress component UI (#8276)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the `WalletAddress` component and its UI by improving the handling of social profiles, adding new story variants, and refining the UI elements for better user experience. ### Detailed summary - Changed `retry` in `useSocialProfiles` from `3` to `false`. - Added new Storybook stories for `WalletAddress` and `WalletAddressUI`. - Introduced a `Skeleton` component for loading states. - Refactored `WalletAddress` to use `useSocialProfiles` directly. - Updated UI structure and styling for displaying social profiles. - Improved the handling of address display and copy functionality. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Storybook examples showcasing Wallet Address states (loaded, loading, empty, invalid, zero address, ENS) * **Refactor** * Split wallet address UI for clearer rendering and moved profile-driven rendering to a dedicated UI component * Updated copy button styling to use a unified copy control * Improved social profiles panel layout and presentation * **Bug Fixes / Behavior** * Adjusted profile loading behavior to avoid automatic retry on failure, surfacing empty/error states sooner <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ca16ec7 commit 1e7df5a

File tree

3 files changed

+213
-81
lines changed

3 files changed

+213
-81
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
3+
import type { SocialProfile } from "thirdweb/react";
4+
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
5+
import { WalletAddress, WalletAddressUI } from "./wallet-address";
6+
7+
const meta = {
8+
component: Story,
9+
parameters: {
10+
nextjs: {
11+
appDirectory: true,
12+
},
13+
},
14+
title: "blocks/WalletAddress",
15+
} satisfies Meta<typeof Story>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
export const Variants: Story = {
21+
args: {},
22+
};
23+
24+
function Story() {
25+
const client = storybookThirdwebClient as unknown as ThirdwebClient;
26+
27+
return (
28+
<div className="container flex max-w-4xl flex-col gap-8 py-10">
29+
<BadgeContainer label="Social Profiles Loaded">
30+
<WalletAddressUI
31+
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
32+
client={client}
33+
profiles={{
34+
data: vitalikEth,
35+
isPending: false,
36+
}}
37+
/>
38+
</BadgeContainer>
39+
40+
<BadgeContainer label="No Social Profiles">
41+
<WalletAddressUI
42+
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
43+
client={client}
44+
shortenAddress
45+
profiles={{
46+
data: [],
47+
isPending: false,
48+
}}
49+
/>
50+
</BadgeContainer>
51+
52+
<BadgeContainer label="Loading Social Profiles">
53+
<WalletAddressUI
54+
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
55+
client={client}
56+
shortenAddress
57+
profiles={{
58+
data: [],
59+
isPending: true,
60+
}}
61+
/>
62+
</BadgeContainer>
63+
64+
<BadgeContainer label="Zero address">
65+
<WalletAddressUI
66+
address={ZERO_ADDRESS}
67+
client={client}
68+
profiles={{
69+
data: [],
70+
isPending: false,
71+
}}
72+
/>
73+
</BadgeContainer>
74+
75+
<BadgeContainer label="Real Component - Invalid Address">
76+
<WalletAddress address="not-an-address" client={client} />
77+
</BadgeContainer>
78+
79+
<BadgeContainer label="Real Component - vitalik.eth">
80+
<WalletAddress
81+
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
82+
client={client}
83+
/>
84+
</BadgeContainer>
85+
</div>
86+
);
87+
}
88+
89+
const vitalikEth: SocialProfile[] = [
90+
{
91+
type: "ens",
92+
name: "vitalik.eth",
93+
bio: "mi pinxe lo crino tcati",
94+
avatar: "https://euc.li/vitalik.eth",
95+
metadata: {
96+
name: "vitalik.eth",
97+
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
98+
avatar: "https://euc.li/vitalik.eth",
99+
description: "mi pinxe lo crino tcati",
100+
url: "https://vitalik.ca",
101+
},
102+
},
103+
{
104+
type: "farcaster",
105+
name: "vitalik.eth",
106+
bio: undefined,
107+
avatar:
108+
"https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
109+
metadata: {
110+
fid: 5650,
111+
bio: undefined,
112+
pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
113+
username: "vitalik.eth",
114+
addresses: [
115+
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
116+
"0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279",
117+
],
118+
},
119+
},
120+
];

apps/dashboard/src/@/components/blocks/wallet-address.tsx

Lines changed: 92 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,60 @@
11
"use client";
2-
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
2+
import { CircleSlashIcon, XIcon } from "lucide-react";
33
import { useMemo } from "react";
44
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
55
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
66
import { Img } from "@/components/blocks/Img";
77
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
8-
import { Badge } from "@/components/ui/badge";
98
import { Button } from "@/components/ui/button";
109
import {
1110
HoverCard,
1211
HoverCardContent,
1312
HoverCardTrigger,
1413
} from "@/components/ui/hover-card";
1514
import { ToolTipLabel } from "@/components/ui/tooltip";
16-
import { useClipboard } from "@/hooks/useClipboard";
1715
import { cn } from "@/lib/utils";
1816
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
17+
import { CopyTextButton } from "../ui/CopyTextButton";
18+
import { Skeleton } from "../ui/skeleton";
1919

20-
export function WalletAddress(props: {
20+
type WalletAddressProps = {
2121
address: string | undefined;
2222
shortenAddress?: boolean;
2323
className?: string;
2424
iconClassName?: string;
2525
client: ThirdwebClient;
2626
fallbackIcon?: React.ReactNode;
27-
}) {
27+
};
28+
29+
export function WalletAddress(props: WalletAddressProps) {
30+
const profiles = useSocialProfiles({
31+
address: props.address,
32+
client: props.client,
33+
});
34+
35+
return (
36+
<WalletAddressUI
37+
{...props}
38+
profiles={{
39+
data: profiles.data || [],
40+
isPending: profiles.isPending,
41+
}}
42+
/>
43+
);
44+
}
45+
46+
export function WalletAddressUI(
47+
props: WalletAddressProps & {
48+
profiles: {
49+
data: SocialProfile[];
50+
isPending: boolean;
51+
};
52+
},
53+
) {
2854
// default back to zero address if no address provided
2955
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
3056

31-
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
57+
const [shortenedAddress, _lessShortenedAddress] = useMemo(() => {
3258
return [
3359
props.shortenAddress !== false
3460
? `${address.slice(0, 6)}...${address.slice(-4)}`
@@ -37,17 +63,10 @@ export function WalletAddress(props: {
3763
];
3864
}, [address, props.shortenAddress]);
3965

40-
const profiles = useSocialProfiles({
41-
address: address,
42-
client: props.client,
43-
});
44-
45-
const { onCopy, hasCopied } = useClipboard(address, 2000);
46-
4766
if (!isAddress(address)) {
4867
return (
4968
<ToolTipLabel hoverable label={address}>
50-
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
69+
<span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit">
5170
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
5271
<XIcon className="size-4 text-muted-foreground" />
5372
</div>
@@ -88,89 +107,82 @@ export function WalletAddress(props: {
88107
<WalletAvatar
89108
address={address}
90109
iconClassName={props.iconClassName}
91-
profiles={profiles.data || []}
110+
profiles={props.profiles.data}
92111
thirdwebClient={props.client}
93112
fallbackIcon={props.fallbackIcon}
94113
/>
95114
)}
96115
<span className="cursor-pointer font-mono max-w-full truncate">
97-
{profiles.data?.[0]?.name || shortenedAddress}
116+
{props.profiles.data?.[0]?.name || shortenedAddress}
98117
</span>
99118
</Button>
100119
</HoverCardTrigger>
101120
<HoverCardContent
102-
className="w-80 border-border"
121+
className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6"
103122
onClick={(e) => {
104123
// do not close the hover card when clicking anywhere in the content
105124
e.stopPropagation();
106125
}}
107126
>
108127
<div className="space-y-4">
109-
<div className="flex items-center justify-between">
110-
<h3 className="font-semibold text-lg">Wallet Address</h3>
111-
<Button
112-
className="flex items-center gap-2"
113-
onClick={onCopy}
114-
size="sm"
115-
variant="outline"
116-
>
117-
{hasCopied ? (
118-
<CheckIcon className="h-4 w-4" />
119-
) : (
120-
<CopyIcon className="h-4 w-4" />
121-
)}
122-
{hasCopied ? "Copied!" : "Copy"}
123-
</Button>
128+
<div className="space-y-1">
129+
<h3 className="font-medium text-sm">Wallet Address</h3>
130+
131+
<CopyTextButton
132+
textToShow={address}
133+
textToCopy={address}
134+
tooltip="Copy address"
135+
copyIconPosition="right"
136+
variant="ghost"
137+
className="text-muted-foreground -translate-x-1.5"
138+
/>
124139
</div>
125-
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
126-
{lessShortenedAddress}
127-
</p>
128-
<h3 className="font-semibold text-lg">Social Profiles</h3>
129-
{profiles.isPending ? (
130-
<p className="text-muted-foreground text-sm">Loading profiles...</p>
131-
) : !profiles.data?.length ? (
132-
<p className="text-muted-foreground text-sm">No profiles found</p>
133-
) : (
134-
profiles.data?.map((profile) => {
135-
const walletAvatarLink = resolveSchemeWithErrorHandler({
136-
client: props.client,
137-
uri: profile.avatar,
138-
});
139-
140-
return (
141-
<div
142-
className="flex flex-row items-center gap-2"
143-
key={profile.type + profile.name}
144-
>
145-
{walletAvatarLink && (
146-
<Avatar>
147-
<AvatarImage
148-
alt={profile.name}
149-
className="object-cover"
150-
src={walletAvatarLink}
151-
/>
152-
{profile.name && (
153-
<AvatarFallback>
154-
{profile.name.slice(0, 2)}
155-
</AvatarFallback>
156-
)}
157-
</Avatar>
158-
)}
159-
<div className="flex w-full flex-col gap-1">
160-
<div className="flex w-full flex-row items-center justify-between gap-4">
161-
<h4 className="font-semibold text-md">{profile.name}</h4>
162-
<Badge variant="outline">{profile.type}</Badge>
140+
141+
<div className="space-y-1 border-t pt-5 border-dashed">
142+
<h3 className="font-medium text-sm">Social Profiles</h3>
143+
144+
{props.profiles.isPending ? (
145+
<Skeleton className="h-4 w-[50%]" />
146+
) : !props.profiles.data?.length ? (
147+
<p className="text-muted-foreground text-sm">No profiles found</p>
148+
) : (
149+
<div className="!mt-2">
150+
{props.profiles.data?.map((profile) => {
151+
const walletAvatarLink = resolveSchemeWithErrorHandler({
152+
client: props.client,
153+
uri: profile.avatar,
154+
});
155+
156+
return (
157+
<div
158+
className="flex flex-row items-center gap-3 py-2"
159+
key={profile.type + profile.name}
160+
>
161+
<Avatar className="size-9">
162+
<AvatarImage
163+
alt={profile.name}
164+
className="object-cover"
165+
src={walletAvatarLink}
166+
/>
167+
{profile.name && (
168+
<AvatarFallback>
169+
{profile.name.slice(0, 2)}
170+
</AvatarFallback>
171+
)}
172+
</Avatar>
173+
174+
<div className="space-y-0.5">
175+
<h4 className="text-sm leading-none">{profile.name}</h4>
176+
<span className="text-muted-foreground text-xs leading-none capitalize">
177+
{profile.type === "ens" ? "ENS" : profile.type}
178+
</span>
179+
</div>
163180
</div>
164-
{profile.bio && (
165-
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
166-
{profile.bio}
167-
</p>
168-
)}
169-
</div>
170-
</div>
171-
);
172-
})
173-
)}
181+
);
182+
})}
183+
</div>
184+
)}
185+
</div>
174186
</div>
175187
</HoverCardContent>
176188
</HoverCard>

packages/thirdweb/src/react/core/social/useSocialProfiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ export function useSocialProfiles(options: {
3737
return await getSocialProfiles({ address, client });
3838
},
3939
queryKey: ["social-profiles", address],
40-
retry: 3,
40+
retry: false,
4141
});
4242
}

0 commit comments

Comments
 (0)