Skip to content

PRE-3570: Fix good order status after paying with IP#1202

Merged
jhoaraupp merged 1 commit into
release/3.0.0from
fix/PRE-3570
Jul 22, 2026
Merged

PRE-3570: Fix good order status after paying with IP#1202
jhoaraupp merged 1 commit into
release/3.0.0from
fix/PRE-3570

Conversation

@jhoaraupp

@jhoaraupp jhoaraupp commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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 onPaymentSetup and then did a raw window.location redirect, without ever returning a response to WooCommerce Blocks. Per the Blocks payment API contract, onPaymentSetup must 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 reading order_id from the client-side checkout store and calling our own payplug_create_intent AJAX endpoint from onPaymentSetup, before that real order existed — always sending order_id: 0, which the server correctly rejected as "Invalid order."

Fix: for a regular checkout, onPaymentSetup now just returns {type: 'success'} immediately and lets WooCommerce place the order normally. The PayPlug payment intent is created server-side inside process_payment() (already implemented and shared with classic checkout — no PHP changes needed there), and the resulting payment_id/redirect come back to the client via payment_details on the onCheckoutSuccess event, 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 with hash_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) and ajax_apple_pay_create_order_pay() (Apple Pay) both resolve the gateway via get_available_payment_gateways(). On order-pay, this always came back empty. Root cause: this AJAX request's own URL never carries the order-pay query var (only the page that triggered it does), but WC_Payment_Gateway::get_order_total() — used by this plugin's own check_gateway() filter on woocommerce_available_payment_gateways to 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-pay query 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)

  • Popup/Amex's onCheckoutSuccess now reads the payment redirect/cancel URLs from processingResponse.paymentDetails (regular checkout) or from its own onPaymentSetup AJAX response (order-pay), instead of a stale local variable; showPopupPayment() now actually calls resolve() on success (it never did, which could hang the checkout indefinitely) and the success/error branches, previously swapped, now map to the right outcome.
  • ObjIntegratedPayment was a plain object re-created on every React re-render, silently discarding paymentId/return_url/the SDK instance between renders; it's now a useRef, matching how the Apple Pay Blocks component in this repo already avoids the same issue.
  • getPayment() now includes order_pay_key in 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.showSaveOption read a non-existent settings.oneclick field instead of settings.showSaveOption, so saved-card/tokenization UI could never be enabled in Blocks.
  • Replaced deprecated jQuery jqXHR.success()/.error() (removed in jQuery 3.x) with .done()/.fail(), and normalized the .fail() callback (which receives a jqXHR, not an Error) into a proper Error with a usable .message.
  • parse_url()wp_parse_url() in ajax_create_payment(), consistent with the rest of the plugin (e.g. Apple Pay), plus defensive handling of a missing/malformed _wp_http_referer.
  • Removed an unused getSetting/settings import in the Popup component.
  • Extracted the Integrated Payment SDK's secureDomain (previously hardcoded identically in two files) into SECURE_DOMAIN_LIVE/SECURE_DOMAIN_QA constants (payplug-config.php) plus a single PayplugWoocommerceHelper::get_secure_domain() helper.

Known open question

get_secure_domain()'s check_mode() ? QA : LIVE mapping was flagged by review as inverted relative to how check_mode() is used everywhere else in this codebase (true selects 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):

  • Regular checkout, Integrated Payment via Checkout Block: payment succeeds, order correctly transitions to "Processing" with transaction_id set.
  • Order-pay page, Integrated Payment (classic form): payment succeeds with an empty session cart, order's transaction_id set; spoofed/wrong order key correctly rejected; missing referer no longer raises PHP notices.
  • Order-pay page, Apple Pay: same gateway-resolution fix applies; not independently verified locally since Apple Pay requires HTTPS, unavailable in the local Docker setup — please confirm on staging.
  • Popup/American Express mode: fix applies the established pattern consistently, but not independently verified locally (no test account configured for this mode) — please smoke-test before merging.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue) [x]
  • ✨ New feature (non-breaking change that adds functionality) [ ]
  • 💥 Breaking change (fix or feature that causes existing functionality to change and that could impact other libs) [ ]
  • 🔧 Refactor (no functional changes, code improvement only) [ ]
  • 📦 Dependency update [ ]
  • 🔒 Security fix [ ]
  • 📝 Documentation update [ ]

Checklist

Code Quality

  • Code is linted and formatted
  • No unnecessary commented-out code or debug logs
  • No hardcoded values (use env variables or config)

Testing

  • Unit tests added / updated

Security & Ops

  • No sensitive data or secrets introduced
  • Logging and error handling are appropriate

Copilot AI review requested due to automatic review settings July 21, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 to process_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 secureDomain value 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 ⚠️ Medium (more JS flow complexity; needs small robustness tweaks)

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

Comment thread src/PayplugWoocommerceRequest.php Outdated
Comment thread resources/js/frontend/helper/wc-payplug-requests.js
Comment thread resources/js/frontend/helper/wc-payplug-requests.js
Comment thread resources/js/frontend/wc-payplug-popup-blocks.js Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file

Comment thread resources/js/frontend/wc-payplug-blocks.js
Comment thread resources/js/frontend/helper/wc-payplug-requests.js
Comment thread resources/js/frontend/wc-payplug-popup-blocks.js
Comment thread resources/js/frontend/wc-payplug-integratedPayment-blocks.js Outdated
Comment thread resources/js/frontend/wc-payplug-popup-blocks.js
Comment thread resources/js/frontend/wc-payplug-popup-blocks.js Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 07:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file

Comment thread src/Gateway/Blocks/PayplugCreditCard.php Outdated
Comment thread resources/js/frontend/helper/wc-payplug-requests.js Outdated
Comment thread resources/js/frontend/helper/wc-payplug-requests.js Outdated
Comment thread resources/js/frontend/wc-payplug-popup-blocks.js Outdated
Comment thread src/PayplugWoocommerceRequest.php Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 08:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • secureDomain selection appears inverted relative to how check_mode() is used elsewhere (e.g. PayplugWoocommerceRequest::applepay_update_payment() treats check_mode() === true as 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';

Comment thread src/PayplugWoocommerceRequest.php Outdated
Comment thread src/PayplugWoocommerceRequest.php Outdated
Copilot AI review requested due to automatic review settings July 22, 2026 08:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • secureDomain is inverted relative to PayplugWoocommerceHelper::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 on get_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;

Copilot AI review requested due to automatic review settings July 22, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • assets/js/blocks/wc-payplug-payplug-blocks.js: Generated file

Comment thread src/PayplugWoocommerceHelper.php
Comment thread src/PayplugWoocommerceRequest.php
Copilot AI review requested due to automatic review settings July 22, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@jhoaraupp
jhoaraupp requested a review from Copilot July 22, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@jhoaraupp
jhoaraupp merged commit 0085920 into release/3.0.0 Jul 22, 2026
10 of 13 checks passed
@jhoaraupp
jhoaraupp deleted the fix/PRE-3570 branch July 22, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants