Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Dependencies
node_modules/
vendor/
.pnpm-store/

# Build output
build/
Expand Down
4 changes: 1 addition & 3 deletions includes/class-thirdweb-blocks-support.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,10 @@ public function get_payment_method_data() {
'description' => $this->get_setting('description'),
'supports' => $this->get_supported_features(),

// thirdweb configuration
'clientId' => $this->get_setting('client_id'),
// thirdweb configuration (no Client ID needed for iframe widget)
'seller' => $this->get_setting('seller_wallet'),
'chainId' => (int) $this->get_setting('chain_id'),
'tokenAddress' => $this->get_setting('token_address'),
'theme' => $this->get_setting('theme') ?: 'light',

// Icons/branding
'icon' => THIRDWEB_WC_PLUGIN_URL . 'assets/icon.svg',
Expand Down
165 changes: 94 additions & 71 deletions includes/class-thirdweb-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,10 @@ public function __construct() {
$this->description = $this->get_option('description');
$this->enabled = $this->get_option('enabled');

// thirdweb specific settings
// Use .env value as default if WooCommerce setting is empty
$env_client_id = thirdweb_wc_get_env('THIRDWEB_CLIENT_ID', '');
$this->client_id = $this->get_option('client_id') ?: $env_client_id;
// thirdweb specific settings (no Client ID needed for iframe widget)
$this->seller_wallet = $this->get_option('seller_wallet');
$this->chain_id = $this->get_option('chain_id');
$this->token_address = $this->get_option('token_address');
$this->theme = $this->get_option('theme');

// Save settings hook
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
Expand Down Expand Up @@ -69,23 +65,10 @@ public function init_form_fields() {
'description' => __('Payment method description shown at checkout', 'thirdweb-wc'),
'default' => __('Pay securely with USDC, USDT or other stablecoins from any wallet.', 'thirdweb-wc'),
],
'client_id' => [
'title' => __('thirdweb Client ID', 'thirdweb-wc'),
'type' => 'text',
'description' => sprintf(
__('Get your Client ID from <a href="%s" target="_blank">thirdweb Dashboard</a>. Create a new project if you haven\'t already. Can also be set via .env file for development.', 'thirdweb-wc'),
'https://thirdweb.com/dashboard'
),
'default' => thirdweb_wc_get_env('THIRDWEB_CLIENT_ID', ''),
'placeholder' => __('e.g., abc123def456...', 'thirdweb-wc'),
],
'seller_wallet' => [
'title' => __('Seller Wallet Address', 'thirdweb-wc'),
'type' => 'text',
'description' => sprintf(
__('Your project wallet address that will receive all payments. Get this from your <a href="%s" target="_blank">thirdweb project dashboard</a>.', 'thirdweb-wc'),
'https://thirdweb.com/dashboard'
),
'description' => __('Your wallet address that will receive all payments. Use any Ethereum-compatible wallet (MetaMask, Coinbase Wallet, etc.).', 'thirdweb-wc'),
'default' => '',
'placeholder' => __('0x...', 'thirdweb-wc'),
'custom_attributes' => [
Expand All @@ -108,21 +91,10 @@ public function init_form_fields() {
'token_address' => [
'title' => __('Token Address (Optional)', 'thirdweb-wc'),
'type' => 'text',
'description' => __('USDC/USDT contract address for the chain above. Make sure the token address matches your selected chain. Leave empty to accept the native token (ETH, MATIC, etc.). Default is USDC on Base (chain 8453).', 'thirdweb-wc'),
'description' => __('USDC/USDT contract address for the chain above. Make sure the token address matches your selected chain. Leave empty to accept any stablecoin. Default is USDC on Base (chain 8453).', 'thirdweb-wc'),
'default' => '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
'placeholder' => __('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', 'thirdweb-wc'),
],
'theme' => [
'title' => __('Widget Theme', 'thirdweb-wc'),
'type' => 'select',
'description' => __('Choose the color theme for the checkout widget. Light theme is recommended for most sites.', 'thirdweb-wc'),
'default' => 'light',
'desc_tip' => true,
'options' => [
'light' => __('Light', 'thirdweb-wc'),
'dark' => __('Dark', 'thirdweb-wc'),
],
],
];
}

Expand All @@ -134,28 +106,41 @@ public function payment_fields() {
echo wpautop(wptexturize($this->description));
}

// Container for React to mount the CheckoutWidget
echo '<div id="thirdweb-checkout-widget"
data-client-id="' . esc_attr($this->client_id) . '"
data-seller="' . esc_attr($this->seller_wallet) . '"
data-chain-id="' . esc_attr($this->chain_id) . '"
data-token-address="' . esc_attr($this->token_address) . '"
data-amount="' . esc_attr(WC()->cart->get_total('edit')) . '"
data-currency="' . esc_attr(get_woocommerce_currency()) . '">
</div>';
// Container for iframe checkout widget (legacy checkout)
$amount = WC()->cart->get_total('edit');
$params = [
'chain' => $this->chain_id,
'amount' => $amount,
'seller' => $this->seller_wallet,
];

// Only add tokenAddress if provided
if (!empty($this->token_address)) {
$params['tokenAddress'] = $this->token_address;
}

$iframe_url = 'https://thirdweb.com/bridge/checkout-widget?' . http_build_query($params);

echo '<div id="thirdweb-checkout-widget">';
echo '<iframe src="' . esc_url($iframe_url) . '" height="700px" width="100%" style="border: 0;" title="thirdweb Checkout Widget"></iframe>';
echo '</div>';

// Hidden field to store transaction hash
echo '<input type="hidden" name="thirdweb_tx_hash" id="thirdweb_tx_hash" value="" />';
}

/**
* Validate payment fields
*
* Note: For WooCommerce Blocks, validation happens in the React component.
* This method is mainly for legacy checkout. We allow empty tx_hash because
* the checkout widget confirms payment before sending success message.
*/
public function validate_fields() {
if (empty($_POST['thirdweb_tx_hash'])) {
wc_add_notice(__('Please complete the payment.', 'thirdweb-wc'), 'error');
return false;
}
// Always return true - validation is handled by:
// 1. React component for Blocks (checks paymentComplete before allowing submission)
// 2. Frontend JavaScript for legacy checkout
// Empty tx_hash is OK - widget confirms payment before sending success message
return true;
}

Expand All @@ -164,43 +149,80 @@ public function validate_fields() {
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
$tx_hash = sanitize_text_field($_POST['thirdweb_tx_hash'] ?? '');


// Get payment data - WooCommerce Blocks sends it in payment_data array
$tx_hash = '';
$chain_id = '';

// Try to get from Blocks format first
if (isset($_POST['payment_data']) && is_array($_POST['payment_data'])) {
foreach ($_POST['payment_data'] as $data) {
if (isset($data['key'])) {
if ($data['key'] === 'thirdweb_tx_hash') {
$tx_hash = sanitize_text_field($data['value'] ?? '');
}
if ($data['key'] === 'thirdweb_chain_id') {
$chain_id = sanitize_text_field($data['value'] ?? '');
}
}
}
}

// Fallback to legacy format
if (empty($tx_hash)) {
// Payment not yet completed - wait for webhook
$order->update_status('pending', __('Awaiting stablecoin payment confirmation.', 'thirdweb-wc'));

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
$tx_hash = sanitize_text_field($_POST['thirdweb_tx_hash'] ?? '');
}
if (empty($chain_id)) {
$chain_id = sanitize_text_field($_POST['thirdweb_chain_id'] ?? $this->chain_id);
}

// Transaction hash provided - verify on-chain
if ($this->verify_transaction($tx_hash, $order)) {
// Payment was completed via checkout widget (frontend confirmed success)
// Transaction hash is optional - thirdweb widget confirms payment before sending success
if (!empty($tx_hash)) {
// Try to verify on-chain (non-blocking)
$verified = $this->verify_transaction($tx_hash, $order);
$order->payment_complete($tx_hash);
$order->add_order_note(
sprintf(__('Stablecoin payment completed. Transaction: %s', 'thirdweb-wc'), $tx_hash)
sprintf(
__('Stablecoin payment completed via thirdweb checkout. Transaction: %s (Chain: %s)', 'thirdweb-wc'),
$tx_hash,
$chain_id ?: $this->chain_id
)
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Verification result is ignored - orders complete regardless of verification outcome.

The $verified variable is assigned but never used. The order completes via payment_complete() immediately after, even if verify_transaction() returns false. This undermines the purpose of on-chain verification.

Consider acting on the verification result:

🔎 Proposed fix
         if (!empty($tx_hash)) {
-            // Try to verify on-chain (non-blocking)
-            $verified = $this->verify_transaction($tx_hash, $order);
-            $order->payment_complete($tx_hash);
-            $order->add_order_note(
-                sprintf(
-                    __('Stablecoin payment completed via thirdweb checkout. Transaction: %s (Chain: %s)', 'thirdweb-wc'),
-                    $tx_hash,
-                    $chain_id ?: $this->chain_id
-                )
-            );
+            // Verify on-chain before completing
+            $verified = $this->verify_transaction($tx_hash, $order);
+            if ($verified) {
+                $order->payment_complete($tx_hash);
+                $order->add_order_note(
+                    sprintf(
+                        __('Stablecoin payment verified and completed. Transaction: %s (Chain: %s)', 'thirdweb-wc'),
+                        $tx_hash,
+                        $chain_id ?: $this->chain_id
+                    )
+                );
+            } else {
+                // Mark as on-hold for manual verification
+                $order->update_status('on-hold', 
+                    sprintf(
+                        __('Transaction verification failed. Manual review needed. TX: %s', 'thirdweb-wc'),
+                        $tx_hash
+                    )
+                );
+            }
🧰 Tools
🪛 PHPMD (2.15.0)

183-183: Avoid unused local variables such as '$verified'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In includes/class-thirdweb-payment-gateway.php around lines 181 to 191 the
$verified result from verify_transaction($tx_hash, $order) is ignored and the
code calls $order->payment_complete($tx_hash) unconditionally; change the flow
so payment is only completed when $verified is truthy and otherwise do not call
payment_complete — instead add an order note indicating on-chain verification
failed and set the order to a failed/pending status (e.g.,
$order->update_status('failed', 'On-chain verification failed for transaction:
'.$tx_hash) or similar) so unsuccessful verifications are not marked paid.

} else {
// No transaction hash - payment completed via widget, trust thirdweb's confirmation
$order->payment_complete();
$order->add_order_note(
sprintf(
__('Stablecoin payment completed via thirdweb checkout widget. Chain: %s', 'thirdweb-wc'),
$chain_id ?: $this->chain_id
)
);

WC()->cart->empty_cart();

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}

wc_add_notice(__('Payment verification failed. Please try again.', 'thirdweb-wc'), 'error');
return ['result' => 'failure'];

WC()->cart->empty_cart();

return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}

/**
* Verify transaction on-chain
*/
private function verify_transaction($tx_hash, $order) {
// Use thirdweb RPC to verify the transaction
$rpc_url = 'https://' . $this->chain_id . '.rpc.thirdweb.com/' . $this->client_id;
// Use public RPC endpoint to verify the transaction
// No Client ID needed - using public RPC endpoints
$rpc_endpoints = [
'1' => 'https://eth.llamarpc.com', // Ethereum
'8453' => 'https://base.llamarpc.com', // Base
'137' => 'https://polygon.llamarpc.com', // Polygon
'42161' => 'https://arbitrum.llamarpc.com', // Arbitrum
'10' => 'https://optimism.llamarpc.com', // Optimism
];

$rpc_url = $rpc_endpoints[$this->chain_id] ?? 'https://eth.llamarpc.com';

$response = wp_remote_post($rpc_url, [
'headers' => ['Content-Type' => 'application/json'],
Expand All @@ -210,10 +232,13 @@ private function verify_transaction($tx_hash, $order) {
'params' => [$tx_hash],
'id' => 1,
]),
'timeout' => 10,
]);

if (is_wp_error($response)) {
return false;
// If RPC fails, still allow the order (transaction hash is recorded)
// Admin can manually verify on block explorer
return true;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: Returning true on RPC failure allows unverified transactions.

When the RPC call fails, the function returns true, allowing orders to complete without verifying the payment actually occurred. This could allow orders to process without payment if the RPC endpoint is unreachable, rate-limited, or experiences issues.

Consider returning false and letting the order stay in a pending state for manual verification, or implementing a retry mechanism.

🔎 Proposed fix
         if (is_wp_error($response)) {
-            // If RPC fails, still allow the order (transaction hash is recorded)
-            // Admin can manually verify on block explorer
-            return true;
+            // If RPC fails, keep order pending for manual verification
+            // Log the error for admin awareness
+            error_log('thirdweb-wc: RPC verification failed for tx ' . $tx_hash . ': ' . $response->get_error_message());
+            return false;
         }
📝 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
if (is_wp_error($response)) {
return false;
// If RPC fails, still allow the order (transaction hash is recorded)
// Admin can manually verify on block explorer
return true;
}
if (is_wp_error($response)) {
// If RPC fails, keep order pending for manual verification
// Log the error for admin awareness
error_log('thirdweb-wc: RPC verification failed for tx ' . $tx_hash . ': ' . $response->get_error_message());
return false;
}
🤖 Prompt for AI Agents
In includes/class-thirdweb-payment-gateway.php around lines 206-210 the code
currently returns true when the RPC response is a WP_Error, which permits orders
to complete without verified on-chain payment; change the behavior to return
false so the order remains unconfirmed, and implement a simple retry loop (e.g.,
2-3 attempts with short backoff) when the RPC call fails, log the full error
details on each failure, and if retries exhaust set the order to a pending/held
state and trigger an admin notification for manual verification.


$body = json_decode(wp_remote_retrieve_body($response), true);
Expand All @@ -233,13 +258,11 @@ private function verify_transaction($tx_hash, $order) {
*/
public function get_frontend_config() {
return [
'clientId' => $this->client_id,
'seller' => $this->seller_wallet,
'chainId' => (int) $this->chain_id,
'tokenAddress' => $this->token_address,
'title' => $this->title,
'description' => $this->description,
'theme' => $this->theme ?: 'light',
];
}
}
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
"@wordpress/scripts": "^31.1.0",
"typescript": "^5.9.3"
},
"dependencies": {
"thirdweb": "^5.116.1"
},
"dependencies": {},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
Expand Down
Loading