Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 90 additions & 47 deletions apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx
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 {
Expand Down Expand Up @@ -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) {
Copy link
Contributor

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:

-export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
+export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps): JSX.Element {
🤖 Prompt for AI Agents
In apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx around line 69,
the BuyAndSwapEmbed function declaration lacks an explicit return type; update
the function signature to include the correct React return type (e.g.,
React.ReactElement or JSX.Element | null as appropriate) so the declaration
becomes explicitly typed, and import React types if needed.

const { theme } = useTheme();
const [tab, setTab] = useState<"buy" | "swap">("swap");
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
Expand Down Expand Up @@ -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}
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 onError, onCancel, and onSuccess callbacks. This violates the DRY principle and increases maintenance burden.

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 onCancel and onSuccess.

Also applies to: 171-180, 203-212

🤖 Prompt for AI Agents
In apps/dashboard/src/@/components/blocks/BuyAndSwapEmbed.tsx around lines
138-147 (and similarly at 171-180 and 203-212), the logic that derives
buyChainId from the quote object is duplicated in the onError, onCancel, and
onSuccess callbacks; extract that logic into a single helper function
getBuyChainId (as specified in the review) placed near the top of the file,
returning number | undefined, then replace the repeated conditional blocks in
each callback with a call to getBuyChainId(quote) and use the returned value
(early return if undefined) so all three callbacks share the same
implementation.


reportTokenBuyFailed({
buyTokenChainId:
quote?.type === "buy"
? quote.intent.destinationChainId
: quote?.type === "onramp"
? quote.intent.chainId
: undefined,
buyTokenChainId: buyChainId,
buyTokenAddress:
quote?.type === "buy"
? quote.intent.destinationTokenAddress
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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"]}
/>
)}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<GridPatternEmbedContainer>
<BuyAndSwapEmbed
isTestnet={props.chain.testnet}
// eslint-disable-next-line no-restricted-syntax
chain={defineDashboardChain(props.chain.chainId, props.chain)}
buyAmount={undefined}
tokenAddress={undefined}
swapTab={{
sellToken: {
chainId: props.chain.chainId,
tokenAddress: NATIVE_TOKEN_ADDRESS,
},
buyToken: undefined,
}}
buyTab={{
buyToken: {
chainId: props.chain.chainId,
tokenAddress: NATIVE_TOKEN_ADDRESS,
},
}}
pageType="chain"
/>
</GridPatternEmbedContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,21 @@ function BuyEmbed(props: {
if (!props.claimConditionMeta) {
return (
<BuyAndSwapEmbed
chain={props.clientContract.chain}
tokenAddress={props.clientContract.address}
buyAmount={undefined}
// chain={props.clientContract.chain}
swapTab={{
sellToken: {
chainId: props.clientContract.chain.id,
tokenAddress: props.clientContract.address,
},
buyToken: undefined,
}}
buyTab={{
buyToken: {
chainId: props.clientContract.chain.id,
tokenAddress: props.clientContract.address,
},
}}
pageType="asset"
isTestnet={props.chainMetadata.testnet}
/>
);
}
Expand Down
113 changes: 113 additions & 0 deletions apps/dashboard/src/app/bridge/components/bridge-page.tsx
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace inline styles with Tailwind arbitrary properties.

Inline style violates the Tailwind-only rule. Use arbitrary properties instead.

 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

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/dashboard/src/app/bridge/components/bridge-page.tsx around lines 59 to
74, the component uses an inline style object for backgroundImage,
backgroundSize, and maskImage which violates the Tailwind-only rule; remove the
style prop and move those rules into the className using Tailwind arbitrary
properties (e.g. add classes like bg-[radial-gradient(...)] for
background-image, bg-[length:24px_24px] or equivalent arbitrary background-size,
and mask-image via mask-[radial-gradient(...)]), keeping the existing cn(...)
call and props.className; ensure proper escaping/encoding of parentheses and
commas in the arbitrary values and that class order preserves priority.


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>
);
}
Loading
Loading