-
+function WalletDetails() {
+ const [tab, setTab] = useState<"assets" | "transactions">("assets");
+ const [isExpanded, setIsExpanded] = useState(true);
+ return (
+
+
+
+
+
+ {isExpanded && (
+
+
setTab("assets"),
+ isActive: tab === "assets",
+ },
+ {
+ name: "Recent Activity",
+ onClick: () => setTab("transactions"),
+ isActive: tab === "transactions",
+ },
+ ]}
+ />
+
+ {isExpanded && (
+
+ {tab === "assets" && (
+
+ )}
+
+ {tab === "transactions" && (
+
+ )}
+
+ )}
+
+ )}
+
+ );
+}
+
+function CustomConnectButton() {
+ const activeWallet = useActiveWallet();
+ const accountBlobbie =
;
+ const accountAvatarFallback = (
+
+ );
+
+ return (
+
+
{
+ return (
+
+
+
+
+
+ );
+ }
+ : undefined
+ }
+ />
);
}
@@ -140,7 +273,7 @@ function ToggleThemeButton() {
icon={
isClientMounted ? (theme === "light" ? SunIcon : MoonIcon) : Spinner
}
- label="Theme"
+ label="Toggle Theme"
/>
);
}
@@ -152,16 +285,22 @@ function SidebarIconLink(props: {
href: string;
}) {
return (
-
+
+
+
);
}
@@ -171,13 +310,15 @@ function SidebarIconButton(props: {
onClick: () => void;
}) {
return (
-
+
+
+
);
}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/NebulaConnectButton.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/NebulaConnectButton.tsx
index 4255793dd7e..c7f6e860e10 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/components/NebulaConnectButton.tsx
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/NebulaConnectButton.tsx
@@ -21,6 +21,7 @@ export const NebulaConnectWallet = (props: {
connectButtonClassName?: string;
signInLinkButtonClassName?: string;
detailsButtonClassName?: string;
+ customDetailsButton?: (address: string) => React.ReactElement;
}) => {
const router = useDashboardRouter();
const { theme } = useTheme();
@@ -63,6 +64,7 @@ export const NebulaConnectWallet = (props: {
);
}
+ const { customDetailsButton } = props;
return (
customDetailsButton(account.address)
+ : undefined,
}}
chains={allChainsV5}
// we have an AutoConnect already added in root layout with AA configuration
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.stories.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.stories.tsx
new file mode 100644
index 00000000000..e454463dee3
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.stories.tsx
@@ -0,0 +1,153 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { storybookThirdwebClient } from "../../../../../stories/utils";
+import {
+ TransactionSectionUI,
+ type WalletTransaction,
+} from "./TransactionsSection";
+
+const meta = {
+ title: "Nebula/TransactionsSection",
+ component: TransactionSectionUI,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+const transactionsStub: WalletTransaction[] = [
+ {
+ chain_id: "8453",
+ hash: "0x2a098695dcfa32a67ec115af7c8da1ef6f443ea72baf7e49525204dd521a985e",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
+ value: "0",
+ decoded: {
+ name: "transfer",
+ signature: "transfer(address,uint256)",
+ inputs: {
+ to: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
+ value: "10000",
+ },
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xc521bfa0ba3e68fa1a52c67f93a8e215d3ade0b45956ba215390bcc0576202f1",
+ value: "1000000000000000",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
+ decoded: {
+ name: "",
+ signature: "",
+ inputs: null,
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xf2d92059c9ea425ccf7568bfe2589b3c7e45b108b5af658ec79c2f2d3723e410",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x83dd93fa5d8343094f850f90b3fb90088c1bb425",
+ value: "1000000000000000",
+ decoded: {
+ name: "",
+ signature: "",
+ inputs: null,
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xea3da430876c09acfa665450a91edb99fe8dc018864c5dfa3ac53bf265ce8d66",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
+ value: "0",
+ decoded: {
+ name: "transfer",
+ signature: "transfer(address,uint256)",
+ inputs: {
+ to: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ value: "100000",
+ },
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xad03e5b350645f2e4cdd066c30b2f6b708aa34bd4d1c5ca20fd81ecfe1656164",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
+ value: "0",
+ decoded: {
+ name: "approve",
+ signature: "approve(address,uint256)",
+ inputs: {
+ spender: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
+ value: "10000000",
+ },
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xd3106aaa4b7e530ac7530c8ea4984eec52670aabaf5969ae6bc8d8246e74c3c0",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
+ value: "0",
+ decoded: {
+ name: "initiateTransaction",
+ signature:
+ "initiateTransaction((bytes32,address,uint256,address,address,uint256,address,uint256,bytes,bytes),bytes)",
+ inputs: {},
+ },
+ },
+ {
+ chain_id: "8453",
+ hash: "0xd284b6e0dd938b4610ff1877c1d4692a8d10d83ec6be24789dc87e1ef4aa4756",
+ from_address: "0x1f846f6dae38e1c88d71eaa191760b15f38b7a37",
+ to_address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
+ value: "0",
+ decoded: {
+ name: "approve",
+ signature: "approve(address,uint256)",
+ inputs: {
+ spender: "0xf8ab2dbe6c43bf1a856471182290f91d621ba76d",
+ value: "1000000",
+ },
+ },
+ },
+];
+
+export const MultipleAssets: Story = {
+ args: {
+ data: transactionsStub,
+ isPending: false,
+ client: storybookThirdwebClient,
+ },
+};
+
+export const SingleAsset: Story = {
+ args: {
+ data: transactionsStub.slice(0, 1),
+ isPending: false,
+ client: storybookThirdwebClient,
+ },
+};
+
+export const EmptyAssets: Story = {
+ args: {
+ data: [],
+ isPending: false,
+ client: storybookThirdwebClient,
+ },
+};
+
+export const Loading: Story = {
+ args: {
+ data: [],
+ isPending: true,
+ client: storybookThirdwebClient,
+ },
+};
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.tsx
new file mode 100644
index 00000000000..004a98d134f
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/components/TransactionsSection/TransactionsSection.tsx
@@ -0,0 +1,196 @@
+import { Skeleton } from "@/components/ui/skeleton";
+import { isProd } from "@/constants/env-utils";
+import { useQuery } from "@tanstack/react-query";
+import { XIcon } from "lucide-react";
+import Link from "next/link";
+import type { ThirdwebClient } from "thirdweb";
+import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
+import { shortenAddress } from "thirdweb/utils";
+import { ChainIconClient } from "../../../../../components/icons/ChainIcon";
+import { useAllChainsData } from "../../../../../hooks/chains/allChains";
+import { nebulaAppThirdwebClient } from "../../utils/nebulaThirdwebClient";
+
+// Note: this is not the full object type returned from insight API, it only includes fields we care about
+export type WalletTransaction = {
+ chain_id: string;
+ value: string;
+ hash: string;
+ from_address: string;
+ to_address: string;
+ decoded: {
+ name: string;
+ signature: string;
+ inputs: null | object;
+ };
+};
+
+export function TransactionSectionUI(props: {
+ data: WalletTransaction[];
+ isPending: boolean;
+ client: ThirdwebClient;
+}) {
+ if (props.data.length === 0 && !props.isPending) {
+ return (
+
+
+
+
+
No Recent Activity
+
+ );
+ }
+
+ return (
+
+ {!props.isPending &&
+ props.data.map((asset) => (
+
+ ))}
+
+ {props.isPending &&
+ new Array(10).fill(null).map((_, index) => (
+ // biome-ignore lint/suspicious/noArrayIndexKey:
+
+ ))}
+
+ );
+}
+
+function SkeletonAssetItem() {
+ return (
+
+ );
+}
+
+function TransactionInfo(props: {
+ transaction: WalletTransaction;
+ client: ThirdwebClient;
+}) {
+ const { idToChain } = useAllChainsData();
+ const chainMeta = idToChain.get(Number(props.transaction.chain_id));
+ const title = getTransactionTitle(props.transaction);
+ const description = getTransactionDescription(props.transaction);
+ const explorer =
+ chainMeta?.explorers?.[0]?.url ||
+ `https://thirdweb.com/${props.transaction.chain_id}`;
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ {description && (
+
{description}
+ )}
+
+
+ );
+}
+
+function getTransactionTitle(transaction: WalletTransaction) {
+ if (transaction.decoded.name) {
+ return transaction.decoded.name;
+ }
+
+ if (transaction.decoded.signature) {
+ const nameFromSignature = transaction.decoded.signature.split("(")[0];
+ if (nameFromSignature) {
+ return nameFromSignature;
+ }
+ }
+
+ if (transaction.value !== "0") {
+ return "Transfer";
+ }
+
+ return "Transaction Sent";
+}
+
+function getTransactionDescription(transaction: WalletTransaction) {
+ if (
+ typeof transaction.decoded.inputs === "object" &&
+ transaction.decoded.inputs !== null
+ ) {
+ if (
+ "to" in transaction.decoded.inputs &&
+ typeof transaction.decoded.inputs.to === "string"
+ ) {
+ return `To: ${shortenAddress(transaction.decoded.inputs.to)}`;
+ }
+
+ if (
+ "spender" in transaction.decoded.inputs &&
+ typeof transaction.decoded.inputs.spender === "string"
+ ) {
+ return `Spender: ${shortenAddress(transaction.decoded.inputs.spender)}`;
+ }
+ }
+
+ return `To: ${shortenAddress(transaction.to_address)}`;
+}
+
+export function TransactionsSection(props: {
+ client: ThirdwebClient;
+}) {
+ const account = useActiveAccount();
+ const activeChain = useActiveWalletChain();
+
+ const txQuery = useQuery({
+ queryKey: ["v1/wallets/transactions", account?.address, activeChain?.id],
+ queryFn: async () => {
+ if (!account || !activeChain) {
+ return [];
+ }
+ const chains = [...new Set([1, 8453, 10, 137, activeChain.id])];
+ const url = new URL(
+ `https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/wallets/${account.address}/transactions`,
+ );
+ url.searchParams.set("limit", "10");
+ url.searchParams.set("decode", "true");
+ url.searchParams.set("clientId", nebulaAppThirdwebClient.clientId);
+
+ for (const chain of chains) {
+ url.searchParams.append("chain", chain.toString());
+ }
+
+ const response = await fetch(url.toString());
+ const json = (await response.json()) as {
+ data: WalletTransaction[];
+ };
+
+ return json.data;
+ },
+ retry: false,
+ enabled: !!account && !!activeChain,
+ });
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/hooks/useSessionsWithLocalOverrides.ts b/apps/dashboard/src/app/nebula-app/(app)/hooks/useSessionsWithLocalOverrides.ts
index c1e46724a42..4ab019f2772 100644
--- a/apps/dashboard/src/app/nebula-app/(app)/hooks/useSessionsWithLocalOverrides.ts
+++ b/apps/dashboard/src/app/nebula-app/(app)/hooks/useSessionsWithLocalOverrides.ts
@@ -15,7 +15,7 @@ export function useSessionsWithLocalOverrides(
if (index !== -1) {
mergedSessions[index] = session;
} else {
- mergedSessions.push(session);
+ mergedSessions.unshift(session);
}
}