Skip to content

[PIWOO-792] Update PayPal button logic: restrict physical items #1195

Open
Biont wants to merge 7 commits intodev/developfrom
dev/PIWOO-792-paypal-filter-by-virtual-product-type
Open

[PIWOO-792] Update PayPal button logic: restrict physical items #1195
Biont wants to merge 7 commits intodev/developfrom
dev/PIWOO-792-paypal-filter-by-virtual-product-type

Conversation

@Biont
Copy link
Copy Markdown
Collaborator

@Biont Biont commented Apr 15, 2026

Restrict PayPal Express button to virtual products only

Problem

The PayPal Express button (and its "— OR —" divider in the block cart) was appearing for physical products. It should only appear for virtual products, because PayPal Express bypasses the standard WooCommerce checkout address-collection flow — a workflow that is unsuitable for orders that require physical shipping.

The root cause is a reliance on needs_shipping / WC_Cart::needs_shipping() /
WC Blocks' getNeedsShipping() as the "show button?" signal. These helpers short-circuit to false when:

  • shipping is globally disabled in WooCommerce settings, or
  • no shipping methods are configured for any zone.

On such sites every cart appears non-shipping — even carts containing physical products — so the button was unconditionally shown.

The regression was made more visible by the bootstrap timing fix in bec6d738, which deferred button initialisation to woocommerce_init. Before that commit, the PayPal gateway was sometimes absent from the registered gateways at bootstrap time, suppressing the button as a side-effect.


Solution

PHP — classic cart and product page (PayPalExpressButton.php)

Replace all needs_shipping calls with WC_Product::is_virtual() checks:

  • Product page — the existing mollieWooCommerceCheckIfNeedShipping() helper is replaced by a new isVirtualProduct(\WC_Product) method. For simple products this returns $product->is_virtual(). For variable products it returns true if at least one available variation is virtual, so the button appears on product pages where a virtual variant can be selected.
  • Cart page$cart->needs_shipping() is replaced by iterating over $cart->get_cart_contents() and calling $product->is_virtual() on each item; the button is suppressed as soon as any physical item is found.

JS — WC Blocks express registration (resources/js/src/checkout/blocks/index.js)

The express_payment_methods filter controls whether registerExpressPaymentMethod is called at all. This filter now gates only on PayPal being configured and enabled (isExpressEnabled); it no longer inspects cart contents. Cart-content gating at registration time was deliberately avoided: registerExpressPaymentMethod is called once at page load and cannot be re-invoked if items are later removed, so a page-load cart snapshot would prevent the button from appearing after a physical item is removed without a full reload.

The canMakePayment callback in express_payment_method_args carries the virtual-only check. WC Blocks calls canMakePayment reactively on every cart mutation. The callback reads item data directly from the store via select('wc/store/cart').getCartData().items — the cartItems argument that WC Blocks passes to canMakePayment does not carry Store API extension fields, so the store must be read explicitly. When the cart changes from mixed to all-virtual, canMakePayment returns true and both the button and the "— OR —" divider appear without a page reload. When any physical item is present, it returns false and WC Blocks suppresses the entire express payment section.

WC Store API extension (PayPalExpressButton::registerStoreApiExtension())

A new woocommerce_store_api_register_endpoint_data registration exposes a virtual boolean per cart item under the mollie-payments namespace:

"extensions": {
  "mollie-payments": { "virtual": false }
}

This makes WC_Product::is_virtual() available to JavaScript through the standard WC Blocks cart store (select('wc/store/cart').getCartData().items), so the JS layer never has to infer product type from shipping configuration.

JS — classic-cart and product-page scripts (paypalButtonCart.js, paypalButton.js)

Both scripts are enqueued unconditionally on their respective pages, but the PHP layer now only renders #mollie-PayPal-button when the product/cart qualifies (all-virtual). Two defensive guards were added:

  • paypalButtonCart.jscalculateTotal() now returns Infinity when the .cart-subtotal DOM element is absent (i.e. the page is using the block cart, which has no classic-cart markup). Infinity means minFee > Infinity is always false, so the script never attempts to hide the button.
  • paypalButton.js — an early return is added after document.querySelector('#mollie-PayPal-button') when the element is null (i.e. the product is physical and PHP did not render the button).

Rationale: is_virtual vs needs_shipping

needs_shipping / getNeedsShipping() is a logistics concern: it answers "does this cart require a shipping method to be selected?" — a question that depends on store configuration (globally enabled shipping, configured zones, product shipping classes). It is a valid guard for blocking checkout progression but is not a reliable product-type signal.

is_virtual is a product attribute: it is set on the product itself and is independent of shipping configuration. It answers "is this product delivered digitally?" — which is exactly the question the PayPal Express gating needs answered.

PayPal Express is only appropriate for virtual goods because:

  1. It redirects the customer to PayPal immediately without collecting a shipping address from WooCommerce.
  2. Physical orders require a WooCommerce-collected shipping address for fulfilment and tax calculation.
  3. Relying on needs_shipping creates a false equivalence between "shipping is disabled" and "the product is digital", which breaks on any site that sells mixed or physical catalogues with shipping globally disabled.

Biont added 2 commits April 15, 2026 12:27
…al item handling

Refactor `canMakePayment` checks to filter out carts containing physical products. Introduce a WC Store API extension to expose the `is_virtual` flag for enhanced product-level validation. Optimize Express Button registration logic to ensure compatibility and correctness.
- Replace `getCartItems` with `getCartData().items` for more comprehensive cart data extraction.
- Add fallback check for missing subtotal element in `calculateTotal` to handle non-classic cart pages.
Biont added 2 commits April 15, 2026 13:00
… and move virtual item checks for Express Button registration to `canMakePayment` for reactive updates.
@Biont Biont marked this pull request as ready for review April 15, 2026 11:21
@Biont Biont requested a review from mmaymo April 15, 2026 11:21
Biont added 2 commits April 15, 2026 16:18
…to AssetsModule

Refactor the `is_virtual` flag registration to consolidate API-related logic within `AssetsModule`, improving implementation clarity and reducing duplication.
- Replace `return Infinity` with a thrown error when the `cart-subtotal` element is not found.
- Add a try-catch block in `underRange` to handle non-classic cart pages gracefully.
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.

2 participants