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
5 changes: 5 additions & 0 deletions .changeset/curly-crews-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Minor UI adjustments in CheckoutWidget
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions apps/dashboard/src/@/utils/sdk-component-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
primaryButtonText: "hsl(var(--inverted-foreground))",
primaryText: "hsl(var(--foreground))",
scrollbarBg: "hsl(var(--muted))",
secondaryButtonBg: "hsl(var(--secondary))",
secondaryButtonHoverBg: "hsl(var(--secondary)/80%)",
secondaryButtonBg: "hsl(var(--secondary)/70%)",
secondaryButtonHoverBg: "hsl(var(--secondary))",
secondaryButtonText: "hsl(var(--secondary-foreground))",
secondaryIconColor: "hsl(var(--secondary-foreground))",
secondaryIconHoverBg: "hsl(var(--accent))",
Expand All @@ -33,7 +33,7 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
separatorLine: "hsl(var(--border))",
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
success: "hsl(var(--success-text))",
tertiaryBg: "hsl(var(--muted)/30%)",
tertiaryBg: "hsl(var(--muted)/50%)",
tooltipBg: "hsl(var(--popover))",
tooltipText: "hsl(var(--popover-foreground))",
},
Expand Down
72 changes: 72 additions & 0 deletions apps/dashboard/src/app/pay/[id]/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";
import { Img } from "@workspace/ui/components/img";
import { MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
import { ClientOnly } from "@/components/blocks/client-only";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { payAppThirdwebClient } from "../constants";

export function PayIdPageHeader(props: {
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 types.

Per coding guidelines, all function declarations should have explicit return types.

As per coding guidelines

Apply this diff:

-export function PayIdPageHeader(props: {
+export function PayIdPageHeader(props: {
   projectName: string;
   projectIcon: string | undefined;
-}) {
+}): JSX.Element {

-function ToggleThemeButton(props: { className?: string }) {
+function ToggleThemeButton(props: { className?: string }): JSX.Element {

Also applies to: 46-46

🤖 Prompt for AI Agents
In apps/dashboard/src/app/pay/[id]/header.tsx around lines 12 and 46, the two
function component declarations lack explicit return types; update each function
signature to include an explicit return type (e.g., : JSX.Element or :
React.ReactElement) so they comply with the coding guidelines, and ensure any
union with null (e.g., JSX.Element | null) is used if the component can return
null.

projectName: string;
projectIcon: string | undefined;
}) {
return (
<div className="border-b border-border/70">
<header className="container flex max-w-7xl justify-between py-4">
<div className="flex items-center gap-3">
{props.projectIcon && (
<Img
src={
resolveSchemeWithErrorHandler({
uri: props.projectIcon,
client: payAppThirdwebClient,
}) || ""
}
alt=""
className="rounded-full size-6 object-cover"
/>
)}

<h2 className="text-xl font-semibold tracking-tight">
{props.projectName}
</h2>
</div>

<div className="flex items-center gap-3 lg:gap-5">
<ToggleThemeButton />
</div>
</header>
</div>
);
}

function ToggleThemeButton(props: { className?: string }) {
const { setTheme, theme } = useTheme();

return (
<ClientOnly
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
>
<Button
aria-label="Toggle theme"
className={cn(
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
props.className,
)}
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
variant="ghost"
>
{theme === "light" ? (
<SunIcon className="size-5 " />
) : (
<MoonIcon className="size-5 " />
)}
</Button>
</ClientOnly>
);
}
Comment on lines +46 to +72
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

Code duplication - reuse existing ToggleThemeButton.

This ToggleThemeButton implementation duplicates the existing component at apps/dashboard/src/@/components/blocks/color-mode-toggle.tsx. The only differences are minor styling adjustments (variant="ghost" vs variant="outline", and slightly different button sizes).

Consider importing and reusing the existing component:

+import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
+
 export function PayIdPageHeader(props: {
   projectName: string;
   projectIcon: string | undefined;
 }) {
   return (
     <div className="border-b border-border/70">
       <header className="container flex max-w-7xl justify-between py-4">
         <div className="flex items-center gap-3">
           {props.projectIcon && (
             <Img
               src={
                 resolveSchemeWithErrorHandler({
                   uri: props.projectIcon,
                   client: payAppThirdwebClient,
                 }) || ""
               }
               alt=""
               className="rounded-full size-6 object-cover"
             />
           )}

           <h2 className="text-xl font-semibold tracking-tight">
             {props.projectName}
           </h2>
         </div>

         <div className="flex items-center gap-3 lg:gap-5">
-          <ToggleThemeButton />
+          <ToggleThemeButton className="h-auto w-auto p-2 text-muted-foreground hover:text-foreground" />
         </div>
       </header>
     </div>
   );
 }
-
-function ToggleThemeButton(props: { className?: string }) {
-  const { setTheme, theme } = useTheme();
-
-  return (
-    <ClientOnly
-      ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
-    >
-      <Button
-        aria-label="Toggle theme"
-        className={cn(
-          "h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
-          props.className,
-        )}
-        onClick={() => {
-          setTheme(theme === "dark" ? "light" : "dark");
-        }}
-        variant="ghost"
-      >
-        {theme === "light" ? (
-          <SunIcon className="size-5 " />
-        ) : (
-          <MoonIcon className="size-5 " />
-        )}
-      </Button>
-    </ClientOnly>
-  );
-}

If styling differences are needed, extend the existing component to accept additional props rather than duplicating the implementation.

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

🤖 Prompt for AI Agents
In apps/dashboard/src/app/pay/[id]/header.tsx around lines 46 to 72, this local
ToggleThemeButton duplicates the component defined at
apps/dashboard/src/@/components/blocks/color-mode-toggle.tsx; replace the
duplicate by importing and reusing the existing component (or extend the
existing component to accept props for variant, className and size overrides),
then pass the needed props (e.g., variant="ghost" and any extra className) from
this file so styling differences are applied without duplicating logic; remove
the local ToggleThemeButton implementation after switching to the shared
component and update imports accordingly.

207 changes: 46 additions & 161 deletions apps/dashboard/src/app/pay/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { ShieldCheckIcon } from "lucide-react";
import { cn } from "@workspace/ui/lib/utils";
import type { Metadata } from "next";
import { ThemeProvider } from "next-themes";
import { Bridge, defineChain, toTokens } from "thirdweb";
import { getChainMetadata } from "thirdweb/chains";
import { shortenAddress } from "thirdweb/utils";
import { Bridge } from "thirdweb";
import { getPaymentLink } from "@/api/universal-bridge/links";
import { Badge } from "@/components/ui/badge";
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
import {
API_SERVER_SECRET,
DASHBOARD_THIRDWEB_SECRET_KEY,
} from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { resolveEns } from "@/lib/ens";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { PayPageWidget } from "../components/client/PayPageWidget.client";
import { payAppThirdwebClient } from "../constants";
import { PayIdPageHeader } from "./header";

const title = "thirdweb Pay";
const description = "Fast, secure, and simple payments.";
Expand Down Expand Up @@ -54,171 +49,44 @@ export default async function PayPage({
tokenAddress: paymentLink.destinationToken.address,
});

const chainPromise = getChainMetadata(
// eslint-disable-next-line no-restricted-syntax
defineChain(Number(paymentLink.destinationToken.chainId)),
);

const recipientPromise = resolveEns(
paymentLink.receiver,
getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
);

const [tokens, projectMetadata, chain, recipientEnsOrAddress] =
await Promise.all([
tokensPromise,
projectMetadataPromise,
chainPromise,
recipientPromise,
]);
const [tokens, projectMetadata] = await Promise.all([
tokensPromise,
projectMetadataPromise,
]);

const token = tokens[0];
if (!token) {
throw new Error("Token not found");
}

return (
<div className="relative flex h-dvh w-full items-center justify-center">
<div className="relative flex flex-col min-h-dvh w-full">
<ThemeProvider
forcedTheme={theme === "light" ? "light" : "dark"}
defaultTheme={theme === "light" ? "light" : "dark"}
attribute="class"
disableTransitionOnChange
enableSystem={false}
>
<div className="flex z-10 flex-col lg:flex-row h-full w-full">
<header className="min-w-full lg:min-w-[500px] border-b lg:border-r lg:h-full bg-card flex flex-col gap-4 items-start p-4 lg:p-8">
<div>
<div className="flex flex-row items-center justify-start gap-4">
{projectMetadata.image && (
<img
src={
resolveSchemeWithErrorHandler({
uri: projectMetadata.image,
client: payAppThirdwebClient,
}) || ""
}
alt={projectMetadata.name}
width={25}
height={25}
className="rounded-full overflow-hidden"
/>
)}
<h2 className="text-xl font-bold">{projectMetadata.name}</h2>
</div>
{projectMetadata.description && (
<p className="mt-2 text-sm text-muted-foreground">
{projectMetadata.description}
</p>
)}
</div>

<div className="hidden lg:block my-4 w-full">
{paymentLink.amount && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Details</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{token.iconUri && (
<img
src={resolveSchemeWithErrorHandler({
uri: token.iconUri,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={token.name}
width={25}
height={25}
className="size-5 rounded-full overflow-hidden"
/>
)}
{toTokens(BigInt(paymentLink.amount), token.decimals)}{" "}
{token.symbol}
</div>
{token.prices.USD && (
<span>
$
{(
Number(token.prices.USD) *
Number(
toTokens(
BigInt(paymentLink.amount),
token.decimals,
),
)
).toFixed(2)}
</span>
)}
</div>
</div>
)}
{chain && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">Network</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
<div className="flex flex-row items-center gap-2">
{chain.icon?.url && (
<img
src={resolveSchemeWithErrorHandler({
uri: chain.icon.url,
client: getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
}),
})}
alt={chain.name}
width={chain.icon.width}
height={chain.icon.height}
className="size-5 rounded-full overflow-hidden"
/>
)}
{chain.name}
</div>
</div>
</div>
)}
{recipientEnsOrAddress.ensName ||
(recipientEnsOrAddress.address && (
<div className="flex flex-col gap-1 w-full my-4">
<span className="text-muted-foreground text-xs">
Seller
</span>
<div className="font-medium flex-row flex justify-between items-center w-full">
{recipientEnsOrAddress.ensName ??
shortenAddress(recipientEnsOrAddress.address)}
</div>
</div>
))}
</div>
<PayIdPageHeader
projectIcon={projectMetadata.image || undefined}
projectName={projectMetadata.name}
/>

<div className="mt-auto hidden lg:block">
<Badge className="flex items-center gap-1.5 bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-800">
<ShieldCheckIcon className="size-3" />
Secured by thirdweb
</Badge>
</div>
</header>
<main className="flex justify-center py-12 w-full items-center grow">
<PayPageWidget
amount={
paymentLink.amount ? BigInt(paymentLink.amount) : undefined
}
chainId={Number(paymentLink.destinationToken.chainId)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
token={token}
/>
</main>
</div>
<main className="flex justify-center py-12 w-full items-center grow overflow-hidden relative">
<DotsBackgroundPattern />
<PayPageWidget
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
chainId={Number(paymentLink.destinationToken.chainId)}
clientId={undefined} // Payment links don't need to use the same client ID to be executed
image={paymentLink.imageUrl}
name={paymentLink.title}
paymentLinkId={id}
purchaseData={paymentLink.purchaseData}
recipientAddress={paymentLink.receiver}
redirectUri={redirectUri}
token={token}
/>
</main>
</ThemeProvider>
</div>
);
Expand All @@ -237,7 +105,24 @@ async function getProjectMetadata(clientId: string) {
}

const { data } = (await response.json()) as {
data: { name: string; image: string | null; description: string | null };
data: { name: string; image: string | null };
};
return data;
}

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/15 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 +113 to +128
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

Code duplication - import existing DotsBackgroundPattern.

This DotsBackgroundPattern implementation duplicates the existing component at apps/dashboard/src/@/components/ui/background-patterns.tsx (shown in relevant code snippets). The only differences are the className values.

Import and reuse the existing component:

+import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
+
 // ...

         <main className="flex justify-center py-12 w-full items-center grow overflow-hidden relative">
-          <DotsBackgroundPattern />
+          <DotsBackgroundPattern className="pointer-events-none absolute -inset-x-36 -inset-y-24 text-foreground/20 dark:text-muted-foreground/15 hidden lg:block" />
           <PayPageWidget
             amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
             chainId={Number(paymentLink.destinationToken.chainId)}
             clientId={undefined}
             image={paymentLink.imageUrl}
             name={paymentLink.title}
             paymentLinkId={id}
             purchaseData={paymentLink.purchaseData}
             recipientAddress={paymentLink.receiver}
             redirectUri={redirectUri}
             token={token}
           />
         </main>
       </ThemeProvider>
     </div>
   );
 }
 
 async function getProjectMetadata(clientId: string) {
   // ...
 }
-
-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/15 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%)",
-      }}
-    />
-  );
-}

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

🤖 Prompt for AI Agents
In apps/dashboard/src/app/pay/[id]/page.tsx around lines 113 to 128, the local
DotsBackgroundPattern duplicates the component defined in
apps/dashboard/src/@/components/ui/background-patterns.tsx; remove the duplicate
function, add an import for the existing DotsBackgroundPattern from that file,
and use it in place of the local component while forwarding the className prop
(combine any local className additions with the incoming className using the
existing cn utility) so the only differences are preserved via props rather than
duplicated implementation.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { cn } from "@workspace/ui/lib/utils";
import { payAppThirdwebClient } from "app/pay/constants";
import { useTheme } from "next-themes";
import { createThirdwebClient, NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
Expand Down Expand Up @@ -63,7 +64,12 @@ export function PayPageWidget({
client={
clientId ? createThirdwebClient({ clientId }) : payAppThirdwebClient
}
image={image}
className={cn(
"shadow-xl",
!image &&
"[&_.tw-header-image]:invert dark:[&_.tw-header-image]:invert-0",
)}
image={image || "/assets/pay/general-pay.png"}
name={name}
onSuccess={() => {
reportPaymentLinkBuySuccessful();
Expand Down
Loading
Loading