Skip to content

Commit fbcf268

Browse files
committed
Merge remote-tracking branch 'origin/release/9.5.3' into trunk
2 parents 69515f1 + 155396c commit fbcf268

24 files changed

+373
-123
lines changed

changelog.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
*** Changelog ***
22

3+
= 9.5.3 - 2025-06-23 =
4+
* Fix - Reimplement mapping of Express Checkout state values to align with WooCommerce's expected state formats
5+
* Fix - Adds an exception to be thrown when the order item quantity is zero, during the retrieval of level 3 data from an order.
6+
* Tweak - Track charge completed via webhooks in order notes
7+
* Fix - Ensure that we migrate payment_request_button_size=medium on upgrade
8+
* Fix - Show correct price in express checkout for zero decimal currencies
9+
* Fix - Fixes a possible fatal error with Multibanco purchases when generating the email instructions.
10+
* Fix - Fix buggy unsaved changes warning in settings page
11+
* Fix - Use the platform's payment method configuration id constant when rendering the Optimized Checkout
12+
* Update - Improve checks in voucher purchase flow
13+
314
= 9.5.2 - 2025-05-22 =
415
* Add - Implement custom database cache for persistent caching with in-memory optimization.
516
* Update - Remove feature that flags 401s and proactively blocks subsequent API calls until the store has reauthenticated.

client/api/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,23 @@ export default class WCStripeAPI {
497497
);
498498
}
499499

