Skip to content

Commit dfa8603

Browse files
committed
[NEB-213] Nebula: Show wallet Assets and Activity in sidebar (#7046)
<!-- ## 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 primarily focuses on enhancing the `AssetsSection` and `TransactionsSection` components by adding new features and improving the UI. It also modifies several existing components for better functionality and user experience. ### Detailed summary - Changed `mergedSessions.push(session)` to `mergedSessions.unshift(session)` in `useSessionsWithLocalOverrides.ts`. - Updated the width of the sidebar in `ChatPageLayout.tsx` from `280px` to `300px`. - Added `customDetailsButton` prop to `NebulaConnectWallet` in `NebulaConnectButton.tsx`. - Implemented conditional rendering for `customDetailsButton` in `NebulaConnectWallet`. - Created storybook stories for `AssetsSection` and `TransactionsSection` with stubs for tokens and transactions. - Enhanced `AssetsSection` and `TransactionsSection` components to handle loading states and display messages when there are no assets or transactions. - Introduced `WalletDetails` component in `ChatSidebar` to manage assets and transactions tabs. - Refactored `WalletSelector` to integrate `useActiveWallet` for better wallet management and user feedback. - Improved button components with tooltips for better accessibility and UX. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 9f1ca54 commit dfa8603

File tree

9 files changed

+927
-122
lines changed

9 files changed

+927
-122
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { storybookThirdwebClient } from "../../../../../stories/utils";
3+
import { type AssetBalance, AssetsSectionUI } from "./AssetsSection";
4+
5+
const meta = {
6+
title: "Nebula/AssetsSection",
7+
component: AssetsSectionUI,
8+
decorators: [
9+
(Story) => (
10+
<div className="mx-auto h-dvh w-full max-w-[300px] bg-card p-2">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
} satisfies Meta<typeof AssetsSectionUI>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
const tokensStub: AssetBalance[] = [
21+
{
22+
chain_id: 8453,
23+
token_address: "0xe8e55a847bb446d967ef92f4580162fb8f2d3f38",
24+
name: "Broge",
25+
symbol: "BROGE",
26+
decimals: 18,
27+
balance: "10000000000000000000000",
28+
},
29+
{
30+
chain_id: 8453,
31+
token_address: "0x8d2757ea27aabf172da4cca4e5474c76016e3dc5",
32+
name: "clBTC",
33+
symbol: "clBTC",
34+
decimals: 18,
35+
balance: "2",
36+
},
37+
{
38+
chain_id: 8453,
39+
token_address: "0xb56d0839998fd79efcd15c27cf966250aa58d6d3",
40+
name: "BASED USA",
41+
symbol: "USA",
42+
decimals: 18,
43+
balance: "1000000000000000000",
44+
},
45+
{
46+
chain_id: 8453,
47+
token_address: "0x600c9b69a65fb6d2551623a53ddef17b050233cd",
48+
name: "BearPaw",
49+
symbol: "PAW",
50+
decimals: 18,
51+
balance: "48888800000000000000",
52+
},
53+
{
54+
chain_id: 8453,
55+
token_address: "0x4c96a67b0577358894407af7bc3158fc1dffbeb5",
56+
name: "Degen Point Of View",
57+
symbol: "POV",
58+
decimals: 18,
59+
balance: "69000000000000000000",
60+
},
61+
{
62+
chain_id: 8453,
63+
token_address: "0x4200000000000000000000000000000000000006",
64+
name: "Wrapped Ether",
65+
symbol: "WETH",
66+
decimals: 18,
67+
balance: "6237535850425",
68+
},
69+
];
70+
71+
export const MultipleAssets: Story = {
72+
args: {
73+
data: tokensStub,
74+
isPending: false,
75+
client: storybookThirdwebClient,
76+
},
77+
};
78+
79+
export const SingleAsset: Story = {
80+
args: {
81+
data: tokensStub.slice(0, 1),
82+
isPending: false,
83+
client: storybookThirdwebClient,
84+
},
85+
};
86+
87+
export const EmptyAssets: Story = {
88+
args: {
89+
data: [],
90+
isPending: false,
91+
client: storybookThirdwebClient,
92+
},
93+
};
94+
95+
export const Loading: Story = {
96+
args: {
97+
data: [],
98+
isPending: true,
99+
client: storybookThirdwebClient,
100+
},
101+
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Skeleton } from "@/components/ui/skeleton";
2+
import { isProd } from "@/constants/env-utils";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { XIcon } from "lucide-react";
5+
import Link from "next/link";
6+
import { type ThirdwebClient, defineChain, toTokens } from "thirdweb";
7+
import {
8+
Blobbie,
9+
TokenIcon,
10+
TokenProvider,
11+
useActiveAccount,
12+
useActiveWalletChain,
13+
} from "thirdweb/react";
14+
import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
15+
import { useAllChainsData } from "../../../../../hooks/chains/allChains";
16+
import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
17+
18+
export type AssetBalance = {
19+
chain_id: number;
20+
token_address: string;
21+
balance: string;
22+
name: string;
23+
symbol: string;
24+
decimals: number;
25+
};
26+
27+
export function AssetsSectionUI(props: {
28+
data: AssetBalance[];
29+
isPending: boolean;
30+
client: ThirdwebClient;
31+
}) {
32+
if (props.data.length === 0 && !props.isPending) {
33+
return (
34+
<div className="flex h-full flex-col items-center justify-center gap-3 px-2 py-1">
35+
<div className="rounded-full border p-1">
36+
<XIcon className="size-4" />
37+
</div>
38+
<div className="text-muted-foreground text-sm">No Assets </div>
39+
</div>
40+
);
41+
}
42+
43+
return (
44+
<div className="flex flex-col gap-1">
45+
{!props.isPending &&
46+
props.data.map((asset) => (
47+
<AssetItem
48+
key={asset.token_address}
49+
asset={asset}
50+
client={props.client}
51+
/>
52+
))}
53+
54+
{props.isPending &&
55+
new Array(10).fill(null).map((_, index) => (
56+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
57+
<SkeletonAssetItem key={index} />
58+
))}
59+
</div>
60+
);
61+
}
62+
63+
function SkeletonAssetItem() {
64+
return (
65+
<div className="flex h-[48px] items-center gap-2 px-2 py-1">
66+
<Skeleton className="size-8 rounded-full" />
67+
<div className="flex flex-col gap-1">
68+
<Skeleton className="h-3 w-32 bg-muted" />
69+
<Skeleton className="h-3 w-24 bg-muted" />
70+
</div>
71+
</div>
72+
);
73+
}
74+
75+
function AssetItem(props: {
76+
asset: AssetBalance;
77+
client: ThirdwebClient;
78+
}) {
79+
const { idToChain } = useAllChainsData();
80+
const chainMeta = idToChain.get(props.asset.chain_id);
81+
return (
82+
<TokenProvider
83+
address={props.asset.token_address}
84+
client={props.client}
85+
// eslint-disable-next-line no-restricted-syntax
86+
chain={defineChain(props.asset.chain_id)}
87+
>
88+
<div className="relative flex h-[48px] items-center gap-2.5 rounded-lg px-2 py-1 hover:bg-accent">
89+
<div className="relative">
90+
<TokenIcon
91+
className="size-8 rounded-full"
92+
loadingComponent={
93+
<Blobbie
94+
address={props.asset.token_address}
95+
className="size-8 rounded-full"
96+
/>
97+
}
98+
fallbackComponent={
99+
<Blobbie
100+
address={props.asset.token_address}
101+
className="size-8 rounded-full"
102+
/>
103+
}
104+
/>
105+
<div className="-right-0.5 -bottom-0.5 absolute rounded-full border bg-background p-0.5">
106+
<ChainIconClient
107+
client={props.client}
108+
className="size-3.5"
109+
src={chainMeta?.icon?.url || ""}
110+
/>
111+
</div>
112+
</div>
113+
114+
<div className="flex min-w-0 flex-col text-sm">
115+
<Link
116+
href={`https://thirdweb.com/${props.asset.chain_id}/${props.asset.token_address}`}
117+
target="_blank"
118+
className="truncate font-medium before:absolute before:inset-0"
119+
>
120+
{props.asset.name}
121+
</Link>
122+
123+
<p className="text-muted-foreground text-sm">
124+
{`${toTokens(BigInt(props.asset.balance), props.asset.decimals)} ${props.asset.symbol}`}
125+
</p>
126+
</div>
127+
</div>
128+
</TokenProvider>
129+
);
130+
}
131+
132+
export function AssetsSection(props: {
133+
client: ThirdwebClient;
134+
}) {
135+
const account = useActiveAccount();
136+
const activeChain = useActiveWalletChain();
137+
138+
const assetsQuery = useQuery({
139+
queryKey: ["v1/tokens/erc20", account?.address, activeChain?.id],
140+
queryFn: async () => {
141+
if (!account || !activeChain) {
142+
return [];
143+
}
144+
const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
145+
const url = new URL(
146+
`https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/erc20/${account?.address}`,
147+
);
148+
url.searchParams.set("limit", "50");
149+
url.searchParams.set("metadata", "true");
150+
url.searchParams.set("include_spam", "false");
151+
url.searchParams.set("clientId", nebulaAppThirdwebClient.clientId);
152+
for (const chain of chains) {
153+
url.searchParams.append("chain", chain.toString());
154+
}
155+
156+
const response = await fetch(url.toString());
157+
const json = (await response.json()) as {
158+
data: AssetBalance[];
159+
};
160+
161+
return json.data;
162+
},
163+
enabled: !!account && !!activeChain,
164+
});
165+
166+
return (
167+
<AssetsSectionUI
168+
data={assetsQuery.data ?? []}
169+
isPending={assetsQuery.isPending}
170+
client={props.client}
171+
/>
172+
);
173+
}

0 commit comments

Comments
 (0)