-
Notifications
You must be signed in to change notification settings - Fork 619
[MNY-274] Dashboard: Generate swap token pages for popular tokens #8278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" && ( | ||
| <BuyWidget | ||
| amount={props.buyAmount || "1"} | ||
| chain={props.chain} | ||
| amount={props.buyTab?.buyToken?.amount || "1"} | ||
| chain={ | ||
| props.buyTab?.buyToken?.chainId | ||
| ? defineChain(props.buyTab.buyToken.chainId) | ||
| : undefined | ||
| } | ||
| tokenAddress={ | ||
| props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined | ||
| } | ||
| className="!rounded-2xl !border-none" | ||
| title="" | ||
| client={client} | ||
|
|
@@ -100,13 +135,19 @@ export function BuyAndSwapEmbed(props: { | |
| onError={(e, quote) => { | ||
| const errorMessage = parseError(e); | ||
|
|
||
| const buyChainId = | ||
| quote?.type === "buy" | ||
| ? quote.intent.destinationChainId | ||
| : quote?.type === "onramp" | ||
| ? quote.intent.chainId | ||
| : undefined; | ||
|
|
||
| if (!buyChainId) { | ||
| return; | ||
| } | ||
|
Comment on lines
+138
to
+147
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Extract duplicated buyChainId logic into a helper function. The buyChainId extraction logic is duplicated across Extract a helper function at the top of the file: function getBuyChainId(
quote:
| { type: "buy"; intent: { destinationChainId: number } }
| { type: "onramp"; intent: { chainId: number } }
| undefined
): number | undefined {
return quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined;
}Then simplify all three callbacks: onError={(e, quote) => {
const errorMessage = parseError(e);
- const buyChainId =
- quote?.type === "buy"
- ? quote.intent.destinationChainId
- : quote?.type === "onramp"
- ? quote.intent.chainId
- : undefined;
-
- if (!buyChainId) {
- return;
- }
+ const buyChainId = getBuyChainId(quote);
+ if (!buyChainId) return;Apply the same pattern to Also applies to: 171-180, 203-212 🤖 Prompt for AI Agents |
||
|
|
||
| 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, | ||
MananTank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
| } | ||
| }} | ||
| 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="grow flex flex-col"> | ||
| <BridgePageHeader /> | ||
|
|
||
| <div className="flex grow items-center justify-center px-4 relative pt-12 pb-20 lg:py-28 min-h-[calc(100dvh-60px)]"> | ||
| <DotsBackgroundPattern /> | ||
| <UniversalBridgeEmbed buyTab={props.buyTab} swapTab={props.swapTab} /> | ||
| </div> | ||
|
|
||
| <HeadingSection title={props.title} /> | ||
|
|
||
| <div className="h-20 lg:h-40" /> | ||
|
|
||
| <BridgeFaqSection /> | ||
|
|
||
| <div className="h-32" /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function HeadingSection(props: { title: React.ReactNode }) { | ||
| return ( | ||
| <div className="container"> | ||
| <div className="mb-3 lg:mb-6">{props.title}</div> | ||
|
|
||
| <p className="text-muted-foreground text-sm text-pretty text-center lg:text-lg mb-6 lg:mb-8"> | ||
| Seamlessly move your assets across 85+ chains with the best rates and | ||
| fastest execution | ||
| </p> | ||
|
|
||
| <div className="flex flex-col lg:flex-row gap-3 lg:gap-2 items-center justify-center"> | ||
| <DataPill>85+ Chains Supported</DataPill> | ||
| <DataPill>4500+ Tokens Supported</DataPill> | ||
| <DataPill>9+ Million Routes Available</DataPill> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function DataPill(props: { children: React.ReactNode }) { | ||
| return ( | ||
| <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"> | ||
| {props.children} | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| function DotsBackgroundPattern(props: { className?: string }) { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| "pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block", | ||
| props.className, | ||
| )} | ||
| style={{ | ||
| backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)", | ||
| backgroundSize: "24px 24px", | ||
| maskImage: | ||
| "radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)", | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
|
Comment on lines
+59
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace inline styles with Tailwind arbitrary properties. Inline function DotsBackgroundPattern(props: { className?: string }) {
return (
<div
- className={cn(
- "pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
- props.className,
- )}
- style={{
- backgroundImage: "radial-gradient(currentColor 1px, transparent 1px)",
- backgroundSize: "24px 24px",
- maskImage:
- "radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 50%)",
- }}
+ className={cn(
+ "pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/20 hidden lg:block",
+ "bg-[radial-gradient(currentColor_1px,transparent_1px)]",
+ "bg-[length:24px_24px]",
+ "[mask-image:radial-gradient(ellipse_100%_100%_at_50%_50%,black_30%,transparent_50%)]",
+ props.className,
+ )}
/>
);
}As per coding guidelines
🤖 Prompt for AI Agents |
||
|
|
||
| const bridgeFaqs: Array<{ title: string; description: string }> = [ | ||
| { | ||
| title: "What is bridging in crypto?", | ||
| description: | ||
| "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.", | ||
| }, | ||
| { | ||
| title: "How does crypto bridging work?", | ||
| description: | ||
| "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", | ||
| }, | ||
| { | ||
| title: "What is a crypto asset swap?", | ||
| description: | ||
| "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.", | ||
| }, | ||
| { | ||
| title: "How can I get stablecoins like USDC or USDT?", | ||
| description: | ||
| "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.", | ||
| }, | ||
| { | ||
| title: "What is the cost of bridging and swapping?", | ||
| description: | ||
| "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.", | ||
| }, | ||
| ]; | ||
|
|
||
| function BridgeFaqSection() { | ||
| return ( | ||
| <section className="container max-w-2xl"> | ||
| <h2 className="text-2xl md:text-3xl font-semibold mb-4 lg:mb-8 tracking-tight text-center"> | ||
| Frequently Asked Questions | ||
| </h2> | ||
| <FaqAccordion faqs={bridgeFaqs} /> | ||
| </section> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add explicit return type to function declaration.
The function is missing an explicit return type, which violates the coding guideline requiring explicit return types in TypeScript.
As per coding guidelines, apply this diff:
🤖 Prompt for AI Agents