500+
/**
501+
* Normalizes address fields in WooCommerce supported format.
502+
*
503+
* @param {Object} billingAddress Billing address.
504+
* @param {Object} shippingAddress Shipping address.
505+
* @return {Promise} Promise for the request to the server.
506+
*/
507+
expressCheckoutNormalizeAddress( billingAddress, shippingAddress ) {
508+
return this.request( getExpressCheckoutAjaxURL( 'normalize_address' ), {
509+
security: getExpressCheckoutData( 'nonce' )?.normalize_address,
510+
data: {
511+
billing_address: billingAddress,
512+
shipping_address: shippingAddress,
513+
},
514+
} );
515+
}
516+
500517
/**
501518
* Get cart items and total amount.
502519
*

client/classic/upe/payment-processing.js

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,8 @@ export const createAndConfirmSetupIntent = (
567567
* @param {Object} jQueryForm The jQuery object for the form being submitted.
568568
*/
569569
export const confirmVoucherPayment = async ( api, jQueryForm ) => {
570-
const isOrderPay = getStripeServerData()?.isOrderPay;
570+
const stripeServerData = getStripeServerData();
571+
const isOrderPay = stripeServerData?.isOrderPay;
571572

572573
// The Order Pay page does a hard refresh when the hash changes, so we need to block the UI again.
573574
if ( isOrderPay ) {
@@ -596,7 +597,7 @@ export const confirmVoucherPayment = async ( api, jQueryForm ) => {
596597
// Verify the request using the data added to the URL.
597598
if (
598599
! clientSecret ||
599-
( isOrderPay && orderId !== getStripeServerData()?.orderId )
600+
( isOrderPay && orderId !== stripeServerData?.orderId )
600601
) {
601602
jQueryForm.removeClass( 'processing' ).unblock();
602603
return;
@@ -624,13 +625,63 @@ export const confirmVoucherPayment = async ( api, jQueryForm ) => {
624625
if ( confirmPayment.error ) {
625626
throw confirmPayment.error;
626627
}
627-
628-
// Once the customer closes the voucher and there are no errors, redirect them to the order received page.
629-
window.location.href = decodeURIComponent( partials[ 4 ] );
630628
} catch ( error ) {
631629
jQueryForm.removeClass( 'processing' ).unblock();
632630
showErrorCheckout( error.message );
631+
return;
633632
}
633+
634+
let postPaymentUrl = null;
635+
try {
636+
postPaymentUrl = decodeURIComponent( partials[ 4 ] || '' );
637+
} catch ( error ) {}
638+
639+
let validatedRedirectUrl = null;
640+
if ( postPaymentUrl ) {
641+
try {
642+
const redirectUrl = new URL(
643+
postPaymentUrl,
644+
window.location.origin
645+
);
646+
647+
if ( redirectUrl.origin === window.location.origin ) {
648+
validatedRedirectUrl = redirectUrl;
649+
}
650+
} catch ( error ) {}
651+
}
652+
653+
if ( validatedRedirectUrl ) {
654+
window.location.href = validatedRedirectUrl.toString();
655+
return;
656+
}
657+
658+
if ( ! stripeServerData?.orderReceivedURL ) {
659+
showErrorCheckout(
660+
__(
661+
'There was a problem processing the payment. Please refresh the page to try again.',
662+
'woocommerce-gateway-stripe'
663+
)
664+
);
665+
return;
666+
}
667+
668+
// We didn't get a valid redirect URL, so redirect to the order received page.
669+
// If we have a numeric order ID, navigate to the order received page for that order.
670+
if (
671+
orderId &&
672+
orderId !== 'NaN' &&
673+
orderId === String( parseInt( orderId, 10 ) )
674+
) {
675+
window.location.href =
676+
stripeServerData.orderReceivedURL +
677+
'/' +
678+
encodeURIComponent( orderId ) +
679+
'/';
680+
return;
681+
}
682+
683+
// Otherwise go to the generic page.
684+
window.location.href = stripeServerData.orderReceivedURL;
634685
};
635686

636687
/**

client/express-checkout/__tests__/event-handler.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ describe( 'Express checkout event handlers', () => {
219219

220220
beforeEach( () => {
221221
api = {
222+
expressCheckoutNormalizeAddress: jest.fn(),
222223
expressCheckoutECECreateOrder: jest.fn(),
223224
expressCheckoutECEPayForOrder: jest.fn(),
224225
confirmIntent: jest.fn(),

client/express-checkout/payment-flow.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,33 @@ const processOrder = async ( {
148148
orderDetails = {},
149149
} ) => {
150150
let orderResponse;
151+
152+
const normalizedOrderData = normalizeOrderData( {
153+
event,
154+
paymentMethodId,
155+
confirmationTokenId,
156+
} );
157+
158+
const normalizedAddress = await api.expressCheckoutNormalizeAddress(
159+
normalizedOrderData.billing_address,
160+
normalizedOrderData.shipping_address
161+
);
162+
163+
if ( normalizedAddress ) {
164+
normalizedOrderData.billing_address = normalizedAddress.billing_address;
165+
normalizedOrderData.shipping_address =
166+
normalizedAddress.shipping_address;
167+
}
168+
151169
if ( order ) {
152170
orderResponse = await api.expressCheckoutECEPayForOrder(
153171
order,
154172
orderDetails,
155-
normalizeOrderData( {
156-
event,
157-
paymentMethodId,
158-
confirmationTokenId,
159-
} )
173+
normalizedOrderData
160174
);
161175
} else {
162176
orderResponse = await api.expressCheckoutECECreateOrder(
163-
normalizeOrderData( {
164-
event,
165-
paymentMethodId,
166-
confirmationTokenId,
167-
} )
177+
normalizedOrderData
168178
);
169179
}
170180

client/settings/general-settings-section/customize-payment-method.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ const CustomizePaymentMethod = ( { method, onClose } ) => {
9494
) }
9595
/>
9696
) }
97-
<ButtonWrapper>
97+
{ /* The 'submit' class is used by WC core to clear unsaved changes warnings.
98+
See https://github.com/woocommerce/woocommerce/blob/fc7ffce309662758c0d3383de8cc8e8c6a57a167/plugins/woocommerce/client/legacy/js/admin/settings.js#L139 */ }
99+
<ButtonWrapper className="submit">
98100
<Button
99101
variant="tertiary"
100102
disabled={ isCustomizing }

client/settings/save-settings-section/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ const SaveSettingsSection = ( { onSettingsSave } ) => {
2020
};
2121

2222
return (
23-
<SaveSettingsSectionWrapper>
23+
// The 'submit' class is used by WC core to clear unsaved changes warnings.
24+
// See https://github.com/woocommerce/woocommerce/blob/fc7ffce309662758c0d3383de8cc8e8c6a57a167/plugins/woocommerce/client/legacy/js/admin/settings.js#L139
25+
<SaveSettingsSectionWrapper className="submit">
2426
<Button
2527
isPrimary
2628
isBusy={ isSaving }

client/settings/settings-manager/index.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import React, { useState } from 'react';
44
import { TabPanel } from '@wordpress/components';
55
import { getQuery, updateQueryString } from '@woocommerce/navigation';
66
import styled from '@emotion/styled';
7-
import { isEmpty, isEqual } from 'lodash';
7+
import { isEmpty } from 'lodash';
88
import SettingsLayout from '../settings-layout';
99
import PaymentSettingsPanel from '../payment-settings';
1010
import PaymentMethodsPanel from '../payment-methods';
1111
import SaveSettingsSection from '../save-settings-section';
1212
import { useSettings } from '../../data';
13-
import useConfirmNavigation from 'utils/use-confirm-navigation';
1413

1514
const StyledTabPanel = styled( TabPanel )`
1615
.components-tab-panel__tabs {
@@ -51,18 +50,6 @@ const SettingsManager = () => {
5150
} );
5251
};
5352

54-
const isPristine =
55-
! isEmpty( initialSettings ) && isEqual( initialSettings, settings );
56-
const displayPrompt = ! isPristine;
57-
const confirmationNavigationCallback = useConfirmNavigation(
58-
displayPrompt
59-
);
60-
61-
useEffect( confirmationNavigationCallback, [
62-
displayPrompt,
63-
confirmationNavigationCallback,
64-
] );
65-
6653
// This grabs the "panel" URL query string value to allow for opening a specific tab.
6754
const { panel } = getQuery();
6855

includes/abstracts/abstract-wc-stripe-payment-gateway.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,9 @@ public function process_response( $response, $order ) {
611611

612612
/* translators: transaction id */
613613
$message = sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $response->id );
614+
if ( isset( $response->is_webhook_response ) ) {
615+
$message .= ' (via webhook)';
616+
}
614617
$order->add_order_note( $message );
615618
}
616619
}
@@ -1433,15 +1436,17 @@ public function generate_create_intent_request( $order, $prepared_source ) {
14331436
*
14341437
* @param WC_Order $order The order that is being paid for.
14351438
* @return array The level 3 data to send to Stripe.
1439+
* @throws WC_Stripe_Exception If an order item has no quantity set.
14361440
*/
14371441
public function get_level3_data_from_order( $order ) {
14381442
// Get the order items. Don't need their keys, only their values.
14391443
// Order item IDs are used as keys in the original order items array.
14401444
$order_items = array_values( $order->get_items( [ 'line_item', 'fee' ] ) );
14411445
$currency = $order->get_currency();
1446+
$order_id = $order->get_id();
14421447

14431448
$stripe_line_items = array_map(
1444-
function ( $item ) use ( $currency ) {
1449+
function ( $item ) use ( $currency, $order_id ) {
14451450
if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
14461451
$product_id = $item->get_variation_id()
14471452
? $item->get_variation_id()
@@ -1453,9 +1458,14 @@ function ( $item ) use ( $currency ) {
14531458
}
14541459
$product_description = substr( $item->get_name(), 0, 26 );
14551460
$quantity = $item->get_quantity();
1456-
$unit_cost = WC_Stripe_Helper::get_stripe_amount( ( $subtotal / $quantity ), $currency );
1457-
$tax_amount = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
1458-
$discount_amount = WC_Stripe_Helper::get_stripe_amount( $subtotal - $item->get_total(), $currency );
1461+
if ( ! $quantity ) {
1462+
$error_msg = "Stripe Level 3 data: Order item with ID {$item->get_id()} from order ID {$order_id} has no quantity set.";
1463+
WC_Stripe_Logger::error( $error_msg );
1464+
throw new WC_Stripe_Exception( $error_msg );
1465+
}
1466+
$unit_cost = WC_Stripe_Helper::get_stripe_amount( ( $subtotal / $quantity ), $currency );
1467+
$tax_amount = WC_Stripe_Helper::get_stripe_amount( $item->get_total_tax(), $currency );
1468+
$discount_amount = WC_Stripe_Helper::get_stripe_amount( $subtotal - $item->get_total(), $currency );
14591469

14601470
return (object) [
14611471
'product_code' => (string) $product_id, // Up to 12 characters that uniquely identify the product.

includes/class-wc-stripe-payment-method-configurations.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,12 @@ private static function get_payment_method_configuration_from_stripe() {
165165
}
166166

167167
/**
168-
* Get the parent configuration ID.
168+
* Get the WooCommerce Platform payment method configuration id.
169169
*
170-
* @return string|null
170+
* @return string
171171
*/
172172
public static function get_parent_configuration_id() {
173-
return self::get_primary_configuration()->parent ?? null;
173+
return WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_PARENT_ID : self::LIVE_MODE_CONFIGURATION_PARENT_ID;
174174
}
175175

176176
/**

0 commit comments

Comments
 (0)