From 49824ce29e4c6f0a5f2b34a847d6d0fa68ebbd09 Mon Sep 17 00:00:00 2001 From: MananTank Date: Wed, 22 Oct 2025 22:19:49 +0000 Subject: [PATCH 1/2] [MNY-286] SDK: Do not require connecting wallet in BuyWidget if receiverAddress is set (#8296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on modifying the `BuyWidget` component to allow users to bypass wallet connection if a `receiverAddress` is provided. It updates the logic for displaying connection prompts based on the presence of this address. ### Detailed summary - Updated `BuyWidget` to not require wallet connection if `receiverAddress` is set. - Removed `activeWalletInfo` dependency from the condition for displaying the `FundWallet`. - Introduced `showConnectButton` variable to determine if the connect button should be shown. - Simplified the rendering logic for the connect button in `FundWallet`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .changeset/light-signs-send.md | 5 +++ .../src/react/web/ui/Bridge/BuyWidget.tsx | 4 +-- .../src/react/web/ui/Bridge/FundWallet.tsx | 31 ++++++++++--------- 3 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 .changeset/light-signs-send.md diff --git a/.changeset/light-signs-send.md b/.changeset/light-signs-send.md new file mode 100644 index 00000000000..3636a1f118a --- /dev/null +++ b/.changeset/light-signs-send.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Do not require connecting wallet in `BuyWidget` if `receiverAddress` is set diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx index ef1441d65e2..f79eece83cf 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx @@ -39,7 +39,6 @@ import { PaymentSelection } from "./payment-selection/PaymentSelection.js"; import { SuccessScreen } from "./payment-success/SuccessScreen.js"; import { QuoteLoader } from "./QuoteLoader.js"; import { StepRunner } from "./StepRunner.js"; -import { useActiveWalletInfo } from "./swap-widget/hooks.js"; import type { PaymentMethod, RequiredParams } from "./types.js"; export type BuyOrOnrampPrepareResult = Extract< @@ -432,7 +431,6 @@ function BridgeWidgetContent( >, ) { const [screen, setScreen] = useState({ id: "1:buy-ui" }); - const activeWalletInfo = useActiveWalletInfo(); const handleError = useCallback( (error: Error, quote: BridgePrepareResult | undefined) => { @@ -478,7 +476,7 @@ function BridgeWidgetContent( }; }); - if (screen.id === "1:buy-ui" || !activeWalletInfo) { + if (screen.id === "1:buy-ui") { return ( + ) : ( - ) : ( - )} {props.showThirdwebBranding ? ( From 8d5d3b71225b456dcadb12cb7807c344229c8707 Mon Sep 17 00:00:00 2001 From: MananTank Date: Wed, 22 Oct 2025 22:19:50 +0000 Subject: [PATCH 2/2] [MNY-243] Update /pay page to render BuyWidget (#8297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on enhancing the payment functionalities in the `pay` module by adding new wallet integrations, improving the UI components, and refactoring the `PayPage` logic for better handling of search parameters and rendering. ### Detailed summary - Added `createWallet` for `com.okex.wallet` in multiple components. - Introduced `payLandingWallets` with new wallet options. - Refactored `PayPage` to use `StyledBuyWidget` and `PayLandingHeader`. - Improved theme toggling with `ToggleThemeButton`. - Simplified `PayPage` logic for handling search parameters. - Removed unused imports and components, including `PaymentLinkForm`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` --- .../client/UniversalBridgeEmbed.tsx | 1 + apps/dashboard/src/app/login/LoginPage.tsx | 1 + apps/dashboard/src/app/pay/[id]/page.tsx | 249 +++++------ .../client/PaymentLinkForm.client.tsx | 399 ------------------ .../dashboard/src/app/pay/components/types.ts | 11 - apps/dashboard/src/app/pay/landing/header.tsx | 61 +++ .../src/app/pay/landing/styled-buy-widget.tsx | 27 ++ apps/dashboard/src/app/pay/landing/wallets.ts | 13 + apps/dashboard/src/app/pay/layout.tsx | 8 +- apps/dashboard/src/app/pay/page.tsx | 147 +++---- 10 files changed, 293 insertions(+), 624 deletions(-) delete mode 100644 apps/dashboard/src/app/pay/components/client/PaymentLinkForm.client.tsx delete mode 100644 apps/dashboard/src/app/pay/components/types.ts create mode 100644 apps/dashboard/src/app/pay/landing/header.tsx create mode 100644 apps/dashboard/src/app/pay/landing/styled-buy-widget.tsx create mode 100644 apps/dashboard/src/app/pay/landing/wallets.ts diff --git a/apps/dashboard/src/app/bridge/components/client/UniversalBridgeEmbed.tsx b/apps/dashboard/src/app/bridge/components/client/UniversalBridgeEmbed.tsx index d3685148d68..e80606e7c4b 100644 --- a/apps/dashboard/src/app/bridge/components/client/UniversalBridgeEmbed.tsx +++ b/apps/dashboard/src/app/bridge/components/client/UniversalBridgeEmbed.tsx @@ -15,6 +15,7 @@ export const bridgeWallets = [ createWallet("me.rainbow"), createWallet("io.rabby"), createWallet("io.zerion.wallet"), + createWallet("com.okex.wallet"), ]; export function UniversalBridgeEmbed(props: { diff --git a/apps/dashboard/src/app/login/LoginPage.tsx b/apps/dashboard/src/app/login/LoginPage.tsx index e087a74952e..4f47d525975 100644 --- a/apps/dashboard/src/app/login/LoginPage.tsx +++ b/apps/dashboard/src/app/login/LoginPage.tsx @@ -49,6 +49,7 @@ const loginOptions = [ createWallet("io.rabby"), createWallet("me.rainbow"), createWallet("io.zerion.wallet"), + createWallet("com.okex.wallet"), ]; const inAppWalletLoginOptions = [ diff --git a/apps/dashboard/src/app/pay/[id]/page.tsx b/apps/dashboard/src/app/pay/[id]/page.tsx index 19a809bb5af..3e46240c99e 100644 --- a/apps/dashboard/src/app/pay/[id]/page.tsx +++ b/apps/dashboard/src/app/pay/[id]/page.tsx @@ -81,137 +81,146 @@ export default async function PayPage({ } return ( - -
-
-
-
- {projectMetadata.image && ( - {projectMetadata.name} +
+ +
+
+
+
+ {projectMetadata.image && ( + {projectMetadata.name} + )} +

{projectMetadata.name}

+
+ {projectMetadata.description && ( +

+ {projectMetadata.description} +

)} -

{projectMetadata.name}

- {projectMetadata.description && ( -

- {projectMetadata.description} -

- )} -
-
- {paymentLink.amount && ( -
- Details -
-
- {token.iconUri && ( - {token.name} - )} - {toTokens(BigInt(paymentLink.amount), token.decimals)}{" "} - {token.symbol} -
- {token.prices.USD && ( - - $ - {( - Number(token.prices.USD) * - Number( - toTokens(BigInt(paymentLink.amount), token.decimals), - ) - ).toFixed(2)} - - )} -
-
- )} - {chain && ( -
- Network -
-
- {chain.icon?.url && ( - {chain.name} +
+ {paymentLink.amount && ( +
+ Details +
+
+ {token.iconUri && ( + {token.name} + )} + {toTokens(BigInt(paymentLink.amount), token.decimals)}{" "} + {token.symbol} +
+ {token.prices.USD && ( + + $ + {( + Number(token.prices.USD) * + Number( + toTokens( + BigInt(paymentLink.amount), + token.decimals, + ), + ) + ).toFixed(2)} + )} - {chain.name}
-
- )} - {recipientEnsOrAddress.ensName || - (recipientEnsOrAddress.address && ( + )} + {chain && (
- Seller + Network
- {recipientEnsOrAddress.ensName ?? - shortenAddress(recipientEnsOrAddress.address)} +
+ {chain.icon?.url && ( + {chain.name} + )} + {chain.name} +
- ))} -
+ )} + {recipientEnsOrAddress.ensName || + (recipientEnsOrAddress.address && ( +
+ + Seller + +
+ {recipientEnsOrAddress.ensName ?? + shortenAddress(recipientEnsOrAddress.address)} +
+
+ ))} +
-
- - - Secured by thirdweb - -
-
-
- -
-
-
+
+ + + Secured by thirdweb + +
+ +
+ +
+ + + ); } diff --git a/apps/dashboard/src/app/pay/components/client/PaymentLinkForm.client.tsx b/apps/dashboard/src/app/pay/components/client/PaymentLinkForm.client.tsx deleted file mode 100644 index 4de4842709d..00000000000 --- a/apps/dashboard/src/app/pay/components/client/PaymentLinkForm.client.tsx +++ /dev/null @@ -1,399 +0,0 @@ -"use client"; - -import { payAppThirdwebClient } from "app/pay/constants"; -import { ChevronDownIcon, CreditCardIcon } from "lucide-react"; -import { useCallback, useId, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { - createThirdwebClient, - defineChain, - getContract, - type ThirdwebClient, - toUnits, -} from "thirdweb"; -import { getCurrencyMetadata } from "thirdweb/extensions/erc20"; -import { resolveScheme, upload } from "thirdweb/storage"; -import { FileInput } from "@/components/blocks/FileInput"; -import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; -import { TokenSelector } from "@/components/blocks/TokenSelector"; -import { Button } from "@/components/ui/button"; -import { CopyTextButton } from "@/components/ui/CopyTextButton"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { resolveEns } from "@/lib/ens"; -import { cn } from "@/lib/utils"; - -export function PaymentLinkForm() { - const [chainId, setChainId] = useState(); - const [recipientAddress, setRecipientAddress] = useState(""); - // TODO - clean this up later - const [tokenAddressWithChain, setTokenAddressWithChain] = useState(""); - const [amount, setAmount] = useState(""); - const [title, setTitle] = useState(""); - const [image, setImage] = useState(null); - const [imageUri, setImageUri] = useState(""); - const [uploadingImage, setUploadingImage] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [showAdvanced, setShowAdvanced] = useState(false); - const [paymentUrl, setPaymentUrl] = useState(""); - - const isFormComplete = useMemo(() => { - return chainId && recipientAddress && tokenAddressWithChain && amount; - }, [chainId, recipientAddress, tokenAddressWithChain, amount]); - - const handleImageUpload = useCallback(async (file: File) => { - try { - setImage(file); - setUploadingImage(true); - - const uploadClient = createThirdwebClient({ - clientId: "7ae789153cf9ecde8f35649f2d8a4333", - }); - const uri = await upload({ - client: uploadClient, - files: [file], - }); - - // eslint-disable-next-line no-restricted-syntax - const resolvedUrl = resolveScheme({ - client: uploadClient, - uri, - }); - - setImageUri(resolvedUrl); - toast.success("Image uploaded successfully"); - } catch (error) { - console.error("Error uploading image:", error); - toast.error("Failed to upload image"); - setImage(null); - } finally { - setUploadingImage(false); - } - }, []); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - try { - if ( - !chainId || - !recipientAddress || - !tokenAddressWithChain || - !amount - ) { - throw new Error("All fields are required"); - } - - const inputs = await parseInputs( - payAppThirdwebClient, - chainId, - tokenAddressWithChain, - recipientAddress, - amount, - ); - - // Build payment URL - const params = new URLSearchParams({ - amount: inputs.amount.toString(), - chainId: inputs.chainId.toString(), - clientId: payAppThirdwebClient.clientId, - recipientAddress: inputs.recipientAddress, - tokenAddress: inputs.tokenAddress, - }); - - // Add title as name parameter if provided - if (title) { - params.set("name", title); - } - - // Add image URI if available - if (imageUri) { - params.set("image", imageUri); - } - - const url = `${window.location.origin}/pay?${params.toString()}`; - setPaymentUrl(url); - toast.success("Payment created!"); - } catch (err) { - toast.error(err instanceof Error ? err.message : "An error occurred"); - } finally { - setIsLoading(false); - } - }, - [amount, chainId, imageUri, recipientAddress, title, tokenAddressWithChain], - ); - - const handlePreview = useCallback(async () => { - if (!chainId || !recipientAddress || !tokenAddressWithChain || !amount) { - toast.error("Please fill in all fields first"); - return; - } - - try { - const inputs = await parseInputs( - payAppThirdwebClient, - chainId, - tokenAddressWithChain, - recipientAddress, - amount, - ); - - const params = new URLSearchParams({ - amount: inputs.amount.toString(), - chainId: inputs.chainId.toString(), - clientId: payAppThirdwebClient.clientId, - recipientAddress: inputs.recipientAddress, - tokenAddress: inputs.tokenAddress, - }); - - // Add title as name parameter if provided - if (title) { - params.set("name", title); - } - - // Add image URI if available - if (imageUri) { - params.set("image", imageUri); - } - - window.open(`/pay?${params.toString()}`, "_blank"); - } catch (err) { - toast.error(err instanceof Error ? err.message : "An error occurred"); - } - }, [ - amount, - chainId, - imageUri, - recipientAddress, - title, - tokenAddressWithChain, - ]); - - const [selectedChainId, selectedTokenAddress] = tokenAddressWithChain - ? tokenAddressWithChain.split(":") - : []; - - const recipientId = useId(); - const amountId = useId(); - const titleId = useId(); - - return ( - - -
-
- -
- - Create a Payment - -
-
- -
-
- - -
- -
- - { - setTokenAddressWithChain(`${value.chainId}:${value.address}`); - }} - selectedToken={ - selectedChainId && selectedTokenAddress - ? { - address: selectedTokenAddress, - chainId: Number(selectedChainId), - } - : undefined - } - showCheck={false} - /> -
- -
- - setRecipientAddress(e.target.value)} - placeholder="Address or ENS" - required - value={recipientAddress} - /> -
- -
- - setAmount(e.target.value)} - placeholder="0.0" - required - step="any" - type="number" - value={amount} - /> -
- -
- - -
-
-
-
- - setTitle(e.target.value)} - placeholder="Payment for..." - value={title} - /> -
- -
- -
- -
-
-
-
-
-
- -
- {paymentUrl && ( - - )} - -
- - -
-
-
-
-
- ); -} - -async function parseInputs( - client: ThirdwebClient, - chainId: number, - tokenAddressWithChain: string, - recipientAddressOrEns: string, - decimalAmount: string, -) { - const [_chainId, tokenAddress] = tokenAddressWithChain.split(":"); - if (Number(_chainId) !== chainId) { - throw new Error("Chain ID does not match token chain"); - } - if (!tokenAddress) { - throw new Error("Missing token address"); - } - - const ensPromise = resolveEns(recipientAddressOrEns, client); - const currencyPromise = getCurrencyMetadata({ - contract: getContract({ - address: tokenAddress, - // eslint-disable-next-line no-restricted-syntax - chain: defineChain(chainId), - client, - }), - }); - const [ens, currencyMetadata] = await Promise.all([ - ensPromise, - currencyPromise, - ]); - if (!ens.address) { - throw new Error("Invalid recipient address"); - } - - const amountInWei = toUnits(decimalAmount, currencyMetadata.decimals); - - return { - amount: amountInWei, - chainId, - recipientAddress: ens.address, - tokenAddress, - }; -} diff --git a/apps/dashboard/src/app/pay/components/types.ts b/apps/dashboard/src/app/pay/components/types.ts deleted file mode 100644 index dd083455428..00000000000 --- a/apps/dashboard/src/app/pay/components/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type PayParams = { - chainId: string; - recipientAddress: string; - tokenAddress: string; - amount: string; - name?: string; - image?: string; - theme?: "light" | "dark"; - redirectUri?: string; - clientId?: string; -}; diff --git a/apps/dashboard/src/app/pay/landing/header.tsx b/apps/dashboard/src/app/pay/landing/header.tsx new file mode 100644 index 00000000000..e885fa52b12 --- /dev/null +++ b/apps/dashboard/src/app/pay/landing/header.tsx @@ -0,0 +1,61 @@ +"use client"; +import { MoonIcon, SunIcon } from "lucide-react"; +import Link from "next/link"; +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 { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo"; + +export function PayLandingHeader(props: { containerClassName?: string }) { + return ( +
+
+
+ + + thirdweb + +
+ +
+ +
+
+
+ ); +} + +function ToggleThemeButton(props: { className?: string }) { + const { setTheme, theme } = useTheme(); + + return ( + } + > + + + ); +} diff --git a/apps/dashboard/src/app/pay/landing/styled-buy-widget.tsx b/apps/dashboard/src/app/pay/landing/styled-buy-widget.tsx new file mode 100644 index 00000000000..f28a89b16a9 --- /dev/null +++ b/apps/dashboard/src/app/pay/landing/styled-buy-widget.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { BuyWidget, type BuyWidgetProps } from "thirdweb/react"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { getSDKTheme } from "@/utils/sdk-component-theme"; +import { payLandingWallets } from "./wallets"; + +const client = getClientThirdwebClient(); + +export function StyledBuyWidget( + props: Omit, +) { + const { theme } = useTheme(); + + return ( + + ); +} diff --git a/apps/dashboard/src/app/pay/landing/wallets.ts b/apps/dashboard/src/app/pay/landing/wallets.ts new file mode 100644 index 00000000000..c60084e9b36 --- /dev/null +++ b/apps/dashboard/src/app/pay/landing/wallets.ts @@ -0,0 +1,13 @@ +import { createWallet } from "thirdweb/wallets"; +import { appMetadata } from "@/constants/connect"; + +export const payLandingWallets = [ + createWallet("io.metamask"), + createWallet("com.coinbase.wallet", { + appMetadata, + }), + createWallet("me.rainbow"), + createWallet("io.rabby"), + createWallet("io.zerion.wallet"), + createWallet("com.okex.wallet"), +]; diff --git a/apps/dashboard/src/app/pay/layout.tsx b/apps/dashboard/src/app/pay/layout.tsx index 932d52c675c..851c92c0b67 100644 --- a/apps/dashboard/src/app/pay/layout.tsx +++ b/apps/dashboard/src/app/pay/layout.tsx @@ -18,15 +18,11 @@ export default async function PayLayout({ - -
- {children} -
-
+ {children} ); diff --git a/apps/dashboard/src/app/pay/page.tsx b/apps/dashboard/src/app/pay/page.tsx index 7e6d6e60e65..7f21c8978a8 100644 --- a/apps/dashboard/src/app/pay/page.tsx +++ b/apps/dashboard/src/app/pay/page.tsx @@ -1,12 +1,9 @@ +import { cn } from "@workspace/ui/lib/utils"; import type { Metadata } from "next"; import { ThemeProvider } from "next-themes"; -import { createThirdwebClient, defineChain, getContract } from "thirdweb"; -import { getCurrencyMetadata } from "thirdweb/extensions/erc20"; -import { checksumAddress } from "thirdweb/utils"; -import { PaymentLinkForm } from "./components/client/PaymentLinkForm.client"; -import { PayPageWidget } from "./components/client/PayPageWidget.client"; -import type { PayParams } from "./components/types"; -import { payAppThirdwebClient } from "./constants"; +import { defineChain, isAddress } from "thirdweb"; +import { PayLandingHeader } from "./landing/header"; +import { StyledBuyWidget } from "./landing/styled-buy-widget"; const title = "thirdweb Payments"; const description = "Fast, secure, and simple payments."; @@ -20,97 +17,71 @@ export const metadata: Metadata = { title, }; -export default async function PayPage({ - searchParams, -}: { - searchParams: Promise; -}) { - const params = await searchParams; - - // If no query parameters are provided, show the form - if ( - !params.chainId && - !params.recipientAddress && - !params.tokenAddress && - !params.amount - ) { - return ( - - - - ); - } +type SearchParams = { + [key: string]: string | string[] | undefined; +}; - // Validate query parameters - if (Array.isArray(params.chainId)) { - throw new Error("A single chainId parameter is required."); - } - if (Array.isArray(params.recipientAddress)) { - throw new Error("A single recipientAddress parameter is required."); - } - if (Array.isArray(params.tokenAddress)) { - throw new Error("A single tokenAddress parameter is required."); - } - if (Array.isArray(params.amount)) { - throw new Error("A single amount parameter is required."); - } - if (Array.isArray(params.clientId)) { - throw new Error("A single clientId parameter is required."); - } - if (Array.isArray(params.redirectUri)) { - throw new Error("A single redirectUri parameter is required."); - } +export default async function PayPage(props: { + searchParams: Promise; +}) { + const searchParams = await props.searchParams; - // Use any provided clientId or use the dashboard client - const client = - params.clientId && !Array.isArray(params.clientId) - ? createThirdwebClient({ clientId: params.clientId }) - : payAppThirdwebClient; + const onlyAddress = (v: string) => (isAddress(v) ? v : undefined); + const onlyNumber = (v: string) => + Number.isNaN(Number(v)) ? undefined : Number(v); - const tokenContract = getContract({ - address: params.tokenAddress, - // eslint-disable-next-line no-restricted-syntax - chain: defineChain(Number(params.chainId)), - client: payAppThirdwebClient, - }); - const { - symbol, - decimals, - name: tokenName, - } = await getCurrencyMetadata({ - contract: tokenContract, - }); - const token = { - address: checksumAddress(params.tokenAddress), - chainId: Number(params.chainId), - decimals, - name: tokenName, - symbol, - }; + const receiver = parse(searchParams.receiver, onlyAddress); + const token = parse(searchParams.token, onlyAddress); + const chain = parse(searchParams.chain, onlyNumber); + const amount = parse(searchParams.amount, onlyNumber); return ( - +
+ +
+ + +
+
); } + +function parse( + value: string | string[] | undefined, + fn: (value: string) => T | undefined, +): T | undefined { + if (typeof value === "string") { + return fn(value); + } + + return undefined; +} + +function DotsBackgroundPattern(props: { className?: string }) { + return ( +