From b1a780832ed97d80399a650beab537a89c628278 Mon Sep 17 00:00:00 2001 From: MananTank Date: Fri, 17 Oct 2025 23:49:31 +0000 Subject: [PATCH] [MNY-274] Dashboard: Generate swap token pages for popular tokens (#8278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on refactoring the `BuyAndSwapEmbed` component and related functionalities to improve the handling of buy and swap operations across different tokens and chains, enhancing the user experience in the bridge application. ### Detailed summary - Removed `chain` and `tokenAddress` props from `BuyAndSwapEmbed`. - Introduced `buyTab` and `swapTab` props to manage buy/sell token information. - Updated `UniversalBridgeEmbed` to accept new props structure. - Added token pair management in `slug-map.ts`. - Created utility functions for token pair data retrieval. - Refactored `BridgePageUI` to accommodate the new props and structure. - Enhanced error handling and reporting in `BuyAndSwapEmbed`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit - **New Features** - Added a Bridge UI and dedicated token-pair pages with static routes, metadata, and token-pair utilities for cross-chain slugs. - Introduced a Bridge page UI with FAQ, data summary, and an embedded universal bridge widget. - **Improvements** - Unified buy/sell input structure for embeds and prefills, simplifying embed wiring and page rendering. - More robust handling of chain/token data and reporting for buy/swap flows; removed legacy testnet flag usage. --- .../@/components/blocks/BuyAndSwapEmbed.tsx | 137 ++++++++----- .../components/client/BuyFundsSection.tsx | 20 +- .../public-pages/erc20/erc20.tsx | 18 +- .../src/app/bridge/components/bridge-page.tsx | 113 +++++++++++ .../client/UniversalBridgeEmbed.tsx | 28 +-- apps/dashboard/src/app/bridge/constants.ts | 59 ------ .../exchange/[token-pair]/opengraph-image.png | Bin 0 -> 106987 bytes .../app/bridge/exchange/[token-pair]/page.tsx | 181 ++++++++++++++++++ .../bridge/exchange/[token-pair]/slug-map.ts | 149 ++++++++++++++ apps/dashboard/src/app/bridge/page.tsx | 163 ++-------------- 10 files changed, 582 insertions(+), 286 deletions(-) create mode 100644 apps/dashboard/src/app/bridge/components/bridge-page.tsx delete mode 100644 apps/dashboard/src/app/bridge/constants.ts create mode 100644 apps/dashboard/src/app/bridge/exchange/[token-pair]/opengraph-image.png create mode 100644 apps/dashboard/src/app/bridge/exchange/[token-pair]/page.tsx create mode 100644 apps/dashboard/src/app/bridge/exchange/[token-pair]/slug-map.ts diff --git a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx index 6fbd4981435..71dcdcee11a 100644 --- a/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx +++ b/apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx @@ -1,8 +1,9 @@ +/* eslint-disable no-restricted-syntax */ "use client"; import { useTheme } from "next-themes"; import { useEffect, useMemo, useRef, useState } from "react"; -import type { Chain } from "thirdweb"; +import { defineChain } from "thirdweb"; import { BuyWidget, SwapWidget } from "thirdweb/react"; import type { Wallet } from "thirdweb/wallets"; import { @@ -31,14 +32,41 @@ import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server"; type PageType = "asset" | "bridge" | "chain"; -export function BuyAndSwapEmbed(props: { - chain: Chain; - tokenAddress: string | undefined; - buyAmount: string | undefined; +export type BuyAndSwapEmbedProps = { + buyTab: + | { + buyToken: + | { + tokenAddress: string; + chainId: number; + amount?: string; + } + | undefined; + } + | undefined; + swapTab: + | { + sellToken: + | { + chainId: number; + tokenAddress: string; + amount?: string; + } + | undefined; + buyToken: + | { + chainId: number; + tokenAddress: string; + amount?: string; + } + | undefined; + } + | undefined; pageType: PageType; - isTestnet: boolean | undefined; wallets?: Wallet[]; -}) { +}; + +export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) { const { theme } = useTheme(); const [tab, setTab] = useState<"buy" | "swap">("swap"); const themeObj = getSDKTheme(theme === "light" ? "light" : "dark"); @@ -87,8 +115,15 @@ export function BuyAndSwapEmbed(props: { {tab === "buy" && ( { const errorMessage = parseError(e); + const buyChainId = + quote?.type === "buy" + ? quote.intent.destinationChainId + : quote?.type === "onramp" + ? quote.intent.chainId + : undefined; + + if (!buyChainId) { + return; + } + reportTokenBuyFailed({ - buyTokenChainId: - quote?.type === "buy" - ? quote.intent.destinationChainId - : quote?.type === "onramp" - ? quote.intent.chainId - : undefined, + buyTokenChainId: buyChainId, buyTokenAddress: quote?.type === "buy" ? quote.intent.destinationTokenAddress @@ -119,21 +160,27 @@ export function BuyAndSwapEmbed(props: { if (props.pageType === "asset") { reportAssetBuyFailed({ assetType: "coin", - chainId: props.chain.id, + chainId: buyChainId, error: errorMessage, contractType: undefined, - is_testnet: props.isTestnet, + is_testnet: false, }); } }} onCancel={(quote) => { + const buyChainId = + quote?.type === "buy" + ? quote.intent.destinationChainId + : quote?.type === "onramp" + ? quote.intent.chainId + : undefined; + + if (!buyChainId) { + return; + } + reportTokenBuyCancelled({ - buyTokenChainId: - quote?.type === "buy" - ? quote.intent.destinationChainId - : quote?.type === "onramp" - ? quote.intent.chainId - : undefined, + buyTokenChainId: buyChainId, buyTokenAddress: quote?.type === "buy" ? quote.intent.destinationTokenAddress @@ -146,24 +193,30 @@ export function BuyAndSwapEmbed(props: { if (props.pageType === "asset") { reportAssetBuyCancelled({ assetType: "coin", - chainId: props.chain.id, + chainId: buyChainId, contractType: undefined, - is_testnet: props.isTestnet, + is_testnet: false, }); } }} onSuccess={({ quote }) => { + const buyChainId = + quote?.type === "buy" + ? quote.intent.destinationChainId + : quote?.type === "onramp" + ? quote.intent.chainId + : undefined; + + if (!buyChainId) { + return; + } + reportTokenBuySuccessful({ - buyTokenChainId: - quote.type === "buy" - ? quote.intent.destinationChainId - : quote.type === "onramp" - ? quote.intent.chainId - : undefined, + buyTokenChainId: buyChainId, buyTokenAddress: - quote.type === "buy" + quote?.type === "buy" ? quote.intent.destinationTokenAddress - : quote.type === "onramp" + : quote?.type === "onramp" ? quote.intent.tokenAddress : undefined, pageType: props.pageType, @@ -172,14 +225,13 @@ export function BuyAndSwapEmbed(props: { if (props.pageType === "asset") { reportAssetBuySuccessful({ assetType: "coin", - chainId: props.chain.id, + chainId: buyChainId, contractType: undefined, - is_testnet: props.isTestnet, + is_testnet: false, }); } }} theme={themeObj} - tokenAddress={props.tokenAddress as `0x${string}`} paymentMethods={["card"]} /> )} @@ -195,17 +247,8 @@ export function BuyAndSwapEmbed(props: { appMetadata: appMetadata, }} prefill={{ - // buy this token by default - buyToken: { - chainId: props.chain.id, - tokenAddress: props.tokenAddress, - }, - // sell the native token by default (but if buytoken is a native token, don't set) - sellToken: props.tokenAddress - ? { - chainId: props.chain.id, - } - : undefined, + buyToken: props.swapTab?.buyToken, + sellToken: props.swapTab?.sellToken, }} onError={(error, quote) => { const errorMessage = parseError(error); diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/BuyFundsSection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/BuyFundsSection.tsx index fb43823c420..eaf23075770 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/BuyFundsSection.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/BuyFundsSection.tsx @@ -1,18 +1,26 @@ "use client"; +import { NATIVE_TOKEN_ADDRESS } from "thirdweb"; import type { ChainMetadata } from "thirdweb/chains"; import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed"; import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container"; -import { defineDashboardChain } from "@/lib/defineDashboardChain"; export function BuyFundsSection(props: { chain: ChainMetadata }) { return ( diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx index fb97859854c..b3952ca31d4 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx @@ -199,11 +199,21 @@ function BuyEmbed(props: { if (!props.claimConditionMeta) { return ( ); } diff --git a/apps/dashboard/src/app/bridge/components/bridge-page.tsx b/apps/dashboard/src/app/bridge/components/bridge-page.tsx new file mode 100644 index 00000000000..7378b87b25f --- /dev/null +++ b/apps/dashboard/src/app/bridge/components/bridge-page.tsx @@ -0,0 +1,113 @@ +import { cn } from "@workspace/ui/lib/utils"; +import type { BuyAndSwapEmbedProps } from "@/components/blocks/BuyAndSwapEmbed"; +import { FaqAccordion } from "@/components/blocks/faq-section"; +import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed"; +import { BridgePageHeader } from "./header"; + +export function BridgePageUI(props: { + title: React.ReactNode; + buyTab: BuyAndSwapEmbedProps["buyTab"]; + swapTab: BuyAndSwapEmbedProps["swapTab"]; +}) { + return ( +
+ + +
+ + +
+ + + +
+ + + +
+
+ ); +} + +function HeadingSection(props: { title: React.ReactNode }) { + return ( +
+
{props.title}
+ +

+ Seamlessly move your assets across 85+ chains with the best rates and + fastest execution +

+ +
+ 85+ Chains Supported + 4500+ Tokens Supported + 9+ Million Routes Available +
+
+ ); +} + +function DataPill(props: { children: React.ReactNode }) { + return ( +

+ {props.children} +

+ ); +} + +function DotsBackgroundPattern(props: { className?: string }) { + return ( +