Skip to content

[SDK] Add enableCard prop to control fiat payment visibility #7465

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

Merged
merged 6 commits into from
Jun 28, 2025
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/dull-breads-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Adds paymentMethods prop to BuyWidget, CheckoutWidget, and TransactionWidget to control available payment options. Accepts an array of "crypto" and/or "card" values.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function Example() {
<${componentName}
client={client}
chain={defineChain(${options.payOptions.buyTokenChain.id})}
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${
amount="${options.payOptions.buyTokenAmount}"${options.payOptions.buyTokenAddress ? `\n\t token="${options.payOptions.buyTokenAddress}"` : ""}${options.payOptions.sellerAddress ? `\n\t seller="${options.payOptions.sellerAddress}"` : ""}${options.payOptions.title ? `\n\t ${options.payOptions.widget === "checkout" ? "name" : "title"}="${options.payOptions.title}"` : ""}${options.payOptions.image ? `\n\t image="${options.payOptions.image}"` : ""}${options.payOptions.description ? `\n\t description="${options.payOptions.description}"` : ""}${options.payOptions.paymentMethods && options.payOptions.paymentMethods.length > 0 ? `\n\t paymentMethods={${JSON.stringify(options.payOptions.paymentMethods)}}` : ""}${
options.payOptions.widget === "transaction"
? `\n\t transaction={claimTo({
contract: nftContract,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Address, Chain } from "thirdweb";
import type { Chain } from "thirdweb/chains";
import type { ThemeOverrides } from "thirdweb/react";
import type { Address } from "thirdweb/utils";

export type BridgeComponentsPlaygroundOptions = {
theme: {
Expand All @@ -22,5 +23,7 @@ export type BridgeComponentsPlaygroundOptions = {

// transaction mode options
transactionData?: string; // Simplified for demo; could be more complex in real implementation

paymentMethods: ("crypto" | "card")[];
};
};
121 changes: 121 additions & 0 deletions apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useId, useState } from "react";
import type { Address } from "thirdweb";
import { defineChain } from "thirdweb/chains";
import { CustomRadioGroup } from "@/components/ui/CustomRadioGroup";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "../../../../lib/utils";
Expand Down Expand Up @@ -53,6 +54,8 @@ export function LeftSection(props: {
const modalTitleIconId = useId();
const modalDescriptionId = useId();
const themeId = useId();
const cryptoPaymentId = useId();
const cardPaymentId = useId();
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

ID collision issue: Generate unique IDs for each section.

The same checkbox IDs (cryptoPaymentId, cardPaymentId) are being reused in both the "buy" and "checkout" sections, which violates the requirement that DOM IDs be unique and could cause accessibility issues.

Generate separate IDs for each section:

  const cryptoPaymentId = useId();
  const cardPaymentId = useId();
+ const cryptoPaymentCheckoutId = useId();
+ const cardPaymentCheckoutId = useId();

Then use the checkout-specific IDs in the checkout section (lines 324, 349).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cryptoPaymentId = useId();
const cardPaymentId = useId();
const cryptoPaymentId = useId();
const cardPaymentId = useId();
const cryptoPaymentCheckoutId = useId();
const cardPaymentCheckoutId = useId();
🤖 Prompt for AI Agents
In apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx around lines 57
to 58, the IDs cryptoPaymentId and cardPaymentId are reused in both "buy" and
"checkout" sections causing ID collisions. To fix this, generate separate unique
IDs for the checkout section, for example checkoutCryptoPaymentId and
checkoutCardPaymentId, and replace the IDs used in the checkout section at lines
324 and 349 with these new IDs to ensure all DOM IDs are unique.


return (
<div className="flex flex-col gap-4">
Expand Down Expand Up @@ -161,6 +164,65 @@ export function LeftSection(props: {
/>
</div>
</div>

{/* Payment Methods */}
<div className="flex flex-col gap-3 pt-4">
<Label>Payment Methods</Label>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes(
"crypto",
)}
id={cryptoPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
"crypto",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
},
}));
}}
/>
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes("card")}
id={cardPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
"card",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
},
}));
}}
/>
<Label htmlFor={cardPaymentId}>Card</Label>
</div>
</div>
</div>
Comment on lines +168 to +225
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

Eliminate code duplication by extracting payment methods UI.

The payment methods selection UI and logic is duplicated between the buy and checkout sections. This violates the DRY principle and makes maintenance harder.

Consider extracting this into a reusable component:

function PaymentMethodsSelector({ 
  paymentMethods, 
  onPaymentMethodsChange,
  cryptoId,
  cardId 
}: {
  paymentMethods: ("crypto" | "card")[];
  onPaymentMethodsChange: (methods: ("crypto" | "card")[]) => void;
  cryptoId: string;
  cardId: string;
}) {
  const handleMethodChange = (method: "crypto" | "card", checked: boolean) => {
    const newMethods = checked 
      ? [...new Set([...paymentMethods, method])]
      : paymentMethods.filter(m => m !== method);
    onPaymentMethodsChange(newMethods);
  };

  return (
    <div className="flex flex-col gap-3">
      <Label>Payment Methods</Label>
      <div className="flex gap-4">
        <div className="flex items-center space-x-2">
          <Checkbox
            checked={paymentMethods.includes("crypto")}
            id={cryptoId}
            onCheckedChange={(checked) => handleMethodChange("crypto", !!checked)}
          />
          <Label htmlFor={cryptoId}>Crypto</Label>
        </div>
        <div className="flex items-center space-x-2">
          <Checkbox
            checked={paymentMethods.includes("card")}
            id={cardId}
            onCheckedChange={(checked) => handleMethodChange("card", !!checked)}
          />
          <Label htmlFor={cardId}>Card</Label>
        </div>
      </div>
    </div>
  );
}

This approach also simplifies the checkbox change logic by using Set to prevent duplicates naturally.

Also applies to: 315-372

🤖 Prompt for AI Agents
In apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx around lines
168 to 225, the payment methods UI and logic are duplicated, violating the DRY
principle. Extract this repeated UI and logic into a reusable
PaymentMethodsSelector component that accepts paymentMethods,
onPaymentMethodsChange, cryptoId, and cardId as props. Implement a single
handler inside this component to manage checkbox changes using a Set to avoid
duplicates. Replace the duplicated code blocks with this new component and apply
the same refactor to lines 315 to 372.

</div>
</div>
</div>
Expand Down Expand Up @@ -249,6 +311,65 @@ export function LeftSection(props: {
/>
</div>
</div>

{/* Payment Methods */}
<div className="flex flex-col gap-3">
<Label>Payment Methods</Label>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes(
"crypto",
)}
id={cryptoPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
"crypto",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "crypto",
),
},
}));
}}
/>
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={payOptions.paymentMethods.includes("card")}
id={cardPaymentId}
onCheckedChange={(checked) => {
setOptions((v) => ({
...v,
payOptions: {
...v.payOptions,
paymentMethods: checked
? [
...v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
"card",
]
: v.payOptions.paymentMethods.filter(
(m) => m !== "card",
),
},
}));
}}
/>
<Label htmlFor={cardPaymentId}>Card</Label>
</div>
</div>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function RightSection(props: {
client={THIRDWEB_CLIENT}
description={props.options.payOptions.description}
image={props.options.payOptions.image}
paymentMethods={props.options.payOptions.paymentMethods}
theme={themeObj}
title={props.options.payOptions.title}
tokenAddress={props.options.payOptions.buyTokenAddress}
Expand All @@ -81,6 +82,7 @@ export function RightSection(props: {
getDefaultImage(props.options.theme.type)
}
name={props.options.payOptions.title || "Your Product Name"}
paymentMethods={props.options.payOptions.paymentMethods}
presetOptions={[1, 2, 3]}
seller={props.options.payOptions.sellerAddress}
theme={themeObj}
Expand All @@ -95,6 +97,7 @@ export function RightSection(props: {
client={THIRDWEB_CLIENT}
description={props.options.payOptions.description}
image={props.options.payOptions.image}
paymentMethods={props.options.payOptions.paymentMethods}
theme={themeObj}
title={props.options.payOptions.title}
transaction={claimTo({
Expand Down
1 change: 1 addition & 0 deletions apps/playground-web/src/app/connect/pay/embed/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultConnectOptions: BridgeComponentsPlaygroundOptions = {
buyTokenChain: arbitrum,
description: "",
image: "",
paymentMethods: ["crypto", "card"],
sellerAddress: "0x0000000000000000000000000000000000000000",
title: "",
transactionData: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@
* Quick buy amounts
*/
presetOptions: [number, number, number] | undefined;

/**
* Allowed payment methods
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
}

export function BridgeOrchestrator({
Expand All @@ -127,6 +133,7 @@
purchaseData,
paymentLinkId,
presetOptions,
paymentMethods = ["crypto", "card"],

Check warning on line 136 in packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx#L136

Added line #L136 was not covered by tests
}: BridgeOrchestratorProps) {
// Initialize adapters
const adapters = useMemo(
Expand Down Expand Up @@ -270,6 +277,7 @@
}}
onError={handleError}
onPaymentMethodSelected={handlePaymentMethodSelected}
paymentMethods={paymentMethods}

Check warning on line 280 in packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx#L280

Added line #L280 was not covered by tests
receiverAddress={state.context.receiverAddress}
/>
)}
Expand Down
7 changes: 7 additions & 0 deletions packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@
* @hidden
*/
paymentLinkId?: string;

/**
* Allowed payment methods
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
};

// Enhanced UIOptions to handle unsupported token state
Expand Down Expand Up @@ -378,6 +384,7 @@
props.onError?.(err);
}}
paymentLinkId={props.paymentLinkId}
paymentMethods={props.paymentMethods}

Check warning on line 387 in packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/BuyWidget.tsx#L387

Added line #L387 was not covered by tests
presetOptions={props.presetOptions}
purchaseData={props.purchaseData}
receiverAddress={undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@
* @hidden
*/
paymentLinkId?: string;

/**
* Allowed payment methods
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
};

// Enhanced UIOptions to handle unsupported token state
Expand Down Expand Up @@ -341,6 +347,7 @@
props.onError?.(err);
}}
paymentLinkId={props.paymentLinkId}
paymentMethods={props.paymentMethods}

Check warning on line 350 in packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx#L350

Added line #L350 was not covered by tests
presetOptions={props.presetOptions}
purchaseData={props.purchaseData}
receiverAddress={props.seller}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@
* @hidden
*/
paymentLinkId?: string;

/**
* Allowed payment methods
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
};

// Enhanced UIOptions to handle unsupported token state
Expand Down Expand Up @@ -400,6 +406,7 @@
props.onError?.(err);
}}
paymentLinkId={props.paymentLinkId}
paymentMethods={props.paymentMethods}

Check warning on line 409 in packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx#L409

Added line #L409 was not covered by tests
presetOptions={props.presetOptions}
purchaseData={props.purchaseData}
receiverAddress={undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
* Whether to include the destination token in the payment methods
*/
includeDestinationToken?: boolean;

/**
* Allowed payment methods
* @default ["crypto", "card"]
*/
paymentMethods?: ("crypto" | "card")[];
}

type Step =
Expand All @@ -90,6 +96,7 @@
connectOptions,
connectLocale,
includeDestinationToken,
paymentMethods = ["crypto", "card"],

Check warning on line 99 in packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx#L99

Added line #L99 was not covered by tests
}: PaymentSelectionProps) {
const connectedWallets = useConnectedWallets();
const activeWallet = useActiveWallet();
Expand All @@ -115,7 +122,7 @@
? currentStep.selectedWallet
: activeWallet;
const {
data: paymentMethods,
data: suitableTokenPaymentMethods,

Check warning on line 125 in packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx#L125

Added line #L125 was not covered by tests
isLoading: paymentMethodsLoading,
error: paymentMethodsError,
} = usePaymentMethods({
Expand Down Expand Up @@ -248,6 +255,7 @@
onConnectWallet={handleConnectWallet}
onFiatSelected={handleFiatSelected}
onWalletSelected={handleWalletSelected}
paymentMethods={paymentMethods}

Check warning on line 258 in packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx#L258

Added line #L258 was not covered by tests
/>
)}

Expand All @@ -261,7 +269,7 @@
destinationToken={destinationToken}
onBack={handleBackToWalletSelection}
onPaymentMethodSelected={handlePaymentMethodSelected}
paymentMethods={paymentMethods}
paymentMethods={suitableTokenPaymentMethods}

Check warning on line 272 in packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx#L272

Added line #L272 was not covered by tests
paymentMethodsLoading={paymentMethodsLoading}
/>
)}
Expand Down
Loading
Loading