Skip to content

Commit b1a7808

Browse files
committed
[MNY-274] Dashboard: Generate swap token pages for popular tokens (#8278)
<!-- ## 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 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}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9bb65ad commit b1a7808

File tree

10 files changed

+582
-286
lines changed

10 files changed

+582
-286
lines changed

apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
/* eslint-disable no-restricted-syntax */
12
"use client";
23

34
import { useTheme } from "next-themes";
45
import { useEffect, useMemo, useRef, useState } from "react";
5-
import type { Chain } from "thirdweb";
6+
import { defineChain } from "thirdweb";
67
import { BuyWidget, SwapWidget } from "thirdweb/react";
78
import type { Wallet } from "thirdweb/wallets";
89
import {
@@ -31,14 +32,41 @@ import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
3132

3233
type PageType = "asset" | "bridge" | "chain";
3334

34-
export function BuyAndSwapEmbed(props: {
35-
chain: Chain;
36-
tokenAddress: string | undefined;
37-
buyAmount: string | undefined;
35+
export type BuyAndSwapEmbedProps = {
36+
buyTab:
37+
| {
38+
buyToken:
39+
| {
40+
tokenAddress: string;
41+
chainId: number;
42+
amount?: string;
43+
}
44+
| undefined;
45+
}
46+
| undefined;
47+
swapTab:
48+
| {
49+
sellToken:
50+
| {
51+
chainId: number;
52+
tokenAddress: string;
53+
amount?: string;
54+
}
55+
| undefined;
56+
buyToken:
57+
| {
58+
chainId: number;
59+
tokenAddress: string;
60+
amount?: string;
61+
}
62+
| undefined;
63+
}
64+
| undefined;
3865
pageType: PageType;
39-
isTestnet: boolean | undefined;
4066
wallets?: Wallet[];
41-
}) {
67+
};
68+
69+
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
4270
const { theme } = useTheme();
4371
const [tab, setTab] = useState<"buy" | "swap">("swap");
4472
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
@@ -87,8 +115,15 @@ export function BuyAndSwapEmbed(props: {
87115

88116
{tab === "buy" && (
89117
<BuyWidget
90-
amount={props.buyAmount || "1"}
91-
chain={props.chain}
118+
amount={props.buyTab?.buyToken?.amount || "1"}
119+
chain={
120+
props.buyTab?.buyToken?.chainId
121+
? defineChain(props.buyTab.buyToken.chainId)
122+
: undefined
123+
}
124+
tokenAddress={
125+
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
126+
}
92127
className="!rounded-2xl !border-none"
93128
title=""
94129
client={client}
@@ -100,13 +135,19 @@ export function BuyAndSwapEmbed(props: {
100135
onError={(e, quote) => {
101136
const errorMessage = parseError(e);
102137

138+
const buyChainId =
139+
quote?.type === "buy"
140+
? quote.intent.destinationChainId
141+
: quote?.type === "onramp"
142+
? quote.intent.chainId
143+
: undefined;
144+
145+
if (!buyChainId) {
146+
return;
147+
}
148+
103149
reportTokenBuyFailed({
104-
buyTokenChainId:
105-
quote?.type === "buy"
106-
? quote.intent.destinationChainId
107-
: quote?.type === "onramp"
108-
? quote.intent.chainId
109-
: undefined,
150+
buyTokenChainId: buyChainId,
110151
buyTokenAddress:
111152
quote?.type === "buy"
112153
? quote.intent.destinationTokenAddress
@@ -119,21 +160,27 @@ export function BuyAndSwapEmbed(props: {
119160
if (props.pageType === "asset") {
120161
reportAssetBuyFailed({
121162
assetType: "coin",
122-
chainId: props.chain.id,
163+
chainId: buyChainId,
123164
error: errorMessage,
124165
contractType: undefined,
125-
is_testnet: props.isTestnet,
166+
is_testnet: false,
126167
});
127168
}
128169
}}
129170
onCancel={(quote) => {
171+
const buyChainId =
172+
quote?.type === "buy"
173+
? quote.intent.destinationChainId
174+
: quote?.type === "onramp"
175+
? quote.intent.chainId
176+
: undefined;
177+
178+
if (!buyChainId) {
179+
return;
180+
}
181+
130182
reportTokenBuyCancelled({
131-
buyTokenChainId:
132-
quote?.type === "buy"
133-
? quote.intent.destinationChainId
134-
: quote?.type === "onramp"
135-
? quote.intent.chainId
136-
: undefined,
183+
buyTokenChainId: buyChainId,
137184
buyTokenAddress:
138185
quote?.type === "buy"
139186
? quote.intent.destinationTokenAddress
@@ -146,24 +193,30 @@ export function BuyAndSwapEmbed(props: {
146193
if (props.pageType === "asset") {
147194
reportAssetBuyCancelled({
148195
assetType: "coin",
149-
chainId: props.chain.id,
196+
chainId: buyChainId,
150197
contractType: undefined,
151-
is_testnet: props.isTestnet,
198+
is_testnet: false,
152199
});
153200
}
154201
}}
155202
onSuccess={({ quote }) => {
203+
const buyChainId =
204+
quote?.type === "buy"
205+
? quote.intent.destinationChainId
206+
: quote?.type === "onramp"
207+
? quote.intent.chainId
208+
: undefined;
209+
210+
if (!buyChainId) {
211+
return;
212+
}
213+
156214
reportTokenBuySuccessful({
157-
buyTokenChainId:
158-
quote.type === "buy"
159-
? quote.intent.destinationChainId
160-
: quote.type === "onramp"
161-
? quote.intent.chainId
162-
: undefined,
215+
buyTokenChainId: buyChainId,
163216
buyTokenAddress:
164-
quote.type === "buy"
217+
quote?.type === "buy"
165218
? quote.intent.destinationTokenAddress
166-
: quote.type === "onramp"
219+
: quote?.type === "onramp"
167220
? quote.intent.tokenAddress
168221
: undefined,
169222
pageType: props.pageType,
@@ -172,14 +225,13 @@ export function BuyAndSwapEmbed(props: {
172225
if (props.pageType === "asset") {
173226
reportAssetBuySuccessful({
174227
assetType: "coin",
175-
chainId: props.chain.id,
228+
chainId: buyChainId,
176229
contractType: undefined,
177-
is_testnet: props.isTestnet,
230+
is_testnet: false,
178231
});
179232
}
180233
}}
181234
theme={themeObj}
182-
tokenAddress={props.tokenAddress as `0x${string}`}
183235
paymentMethods={["card"]}
184236
/>
185237
)}
@@ -195,17 +247,8 @@ export function BuyAndSwapEmbed(props: {
195247
appMetadata: appMetadata,
196248
}}
197249
prefill={{
198-
// buy this token by default
199-
buyToken: {
200-
chainId: props.chain.id,
201-
tokenAddress: props.tokenAddress,
202-
},
203-
// sell the native token by default (but if buytoken is a native token, don't set)
204-
sellToken: props.tokenAddress
205-
? {
206-
chainId: props.chain.id,
207-
}
208-
: undefined,
250+
buyToken: props.swapTab?.buyToken,
251+
sellToken: props.swapTab?.sellToken,
209252
}}
210253
onError={(error, quote) => {
211254
const errorMessage = parseError(error);

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/BuyFundsSection.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
"use client";
2+
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
23
import type { ChainMetadata } from "thirdweb/chains";
34
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
45
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
5-
import { defineDashboardChain } from "@/lib/defineDashboardChain";
66

77
export function BuyFundsSection(props: { chain: ChainMetadata }) {
88
return (
99
<GridPatternEmbedContainer>
1010
<BuyAndSwapEmbed
11-
isTestnet={props.chain.testnet}
12-
// eslint-disable-next-line no-restricted-syntax
13-
chain={defineDashboardChain(props.chain.chainId, props.chain)}
14-
buyAmount={undefined}
15-
tokenAddress={undefined}
11+
swapTab={{
12+
sellToken: {
13+
chainId: props.chain.chainId,
14+
tokenAddress: NATIVE_TOKEN_ADDRESS,
15+
},
16+
buyToken: undefined,
17+
}}
18+
buyTab={{
19+
buyToken: {
20+
chainId: props.chain.chainId,
21+
tokenAddress: NATIVE_TOKEN_ADDRESS,
22+
},
23+
}}
1624
pageType="chain"
1725
/>
1826
</GridPatternEmbedContainer>

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/erc20.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,21 @@ function BuyEmbed(props: {
199199
if (!props.claimConditionMeta) {
200200
return (
201201
<BuyAndSwapEmbed
202-
chain={props.clientContract.chain}
203-
tokenAddress={props.clientContract.address}
204-
buyAmount={undefined}
202+
// chain={props.clientContract.chain}
203+
swapTab={{
204+
sellToken: {
205+
chainId: props.clientContract.chain.id,
206+
tokenAddress: props.clientContract.address,
207+
},
208+
buyToken: undefined,
209+
}}
210+
buyTab={{
211+
buyToken: {
212+
chainId: props.clientContract.chain.id,
213+
tokenAddress: props.clientContract.address,
214+
},
215+
}}
205216
pageType="asset"
206-
isTestnet={props.chainMetadata.testnet}
207217
/>
208218
);
209219
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { cn } from "@workspace/ui/lib/utils";
2+
import type { BuyAndSwapEmbedProps } from "@/components/blocks/BuyAndSwapEmbed";
3+
import { FaqAccordion } from "@/components/blocks/faq-section";
4+
import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed";
5+
import { BridgePageHeader } from "./header";
6+
7+
export function BridgePageUI(props: {
8+
title: React.ReactNode;
9+
buyTab: BuyAndSwapEmbedProps["buyTab"];
10+
swapTab: BuyAndSwapEmbedProps["swapTab"];
11+
}) {
12+
return (
13+
<div className="grow flex flex-col">
14+
<BridgePageHeader />
15+
16+
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]">
17+
<DotsBackgroundPattern />
18+
<UniversalBridgeEmbed buyTab={props.buyTab} swapTab={props.swapTab} />
19+
</div>
20+
21+
<HeadingSection title={props.title} />
22+
23+
<div className="h-20 lg:h-40" />
24+
25+
<BridgeFaqSection />
26+
27+
<div className="h-32" />
28+
</div>
29+
);
30+
}
31+
32+
function HeadingSection(props: { title: React.ReactNode }) {
33+
return (
34+
<div className="container">
35+
<div className="mb-3 lg:mb-6">{props.title}</div>
36+
37+
<p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8">
38+
Seamlessly move your assets across 85+ chains with the best rates and
39+
fastest execution
40+
</p>
41+
42+
<div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center">
43+
<DataPill>85+ Chains Supported</DataPill>
44+
<DataPill>4500+ Tokens Supported</DataPill>
45+
<DataPill>9+ Million Routes Available</DataPill>
46+
</div>
47+
</div>
48+
);
49+
}
50+
51+
function DataPill(props: { children: React.ReactNode }) {
52+
return (
53+
<p className="bg-card flex items-center text-xs lg:text-sm gap-1.5 text-foreground border rounded-full px-8 lg:px-3 py-1.5 hover:text-foreground transition-colors duration-300">
54+
{props.children}
55+
</p>
56+
);
57+
}
58+
59+
function DotsBackgroundPattern(props: { className?: string }) {
60+
return (
61+
<div
62+
className={cn(
63+
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
64+
props.className,
65+
)}
66+
style={{
67+
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
68+
backgroundSize: "24px 24px",
69+
maskImage:
70+
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
71+
}}
72+
/>
73+
);
74+
}
75+
76+
const bridgeFaqs: Array<{ title: string; description: string }> = [
77+
{
78+
title: "What is bridging in crypto?",
79+
description:
80+
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
81+
},
82+
{
83+
title: "How does crypto bridging work?",
84+
description:
85+
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
86+
},
87+
{
88+
title: "What is a crypto asset swap?",
89+
description:
90+
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
91+
},
92+
{
93+
title: "How can I get stablecoins like USDC or USDT?",
94+
description:
95+
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
96+
},
97+
{
98+
title: "What is the cost of bridging and swapping?",
99+
description:
100+
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
101+
},
102+
];
103+
104+
function BridgeFaqSection() {
105+
return (
106+
<section className="container max-w-2xl">
107+
<h2 className="text-2xl md:text-3xl font-semibold mb-4 lg:mb-8 tracking-tight text-center">
108+
Frequently Asked Questions
109+
</h2>
110+
<FaqAccordion faqs={bridgeFaqs} />
111+
</section>
112+
);
113+
}

0 commit comments

Comments
 (0)