Skip to content

Commit ad25126

Browse files
committed
[MNY-274] Dashboard: Generate swap token pages for popular tokens
1 parent 9bb65ad commit ad25126

File tree

9 files changed

+566
-219
lines changed

9 files changed

+566
-219
lines changed

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

Lines changed: 74 additions & 40 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 {
@@ -32,11 +33,21 @@ import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
3233
type PageType = "asset" | "bridge" | "chain";
3334

3435
export function BuyAndSwapEmbed(props: {
35-
chain: Chain;
36-
tokenAddress: string | undefined;
37-
buyAmount: string | undefined;
36+
buy:
37+
| {
38+
tokenAddress: string;
39+
chainId: number;
40+
amount?: string;
41+
}
42+
| undefined;
43+
sell:
44+
| {
45+
chainId: number;
46+
tokenAddress: string;
47+
amount?: string;
48+
}
49+
| undefined;
3850
pageType: PageType;
39-
isTestnet: boolean | undefined;
4051
wallets?: Wallet[];
4152
}) {
4253
const { theme } = useTheme();
@@ -87,8 +98,10 @@ export function BuyAndSwapEmbed(props: {
8798

8899
{tab === "buy" && (
89100
<BuyWidget
90-
amount={props.buyAmount || "1"}
91-
chain={props.chain}
101+
amount={props.buy?.amount || "1"}
102+
chain={
103+
props.buy?.chainId ? defineChain(props.buy.chainId) : undefined
104+
}
92105
className="!rounded-2xl !border-none"
93106
title=""
94107
client={client}
@@ -100,13 +113,19 @@ export function BuyAndSwapEmbed(props: {
100113
onError={(e, quote) => {
101114
const errorMessage = parseError(e);
102115

116+
const buyChainId =
117+
quote?.type === "buy"
118+
? quote.intent.destinationChainId
119+
: quote?.type === "onramp"
120+
? quote.intent.chainId
121+
: undefined;
122+
123+
if (!buyChainId) {
124+
return;
125+
}
126+
103127
reportTokenBuyFailed({
104-
buyTokenChainId:
105-
quote?.type === "buy"
106-
? quote.intent.destinationChainId
107-
: quote?.type === "onramp"
108-
? quote.intent.chainId
109-
: undefined,
128+
buyTokenChainId: buyChainId,
110129
buyTokenAddress:
111130
quote?.type === "buy"
112131
? quote.intent.destinationTokenAddress
@@ -119,21 +138,27 @@ export function BuyAndSwapEmbed(props: {
119138
if (props.pageType === "asset") {
120139
reportAssetBuyFailed({
121140
assetType: "coin",
122-
chainId: props.chain.id,
141+
chainId: buyChainId,
123142
error: errorMessage,
124143
contractType: undefined,
125-
is_testnet: props.isTestnet,
144+
is_testnet: false,
126145
});
127146
}
128147
}}
129148
onCancel={(quote) => {
149+
const buyChainId =
150+
quote?.type === "buy"
151+
? quote.intent.destinationChainId
152+
: quote?.type === "onramp"
153+
? quote.intent.chainId
154+
: undefined;
155+
156+
if (!buyChainId) {
157+
return;
158+
}
159+
130160
reportTokenBuyCancelled({
131-
buyTokenChainId:
132-
quote?.type === "buy"
133-
? quote.intent.destinationChainId
134-
: quote?.type === "onramp"
135-
? quote.intent.chainId
136-
: undefined,
161+
buyTokenChainId: buyChainId,
137162
buyTokenAddress:
138163
quote?.type === "buy"
139164
? quote.intent.destinationTokenAddress
@@ -146,24 +171,30 @@ export function BuyAndSwapEmbed(props: {
146171
if (props.pageType === "asset") {
147172
reportAssetBuyCancelled({
148173
assetType: "coin",
149-
chainId: props.chain.id,
174+
chainId: buyChainId,
150175
contractType: undefined,
151-
is_testnet: props.isTestnet,
176+
is_testnet: false,
152177
});
153178
}
154179
}}
155180
onSuccess={({ quote }) => {
181+
const buyChainId =
182+
quote?.type === "buy"
183+
? quote.intent.destinationChainId
184+
: quote?.type === "onramp"
185+
? quote.intent.chainId
186+
: undefined;
187+
188+
if (!buyChainId) {
189+
return;
190+
}
191+
156192
reportTokenBuySuccessful({
157-
buyTokenChainId:
158-
quote.type === "buy"
159-
? quote.intent.destinationChainId
160-
: quote.type === "onramp"
161-
? quote.intent.chainId
162-
: undefined,
193+
buyTokenChainId: buyChainId,
163194
buyTokenAddress:
164-
quote.type === "buy"
195+
quote?.type === "buy"
165196
? quote.intent.destinationTokenAddress
166-
: quote.type === "onramp"
197+
: quote?.type === "onramp"
167198
? quote.intent.tokenAddress
168199
: undefined,
169200
pageType: props.pageType,
@@ -172,14 +203,14 @@ export function BuyAndSwapEmbed(props: {
172203
if (props.pageType === "asset") {
173204
reportAssetBuySuccessful({
174205
assetType: "coin",
175-
chainId: props.chain.id,
206+
chainId: buyChainId,
176207
contractType: undefined,
177-
is_testnet: props.isTestnet,
208+
is_testnet: false,
178209
});
179210
}
180211
}}
181212
theme={themeObj}
182-
tokenAddress={props.tokenAddress as `0x${string}`}
213+
tokenAddress={props.buy?.tokenAddress as `0x${string}` | undefined}
183214
paymentMethods={["card"]}
184215
/>
185216
)}
@@ -196,14 +227,17 @@ export function BuyAndSwapEmbed(props: {
196227
}}
197228
prefill={{
198229
// buy this token by default
199-
buyToken: {
200-
chainId: props.chain.id,
201-
tokenAddress: props.tokenAddress,
202-
},
230+
buyToken: props.buy?.chainId
231+
? {
232+
chainId: props.buy.chainId,
233+
tokenAddress: props.buy.tokenAddress,
234+
}
235+
: undefined,
203236
// sell the native token by default (but if buytoken is a native token, don't set)
204-
sellToken: props.tokenAddress
237+
sellToken: props.sell?.chainId
205238
? {
206-
chainId: props.chain.id,
239+
chainId: props.sell.chainId,
240+
tokenAddress: props.sell.tokenAddress,
207241
}
208242
: undefined,
209243
}}

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
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+
sell={{
12+
chainId: props.chain.chainId,
13+
tokenAddress: NATIVE_TOKEN_ADDRESS,
14+
}}
15+
buy={{
16+
chainId: props.chain.chainId,
17+
tokenAddress: NATIVE_TOKEN_ADDRESS,
18+
}}
1619
pageType="chain"
1720
/>
1821
</GridPatternEmbedContainer>

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,16 @@ 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+
sell={{
204+
chainId: props.clientContract.chain.id,
205+
tokenAddress: props.clientContract.address,
206+
}}
207+
buy={{
208+
chainId: props.clientContract.chain.id,
209+
tokenAddress: props.clientContract.address,
210+
}}
205211
pageType="asset"
206-
isTestnet={props.chainMetadata.testnet}
207212
/>
208213
);
209214
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { cn } from "@workspace/ui/lib/utils";
2+
import { FaqAccordion } from "@/components/blocks/faq-section";
3+
import { UniversalBridgeEmbed } from "./client/UniversalBridgeEmbed";
4+
import { BridgePageHeader } from "./header";
5+
6+
export function BridgePageUI(props: {
7+
title: React.ReactNode;
8+
buy:
9+
| {
10+
tokenAddress: string;
11+
chainId: number;
12+
}
13+
| undefined;
14+
sell:
15+
| {
16+
chainId: number;
17+
tokenAddress: string;
18+
}
19+
| undefined;
20+
}) {
21+
return (
22+
<div className="grow flex flex-col">
23+
<BridgePageHeader />
24+
25+
<div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]">
26+
<DotsBackgroundPattern />
27+
<UniversalBridgeEmbed buy={props.buy} sell={props.sell} />
28+
</div>
29+
30+
<HeadingSection title={props.title} />
31+
32+
<div className="h-20 lg:h-40" />
33+
34+
<BridgeFaqSection />
35+
36+
<div className="h-32" />
37+
</div>
38+
);
39+
}
40+
41+
function HeadingSection(props: { title: React.ReactNode }) {
42+
return (
43+
<div className="container">
44+
<div className="mb-3 lg:mb-6">{props.title}</div>
45+
46+
<p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8">
47+
Seamlessly move your assets across 85+ chains with the best rates and
48+
fastest execution
49+
</p>
50+
51+
<div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center">
52+
<DataPill>85+ Chains Supported</DataPill>
53+
<DataPill>4500+ Tokens Supported</DataPill>
54+
<DataPill>9+ Million Routes Available</DataPill>
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
function DataPill(props: { children: React.ReactNode }) {
61+
return (
62+
<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">
63+
{props.children}
64+
</p>
65+
);
66+
}
67+
68+
function DotsBackgroundPattern(props: { className?: string }) {
69+
return (
70+
<div
71+
className={cn(
72+
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
73+
props.className,
74+
)}
75+
style={{
76+
backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
77+
backgroundSize: "24px 24px",
78+
maskImage:
79+
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
80+
}}
81+
/>
82+
);
83+
}
84+
85+
const bridgeFaqs: Array<{ title: string; description: string }> = [
86+
{
87+
title: "What is bridging in crypto?",
88+
description:
89+
"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.",
90+
},
91+
{
92+
title: "How does crypto bridging work?",
93+
description:
94+
"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",
95+
},
96+
{
97+
title: "What is a crypto asset swap?",
98+
description:
99+
"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.",
100+
},
101+
{
102+
title: "How can I get stablecoins like USDC or USDT?",
103+
description:
104+
"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.",
105+
},
106+
{
107+
title: "What is the cost of bridging and swapping?",
108+
description:
109+
"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.",
110+
},
111+
];
112+
113+
function BridgeFaqSection() {
114+
return (
115+
<section className="container max-w-2xl">
116+
<h2 className="text-2xl md:text-3xl font-semibold mb-4 lg:mb-8 tracking-tight text-center">
117+
Frequently Asked Questions
118+
</h2>
119+
<FaqAccordion faqs={bridgeFaqs} />
120+
</section>
121+
);
122+
}

0 commit comments

Comments
 (0)