diff --git a/.changeset/few-moments-add.md b/.changeset/few-moments-add.md deleted file mode 100644 index a709db25de1..00000000000 --- a/.changeset/few-moments-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"thirdweb": patch ---- - -Fixed a regression that prompted the user to pay the full amount in the TransactionWidget, rather than the difference from their current balance diff --git a/apps/dashboard/framer-rewrites.js b/apps/dashboard/framer-rewrites.js index 9016192b8ce..3eeefc525ff 100644 --- a/apps/dashboard/framer-rewrites.js +++ b/apps/dashboard/framer-rewrites.js @@ -9,7 +9,7 @@ module.exports = [ // -- build category "/wallets", "/account-abstraction", - "/universal-bridge", + "/payments", "/auth", "/in-app-wallets", "/transactions", diff --git a/apps/dashboard/redirects.js b/apps/dashboard/redirects.js index d6c1d7d9a6b..e0a4251e5ff 100644 --- a/apps/dashboard/redirects.js +++ b/apps/dashboard/redirects.js @@ -416,7 +416,7 @@ async function redirects() { source: "/connect/account-abstraction", }, { - destination: "/universal-bridge", + destination: "/payments", permanent: false, source: "/connect/universal-bridge", }, @@ -440,6 +440,11 @@ async function redirects() { permanent: false, source: "/rpc-edge", }, + { + destination: "/payments", + permanent: false, + source: "/universal-bridge", + }, ...legacyDashboardToTeamRedirects, ...projectPageRedirects, ...teamPageRedirects, diff --git a/apps/dashboard/src/@/api/support.ts b/apps/dashboard/src/@/api/support.ts new file mode 100644 index 00000000000..c71eaedb58d --- /dev/null +++ b/apps/dashboard/src/@/api/support.ts @@ -0,0 +1,327 @@ +"use server"; +import "server-only"; +import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs"; +import { getAuthToken, getAuthTokenWalletAddress } from "./auth-token"; + +export interface SupportTicket { + id: string; + status: "needs_response" | "in_progress" | "on_hold" | "closed" | "resolved"; + createdAt: string; + updatedAt: string; + messages?: SupportMessage[]; +} + +interface SupportMessage { + id: string; + content: string; + createdAt: string; + timestamp: string; + author?: { + name: string; + email: string; + type: "user" | "customer"; + }; +} + +interface CreateSupportTicketRequest { + message: string; + teamSlug: string; + title: string; +} + +interface SendMessageRequest { + ticketId: string; + teamSlug: string; + message: string; +} + +export async function getSupportTicketsByTeam( + teamSlug: string, + authToken?: string, +): Promise { + if (!teamSlug) { + throw new Error("Team slug is required to fetch support tickets"); + } + + const token = authToken || (await getAuthToken()); + if (!token) { + throw new Error("No auth token available"); + } + + // URL encode the team slug to handle special characters like # + const encodedTeamSlug = encodeURIComponent(teamSlug); + const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/list`; + + // Build the POST payload according to API spec + const payload = { + limit: 50, + descending: true, + }; + + const response = await fetch(apiUrl, { + body: JSON.stringify(payload), + cache: "no-store", + headers: { + Accept: "application/json", + "Accept-Encoding": "identity", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API Server error: ${response.status} - ${errorText}`); + } + const data: { data?: SupportTicket[] } = await response.json(); + const conversations = data.data || []; + return conversations; +} + +interface RawSupportMessage { + id: string; + text?: string; + timestamp?: string; + createdAt?: string; + isPrivateNote?: boolean; + sentByUser?: { + name: string; + email: string; + isExternal: boolean; + }; + // Add any other fields you use from the API +} + +export async function getSupportTicket( + ticketId: string, + teamSlug: string, + authToken?: string, +): Promise { + if (!ticketId || !teamSlug) { + throw new Error("Ticket ID and team slug are required"); + } + + const token = authToken || (await getAuthToken()); + if (!token) { + throw new Error("No auth token available"); + } + + // URL encode the team slug to handle special characters like # + const encodedTeamSlug = encodeURIComponent(teamSlug); + const encodedTicketId = encodeURIComponent(ticketId); + + const messagesPayload = { + limit: 100, + descending: false, + }; + + // Fetch conversation details and messages in parallel + const [conversationResponse, messagesResponse] = await Promise.all([ + fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}`, + { + cache: "no-store", + headers: { + Accept: "application/json", + "Accept-Encoding": "identity", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "GET", + }, + ), + fetch( + `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages/list`, + { + body: JSON.stringify(messagesPayload), + cache: "no-store", + headers: { + Accept: "application/json", + "Accept-Encoding": "identity", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", + }, + ), + ]); + + if (!conversationResponse.ok) { + if (conversationResponse.status === 404) { + return null; // Ticket not found + } + const errorText = await conversationResponse.text(); + throw new Error( + `API Server error: ${conversationResponse.status} - ${errorText}`, + ); + } + + const conversation: SupportTicket = await conversationResponse.json(); + + // Fetch and map messages if the messages request was successful + if (messagesResponse.ok) { + const messagesData: { data?: unknown[] } = await messagesResponse.json(); + const rawMessages = messagesData.data || []; + // Transform the raw messages to match our interface + const messages: SupportMessage[] = (rawMessages as RawSupportMessage[]) + .filter((msg) => { + // Filter out messages without content - check both text and text fields + const hasContent = msg.text && msg.text.length > 0; + const hasText = msg.text && msg.text.trim().length > 0; + // Filter out private notes - they should not be shown to customers + const isNotPrivateNote = !msg.isPrivateNote; + return (hasContent || hasText) && isNotPrivateNote; + }) + .map((msg) => { + // Use text if available and is a non-empty array, otherwise fall back to text + let content = ""; + if (typeof msg.text === "string" && msg.text.trim().length > 0) { + content = msg.text; + } + + // Clean up 'Email:' line to show only the plain email if it contains a mailto link + if (content) { + content = content + .split("\n") + .map((line) => { + if (line.trim().toLowerCase().startsWith("email:")) { + // Extract email from + const match = line.match(/]+)\|[^>]+>/); + if (match) { + return `Email: ${match[1]}`; + } + } + return line; + }) + .join("\n"); + } + + // Map the author information from sentByUser if available + const author = msg.sentByUser + ? { + name: msg.sentByUser.name, + email: msg.sentByUser.email, + type: (msg.sentByUser.isExternal ? "customer" : "user") as + | "user" + | "customer", + } + : undefined; + + return { + id: msg.id, + content: content, + createdAt: msg.timestamp || msg.createdAt || "", + timestamp: msg.timestamp || msg.createdAt || "", + author: author, + }; + }); + + conversation.messages = messages; + } else { + // Don't throw error, just leave messages empty + const _errorText = await messagesResponse.text(); + conversation.messages = []; + } + + return conversation; +} + +export async function createSupportTicket( + request: CreateSupportTicketRequest, +): Promise { + if (!request.teamSlug) { + throw new Error("Team slug is required to create support ticket"); + } + + const token = await getAuthToken(); + if (!token) { + throw new Error("No auth token available"); + } + + // Fetch wallet address (server-side) + const walletAddress = await getAuthTokenWalletAddress(); + + // URL encode the team slug to handle special characters like # + const encodedTeamSlug = encodeURIComponent(request.teamSlug); + const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations`; + + // Build the payload for creating a conversation + // If the message does not already include wallet address, prepend it + let message = request.message; + if (!message.includes("Wallet address:")) { + message = `Wallet address: ${String(walletAddress || "-")}\n${message}`; + } + + const payload = { + markdown: message.trim(), + title: request.title, + }; + + const body = JSON.stringify(payload); + const headers: Record = { + Accept: "application/json", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "Accept-Encoding": "identity", + }; + + const response = await fetch(apiUrl, { + body, + headers, + method: "POST", + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API Server error: ${response.status} - ${errorText}`); + } + + const createdConversation: SupportTicket = await response.json(); + return createdConversation; +} + +export async function sendMessageToTicket( + request: SendMessageRequest, +): Promise { + if (!request.ticketId || !request.teamSlug) { + throw new Error("Ticket ID and team slug are required"); + } + + const token = await getAuthToken(); + if (!token) { + throw new Error("No auth token available"); + } + + // URL encode the team slug and ticket ID to handle special characters like # + const encodedTeamSlug = encodeURIComponent(request.teamSlug); + const encodedTicketId = encodeURIComponent(request.ticketId); + const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages`; + + // Build the payload for sending a message + // Append /unthread send for customer messages to ensure proper routing + const messageWithUnthread = `${request.message.trim()}\n/unthread send`; + const payload = { + markdown: messageWithUnthread, + }; + + const body = JSON.stringify(payload); + const headers: Record = { + Accept: "application/json", + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "Accept-Encoding": "identity", + }; + + const response = await fetch(apiUrl, { + body, + headers, + method: "POST", + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API Server error: ${response.status} - ${errorText}`); + } + // Message sent successfully, no need to return anything +} diff --git a/apps/dashboard/src/@/api/webhook-configs.ts b/apps/dashboard/src/@/api/webhook-configs.ts index f62a12e0c4b..c830ecbc9db 100644 --- a/apps/dashboard/src/@/api/webhook-configs.ts +++ b/apps/dashboard/src/@/api/webhook-configs.ts @@ -37,7 +37,7 @@ type WebhookConfigsResponse = }; interface CreateWebhookConfigRequest { - topics: { id: string; filters: object | null }[]; + topicIdsWithFilters: { id: string; filters: object | null }[]; destinationUrl: string; description: string; isPaused?: boolean; @@ -76,7 +76,7 @@ type TopicsResponse = interface UpdateWebhookConfigRequest { destinationUrl?: string; - topics?: { id: string; filters: object | null }[]; + topicIdsWithFilters?: { id: string; filters: object | null }[]; description?: string; isPaused?: boolean; } diff --git a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx index afc6a22d283..d22a737fd5c 100644 --- a/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx +++ b/apps/dashboard/src/@/components/blocks/charts/area-chart.tsx @@ -29,6 +29,7 @@ type ThirdwebAreaChartProps = { title: string; description?: string; titleClassName?: string; + headerClassName?: string; }; customHeader?: React.ReactNode; // chart config @@ -52,6 +53,12 @@ type ThirdwebAreaChartProps = { toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode; toolTipValueFormatter?: (value: unknown) => React.ReactNode; emptyChartState?: React.ReactElement; + margin?: { + top?: number; + right?: number; + bottom?: number; + left?: number; + }; }; export function ThirdwebAreaChart( @@ -62,7 +69,7 @@ export function ThirdwebAreaChart( return ( {props.header && ( - + {props.header.title} @@ -85,7 +92,16 @@ export function ThirdwebAreaChart( {props.emptyChartState} ) : ( - + {props.yAxis && } )} diff --git a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx index 6d46e6e0092..d0cb45d8964 100644 --- a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx +++ b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx @@ -5,6 +5,7 @@ import { usePathname } from "next/navigation"; import { useCallback, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; import { useActiveWalletConnectionStatus } from "thirdweb/react"; +import type { Team } from "@/api/team"; import { Button } from "@/components/ui/button"; import { NebulaIcon } from "@/icons/NebulaIcon"; import { ChatBar } from "./ChatBar"; @@ -14,7 +15,7 @@ import type { ExamplePrompt, NebulaContext } from "./types"; export default function CustomChatContent(props: { authToken: string | undefined; - teamId: string | undefined; + team: Team; clientId: string | undefined; client: ThirdwebClient; examplePrompts: ExamplePrompt[]; @@ -31,14 +32,14 @@ export default function CustomChatContent(props: { client={props.client} clientId={props.clientId} examplePrompts={props.examplePrompts} - teamId={props.teamId} + team={props.team} /> ); } function CustomChatContentLoggedIn(props: { authToken: string; - teamId: string | undefined; + team: Team; clientId: string | undefined; client: ThirdwebClient; examplePrompts: ExamplePrompt[]; @@ -55,6 +56,10 @@ function CustomChatContentLoggedIn(props: { const [enableAutoScroll, setEnableAutoScroll] = useState(false); const connectionStatus = useActiveWalletConnectionStatus(); + // Support form state + const [showSupportForm, setShowSupportForm] = useState(false); + const [productLabel, setProductLabel] = useState(""); + const handleSendMessage = useCallback( async (userMessage: UserMessage) => { const abortController = new AbortController(); @@ -96,7 +101,7 @@ function CustomChatContentLoggedIn(props: { headers: { Authorization: `Bearer ${props.authToken}`, "Content-Type": "application/json", - ...(props.teamId ? { "x-team-id": props.teamId } : {}), + ...(props.team.id ? { "x-team-id": props.team.id } : {}), ...(props.clientId ? { "x-client-id": props.clientId } : {}), }, method: "POST", @@ -132,7 +137,7 @@ function CustomChatContentLoggedIn(props: { setEnableAutoScroll(false); } }, - [props.authToken, props.clientId, props.teamId, sessionId], + [props.authToken, props.clientId, props.team.id, sessionId], ); const handleFeedback = useCallback( @@ -165,7 +170,7 @@ function CustomChatContentLoggedIn(props: { headers: { Authorization: `Bearer ${props.authToken}`, "Content-Type": "application/json", - ...(props.teamId ? { "x-team-id": props.teamId } : {}), + ...(props.team.id ? { "x-team-id": props.team.id } : {}), }, method: "POST", }); @@ -188,7 +193,7 @@ function CustomChatContentLoggedIn(props: { // Consider implementing retry logic here } }, - [sessionId, props.authToken, props.teamId, messages], + [sessionId, props.authToken, props.team.id, messages], ); const showEmptyState = !userHasSubmittedMessage && messages.length === 0; @@ -212,8 +217,14 @@ function CustomChatContentLoggedIn(props: { sessionId={sessionId} setEnableAutoScroll={setEnableAutoScroll} useSmallText + showSupportForm={showSupportForm} + setShowSupportForm={setShowSupportForm} + productLabel={productLabel} + setProductLabel={setProductLabel} + team={props.team} /> )} + {/* Removed floating support case button and form */} { chatAbortController?.abort(); diff --git a/apps/dashboard/src/@/components/chat/CustomChats.tsx b/apps/dashboard/src/@/components/chat/CustomChats.tsx index d3953f18f7b..ab7bccafb4d 100644 --- a/apps/dashboard/src/@/components/chat/CustomChats.tsx +++ b/apps/dashboard/src/@/components/chat/CustomChats.tsx @@ -1,14 +1,18 @@ import { AlertCircleIcon, + ArrowRightIcon, MessageCircleIcon, ThumbsDownIcon, ThumbsUpIcon, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import type { ThirdwebClient } from "thirdweb"; +import type { Team } from "@/api/team"; import { MarkdownRenderer } from "@/components/blocks/markdown-renderer"; +import { Button } from "@/components/ui/button"; import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; import { cn } from "@/lib/utils"; +import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm"; import { Reasoning } from "./Reasoning"; // Define local types @@ -47,10 +51,18 @@ export function CustomChats(props: { useSmallText?: boolean; sendMessage: (message: UserMessage) => void; onFeedback?: (messageIndex: number, feedback: 1 | -1) => void; + // New props for support form + showSupportForm: boolean; + setShowSupportForm: (v: boolean) => void; + productLabel: string; + setProductLabel: (v: string) => void; + team: Team; }) { const { messages, setEnableAutoScroll, enableAutoScroll } = props; const scrollAnchorRef = useRef(null); const chatContainerRef = useRef(null); + // Add state to track if a support ticket was created + const [supportTicketCreated, setSupportTicketCreated] = useState(false); // auto scroll to bottom when messages change // eslint-disable-next-line no-restricted-syntax @@ -123,6 +135,68 @@ export function CustomChats(props: { sendMessage={props.sendMessage} sessionId={props.sessionId} /> + {/* Support Case Button/Form in last assistant message */} + {message.type === "assistant" && + index === props.messages.length - 1 && ( + <> + {/* Only show button/form if ticket not created */} + {!props.showSupportForm && !supportTicketCreated && ( +
+ +
+ )} + {/* Show form if open and ticket not created */} + {props.showSupportForm && !supportTicketCreated && ( +
+ { + props.setShowSupportForm(false); + props.setProductLabel(""); + setSupportTicketCreated(true); + }} + /> +
+ )} + {/* Show success message if ticket created */} + {supportTicketCreated && ( +
+
+ Your support ticket has been created! Our team + will get back to you soon. +
+ +
+ )} + + )} ); })} diff --git a/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx b/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx index 03941891434..84cb36d2879 100644 --- a/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx +++ b/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx @@ -7,6 +7,7 @@ export function CopyAddressButton(props: { address: string; className?: string; iconClassName?: string; + tooltip?: string; variant?: | "primary" | "default" @@ -24,7 +25,7 @@ export function CopyAddressButton(props: { copyIconPosition={props.copyIconPosition} textToCopy={props.address} textToShow={shortenedAddress} - tooltip="Copy Address" + tooltip={props.tooltip || "Copy Address"} variant={props.variant} /> ); diff --git a/apps/dashboard/src/@/components/ui/background-patterns.tsx b/apps/dashboard/src/@/components/ui/background-patterns.tsx index e9d0cbd0484..dce537223b1 100644 --- a/apps/dashboard/src/@/components/ui/background-patterns.tsx +++ b/apps/dashboard/src/@/components/ui/background-patterns.tsx @@ -1,3 +1,4 @@ +import { useId } from "react"; import { cn } from "@/lib/utils"; export function DotsBackgroundPattern(props: { className?: string }) { @@ -16,3 +17,70 @@ export function DotsBackgroundPattern(props: { className?: string }) { /> ); } + +interface GridPatternProps extends React.SVGProps { + width?: number; + height?: number; + x?: number; + y?: number; + squares?: Array<[x: number, y: number]>; + strokeDasharray?: string; + className?: string; + [key: string]: unknown; +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = "0", + squares, + className, + ...props +}: GridPatternProps) { + const id = useId(); + + return ( + + ); +} diff --git a/apps/dashboard/src/@/components/ui/card.tsx b/apps/dashboard/src/@/components/ui/card.tsx index 4927d2439e0..ae7a0598ea6 100644 --- a/apps/dashboard/src/@/components/ui/card.tsx +++ b/apps/dashboard/src/@/components/ui/card.tsx @@ -8,7 +8,7 @@ const Card = React.forwardRef< >(({ className, ...props }, ref) => (
) { return ( +
+ + + + - + - + + + + + - + - + - + - +
); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/supply-claimed-progress.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/supply-claimed-progress.tsx index 6ecc3eb7c71..c1a81bc96bf 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/supply-claimed-progress.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/_components/supply-claimed-progress.tsx @@ -3,16 +3,16 @@ import { Progress } from "@/components/ui/progress"; import { supplyFormatter } from "../nft/format"; export function SupplyClaimedProgress(props: { - claimedSupply: bigint; - totalSupply: bigint | "unlimited"; + claimedSupplyTokens: number; + totalSupplyTokens: number | "unlimited"; }) { // if total supply is unlimited - if (props.totalSupply === "unlimited") { + if (props.totalSupplyTokens === "unlimited") { return (

Claimed Supply - {supplyFormatter.format(props.claimedSupply)} /{" "} + {supplyFormatter.format(props.claimedSupplyTokens)} /{" "}

@@ -20,29 +20,29 @@ export function SupplyClaimedProgress(props: { } // if total supply is 0 - don't show anything - if (props.totalSupply === 0n) { + if (props.totalSupplyTokens === 0) { return null; } // multiply by 10k to have precision up to 2 decimal places in percentage value - const soldFractionTimes10KBigInt = - (props.claimedSupply * 10000n) / props.totalSupply; - const soldPercentage = Number(soldFractionTimes10KBigInt) / 100; + const soldPercentage = + (props.claimedSupplyTokens / props.totalSupplyTokens) * 100; + + const percentToShow = soldPercentage.toFixed(2); return (
Supply Claimed - {supplyFormatter.format(props.claimedSupply)} /{" "} - {supplyFormatter.format(props.totalSupply)} + {supplyFormatter.format(props.claimedSupplyTokens)} /{" "} + {supplyFormatter.format(props.totalSupplyTokens)}

- {soldPercentage === 0 && props.claimedSupply !== 0n && "~"} - {soldPercentage.toFixed(2)}% Sold + {percentToShow === "0.00" ? "~0.00%" : `${percentToShow}%`} Sold

); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_apis/token-price-data.ts b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_apis/token-price-data.ts new file mode 100644 index 00000000000..c76d25a7e9b --- /dev/null +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_apis/token-price-data.ts @@ -0,0 +1,51 @@ +import "server-only"; +import { isProd } from "@/constants/env-utils"; +import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs"; + +export type TokenPriceData = { + price_usd: number; + price_usd_cents: number; + percent_change_24h: number; + market_cap_usd: number; + volume_24h_usd: number; + volume_change_24h: number; + holders: number; + historical_prices: Array<{ + date: string; + price_usd: number; + price_usd_cents: number; + }>; +}; + +export async function getTokenPriceData(params: { + chainId: number; + contractAddress: string; +}) { + try { + const url = new URL( + `https://insight.${isProd ? "thirdweb" : "thirdweb-dev"}.com/v1/tokens/price`, + ); + + url.searchParams.set("include_historical_prices", "true"); + url.searchParams.set("chain_id", params.chainId.toString()); + url.searchParams.set("address", params.contractAddress); + url.searchParams.set("include_holders", "true"); + + const res = await fetch(url, { + headers: { + "x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY, + }, + }); + if (!res.ok) { + console.error("Failed to fetch token price data", await res.text()); + return undefined; + } + + const json = await res.json(); + const priceData = json.data[0] as TokenPriceData | undefined; + + return priceData; + } catch { + return undefined; + } +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx deleted file mode 100644 index 7bef42d4f05..00000000000 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractCreatorBadge.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ThirdwebContract } from "thirdweb"; -import { WalletAddress } from "@/components/blocks/wallet-address"; - -export function ContractCreatorBadge(props: { - contractCreator: string; - clientContract: ThirdwebContract; -}) { - return ( -
- By - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx index 28bb90f2014..0c926fa4a3a 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx @@ -1,4 +1,9 @@ -import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react"; +import { + ExternalLinkIcon, + GlobeIcon, + Settings2Icon, + UserIcon, +} from "lucide-react"; import Link from "next/link"; import { useMemo } from "react"; import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb"; @@ -19,7 +24,6 @@ import { YoutubeIcon } from "@/icons/brand-icons/YoutubeIcon"; import { ChainIconClient } from "@/icons/ChainIcon"; import { cn } from "@/lib/utils"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; -import { ContractCreatorBadge } from "./ContractCreatorBadge"; const platformToIcons: Record> = { discord: DiscordIcon, @@ -44,6 +48,7 @@ export function ContractHeaderUI(props: { socialUrls: object; imageClassName?: string; contractCreator: string | null; + className?: string; }) { const socialUrls = useMemo(() => { const socialUrlsValue: { name: string; href: string }[] = []; @@ -64,41 +69,45 @@ export function ContractHeaderUI(props: { ?.replace("Mainnet", "") .trim(); - const explorersToShow = getExplorersToShow(props.chainMetadata); + const validBlockExplorer = getExplorerToShow(props.chainMetadata); return ( -
- {props.image && ( - - {props.name[0]} -
- } - src={ - props.image - ? resolveSchemeWithErrorHandler({ - client: props.clientContract.client, - uri: props.image, - }) - : "" - } - /> +
+
+
+ + {props.name[0]} +
+ } + src={ + props.image + ? resolveSchemeWithErrorHandler({ + client: props.clientContract.client, + uri: props.image, + }) + : "" + } + /> +
+
-
+
{/* top row */}
-

+

{props.name}

-
+
- + {props.contractCreator && + validBlockExplorer && + props.contractCreator !== ZERO_ADDRESS && ( + + )} {socialUrls .toSorted((a, b) => { @@ -148,13 +160,14 @@ export function ContractHeaderUI(props: {
{/* bottom row */} -
- {props.contractCreator && props.contractCreator !== ZERO_ADDRESS && ( - - )} +
+ - {explorersToShow?.map((validBlockExplorer) => ( + {validBlockExplorer && ( - ))} + )} {/* TODO - render social links here */}
@@ -204,19 +217,22 @@ function isValidUrl(url: string) { } } -function getExplorersToShow(chainMetadata: ChainMetadata) { - const validBlockExplorers = chainMetadata.explorers - ?.filter((e) => e.standard === "EIP3091") - ?.slice(0, 2); +function getExplorerToShow(chainMetadata: ChainMetadata) { + const validBlockExplorers = chainMetadata.explorers?.filter( + (e) => e.standard === "EIP3091", + ); - return validBlockExplorers?.slice(0, 1); + return validBlockExplorers?.[0]; } -function BadgeLink(props: { name: string; href: string }) { +function BadgeLink(props: { name: string; href: string; className?: string }) { return (