From 8d4b6a1fea53013cbdbe9f450d00470b2f8d3761 Mon Sep 17 00:00:00 2001 From: MananTank Date: Mon, 12 Jan 2026 18:12:38 +0000 Subject: [PATCH] [MNY-356] Add tokenEditable and amountEditable props on BuyWidget (#8621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR introduces two new properties, `amountEditable` and `tokenEditable`, to enhance the `BuyWidget` component, allowing control over editing options for token selection and amount. This improves user experience by providing flexibility in the widget's functionality. ### Detailed summary - Added `amountEditable` and `tokenEditable` properties to `BuyWidget`. - Updated `BuyPlayground.tsx`, `CheckoutPlayground.tsx`, and `TransactionPlayground.tsx` to use new props. - Modified `CodeGen.tsx` to incorporate editability options. - Updated URL generation in `buildBuyIframeUrl.ts` for new props. - Added editability options in `LeftSection.tsx`. - Enhanced `BuyWidgetEmbed` to accept new props. - Implemented stories in `BuyWidget.stories.tsx` for editable and non-editable states. - Added documentation in `iframe/page.mdx` for new features. - Adjusted `FundWallet.tsx` and `TokenSection.tsx` to respect new editability options. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit ## Release Notes * **New Features** * Added `amountEditable` and `tokenEditable` props to the BuyWidget component, allowing developers to disable token selection and/or amount editing in the buy flow. * Enhanced UI controls to prevent interaction with locked fields when these options are disabled. * Updated documentation with examples of how to use the new editability options. * **Tests** * Added story variants demonstrating token and amount lock scenarios. ✏️ Tip: You can customize this high-level summary in your review settings. --- .changeset/dirty-breads-swim.md | 5 ++ .../buy-widget/BuyWidgetEmbed.client.tsx | 6 ++ .../src/app/bridge/buy-widget/page.tsx | 10 +++ .../app/bridge/buy-widget/BuyPlayground.tsx | 2 + .../checkout-widget/CheckoutPlayground.tsx | 2 + .../src/app/bridge/components/CodeGen.tsx | 8 ++ .../src/app/bridge/components/LeftSection.tsx | 40 ++++++++++ .../app/bridge/components/RightSection.tsx | 2 + .../bridge/components/buildBuyIframeUrl.ts | 9 +++ .../src/app/bridge/components/types.ts | 4 + .../TransactionPlayground.tsx | 2 + .../src/app/bridge/buy-widget/iframe/page.mdx | 27 +++++++ .../src/react/web/ui/Bridge/BuyWidget.tsx | 18 +++++ .../src/react/web/ui/Bridge/FundWallet.tsx | 80 ++++++++++++------- .../Bridge/common/selected-token-button.tsx | 37 +++++---- .../src/react/web/ui/components/buttons.tsx | 2 +- .../src/stories/BuyWidget.stories.tsx | 37 +++++++++ 17 files changed, 246 insertions(+), 45 deletions(-) create mode 100644 .changeset/dirty-breads-swim.md diff --git a/.changeset/dirty-breads-swim.md b/.changeset/dirty-breads-swim.md new file mode 100644 index 00000000000..baa9089ef79 --- /dev/null +++ b/.changeset/dirty-breads-swim.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Add `amountEditable` and `tokenEditable` props on `BuyWidget` component to disable token selection and token amount editing diff --git a/apps/dashboard/src/app/bridge/buy-widget/BuyWidgetEmbed.client.tsx b/apps/dashboard/src/app/bridge/buy-widget/BuyWidgetEmbed.client.tsx index d22d738d950..13aea46b57c 100644 --- a/apps/dashboard/src/app/bridge/buy-widget/BuyWidgetEmbed.client.tsx +++ b/apps/dashboard/src/app/bridge/buy-widget/BuyWidgetEmbed.client.tsx @@ -20,6 +20,8 @@ export function BuyWidgetEmbed({ buttonLabel, receiverAddress, country, + amountEditable, + tokenEditable, }: { chainId?: number; tokenAddress?: Address; @@ -34,6 +36,8 @@ export function BuyWidgetEmbed({ buttonLabel?: string; receiverAddress?: Address; country?: string; + amountEditable?: boolean; + tokenEditable?: boolean; }) { const client = useMemo( () => @@ -68,6 +72,8 @@ export function BuyWidgetEmbed({ buttonLabel={buttonLabel} receiverAddress={receiverAddress} country={country} + amountEditable={amountEditable} + tokenEditable={tokenEditable} onSuccess={() => { sendMessageToParent({ source: "buy-widget", diff --git a/apps/dashboard/src/app/bridge/buy-widget/page.tsx b/apps/dashboard/src/app/bridge/buy-widget/page.tsx index 0942e58802a..0e9d9c56471 100644 --- a/apps/dashboard/src/app/bridge/buy-widget/page.tsx +++ b/apps/dashboard/src/app/bridge/buy-widget/page.tsx @@ -73,6 +73,14 @@ export default async function Page(props: { const receiverAddress = parseQueryParams(searchParams.receiver, onlyAddress); const country = parseQueryParams(searchParams.country, (v) => v); + // Editable params + const amountEditable = parseQueryParams(searchParams.amountEditable, (v) => + v === "false" ? false : undefined, + ); + const tokenEditable = parseQueryParams(searchParams.tokenEditable, (v) => + v === "false" ? false : undefined, + ); + return (
@@ -90,6 +98,8 @@ export default async function Page(props: { buttonLabel={buttonLabel} receiverAddress={receiverAddress} country={country} + amountEditable={amountEditable} + tokenEditable={tokenEditable} />
diff --git a/apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx b/apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx index a068b4d2959..caba667077b 100644 --- a/apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx +++ b/apps/playground-web/src/app/bridge/buy-widget/BuyPlayground.tsx @@ -24,6 +24,8 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = { transactionData: "", currency: "USD", showThirdwebBranding: true, + amountEditable: true, + tokenEditable: true, }, theme: { darkColorOverrides: {}, diff --git a/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx b/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx index f6b2b2ee7c6..f2ade2e172a 100644 --- a/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx +++ b/apps/playground-web/src/app/bridge/checkout-widget/CheckoutPlayground.tsx @@ -24,6 +24,8 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = { receiverAddress: undefined, currency: "USD", showThirdwebBranding: true, + amountEditable: true, + tokenEditable: true, }, theme: { darkColorOverrides: {}, diff --git a/apps/playground-web/src/app/bridge/components/CodeGen.tsx b/apps/playground-web/src/app/bridge/components/CodeGen.tsx index d364dc4599e..51b44b68c46 100644 --- a/apps/playground-web/src/app/bridge/components/CodeGen.tsx +++ b/apps/playground-web/src/app/bridge/components/CodeGen.tsx @@ -160,6 +160,14 @@ tokenId: 2n, ? quotes(options.payOptions.buttonLabel) : undefined, transaction: transaction, + amountEditable: + widget === "buy" && options.payOptions.amountEditable === false + ? false + : undefined, + tokenEditable: + widget === "buy" && options.payOptions.tokenEditable === false + ? false + : undefined, }; return `\ diff --git a/apps/playground-web/src/app/bridge/components/LeftSection.tsx b/apps/playground-web/src/app/bridge/components/LeftSection.tsx index 8b97f19fdd4..bdc742a1137 100644 --- a/apps/playground-web/src/app/bridge/components/LeftSection.tsx +++ b/apps/playground-web/src/app/bridge/components/LeftSection.tsx @@ -259,6 +259,46 @@ export function LeftSection(props: { + + {/* Editability Options */} +
+ +
+
+ { + setOptions((v) => ({ + ...v, + payOptions: { + ...v.payOptions, + tokenEditable: checked === true, + }, + })); + }} + /> + +
+ +
+ { + setOptions((v) => ({ + ...v, + payOptions: { + ...v.payOptions, + amountEditable: checked === true, + }, + })); + }} + /> + +
+
+
)} diff --git a/apps/playground-web/src/app/bridge/components/RightSection.tsx b/apps/playground-web/src/app/bridge/components/RightSection.tsx index 766ea8d786d..add4c39dbf4 100644 --- a/apps/playground-web/src/app/bridge/components/RightSection.tsx +++ b/apps/playground-web/src/app/bridge/components/RightSection.tsx @@ -70,6 +70,8 @@ export function RightSection(props: { currency={props.options.payOptions.currency} showThirdwebBranding={props.options.payOptions.showThirdwebBranding} receiverAddress={props.options.payOptions.receiverAddress} + amountEditable={props.options.payOptions.amountEditable} + tokenEditable={props.options.payOptions.tokenEditable} key={JSON.stringify({ amount: props.options.payOptions.buyTokenAmount, chain: props.options.payOptions.buyTokenChain, diff --git a/apps/playground-web/src/app/bridge/components/buildBuyIframeUrl.ts b/apps/playground-web/src/app/bridge/components/buildBuyIframeUrl.ts index 148f0ba52d8..536e5e8ed41 100644 --- a/apps/playground-web/src/app/bridge/components/buildBuyIframeUrl.ts +++ b/apps/playground-web/src/app/bridge/components/buildBuyIframeUrl.ts @@ -68,5 +68,14 @@ export function buildBuyIframeUrl(options: BridgeComponentsPlaygroundOptions) { ); } + // Editability options + if (options.payOptions.amountEditable === false) { + url.searchParams.set("amountEditable", "false"); + } + + if (options.payOptions.tokenEditable === false) { + url.searchParams.set("tokenEditable", "false"); + } + return url.toString(); } diff --git a/apps/playground-web/src/app/bridge/components/types.ts b/apps/playground-web/src/app/bridge/components/types.ts index 48812d3bf67..b2f66fd9048 100644 --- a/apps/playground-web/src/app/bridge/components/types.ts +++ b/apps/playground-web/src/app/bridge/components/types.ts @@ -59,5 +59,9 @@ export type BridgeComponentsPlaygroundOptions = { currency?: SupportedFiatCurrency; showThirdwebBranding: boolean; + + // Editability options + amountEditable: boolean; + tokenEditable: boolean; }; }; diff --git a/apps/playground-web/src/app/bridge/transaction-widget/TransactionPlayground.tsx b/apps/playground-web/src/app/bridge/transaction-widget/TransactionPlayground.tsx index caeb4a6e4eb..b008cec4ff0 100644 --- a/apps/playground-web/src/app/bridge/transaction-widget/TransactionPlayground.tsx +++ b/apps/playground-web/src/app/bridge/transaction-widget/TransactionPlayground.tsx @@ -21,6 +21,8 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = { currency: "USD", showThirdwebBranding: true, receiverAddress: undefined, + amountEditable: true, + tokenEditable: true, }, theme: { darkColorOverrides: {}, diff --git a/apps/portal/src/app/bridge/buy-widget/iframe/page.mdx b/apps/portal/src/app/bridge/buy-widget/iframe/page.mdx index 28794cc5440..f7a14ca1056 100644 --- a/apps/portal/src/app/bridge/buy-widget/iframe/page.mdx +++ b/apps/portal/src/app/bridge/buy-widget/iframe/page.mdx @@ -140,6 +140,33 @@ By default, the widget displays thirdweb branding at the bottom. You can hide th +### Editability Options + +You can control whether users can edit certain fields in the widget. + + +
+ +By default, users can change the token they want to purchase. + +You can disable this by setting the `tokenEditable` query parameter to `false`. + + + +
+ +
+ +By default, users can edit the purchase amount. + +You can disable this by setting the `amountEditable` query parameter to `false`. + + + + +
+ + ## Listening for Events The buy widget iframe sends events to the parent window using `postMessage` when a purchase succeeds or fails. diff --git a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx index ce1bc57231a..6f4be0889fb 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx @@ -213,6 +213,16 @@ export type BuyWidgetProps = { * Callback to be called when the user disconnects the active wallet. */ onDisconnect?: () => void; + + /** + * By default the token amount is editable. Set this to false to disable editing the token amount + */ + amountEditable?: boolean; + + /** + * By default the token selection is editable. Set this to false to disable editing the token selection. + */ + tokenEditable?: boolean; }; /** @@ -490,6 +500,12 @@ function BridgeWidgetContent( }; }); + const amountEditable = + props.amountEditable === undefined ? true : props.amountEditable; + + const tokenEditable = + props.tokenEditable === undefined ? true : props.tokenEditable; + if (screen.id === "1:buy-ui") { return ( ); } diff --git a/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx index a0490e05a6e..3d06ae934d7 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/FundWallet.tsx @@ -113,6 +113,16 @@ type FundWalletProps = { description: string | undefined; image: string | undefined; }; + + /** + * Whether the user can edit the amount. Defaults to true. + */ + amountEditable: boolean; + + /** + * Whether the user can edit the token selection. Defaults to true. + */ + tokenEditable: boolean; }; export type SelectedToken = @@ -264,6 +274,8 @@ export function FundWallet(props: FundWalletProps) { setDetailsModalOpen(true); }} currency={props.currency} + amountEditable={props.amountEditable} + tokenEditable={props.tokenEditable} /> {receiver && isReceiverDifferentFromActiveWallet && ( @@ -415,6 +427,8 @@ function TokenSection(props: { }; onWalletClick: () => void; presetOptions: [number, number, number]; + amountEditable: boolean; + tokenEditable: boolean; }) { const theme = useCustomTheme(); const chainQuery = useBridgeChain({ @@ -467,6 +481,7 @@ function TokenSection(props: { client={props.client} onSelectToken={props.onSelectToken} chain={chain} + disabled={props.tokenEditable === false} /> @@ -479,6 +494,7 @@ function TokenSection(props: { value, }); }} + disabled={props.amountEditable === false} style={{ border: "none", boxShadow: "none", @@ -527,6 +543,7 @@ function TokenSection(props: { value, }); }} + disabled={props.amountEditable === false} style={{ border: "none", boxShadow: "none", @@ -541,36 +558,41 @@ function TokenSection(props: { )} - - {/* suggested amounts */} - - {props.presetOptions.map((amount) => ( - - ))} - + {props.amountEditable && ( + <> + + + {props.presetOptions.map((amount) => ( + + ))} + + + )} {/* balance */} diff --git a/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx b/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx index 50538e2b1ad..b1c5ff73d5b 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/common/selected-token-button.tsx @@ -27,21 +27,26 @@ export function SelectedTokenButton(props: { client: ThirdwebClient; onSelectToken: () => void; chain: BridgeChain | undefined; + disabled?: boolean; }) { const theme = useCustomTheme(); return ( ); } diff --git a/packages/thirdweb/src/react/web/ui/components/buttons.tsx b/packages/thirdweb/src/react/web/ui/components/buttons.tsx index a327fcf40b9..5f6c037041c 100644 --- a/packages/thirdweb/src/react/web/ui/components/buttons.tsx +++ b/packages/thirdweb/src/react/web/ui/components/buttons.tsx @@ -31,7 +31,7 @@ export const Button = /* @__PURE__ */ StyledButton((props: ButtonProps) => { } return { all: "unset", - "&:active": { + "[&:active]:not([disabled])": { transform: "translateY(1px)", }, "&[data-disabled='true']": { diff --git a/packages/thirdweb/src/stories/BuyWidget.stories.tsx b/packages/thirdweb/src/stories/BuyWidget.stories.tsx index e686ea8d701..e768158a713 100644 --- a/packages/thirdweb/src/stories/BuyWidget.stories.tsx +++ b/packages/thirdweb/src/stories/BuyWidget.stories.tsx @@ -63,6 +63,43 @@ export function BuyBaseUSDC() { ); } +export function TokenNotEditable() { + return ( + + ); +} + +export function AmountNotEditable() { + return ( + + ); +} + +export function TokenAndAmountNotEditable() { + return ( + + ); +} + export function CustomTitleDescriptionAndButtonLabel() { return (