PRE-3570: Fix good order status after paying with IP#1202
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses Integrated Payment order finalization issues on WooCommerce Checkout Blocks (orders stuck in “Pending payment”), and fixes order-pay edge cases (cartless flows + gateway resolution) uncovered during end-to-end testing.
Changes:
- Adjusts order-pay handling to resolve gateways from the full gateway list (not
get_available_payment_gateways()), and relaxes the “empty cart” restriction for order-pay. - Updates Blocks Integrated Payment flow to return the expected
{ type: 'success' | 'error' }shapes and defer intent creation toprocess_payment()for regular block checkout. - Adds/updates Blocks frontend JS helpers/components (requests helper, popup, integrated payment) and rebuilds the bundled assets.
1. What's Good
- Correctly identifies the Woo Blocks contract issue (needing an explicit success/error response) and aligns Integrated Payment’s Blocks flow to it.
- The order-pay gateway resolution change is consistent with how
is_available()depends on cart state and explains the rationale inline. - Adds a dedicated
secureDomainvalue for QA/test merchants to avoid the SDK’s prod-domain default.
2. Summary table
| Dimension | Rating |
|---|---|
| Security | ❌ High (order-pay path can be spoofed via referer without order-key validation) |
| Correctness | ❌ High (popup flow can hang + jQuery callbacks may break on jQuery 3.x) |
| Performance | ✅ Fine |
| Maintainability |
3. Closing one-liner
Fix the order-pay validation in ajax_create_payment() and the Blocks popup/request JS issues before merge.
4. Individual findings
Heading: Security ❌ High
Subtitle (bold): Order-pay bypass via spoofable referer without order-key validation (src/PayplugWoocommerceRequest.php:158)
Code block:
$https_referer = $_POST['_wp_http_referer'];
$path = parse_url($https_referer);
...
if (empty($order_id) && WC()->cart->is_empty()) {
wp_send_json_error(__('Empty cart', 'payplug'));
}Explanation paragraph: The new logic treats any request whose referer “looks like” an order-pay URL as exempt from the empty-cart check, but it doesn’t validate the order key (unlike the Apple Pay order-pay endpoint). Because this endpoint is exposed via wc_ajax_*, a crafted request can bypass the cart guard and attempt payment processing on arbitrary order IDs.
Fix line: Fix: Harden referer parsing, normalize order_id, and validate key against the order’s key when an order-pay request is detected (see stored PR comment for the concrete patch).
Heading: Bug ❌ High
Subtitle (bold): Popup Blocks handler can hang indefinitely (promise never resolves) and doesn’t return a Blocks response (resources/js/frontend/wc-payplug-popup-blocks.js:38)
Code block:
return new Promise(async (resolve, reject) => {
try {
await Payplug.showPayment(getPaymentData.data.redirect);
} catch (e) {
reject(e);
}
})Explanation paragraph: resolve() is never called on success, so the awaiting code can block forever; additionally the onCheckoutSuccess handler doesn’t reliably return a { type: ... } response to Blocks, which can prevent the checkout flow from completing.
Fix line: Fix: Resolve the promise after Payplug.showPayment(...) completes and return a { type: 'success' } / { type: 'error' } response object (see stored PR comment for an inline replacement).
Heading: Bug ❌ High
Subtitle (bold): Use of removed jQuery jqXHR success/error callbacks (resources/js/frontend/helper/wc-payplug-requests.js:8)
Code block:
}).success(function (response) {
resolve(response);
}).error(function (error) {
reject(error);
});Explanation paragraph: success()/error() were removed in jQuery 3.x; relying on them can break on sites where jQuery Migrate is not loaded. This can cause payment intent creation / payment status checks to fail at runtime.
Fix line: Fix: Switch to done() / fail() for jqXHR (see stored PR comments for exact replacements).
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/PayplugWoocommerceRequest.php | Adjusts order-pay flow for gateway resolution and cart checks; needs order-pay auth hardening. |
| src/Gateway/Blocks/PayplugCreditCard.php | Exposes additional Blocks settings (secureDomain, order-pay metadata). |
| resources/js/frontend/wc-payplug-popup-blocks.js | Adds popup Blocks handler; currently contains a promise/response-flow bug. |
| resources/js/frontend/wc-payplug-integratedPayment-blocks.js | Reworks Integrated Payment Blocks flow to align with Blocks API expectations. |
| resources/js/frontend/wc-payplug-blocks.js | Registers the payment method and selects Integrated vs popup content. |
| resources/js/frontend/helper/wc-payplug-requests.js | Adds shared AJAX helpers; needs jQuery compatibility fix. |
| assets/js/blocks/wc-payplug-payplug-blocks.js | Rebuilt bundled Blocks script. |
| assets/js/blocks/wc-payplug-payplug-blocks.asset.php | Updates asset version hash. |
Files not reviewed (1)
- assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file
Comments suppressed due to low confidence (1)
src/Gateway/Blocks/PayplugCreditCard.php:68
secureDomainselection appears inverted relative to howcheck_mode()is used elsewhere (e.g.PayplugWoocommerceRequest::applepay_update_payment()treatscheck_mode() === trueas live). As written, live mode would be pointed at the QA domain and test mode at the production domain, which is likely to break Integrated Payment on at least one environment.
// The Integrated Payment SDK defaults to its production domain when none is given,
// which 404s for a test/QA merchant account - point it at the matching QA domain.
$data['secureDomain'] = PayplugWoocommerceHelper::check_mode() ? 'https://secure-qa.payplug.com' : 'https://secure.payplug.com';
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file
Comments suppressed due to low confidence (2)
src/Gateway/Blocks/PayplugCreditCard.php:68
secureDomainis inverted relative toPayplugWoocommerceHelper::check_mode()semantics (truthy indicates live mode in the rest of the codebase). As written, live merchants would be pointed at the QA domain and test merchants at production, which can break Integrated Payment in production.
// The Integrated Payment SDK defaults to its production domain when none is given,
// which 404s for a test/QA merchant account - point it at the matching QA domain.
$data['secureDomain'] = PayplugWoocommerceHelper::check_mode() ? 'https://secure-qa.payplug.com' : 'https://secure.payplug.com';
src/PayplugWoocommerceRequest.php:240
- The PR description says order-pay gateway resolution was changed to use the full (unfiltered) gateway list and check
enabled === 'yes'directly, but this code path still relies onget_available_payment_gateways(). Either update the description to match the implemented approach, or switch the implementation to the unfiltered lookup if that was the intended fix.
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if (!isset($available_gateways[$payment_method])) {
return;
Description
Fixes Integrated Payment orders getting stuck in "Pending payment" on the WooCommerce Checkout Block, plus several related bugs uncovered while testing the fix end-to-end (order-pay page, classic checkout, Popup/Amex mode) and while addressing review feedback.
Root causes & fixes
1. Order status never updated after a successful payment (regression from WOOC-1441)
The Integrated Payment Blocks component performed the card capture inside
onPaymentSetupand then did a rawwindow.locationredirect, without ever returning a response to WooCommerce Blocks. Per the Blocks payment API contract,onPaymentSetupmust resolve with{type: 'success' | 'error'}for WooCommerce to actually place/finalize the order — since it never did, the order was left in "Pending payment" regardless of whether the PayPlug payment itself succeeded.2. Incompatibility with WooCommerce >= 10.8 (order not created yet when payment is requested)
Since WC 10.8, the real order is no longer created eagerly on checkout page load — it's only created when the customer submits "Place order" (
POST /wc/store/v1/checkout). The Integrated Payment component was still readingorder_idfrom the client-side checkout store and calling our ownpayplug_create_intentAJAX endpoint fromonPaymentSetup, before that real order existed — always sendingorder_id: 0, which the server correctly rejected as "Invalid order."Fix: for a regular checkout,
onPaymentSetupnow just returns{type: 'success'}immediately and lets WooCommerce place the order normally. The PayPlug payment intent is created server-side insideprocess_payment()(already implemented and shared with classic checkout — no PHP changes needed there), and the resultingpayment_id/redirectcome back to the client viapayment_detailson theonCheckoutSuccessevent, where the actual card capture (Payplug.IntegratedPayment.pay()) now happens. This follows the same pattern already established for Apple Pay (PRE-3545).Order-pay (repaying an existing order) is unaffected: a real order already exists there, so the payment intent is still created directly from
onPaymentSetup, same as before.Popup/American Express mode had the exact same issue and received the same fix.
3. Order-pay page (classic checkout): empty cart incorrectly rejected
ajax_create_payment()unconditionally required a non-empty cart, but on the order-pay page there is no cart at all (the customer already checked out; they're repaying an existing order). The check now only applies when this isn't an order-pay request — and, since the referer this is detected from is client-supplied and spoofable, the order is only trusted as a genuine order-pay request once its key is verified withhash_equals(), the same way the other order-pay AJAX flows already do.4. Order-pay page: every payment gateway appeared unavailable
process_order_payment()(used by Integrated Payment / classic checkout) andajax_apple_pay_create_order_pay()(Apple Pay) both resolve the gateway viaget_available_payment_gateways(). On order-pay, this always came back empty. Root cause: this AJAX request's own URL never carries theorder-payquery var (only the page that triggered it does), butWC_Payment_Gateway::get_order_total()— used by this plugin's owncheck_gateway()filter onwoocommerce_available_payment_gatewaysto enforce per-payment-method amount permissions — reads that query var to decide whether to use the order's total or the (empty, on order-pay) cart's. Left unset, it fell back to a cart total of 0, which the amount-permission check then rejected, making every gateway look unavailable regardless of its actual configuration.Fix: explicitly set the
order-payquery var ($wp_query->set('order-pay', $order_id)) before resolving gateways, so the existing, fully-featured availability check (API key presence, requirements, amount permissions, etc.) runs correctly instead of being bypassed.Other fixes (from review)
onCheckoutSuccessnow reads the payment redirect/cancel URLs fromprocessingResponse.paymentDetails(regular checkout) or from its ownonPaymentSetupAJAX response (order-pay), instead of a stale local variable;showPopupPayment()now actually callsresolve()on success (it never did, which could hang the checkout indefinitely) and the success/error branches, previously swapped, now map to the right outcome.ObjIntegratedPaymentwas a plain object re-created on every React re-render, silently discardingpaymentId/return_url/the SDK instance between renders; it's now auseRef, matching how the Apple Pay Blocks component in this repo already avoids the same issue.getPayment()now includesorder_pay_keyin its AJAX payload, so the server-side key validation on order-pay (create_payment_intent()) actually has something to check instead of silently skipping it.supports.showSaveOptionread a non-existentsettings.oneclickfield instead ofsettings.showSaveOption, so saved-card/tokenization UI could never be enabled in Blocks.jqXHR.success()/.error()(removed in jQuery 3.x) with.done()/.fail(), and normalized the.fail()callback (which receives a jqXHR, not anError) into a properErrorwith a usable.message.parse_url()→wp_parse_url()inajax_create_payment(), consistent with the rest of the plugin (e.g. Apple Pay), plus defensive handling of a missing/malformed_wp_http_referer.getSetting/settingsimport in the Popup component.secureDomain(previously hardcoded identically in two files) intoSECURE_DOMAIN_LIVE/SECURE_DOMAIN_QAconstants (payplug-config.php) plus a singlePayplugWoocommerceHelper::get_secure_domain()helper.Known open question
get_secure_domain()'scheck_mode() ? QA : LIVEmapping was flagged by review as inverted relative to howcheck_mode()is used everywhere else in this codebase (trueselects the live API key). Confirmed empirically in a local test (mode=true → live key selected → currently resolves to the QA domain; mode=false → test key → currently resolves to the production domain) — the mapping is indeed inconsistent with that convention. Whether that means it should simply be flipped, or whether the QA domain is actually intended for a different condition entirely (e.g. a PayPlug-internal QA merchant tier rather than "any test-mode merchant"), needs the domain/business-logic context which is still being confirmed before changing behavior here.Testing
Verified locally end-to-end (Docker WordPress + MariaDB, WC 10.9.4):
transaction_idset.transaction_idset; spoofed/wrong order key correctly rejected; missing referer no longer raises PHP notices.Type of Change
Checklist
Code Quality
Testing
Security & Ops